mirror of
https://github.com/getgrav/grav-plugin-admin.git
synced 2026-05-05 22:57:05 +02:00
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:
@@ -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));
|
||||
}
|
||||
|
||||
2
themes/grav/css-compiled/fonts.css
vendored
2
themes/grav/css-compiled/fonts.css
vendored
@@ -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 */
|
||||
@@ -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": []
|
||||
}
|
||||
2
themes/grav/css-compiled/nucleus.css
vendored
2
themes/grav/css-compiled/nucleus.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
themes/grav/css-compiled/preset.css
vendored
2
themes/grav/css-compiled/preset.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
themes/grav/css-compiled/simple-fonts.css
vendored
2
themes/grav/css-compiled/simple-fonts.css
vendored
@@ -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 */
|
||||
@@ -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": []
|
||||
}
|
||||
4
themes/grav/css-compiled/template.css
vendored
4
themes/grav/css-compiled/template.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user