Feature/admin fragmentation (#856)

* Separated Admin Controller into a generic and extendable Base controller. Added Autoload for properly loading classes

* Implemented custom class loader to force lowercase

* Removed composer autoloader for classes

* Updates

* Ability to pass custom upload URL for files

* Added new onAdminCanSave event for 3rd party plugins

* Moved files upload GC in onOutputGenerated event

* Cleanup

* Moved autoloader so it is always registering

* Fixed onOutputGenerated event location

* Moved `taskRemoveFileFromBlueprint`,  `taskRemoveMedia `, `canEditMedia` methods to admin base controller

* Allow to globally define `blueprint_type` and `file_url_remove` for the file field

* Moved `isMultilang()` into base controller

* Properly generate thumbnails in proportions for file fields

* Simplified execute restrictions with blacklist
This commit is contained in:
Andy Miller
2016-11-07 09:54:10 -07:00
committed by GitHub
parent 0757d49d9f
commit f361addd60
13 changed files with 2200 additions and 2038 deletions

View File

@@ -10,8 +10,13 @@ use Grav\Common\Page\Page;
use Grav\Common\Page\Pages; use Grav\Common\Page\Pages;
use Grav\Common\Plugin; use Grav\Common\Plugin;
use Grav\Common\Uri; use Grav\Common\Uri;
use Grav\Common\Utils;
use Grav\Common\User\User; use Grav\Common\User\User;
use RocketTheme\Toolbox\File\File; use Grav\Plugin\Admin\Admin;
use Grav\Plugin\Admin\AdminTwigExtension;
use Grav\Plugin\Admin\Popularity;
use Grav\Plugin\Admin\Themes;
use Grav\Plugin\Admin\AdminController;
use RocketTheme\Toolbox\Event\Event; use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\Session\Session; use RocketTheme\Toolbox\Session\Session;
@@ -96,6 +101,13 @@ class AdminPlugin extends Plugin
*/ */
public function setup() public function setup()
{ {
// Autoloader
spl_autoload_register(function ($class) {
if (Utils::startsWith($class, 'Grav\Plugin\Admin')) {
require_once __DIR__ .'/classes/' . strtolower(basename(str_replace("\\", "/", $class))) . '.php';
}
});
$route = $this->config->get('plugins.admin.route'); $route = $this->config->get('plugins.admin.route');
if (!$route) { if (!$route) {
return; return;
@@ -303,14 +315,11 @@ class AdminPlugin extends Plugin
// Replace themes service with admin. // Replace themes service with admin.
$this->grav['themes'] = function () { $this->grav['themes'] = function () {
require_once __DIR__ . '/classes/themes.php';
return new Themes($this->grav); return new Themes($this->grav);
}; };
} }
// We need popularity no matter what // We need popularity no matter what
require_once __DIR__ . '/classes/popularity.php';
$this->popularity = new Popularity(); $this->popularity = new Popularity();
// Fire even to register permissions from other plugins // Fire even to register permissions from other plugins
@@ -319,8 +328,8 @@ class AdminPlugin extends Plugin
protected function initializeController($task, $post) protected function initializeController($task, $post)
{ {
require_once __DIR__ . '/classes/controller.php'; $controller = new AdminController();
$controller = new AdminController($this->grav, $this->template, $task, $this->route, $post); $controller->initialize($this->grav, $this->template, $task, $this->route, $post);
$controller->execute(); $controller->execute();
$controller->redirect(); $controller->redirect();
} }
@@ -375,23 +384,6 @@ class AdminPlugin extends Plugin
exit(); exit();
} }
// Clear flash objects for previously uploaded files
// whenever the user switches page / reloads
// ignoring any JSON / extension call
if (is_null($this->uri->extension()) && $task !== 'save') {
// Discard any previously uploaded files session.
// and if there were any uploaded file, remove them from the filesystem
if ($flash = $this->session->getFlashObject('files-upload')) {
$flash = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($flash));
foreach ($flash as $key => $value) {
if ($key !== 'tmp_name') {
continue;
}
@unlink($value);
}
}
}
$self = $this; $self = $this;
// make sure page is not frozen! // make sure page is not frozen!
@@ -633,17 +625,12 @@ class AdminPlugin extends Plugin
'onAssetsInitialized' => ['onAssetsInitialized', 1000], 'onAssetsInitialized' => ['onAssetsInitialized', 1000],
'onTask.GPM' => ['onTaskGPM', 0], 'onTask.GPM' => ['onTaskGPM', 0],
'onAdminRegisterPermissions' => ['onAdminRegisterPermissions', 0], 'onAdminRegisterPermissions' => ['onAdminRegisterPermissions', 0],
'onOutputGenerated' => ['onOutputGenerated', 0],
]); ]);
// Initialize admin class.
require_once __DIR__ . '/classes/admin.php';
// Autoload classes // Autoload classes
$autoload = __DIR__ . '/vendor/autoload.php'; require_once __DIR__ . '/vendor/autoload.php';
if (!is_file($autoload)) {
throw new \Exception('Admin Plugin failed to load. Composer dependencies not met.');
}
require_once $autoload;
// Check for required plugins // Check for required plugins
if (!$this->grav['config']->get('plugins.login.enabled') || !$this->grav['config']->get('plugins.form.enabled') || !$this->grav['config']->get('plugins.email.enabled')) { if (!$this->grav['config']->get('plugins.login.enabled') || !$this->grav['config']->get('plugins.form.enabled') || !$this->grav['config']->get('plugins.email.enabled')) {
@@ -671,6 +658,7 @@ class AdminPlugin extends Plugin
$this->route = array_shift($array); $this->route = array_shift($array);
} }
// Initialize admin class.
$this->admin = new Admin($this->grav, $this->admin_route, $this->template, $this->route); $this->admin = new Admin($this->grav, $this->admin_route, $this->template, $this->route);
@@ -812,6 +800,26 @@ class AdminPlugin extends Plugin
$this->grav['twig']->plugins_hooked_dashboard_widgets_main[] = ['template' => 'dashboard-pages']; $this->grav['twig']->plugins_hooked_dashboard_widgets_main[] = ['template' => 'dashboard-pages'];
} }
public function onOutputGenerated()
{
// Clear flash objects for previously uploaded files
// whenever the user switches page / reloads
// ignoring any JSON / extension call
if (is_null($this->uri->extension()) && $this->admin->task !== 'save') {
// Discard any previously uploaded files session.
// and if there were any uploaded file, remove them from the filesystem
if ($flash = $this->session->getFlashObject('files-upload')) {
$flash = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($flash));
foreach ($flash as $key => $value) {
if ($key !== 'tmp_name') {
continue;
}
@unlink($value);
}
}
}
}
/** /**
* Initial stab at registering permissions (WIP) * Initial stab at registering permissions (WIP)
* *

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,929 @@
<?php
namespace Grav\Plugin\Admin;
use Grav\Common\Config\Config;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use Grav\Common\Page\Media;
use Grav\Common\Utils;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\File\File;
/**
* Class AdminController
*
* @package Grav\Plugin
*/
class AdminBaseController
{
/**
* @var Grav
*/
public $grav;
/**
* @var string
*/
public $view;
/**
* @var string
*/
public $task;
/**
* @var string
*/
public $route;
/**
* @var array
*/
public $post;
/**
* @var array|null
*/
public $data;
/**
* @var \Grav\Common\Uri
*/
protected $uri;
/**
* @var Admin
*/
protected $admin;
/**
* @var string
*/
protected $redirect;
/**
* @var int
*/
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"
];
/** @var array */
public $blacklist_views = [];
/**
* Performs a task.
*
* @return bool True if the action was performed successfully.
*/
public function execute()
{
if (in_array($this->view, $this->blacklist_views)) {
return false;
}
if (!$this->validateNonce()) {
return false;
}
$method = 'task' . ucfirst($this->task);
if (method_exists($this, $method)) {
try {
$success = call_user_func([$this, $method]);
} catch (\RuntimeException $e) {
$success = true;
$this->admin->setMessage($e->getMessage(), 'error');
}
} else {
$success = $this->grav->fireEvent('onAdminTaskExecute', new Event(['controller' => $this, 'method' => $method]));
}
// Grab redirect parameter.
$redirect = isset($this->post['_redirect']) ? $this->post['_redirect'] : null;
unset($this->post['_redirect']);
// Redirect if requested.
if ($redirect) {
$this->setRedirect($redirect);
}
return $success;
}
protected function validateNonce()
{
if (method_exists('Grav\Common\Utils', 'getNonce')) {
if (strtolower($_SERVER['REQUEST_METHOD']) == 'post') {
if (isset($this->post['admin-nonce'])) {
$nonce = $this->post['admin-nonce'];
} else {
$nonce = $this->grav['uri']->param('admin-nonce');
}
if (!$nonce || !Utils::verifyNonce($nonce, 'admin-form')) {
if ($this->task == 'addmedia') {
$message = sprintf($this->admin->translate('PLUGIN_ADMIN.FILE_TOO_LARGE', null),
ini_get('post_max_size'));
//In this case it's more likely that the image is too big than POST can handle. Show message
$this->admin->json_response = [
'status' => 'error',
'message' => $message
];
return false;
}
$this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'), 'error');
$this->admin->json_response = [
'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN')
];
return false;
}
unset($this->post['admin-nonce']);
} else {
if ($this->task == 'logout') {
$nonce = $this->grav['uri']->param('logout-nonce');
if (!isset($nonce) || !Utils::verifyNonce($nonce, 'logout-form')) {
$this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'),
'error');
$this->admin->json_response = [
'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN')
];
return false;
}
} else {
$nonce = $this->grav['uri']->param('admin-nonce');
if (!isset($nonce) || !Utils::verifyNonce($nonce, 'admin-form')) {
$this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'),
'error');
$this->admin->json_response = [
'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN')
];
return false;
}
}
}
}
return true;
}
/**
* Sets the page redirect.
*
* @param string $path The path to redirect to
* @param int $code The HTTP redirect code
*/
public function setRedirect($path, $code = 303)
{
$this->redirect = $path;
$this->redirectCode = $code;
}
/**
* Handles ajax upload for files.
* Stores in a flash object the temporary file and deals with potential file errors.
*
* @return bool True if the action was performed.
*/
public function taskFilesUpload()
{
if (!$this->authorizeTask('save', $this->dataPermissions()) || !isset($_FILES)) {
return false;
}
/** @var Config $config */
$config = $this->grav['config'];
$data = $this->view == 'pages' ? $this->admin->page(true) : $this->prepareData([]);
$settings = $data->blueprints()->schema()->getProperty($this->post['name']);
$settings = (object)array_merge(
['avoid_overwriting' => false,
'random_name' => false,
'accept' => ['image/*'],
'limit' => 10,
'filesize' => $config->get('system.media.upload_limit', 5242880) // 5MB
],
(array)$settings,
['name' => $this->post['name']]
);
$upload = $this->normalizeFiles($_FILES['data'], $settings->name);
if (!isset($settings->destination)) {
$this->admin->json_response = [
'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.DESTINATION_NOT_SPECIFIED', null)
];
return false;
}
// Do not use self@ outside of pages
if ($this->view != 'pages' && in_array($settings->destination, ['@self', 'self@'])) {
$this->admin->json_response = [
'status' => 'error',
'message' => sprintf($this->admin->translate('PLUGIN_ADMIN.FILEUPLOAD_PREVENT_SELF', null), $settings->destination)
];
return false;
}
// Handle errors and breaks without proceeding further
if ($upload->file->error != UPLOAD_ERR_OK) {
$this->admin->json_response = [
'status' => 'error',
'message' => sprintf($this->admin->translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_UPLOAD', null), $upload->file->name,
$this->upload_errors[$upload->file->error])
];
return false;
} else {
// Remove the error object to avoid storing it
unset($upload->file->error);
// we need to move the file at this stage or else
// it won't be available upon save later on
// since php removes it from the upload location
$tmp_dir = Admin::getTempDir();
$tmp_file = $upload->file->tmp_name;
$tmp = $tmp_dir . '/uploaded-files/' . basename($tmp_file);
Folder::create(dirname($tmp));
if (!move_uploaded_file($tmp_file, $tmp)) {
$this->admin->json_response = [
'status' => 'error',
'message' => sprintf($this->admin->translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_MOVE', null), '', $tmp)
];
return false;
}
$upload->file->tmp_name = $tmp;
}
// Handle file size limits
$settings->filesize *= 1048576; // 2^20 [MB in Bytes]
if ($settings->filesize > 0 && $upload->file->size > $settings->filesize) {
$this->admin->json_response = [
'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.EXCEEDED_GRAV_FILESIZE_LIMIT')
];
return false;
}
// Handle Accepted file types
// Accept can only be mime types (image/png | image/*) or file extensions (.pdf|.jpg)
$accepted = false;
$errors = [];
foreach ((array)$settings->accept as $type) {
// Force acceptance of any file when star notation
if ($type == '*') {
$accepted = true;
break;
}
$isMime = strstr($type, '/');
$find = str_replace('*', '.*', $type);
$match = preg_match('#' . $find . '$#', $isMime ? $upload->file->type : $upload->file->name);
if (!$match) {
$message = $isMime ? 'The MIME type "' . $upload->file->type . '"' : 'The File Extension';
$errors[] = $message . ' for the file "' . $upload->file->name . '" is not an accepted.';
$accepted |= false;
} else {
$accepted |= true;
}
}
if (!$accepted) {
$this->admin->json_response = [
'status' => 'error',
'message' => implode('<br />', $errors)
];
return false;
}
// Retrieve the current session of the uploaded files for the field
// and initialize it if it doesn't exist
$sessionField = base64_encode($this->grav['uri']->url());
$flash = $this->admin->session()->getFlashObject('files-upload');
if (!$flash) {
$flash = [];
}
if (!isset($flash[$sessionField])) {
$flash[$sessionField] = [];
}
if (!isset($flash[$sessionField][$upload->field])) {
$flash[$sessionField][$upload->field] = [];
}
// Set destination
$destination = Folder::getRelativePath(rtrim($settings->destination, '/'));
$destination = $this->admin->getPagePathFromToken($destination);
// Create destination if needed
if (!is_dir($destination)) {
Folder::mkdir($destination);
}
// Generate random name if required
if ($settings->random_name) { // TODO: document
$extension = pathinfo($upload->file->name)['extension'];
$upload->file->name = Utils::generateRandomString(15) . '.' . $extension;
}
// Handle conflicting name if needed
if ($settings->avoid_overwriting) { // TODO: document
if (file_exists($destination . '/' . $upload->file->name)) {
$upload->file->name = date('YmdHis') . '-' . $upload->file->name;
}
}
// Prepare object for later save
$path = $destination . '/' . $upload->file->name;
$upload->file->path = $path;
// $upload->file->route = $page ? $path : null;
// Prepare data to be saved later
$flash[$sessionField][$upload->field][$path] = (array)$upload->file;
// Finally store the new uploaded file in the field session
$this->admin->session()->setFlashObject('files-upload', $flash);
$this->admin->json_response = [
'status' => 'success',
'session' => \json_encode([
'sessionField' => base64_encode($this->grav['uri']->url()),
'path' => $upload->file->path,
'field' => $settings->name
])
];
return true;
}
/**
* Checks if the user is allowed to perform the given task with its associated permissions
*
* @param string $task The task to execute
* @param array $permissions The permissions given
*
* @return bool True if authorized. False if not.
*/
protected function authorizeTask($task = '', $permissions = [])
{
if (!$this->admin->authorize($permissions)) {
if ($this->grav['uri']->extension() === 'json') {
$this->admin->json_response = [
'status' => 'unauthorized',
'message' => $this->admin->translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' ' . $task . '.'
];
} else {
$this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' ' . $task . '.',
'error');
}
return false;
}
return true;
}
/**
* Gets the permissions needed to access a given view
*
* @return array An array of permissions
*/
protected function dataPermissions()
{
$type = $this->view;
$permissions = ['admin.super'];
switch ($type) {
case 'configuration':
case 'system':
$permissions[] = 'admin.configuration';
break;
case 'settings':
case 'site':
$permissions[] = 'admin.settings';
break;
case 'plugins':
$permissions[] = 'admin.plugins';
break;
case 'themes':
$permissions[] = 'admin.themes';
break;
case 'users':
$permissions[] = 'admin.users';
break;
case 'pages':
$permissions[] = 'admin.pages';
break;
}
return $permissions;
}
/**
* Gets the configuration data for a given view & post
*
* @param array $data
*
* @return array
*/
protected function prepareData(array $data)
{
return $data;
}
/**
* Internal method to normalize the $_FILES array
*
* @param array $data $_FILES starting point data
* @param string $key
*
* @return object a new Object with a normalized list of files
*/
protected function normalizeFiles($data, $key = '')
{
$files = new \stdClass();
$files->field = $key;
$files->file = new \stdClass();
foreach ($data as $fieldName => $fieldValue) {
// Since Files Upload are always happening via Ajax
// we are not interested in handling `multiple="true"`
// because they are always handled one at a time.
// For this reason we normalize the value to string,
// in case it is arriving as an array.
$value = (array)Utils::getDotNotation($fieldValue, $key);
$files->file->{$fieldName} = array_shift($value);
}
return $files;
}
/**
* Removes a file from the flash object session, before it gets saved
*
* @return bool True if the action was performed.
*/
public function taskFilesSessionRemove()
{
if (!$this->authorizeTask('save', $this->dataPermissions()) || !isset($_FILES)) {
return false;
}
// Retrieve the current session of the uploaded files for the field
// and initialize it if it doesn't exist
$sessionField = base64_encode($this->grav['uri']->url());
$request = \json_decode($this->post['session']);
// Ensure the URI requested matches the current one, otherwise fail
if ($request->sessionField !== $sessionField) {
return false;
}
// Retrieve the flash object and remove the requested file from it
$flash = $this->admin->session()->getFlashObject('files-upload');
$endpoint = $flash[$request->sessionField][$request->field][$request->path];
if (isset($endpoint)) {
if (file_exists($endpoint['tmp_name'])) {
unlink($endpoint['tmp_name']);
}
unset($endpoint);
}
// Walk backward to cleanup any empty field that's left
// Field
if (!count($flash[$request->sessionField][$request->field])) {
unset($flash[$request->sessionField][$request->field]);
}
// Session Field
if (!count($flash[$request->sessionField])) {
unset($flash[$request->sessionField]);
}
// If there's anything left to restore in the flash object, do so
if (count($flash)) {
$this->admin->session()->setFlashObject('files-upload', $flash);
}
$this->admin->json_response = ['status' => 'success'];
return true;
}
/**
* Redirect to the route stored in $this->redirect
*/
public function redirect()
{
if (!$this->redirect) {
return;
}
$base = $this->admin->base;
$this->redirect = '/' . ltrim($this->redirect, '/');
$multilang = $this->isMultilang();
$redirect = '';
if ($multilang) {
// if base path does not already contain the lang code, add it
$langPrefix = '/' . $this->grav['session']->admin_lang;
if (!Utils::startsWith($base, $langPrefix . '/')) {
$base = $langPrefix . $base;
}
// now the first 4 chars of base contain the lang code.
// if redirect path already contains the lang code, and is != than the base lang code, then use redirect path as-is
if (Utils::pathPrefixedByLangCode($base) && Utils::pathPrefixedByLangCode($this->redirect)
&& substr($base,
0, 4) != substr($this->redirect, 0, 4)
) {
$redirect = $this->redirect;
} else {
if (!Utils::startsWith($this->redirect, $base)) {
$this->redirect = $base . $this->redirect;
}
}
} else {
if (!Utils::startsWith($this->redirect, $base)) {
$this->redirect = $base . $this->redirect;
}
}
if (!$redirect) {
$redirect = $this->redirect;
}
$this->grav->redirect($redirect, $this->redirectCode);
}
/**
* Prepare and return POST data.
*
* @param array $post
*
* @return array
*/
protected function getPost($post)
{
unset($post['task']);
// Decode JSON encoded fields and merge them to data.
if (isset($post['_json'])) {
$post = array_replace_recursive($post, $this->jsonDecode($post['_json']));
unset($post['_json']);
}
$post = $this->cleanDataKeys($post);
return $post;
}
/**
* Recursively JSON decode data.
*
* @param array $data
*
* @return array
*/
protected function jsonDecode(array $data)
{
foreach ($data as &$value) {
if (is_array($value)) {
$value = $this->jsonDecode($value);
} else {
$value = json_decode($value, true);
}
}
return $data;
}
protected function cleanDataKeys($source = [])
{
$out = [];
if (is_array($source)) {
foreach ($source as $key => $value) {
$key = str_replace('%5B', '[', str_replace('%5D', ']', $key));
if (is_array($value)) {
$out[$key] = $this->cleanDataKeys($value);
} else {
$out[$key] = $value;
}
}
}
return $out;
}
/**
* Return true if multilang is active
*
* @return bool True if multilang is active
*/
protected function isMultilang()
{
return count($this->grav['config']->get('system.languages.supported', [])) > 1;
}
/**
* @param \Grav\Common\Page\Page|\Grav\Common\Data\Data $obj
*
* @return \Grav\Common\Page\Page|\Grav\Common\Data\Data
*/
protected function storeFiles($obj)
{
// Process previously uploaded files for the current URI
// and finally store them. Everything else will get discarded
$queue = $this->admin->session()->getFlashObject('files-upload');
$queue = $queue[base64_encode($this->grav['uri']->url())];
if (is_array($queue)) {
foreach ($queue as $key => $files) {
foreach ($files as $destination => $file) {
if (!rename($file['tmp_name'], $destination)) {
throw new \RuntimeException(sprintf($this->admin->translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_MOVE', null),
'"' . $file['tmp_name'] . '"', $destination));
}
unset($files[$destination]['tmp_name']);
}
if ($this->view == 'pages') {
$keys = explode('.', preg_replace('/^header./', '', $key));
$init_key = array_shift($keys);
if (count($keys) > 0) {
$new_data = isset($obj->header()->$init_key) ? $obj->header()->$init_key : [];
Utils::setDotNotation($new_data, implode('.', $keys), $files, true);
} else {
$new_data = $files;
}
if (isset($data['header'][$init_key])) {
$obj->modifyHeader($init_key, array_replace_recursive([], $data['header'][$init_key], $new_data));
} else {
$obj->modifyHeader($init_key, $new_data);
}
} else {
// TODO: [this is JS handled] if it's single file, remove existing and use set, if it's multiple, use join
$obj->join($key, $files); // stores
}
}
}
return $obj;
}
/**
* Used by the filepicker field to get a list of files in a folder.
*/
protected function taskGetFilesInFolder()
{
if (!$this->authorizeTask('save', $this->dataPermissions())) {
return false;
}
$data = $this->view == 'pages' ? $this->admin->page(true) : $this->prepareData([]);
$settings = $data->blueprints()->schema()->getProperty($this->post['name']);
if (isset($settings['folder'])) {
$folder = $settings['folder'];
} else {
$folder = '@self';
}
// Do not use self@ outside of pages
if ($this->view != 'pages' && in_array($folder, ['@self', 'self@'])) {
$this->admin->json_response = [
'status' => 'error',
'message' => sprintf($this->admin->translate('PLUGIN_ADMIN.FILEUPLOAD_PREVENT_SELF', null), $folder)
];
return false;
}
// Set destination
$folder = Folder::getRelativePath(rtrim($folder, '/'));
$folder = $this->admin->getPagePathFromToken($folder);
$media = new Media($folder);
$available_files = [];
foreach ($media->all() as $name => $medium) {
$available_files[] = $name;
}
// Peak in the flashObject for optimistic filepicker updates
$pending_files = [];
$sessionField = base64_encode($this->grav['uri']->url());
$flash = $this->admin->session()->getFlashObject('files-upload');
if ($flash && isset($flash[$sessionField])) {
foreach ($flash[$sessionField] as $field => $data) {
foreach ($data as $file) {
if (dirname($file['path']) === $folder) {
$pending_files[] = $file['name'];
}
}
}
}
$this->admin->session()->setFlashObject('files-upload', $flash);
// Handle Accepted file types
// Accept can only be file extensions (.pdf|.jpg)
if (isset($settings['accept'])) {
$available_files = array_filter($available_files, function ($file) use ($settings) {
return $this->filterAcceptedFiles($file, $settings);
});
$pending_files = array_filter($pending_files, function ($file) use ($settings) {
return $this->filterAcceptedFiles($file, $settings);
});
}
$this->admin->json_response = [
'status' => 'success',
'files' => array_values($available_files),
'pending' => array_values($pending_files),
'folder' => $folder
];
return true;
}
protected function filterAcceptedFiles($file, $settings)
{
$valid = false;
foreach ((array)$settings['accept'] as $type) {
$find = str_replace('*', '.*', $type);
$valid |= preg_match('#' . $find . '$#', $file);
}
return $valid;
}
/**
* Handle deleting a file from a blueprint
*
* @return bool True if the action was performed.
*/
protected function taskRemoveFileFromBlueprint()
{
$uri = $this->grav['uri'];
$blueprint = base64_decode($uri->param('blueprint'));
$path = base64_decode($uri->param('path'));
$proute = base64_decode($uri->param('proute'));
$type = $uri->param('type');
$field = $uri->param('field');
$event = $this->grav->fireEvent('onAdminCanSave', new Event(['controller' => &$this]));
if (!$event['can_save']) {
return false;
}
$this->taskRemoveMedia();
if ($type == 'pages') {
$page = $this->admin->page(true, $proute);
$keys = explode('.', preg_replace('/^header./', '', $field));
$header = (array)$page->header();
$data_path = implode('.', $keys);
$data = Utils::getDotNotation($header, $data_path);
if (isset($data[$path])) {
unset($data[$path]);
Utils::setDotNotation($header, $data_path, $data);
$page->header($header);
}
$page->save();
} else {
$blueprint_prefix = $type == 'config' ? '' : $type . '.';
$blueprint_name = str_replace('/blueprints', '', str_replace('config/', '', $blueprint));
$blueprint_field = $blueprint_prefix . $blueprint_name . '.' . $field;
$files = $this->grav['config']->get($blueprint_field);
if ($files) {
foreach ($files as $key => $value) {
if ($key == $path) {
unset($files[$key]);
}
}
}
$this->grav['config']->set($blueprint_field, $files);
switch ($type) {
case 'config':
$data = $this->grav['config']->get($blueprint_name);
$config = $this->admin->data($blueprint, $data);
$config->save();
break;
case 'themes':
Theme::saveConfig($blueprint_name);
break;
case 'plugins':
Plugin::saveConfig($blueprint_name);
break;
}
}
$this->admin->json_response = [
'status' => 'success',
'message' => $this->admin->translate('PLUGIN_ADMIN.REMOVE_SUCCESSFUL')
];
return true;
}
/**
* Handles removing a media file
*
* @return bool True if the action was performed
*/
public function taskRemoveMedia()
{
if (!$this->canEditMedia()) {
return false;
}
$filename = base64_decode($this->grav['uri']->param('route'));
$file = File::instance($filename);
$resultRemoveMedia = false;
$resultRemoveMediaMeta = true;
if ($file->exists()) {
$resultRemoveMedia = $file->delete();
$metaFilePath = $filename . '.meta.yaml';
$metaFilePath = str_replace('@3x', '', $metaFilePath);
$metaFilePath = str_replace('@2x', '', $metaFilePath);
if (is_file($metaFilePath)) {
$metaFile = File::instance($metaFilePath);
$resultRemoveMediaMeta = $metaFile->delete();
}
}
if ($resultRemoveMedia && $resultRemoveMediaMeta) {
$this->admin->json_response = [
'status' => 'success',
'message' => $this->admin->translate('PLUGIN_ADMIN.REMOVE_SUCCESSFUL')
];
return true;
} else {
$this->admin->json_response = [
'status' => 'success',
'message' => $this->admin->translate('PLUGIN_ADMIN.REMOVE_FAILED')
];
return false;
}
}
/**
* Determine if the user can edit media
*
* @param string $type
*
* @return bool True if the media action is allowed
*/
protected function canEditMedia($type = 'media')
{
if (!$this->authorizeTask('edit media', ['admin.' . $type, 'admin.super'])) {
return false;
}
return true;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,9 @@
<?php <?php
namespace Grav\Plugin; namespace Grav\Plugin\Admin;
use Grav\Common\Config\Config; use Grav\Common\Config\Config;
use Grav\Common\Grav; use Grav\Common\Grav;
use Grav\Common\Page\Page; use Grav\Common\Page\Page;
use Grav\Common\Data;
/** /**
* Class Popularity * Class Popularity

View File

@@ -1,5 +1,5 @@
<?php <?php
namespace Grav\Plugin; namespace Grav\Plugin\Admin;
/** /**
* Admin theme object * Admin theme object

View File

@@ -14,6 +14,8 @@ class Utils
/** /**
* Matches an email to a user * Matches an email to a user
* *
* @param $email
*
* @return User * @return User
*/ */
public static function findUserByEmail($email) public static function findUserByEmail($email)

111
composer.lock generated
View File

@@ -166,16 +166,16 @@
"packages-dev": [ "packages-dev": [
{ {
"name": "behat/gherkin", "name": "behat/gherkin",
"version": "v4.4.4", "version": "v4.4.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Behat/Gherkin.git", "url": "https://github.com/Behat/Gherkin.git",
"reference": "cf8cc94647101e02a33d690245896d83d880aea1" "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Behat/Gherkin/zipball/cf8cc94647101e02a33d690245896d83d880aea1", "url": "https://api.github.com/repos/Behat/Gherkin/zipball/5c14cff4f955b17d20d088dec1bde61c0539ec74",
"reference": "cf8cc94647101e02a33d690245896d83d880aea1", "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -221,20 +221,20 @@
"gherkin", "gherkin",
"parser" "parser"
], ],
"time": "2016-09-18 12:16:14" "time": "2016-10-30 11:50:56"
}, },
{ {
"name": "codeception/codeception", "name": "codeception/codeception",
"version": "2.2.5", "version": "2.2.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Codeception/Codeception.git", "url": "https://github.com/Codeception/Codeception.git",
"reference": "b4729341e469d0f174f3cade85718ff5bf8dd751" "reference": "5fbe312c8138e71458ec1e715b0ce262331ca5a2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Codeception/Codeception/zipball/b4729341e469d0f174f3cade85718ff5bf8dd751", "url": "https://api.github.com/repos/Codeception/Codeception/zipball/5fbe312c8138e71458ec1e715b0ce262331ca5a2",
"reference": "b4729341e469d0f174f3cade85718ff5bf8dd751", "reference": "5fbe312c8138e71458ec1e715b0ce262331ca5a2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -312,7 +312,7 @@
"functional testing", "functional testing",
"unit testing" "unit testing"
], ],
"time": "2016-09-29 01:29:59" "time": "2016-10-27 00:00:34"
}, },
{ {
"name": "doctrine/instantiator", "name": "doctrine/instantiator",
@@ -370,24 +370,25 @@
}, },
{ {
"name": "facebook/webdriver", "name": "facebook/webdriver",
"version": "1.1.3", "version": "1.2.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/facebook/php-webdriver.git", "url": "https://github.com/facebook/php-webdriver.git",
"reference": "b7186fb1bcfda956d237f59face250d06ef47253" "reference": "af21de3ae5306a8ca0bcc02a19735dadc43e83f3"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/facebook/php-webdriver/zipball/b7186fb1bcfda956d237f59face250d06ef47253", "url": "https://api.github.com/repos/facebook/php-webdriver/zipball/af21de3ae5306a8ca0bcc02a19735dadc43e83f3",
"reference": "b7186fb1bcfda956d237f59face250d06ef47253", "reference": "af21de3ae5306a8ca0bcc02a19735dadc43e83f3",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-curl": "*", "ext-curl": "*",
"php": ">=5.3.19" "php": "^5.5 || ~7.0"
}, },
"require-dev": { "require-dev": {
"friendsofphp/php-cs-fixer": "^1.11", "friendsofphp/php-cs-fixer": "^1.11",
"php-mock/php-mock-phpunit": "^1.1",
"phpunit/phpunit": "4.6.* || ~5.0", "phpunit/phpunit": "4.6.* || ~5.0",
"squizlabs/php_codesniffer": "^2.6" "squizlabs/php_codesniffer": "^2.6"
}, },
@@ -412,7 +413,7 @@
"selenium", "selenium",
"webdriver" "webdriver"
], ],
"time": "2016-08-10 00:44:08" "time": "2016-10-14 15:16:51"
}, },
{ {
"name": "fzaninotto/faker", "name": "fzaninotto/faker",
@@ -635,16 +636,16 @@
}, },
{ {
"name": "myclabs/deep-copy", "name": "myclabs/deep-copy",
"version": "1.5.4", "version": "1.5.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/myclabs/DeepCopy.git", "url": "https://github.com/myclabs/DeepCopy.git",
"reference": "ea74994a3dc7f8d2f65a06009348f2d63c81e61f" "reference": "399c1f9781e222f6eb6cc238796f5200d1b7f108"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/ea74994a3dc7f8d2f65a06009348f2d63c81e61f", "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/399c1f9781e222f6eb6cc238796f5200d1b7f108",
"reference": "ea74994a3dc7f8d2f65a06009348f2d63c81e61f", "reference": "399c1f9781e222f6eb6cc238796f5200d1b7f108",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -673,7 +674,7 @@
"object", "object",
"object graph" "object graph"
], ],
"time": "2016-09-16 13:37:59" "time": "2016-10-31 17:19:45"
}, },
{ {
"name": "phpdocumentor/reflection-common", "name": "phpdocumentor/reflection-common",
@@ -885,16 +886,16 @@
}, },
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
"version": "4.0.1", "version": "4.0.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "5f3f7e736d6319d5f1fc402aff8b026da26709a3" "reference": "6cba06ff75a1a63a71033e1a01b89056f3af1e8d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/5f3f7e736d6319d5f1fc402aff8b026da26709a3", "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6cba06ff75a1a63a71033e1a01b89056f3af1e8d",
"reference": "5f3f7e736d6319d5f1fc402aff8b026da26709a3", "reference": "6cba06ff75a1a63a71033e1a01b89056f3af1e8d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -944,7 +945,7 @@
"testing", "testing",
"xunit" "xunit"
], ],
"time": "2016-07-26 14:39:29" "time": "2016-11-01 05:06:24"
}, },
{ {
"name": "phpunit/php-file-iterator", "name": "phpunit/php-file-iterator",
@@ -1129,16 +1130,16 @@
}, },
{ {
"name": "phpunit/phpunit", "name": "phpunit/phpunit",
"version": "5.6.1", "version": "5.6.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git", "url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "60c32c5b5e79c2248001efa2560f831da11cc2d7" "reference": "cd13b23ac5a519a4708e00736c26ee0bb28b2e01"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/60c32c5b5e79c2248001efa2560f831da11cc2d7", "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/cd13b23ac5a519a4708e00736c26ee0bb28b2e01",
"reference": "60c32c5b5e79c2248001efa2560f831da11cc2d7", "reference": "cd13b23ac5a519a4708e00736c26ee0bb28b2e01",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -1207,7 +1208,7 @@
"testing", "testing",
"xunit" "xunit"
], ],
"time": "2016-10-07 13:03:26" "time": "2016-10-25 07:40:25"
}, },
{ {
"name": "phpunit/phpunit-mock-objects", "name": "phpunit/phpunit-mock-objects",
@@ -1880,7 +1881,7 @@
}, },
{ {
"name": "symfony/browser-kit", "name": "symfony/browser-kit",
"version": "v3.1.5", "version": "v3.1.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/browser-kit.git", "url": "https://github.com/symfony/browser-kit.git",
@@ -1937,16 +1938,16 @@
}, },
{ {
"name": "symfony/console", "name": "symfony/console",
"version": "v3.1.5", "version": "v3.1.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/console.git", "url": "https://github.com/symfony/console.git",
"reference": "6cb0872fb57b38b3b09ff213c21ed693956b9eb0" "reference": "c99da1119ae61e15de0e4829196b9fba6f73d065"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/6cb0872fb57b38b3b09ff213c21ed693956b9eb0", "url": "https://api.github.com/repos/symfony/console/zipball/c99da1119ae61e15de0e4829196b9fba6f73d065",
"reference": "6cb0872fb57b38b3b09ff213c21ed693956b9eb0", "reference": "c99da1119ae61e15de0e4829196b9fba6f73d065",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -1994,11 +1995,11 @@
], ],
"description": "Symfony Console Component", "description": "Symfony Console Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2016-09-28 00:11:12" "time": "2016-10-06 01:44:51"
}, },
{ {
"name": "symfony/css-selector", "name": "symfony/css-selector",
"version": "v3.1.5", "version": "v3.1.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/css-selector.git", "url": "https://github.com/symfony/css-selector.git",
@@ -2051,7 +2052,7 @@
}, },
{ {
"name": "symfony/debug", "name": "symfony/debug",
"version": "v3.1.5", "version": "v3.1.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/debug.git", "url": "https://github.com/symfony/debug.git",
@@ -2108,16 +2109,16 @@
}, },
{ {
"name": "symfony/dom-crawler", "name": "symfony/dom-crawler",
"version": "v3.1.5", "version": "v3.1.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/dom-crawler.git", "url": "https://github.com/symfony/dom-crawler.git",
"reference": "bb7395e8b1db3654de82b9f35d019958276de4d7" "reference": "59eee3c76eb89f21857798620ebdad7a05ad14f4"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/dom-crawler/zipball/bb7395e8b1db3654de82b9f35d019958276de4d7", "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/59eee3c76eb89f21857798620ebdad7a05ad14f4",
"reference": "bb7395e8b1db3654de82b9f35d019958276de4d7", "reference": "59eee3c76eb89f21857798620ebdad7a05ad14f4",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -2160,20 +2161,20 @@
], ],
"description": "Symfony DomCrawler Component", "description": "Symfony DomCrawler Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2016-08-05 08:37:39" "time": "2016-10-18 15:46:07"
}, },
{ {
"name": "symfony/event-dispatcher", "name": "symfony/event-dispatcher",
"version": "v3.1.5", "version": "v3.1.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/event-dispatcher.git", "url": "https://github.com/symfony/event-dispatcher.git",
"reference": "c0c00c80b3a69132c4e55c3e7db32b4a387615e5" "reference": "28b0832b2553ffb80cabef6a7a812ff1e670c0bc"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/c0c00c80b3a69132c4e55c3e7db32b4a387615e5", "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/28b0832b2553ffb80cabef6a7a812ff1e670c0bc",
"reference": "c0c00c80b3a69132c4e55c3e7db32b4a387615e5", "reference": "28b0832b2553ffb80cabef6a7a812ff1e670c0bc",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -2220,11 +2221,11 @@
], ],
"description": "Symfony EventDispatcher Component", "description": "Symfony EventDispatcher Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2016-07-19 10:45:57" "time": "2016-10-13 06:28:43"
}, },
{ {
"name": "symfony/finder", "name": "symfony/finder",
"version": "v3.1.5", "version": "v3.1.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/finder.git", "url": "https://github.com/symfony/finder.git",
@@ -2332,16 +2333,16 @@
}, },
{ {
"name": "symfony/yaml", "name": "symfony/yaml",
"version": "v3.1.5", "version": "v3.1.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/yaml.git", "url": "https://github.com/symfony/yaml.git",
"reference": "368b9738d4033c8b93454cb0dbd45d305135a6d3" "reference": "7ff51b06c6c3d5cc6686df69004a42c69df09e27"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/368b9738d4033c8b93454cb0dbd45d305135a6d3", "url": "https://api.github.com/repos/symfony/yaml/zipball/7ff51b06c6c3d5cc6686df69004a42c69df09e27",
"reference": "368b9738d4033c8b93454cb0dbd45d305135a6d3", "reference": "7ff51b06c6c3d5cc6686df69004a42c69df09e27",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -2377,7 +2378,7 @@
], ],
"description": "Symfony Yaml Component", "description": "Symfony Yaml Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2016-09-25 08:27:07" "time": "2016-10-24 18:41:13"
}, },
{ {
"name": "webmozart/assert", "name": "webmozart/assert",

View File

@@ -108,7 +108,10 @@ export default class FilesField {
dropzone.files.push(mock); dropzone.files.push(mock);
dropzone.options.addedfile.call(dropzone, mock); dropzone.options.addedfile.call(dropzone, mock);
if (mock.type.match(/^image\//)) dropzone.options.thumbnail.call(dropzone, mock, data.path); if (mock.type.match(/^image\//)) {
dropzone.options.thumbnail.call(dropzone, mock, data.path);
dropzone.createThumbnailFromUrl(mock, data.path);
}
file.remove(); file.remove();
}); });

