Feature/file upload refactor (#598)

* refactor

* Added support for multiple files

* fixed appearance a bit

* Always store files as full path => obj data

* added some error handling

* Do not go nested when storing file

* Refactored to not need blueprint set in blueprint
This commit is contained in:
Andy Miller
2016-05-17 16:46:00 -06:00
parent 2d63fed421
commit 2d9d71c444
13 changed files with 375 additions and 66 deletions

View File

@@ -73,6 +73,17 @@ class AdminController
*/
protected $redirectCode;
protected $upload_errors = [
0 => "There is no error, the file uploaded with success",
1 => "The uploaded file exceeds the max upload size",
2 => "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML",
3 => "The uploaded file was only partially uploaded",
4 => "No file was uploaded",
6 => "Missing a temporary folder",
7 => "Failed to write file to disk",
8 => "A PHP extension stopped the file upload"
];
/**
* @param Grav $grav
* @param string $view
@@ -1259,66 +1270,90 @@ class AdminController
*
* @return bool
*/
private function cleanFilesData($key, $file)
private function cleanFilesData($obj)
{
$blueprint = isset($this->items['fields'][$key]['files']) ? $this->items['fields'][$key]['files'] : [];
/** @var Page $page */
$page = null;
$cleanFiles[$key] = [];
if (!isset($blueprint)) {
return false;
}
$cleanFiles = [];
$type = trim("{$this->view}/{$this->admin->route}", '/');
$data = $this->admin->data($type, $this->post);
$fields = $data->blueprints()->fields();
$blueprint = isset($fields[$key]) ? $fields[$key] : [];
$blueprints = $data->blueprints();
if (!isset($blueprints['form']['fields'])) {
throw new \RuntimeException('Blueprints missing form fields definition');
}
$blueprint = $blueprints['form']['fields'];
$cleanFiles = [$key => []];
foreach ((array)$file['error'] as $index => $error) {
if ($error == UPLOAD_ERR_OK) {
$tmp_name = $file['tmp_name'][$index];
$name = $file['name'][$index];
$type = $file['type'][$index];
$destination = Folder::getRelativePath(rtrim($blueprint['destination'], '/'));
$file = $_FILES['data'];
if (!$this->match_in_array($type, $blueprint['accept'])) {
throw new \RuntimeException('File "' . $name . '" is not an accepted MIME type.');
}
foreach ((array)$file['error'] as $index => $errors) {
$errors = !is_array($errors) ? [$errors] : $errors;
if (Utils::startsWith($destination, '@page:')) {
$parts = explode(':', $destination);
$route = $parts[1];
$page = $this->grav['page']->find($route);
if (!$page) {
throw new \RuntimeException('Unable to upload file to destination. Page route not found.');
foreach($errors as $multiple_index => $error) {
if ($error == UPLOAD_ERR_OK) {
if (is_array($file['name'][$index])) {
$tmp_name = $file['tmp_name'][$index][$multiple_index];
$name = $file['name'][$index][$multiple_index];
$type = $file['type'][$index][$multiple_index];
$size = $file['size'][$index][$multiple_index];
} else {
$tmp_name = $file['tmp_name'][$index];
$name = $file['name'][$index];
$type = $file['type'][$index];
$size = $file['size'][$index];
}
$destination = $page->relativePagePath();
} else {
if ($destination == '@self') {
$page = $this->admin->page(true);
$destination = Folder::getRelativePath(rtrim($blueprint[$index]['destination'], '/'));
if (!$this->match_in_array($type, $blueprint[$index]['accept'])) {
throw new \RuntimeException('File "' . $name . '" is not an accepted MIME type.');
}
if (Utils::startsWith($destination, '@page:')) {
$parts = explode(':', $destination);
$route = $parts[1];
$page = $this->grav['page']->find($route);
if (!$page) {
throw new \RuntimeException('Unable to upload file to destination. Page route not found.');
}
$destination = $page->relativePagePath();
} else {
Folder::mkdir($destination);
if ($destination == '@self') {
$page = $this->admin->page(true);
$destination = $page->relativePagePath();
} else {
Folder::mkdir($destination);
}
}
}
if (move_uploaded_file($tmp_name, "$destination/$name")) {
$path = $page ? $this->grav['uri']->convertUrl($page,
$page->route() . '/' . $name) : $destination . '/' . $name;
$cleanFiles[$key][] = $path;
if (move_uploaded_file($tmp_name, "$destination/$name")) {
$path = $page ? $this->grav['uri']->convertUrl($page, $page->route() . '/' . $name) : $destination . '/' . $name;
$fileData = [
'name' => $name,
'path' => $path,
'type' => $type,
'size' => $size,
'file' => $destination . '/' . $name,
'route' => $page ? $path : null
];
$cleanFiles[$index][$path] = $fileData;
} else {
throw new \RuntimeException("Unable to upload file(s) to $destination/$name");
}
} else {
throw new \RuntimeException("Unable to upload file(s) to $destination/$name");
if ($error != UPLOAD_ERR_NO_FILE) {
throw new \RuntimeException("Unable to upload file(s) - Error: ".$file['name'][$index].": ".$this->upload_errors[$error]);
}
}
}
}
return $cleanFiles[$key];
return $cleanFiles;
}
/**
@@ -1347,11 +1382,10 @@ class AdminController
*/
private function processFiles($obj)
{
foreach ((array)$_FILES as $key => $file) {
$cleanFiles = $this->cleanFilesData($key, $file);
if ($cleanFiles) {
$obj->set($key, $cleanFiles);
}
$cleanFiles = $this->cleanFilesData($obj);
foreach ($cleanFiles as $key => $data) {
$obj->set($key, $data);
}
return $obj;
@@ -1532,6 +1566,7 @@ class AdminController
$this->admin->setMessage($e->getMessage(), 'error');
return false;
}
$obj->filter();
}
@@ -1910,8 +1945,21 @@ class AdminController
$this->taskRemoveMedia();
$field = $uri->param('field');
$blueprint = $uri->param('blueprint');
$this->grav['config']->set($blueprint . '.' . $field, '');
$path = base64_decode($uri->param('path'));
$files = $this->grav['config']->get($blueprint . '.' . $field);
foreach ($files as $key => $value) {
if ($key == $path) {
unset($files[$key]);
}
}
$this->grav['config']->set($blueprint . '.' . $field, $files);
if (substr($blueprint, 0, 7) == 'plugins') {
Plugin::saveConfig(substr($blueprint, 8));
}

View File

@@ -1,3 +1,3 @@
@import url("//fonts.googleapis.com/css?family=Montserrat:400|Lato:300,400,700|Inconsolata:400,700");body,h5,h6,.badge,.note,.grav-mdeditor-preview,input,select,textarea,button,.selectize-input{font-family:"Lato","Helvetica","Tahoma","Geneva","Arial",sans-serif}h1,h2,h3,h4,#admin-menu li,.form-tabs>label,.label{font-family:"Montserrat","Helvetica","Tahoma","Geneva","Arial",sans-serif}code,kbd,pre,samp,body .CodeMirror{font-family:"Inconsolata","Monaco","Consolas","Lucida Console",monospace !important}
/*# sourceMappingURL=fonts.css.map */
/*# sourceMappingURL=fonts.css.map */

View File

@@ -1 +1,10 @@
{"version":3,"file":"fonts.css","sources":["fonts.scss","configuration/fonts/_support.scss"],"sourcesContent":["$fonts-default: 'Lato' !default;\n$fonts-header: 'Montserrat' !default;\n$fonts-mono: 'Inconsolata' !default;\n\n$font-definitions: (\n Montserrat: '400',\n Lato: '300,400,700',\n Inconsolata: '400,700'\n);\n\n@import \"configuration/fonts/support\";\n\n\n\n\n","@function str-replace($string, $search, $replace: '') {\n $index: str-index($string, $search);\n\n @if $index {\n @return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);\n }\n\n @return $string;\n}\n\n@function admin-font-faces($fonts) {\n $url: \"//fonts.googleapis.com/css?family=\";\n $nb: 0;\n\n @each $fontname, $weights in $fonts {\n\n @if $fontname == $fonts-default or\n $fontname == $fonts-header or\n $fontname == $fonts-mono {\n\n $nb: $nb + 1;\n $nb-word: 0;\n\n $fontname: str-replace(\"#{$fontname}\", \" \", \"+\");\n\n $url: $url + $fontname;\n\n @if $weights != null {\n $url: $url + \":\" + $weights;\n }\n\n @if $nb < 3 {\n $url: $url + \"|\";\n }\n }\n }\n\n @return $url;\n}\n\n@mixin body-fonts($font) {\n body, h5, h6,\n .badge, .note, .grav-mdeditor-preview,\n input, select, textarea, button, .selectize-input {\n font-family: \"#{$font}\", \"Helvetica\", \"Tahoma\", \"Geneva\", \"Arial\", sans-serif;\n }\n}\n\n@mixin header-fonts($font) {\n h1, h2, h3, h4,\n #admin-menu li, .form-tabs > label, .label {\n font-family: \"#{$font}\", \"Helvetica\", \"Tahoma\", \"Geneva\", \"Arial\", sans-serif;\n }\n}\n\n@mixin mono-fonts($font) {\n code, kbd, pre, samp,\n body .CodeMirror {\n font-family: \"#{$font}\", \"Monaco\", \"Consolas\", \"Lucida Console\", monospace !important;\n }\n}\n$font-url: admin-font-faces($font-definitions);\n\n@import url(\"#{$font-url}\");\n\n@include body-fonts($fonts-default);\n\n@include header-fonts($fonts-header);\n\n@include mono-fonts($fonts-mono);\n\n\n\n\n\n"],"mappings":"AC+DA,OAAO,CAAC,4FAAI,CAtBR,AAAA,IAAI,CAAE,AAAA,EAAE,CAAE,AAAA,EAAE,CACZ,AAAA,MAAM,CAAE,AAAA,KAAK,CAAE,AAAA,sBAAsB,CACrC,AAAA,KAAK,CAAE,AAAA,MAAM,CAAE,AAAA,QAAQ,CAAE,AAAA,MAAM,CAAE,AAAA,gBAAgB,AAAC,CAC9C,WAAW,CAAE,MAAU,CAAE,WAAW,CAAE,QAAQ,CAAE,QAAQ,CAAE,OAAO,CAAE,UAAU,CAChF,AAID,AAAA,EAAE,CAAE,AAAA,EAAE,CAAE,AAAA,EAAE,CAAE,AAAA,EAAE,CACd,AAAY,WAAD,CAAC,EAAE,CAAE,AAAa,UAAH,CAAG,KAAK,CAAE,AAAA,MAAM,AAAC,CACvC,WAAW,CAAE,YAAU,CAAE,WAAW,CAAE,QAAQ,CAAE,QAAQ,CAAE,OAAO,CAAE,UAAU,CAChF,AAID,AAAA,IAAI,CAAE,AAAA,GAAG,CAAE,AAAA,GAAG,CAAE,AAAA,IAAI,CACpB,AAAK,IAAD,CAAC,WAAW,AAAC,CACb,WAAW,CAAE,aAAU,CAAE,QAAQ,CAAE,UAAU,CAAE,gBAAgB,CAAE,SAAS,CAAC,UAAU,CACxF","names":[]}
{
"version": 3,
"file": "fonts.css",
"sources": [
"../scss/fonts.scss",
"../scss/configuration/fonts/_support.scss"
],
"mappings": "AC+DA,OAAO,CAAC,4FAAI,CAtBR,AAAA,IAAI,CAAE,AAAA,EAAE,CAAE,AAAA,EAAE,CACZ,AAAA,MAAM,CAAE,AAAA,KAAK,CAAE,AAAA,sBAAsB,CACrC,AAAA,KAAK,CAAE,AAAA,MAAM,CAAE,AAAA,QAAQ,CAAE,AAAA,MAAM,CAAE,AAAA,gBAAgB,AAAC,CAC9C,WAAW,CAAE,MAAU,CAAE,WAAW,CAAE,QAAQ,CAAE,QAAQ,CAAE,OAAO,CAAE,UAAU,CAChF,AAID,AAAA,EAAE,CAAE,AAAA,EAAE,CAAE,AAAA,EAAE,CAAE,AAAA,EAAE,CACd,AAAY,WAAD,CAAC,EAAE,CAAE,AAAa,UAAH,CAAG,KAAK,CAAE,AAAA,MAAM,AAAC,CACvC,WAAW,CAAE,YAAU,CAAE,WAAW,CAAE,QAAQ,CAAE,QAAQ,CAAE,OAAO,CAAE,UAAU,CAChF,AAID,AAAA,IAAI,CAAE,AAAA,GAAG,CAAE,AAAA,GAAG,CAAE,AAAA,IAAI,CACpB,AAAK,IAAD,CAAC,WAAW,AAAC,CACb,WAAW,CAAE,aAAU,CAAE,QAAQ,CAAE,UAAU,CAAE,gBAAgB,CAAE,SAAS,CAAC,UAAU,CACxF",
"names": []
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,3 @@
body,h5,h6,.badge,.note,.grav-mdeditor-preview,input,select,textarea,button,.selectize-input,h1,h2,h3,h4,#admin-menu li,.form-tabs>label,.label{font-family:"Helvetica Neue", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif}code,kbd,pre,samp,body .CodeMirror{font-family:"Monaco", "Consolas", "Lucida Console", monospace}
/*# sourceMappingURL=simple-fonts.css.map */
/*# sourceMappingURL=simple-fonts.css.map */

View File

@@ -1 +1,9 @@
{"version":3,"file":"simple-fonts.css","sources":["simple-fonts.scss"],"sourcesContent":["body, h5, h6,\n.badge, .note, .grav-mdeditor-preview,\ninput, select, textarea, button, .selectize-input,\nh1, h2, h3, h4,\n#admin-menu li, .form-tabs > label, .label {\n font-family: \"Helvetica Neue\", \"Helvetica\", \"Tahoma\", \"Geneva\", \"Arial\", sans-serif;\n}\ncode, kbd, pre, samp,\nbody .CodeMirror {\n font-family: \"Monaco\", \"Consolas\", \"Lucida Console\", monospace;\n}\n"],"mappings":"AAAA,AAAA,IAAI,CAAE,AAAA,EAAE,CAAE,AAAA,EAAE,CACZ,AAAA,MAAM,CAAE,AAAA,KAAK,CAAE,AAAA,sBAAsB,CACrC,AAAA,KAAK,CAAE,AAAA,MAAM,CAAE,AAAA,QAAQ,CAAE,AAAA,MAAM,CAAE,AAAA,gBAAgB,CACjD,AAAA,EAAE,CAAE,AAAA,EAAE,CAAE,AAAA,EAAE,CAAE,AAAA,EAAE,CACd,AAAY,WAAD,CAAC,EAAE,CAAE,AAAa,UAAH,CAAG,KAAK,CAAE,AAAA,MAAM,AAAC,CACvC,WAAW,CAAE,sEAAuE,CACvF,AACD,AAAA,IAAI,CAAE,AAAA,GAAG,CAAE,AAAA,GAAG,CAAE,AAAA,IAAI,CACpB,AAAK,IAAD,CAAC,WAAW,AAAC,CACb,WAAW,CAAE,iDAAkD,CAClE","names":[]}
{
"version": 3,
"file": "simple-fonts.css",
"sources": [
"../scss/simple-fonts.scss"
],
"mappings": "AAAA,AAAA,IAAI,CAAE,AAAA,EAAE,CAAE,AAAA,EAAE,CACZ,AAAA,MAAM,CAAE,AAAA,KAAK,CAAE,AAAA,sBAAsB,CACrC,AAAA,KAAK,CAAE,AAAA,MAAM,CAAE,AAAA,QAAQ,CAAE,AAAA,MAAM,CAAE,AAAA,gBAAgB,CACjD,AAAA,EAAE,CAAE,AAAA,EAAE,CAAE,AAAA,EAAE,CAAE,AAAA,EAAE,CACd,AAAY,WAAD,CAAC,EAAE,CAAE,AAAa,UAAH,CAAG,KAAK,CAAE,AAAA,MAAM,AAAC,CACvC,WAAW,CAAE,sEAAuE,CACvF,AACD,AAAA,IAAI,CAAE,AAAA,GAAG,CAAE,AAAA,GAAG,CAAE,AAAA,IAAI,CACpB,AAAK,IAAD,CAAC,WAAW,AAAC,CACb,WAAW,CAAE,iDAAkD,CAClE",
"names": []
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -28,6 +28,18 @@ form {
@include breakpoint(mobile-only) {
padding-left: 1rem;
}
.thumbnail {
max-width: 200px;
vertical-align: top;
display: inline-block;
}
.thumbnail-remove {
vertical-align: top;
font-size: 1.4rem;
line-height: 1;
}
}
.form-data {

View File

@@ -2,23 +2,34 @@
{% set defaults = config.plugins.form %}
{% set files = defaults.files|merge(field|default([])) %}
{% macro preview(path, value, global) %}
{% if value %}
{% set uri = global.grav.uri %}
{% set files = global.files %}
{% set config = global.grav.config %}
{% set blueprint = (global.plugin ? 'plugins.' : global.theme ? 'themes.' : '') ~ uri.basename %}
<img class="thumbnail" src="{{ uri.rootUrl == '/' ? '/' : uri.rootUrl ~ '/'}}{{ path }}" alt="{{ path|replace({(files.destination ~ '/'): ''}) }}" />
<a class="thumbnail-remove" href="{{ uri.addNonce(global.base_url_relative ~
'/media/' ~ base64_encode(global.base_path ~ '/' ~ path) ~
'/task' ~ config.system.param_sep ~ 'removeFileFromBlueprint' ~
'/blueprint' ~ config.system.param_sep ~ blueprint ~
'/field' ~ config.system.param_sep ~ files.name ~
'/path' ~ config.system.param_sep ~ base64_encode(value.path) ~
'/redirect' ~ config.system.param_sep ~ base64_encode(uri.path), 'admin-form', 'admin-nonce') }}">
<i class="fa fa-close"></i>
</a>
{% endif %}
{% endmacro %}
{% block input %}
{% if not plugin and not theme %}
The "file" input field cannot be used in Pages Blueprints. It's intended to be used for Plugins and Themes blueprints.
Use the "pagemediaselect" type instead.
{% else %}
{% if value %}
<img src="{{ uri.rootUrl == '/' ? '/' : uri.rootUrl ~ '/'}}{{ value }}" alt="{{ value|replace({(files.destination ~ '/'): ''}) }}" />
<a href="{{ uri.addNonce(base_url_relative ~
'/media/' ~ base64_encode(base_path ~ '/' ~ value) ~
'/task' ~ config.system.param_sep ~ 'removeFileFromBlueprint' ~
'/blueprint' ~ config.system.param_sep ~ files.blueprint ~
'/field' ~ config.system.param_sep ~ files.name ~
'/redirect' ~ config.system.param_sep ~ base64_encode(uri.path), 'admin-form', 'admin-nonce') }}">
<i class="fa fa-close"></i>
</a>
{% endif %}
{% for path, file in value %}
{{ _self.preview(path, file, _context) }}
{% endfor %}
<div class="form-input-wrapper {{ field.size }}">
<input