File diff suppressed because one or more lines are too long

View File

@@ -30,7 +30,7 @@
{% set files = global.files %} {% set files = global.files %}
{% set config = global.grav.config %} {% set config = global.grav.config %}
{% set route = global.context.route() %} {% set route = global.context.route() %}
{% set type = get_class(global.context) == 'Grav\\Common\\Page\\Page' ? 'pages' : global.plugin ? 'plugins' : global.theme ? 'themes' : 'config' %} {% set type = global.blueprint_type ? global.blueprint_type : (get_class(global.context) == 'Grav\\Common\\Page\\Page' ? 'pages' : global.plugin ? 'plugins' : global.theme ? 'themes' : 'config') %}
{% set blueprint_name = global.blueprints.getFilename %} {% set blueprint_name = global.blueprints.getFilename %}
{% if type == 'pages' %} {% if type == 'pages' %}
@@ -38,8 +38,9 @@
{% endif %} {% endif %}
{% set blueprint = base64_encode(blueprint_name) %} {% set blueprint = base64_encode(blueprint_name) %}
{% set real_path = global.admin.getPagePathFromToken(path) %} {% set real_path = global.admin.getPagePathFromToken(path) %}
{% set remove = uri.addNonce(global.base_url_relative ~ {% set remove = global.file_url_remove ? global.file_url_remove : (global.base_url_relative ~ '/media.json') %}
'/media.json' ~ {% set remove = uri.addNonce(
remove ~
'/route' ~ config.system.param_sep ~ base64_encode(global.base_path ~ '/' ~ real_path) ~ '/route' ~ config.system.param_sep ~ base64_encode(global.base_path ~ '/' ~ real_path) ~
'/task' ~ config.system.param_sep ~ 'removeFileFromBlueprint' ~ '/task' ~ config.system.param_sep ~ 'removeFileFromBlueprint' ~
'/proute' ~ config.system.param_sep ~ base64_encode(route) ~ '/proute' ~ config.system.param_sep ~ base64_encode(route) ~
@@ -59,7 +60,7 @@
{% set settings = {name: field.name, paramName: (scope ~ field.name)|fieldName ~ (files.multiple ? '[]' : ''), limit: limit, filesize: files.filesize, accept: files.accept} %} {% set settings = {name: field.name, paramName: (scope ~ field.name)|fieldName ~ (files.multiple ? '[]' : ''), limit: limit, filesize: files.filesize, accept: files.accept} %}
<div class="form-input-wrapper dropzone files-upload {% if field.fancy is not same as(false) %}form-input-file{% endif %} {{ field.size|default('xlarge') }}" data-grav-file-settings="{{ settings|json_encode|e('html_attr') }}"> <div class="form-input-wrapper dropzone files-upload {% if field.fancy is not same as(false) %}form-input-file{% endif %} {{ field.size|default('xlarge') }}" data-grav-file-settings="{{ settings|json_encode|e('html_attr') }}" {% if file_url_add %}data-file-url-add="{{ file_url_add }}"{% endif %} {% if file_url_add %}data-file-url-remove="{{ file_url_add }}"{% endif %}>
<input <input
{# required attribute structures #} {# required attribute structures #}
{% block input_attributes %} {% block input_attributes %}

View File

@@ -1,5 +1,5 @@
<?php <?php
namespace Grav\Plugin; namespace Grav\Plugin\Admin;
use Grav\Common\Grav; use Grav\Common\Grav;
use Grav\Common\Language\Language; use Grav\Common\Language\Language;

View File

@@ -53,8 +53,8 @@ class ClassLoader
private $useIncludePath = false; private $useIncludePath = false;
private $classMap = array(); private $classMap = array();
private $classMapAuthoritative = false; private $classMapAuthoritative = false;
private $missingClasses = array();
public function getPrefixes() public function getPrefixes()
{ {
@@ -322,20 +322,20 @@ class ClassLoader
if (isset($this->classMap[$class])) { if (isset($this->classMap[$class])) {
return $this->classMap[$class]; return $this->classMap[$class];
} }
if ($this->classMapAuthoritative) { if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false; return false;
} }
$file = $this->findFileWithExtension($class, '.php'); $file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM // Search for Hack files if we are running on HHVM
if ($file === null && defined('HHVM_VERSION')) { if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh'); $file = $this->findFileWithExtension($class, '.hh');
} }
if ($file === null) { if (false === $file) {
// Remember that this class does not exist. // Remember that this class does not exist.
return $this->classMap[$class] = false; $this->missingClasses[$class] = true;
} }
return $file; return $file;
@@ -399,6 +399,8 @@ class ClassLoader
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file; return $file;
} }
return false;
} }
} }