This commit is contained in:
2021-09-16 14:44:40 +02:00
parent 8bd1b83c5f
commit 4ca5c9f82d
55 changed files with 3279 additions and 482 deletions

View File

@@ -47,7 +47,7 @@ use function is_callable;
* @package Grav\Framework\Flex
* @template T
*/
class FlexDirectory implements FlexDirectoryInterface, FlexAuthorizeInterface
class FlexDirectory implements FlexDirectoryInterface
{
use FlexAuthorizeTrait;
@@ -235,7 +235,17 @@ class FlexDirectory implements FlexDirectoryInterface, FlexAuthorizeInterface
/** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
$filename = $locator->findResource($this->getDirectoryConfigUri($name), true);
$uri = $this->getDirectoryConfigUri($name);
// If configuration is found in main configuration, use it.
if (str_starts_with($uri, 'config://')) {
$path = str_replace('/', '.', substr($uri, 9, -5));
return (array)$grav['config']->get($path);
}
// Load the configuration file.
$filename = $locator->findResource($uri, true);
if ($filename === false) {
return [];
}
@@ -821,20 +831,46 @@ class FlexDirectory implements FlexDirectoryInterface, FlexAuthorizeInterface
* @param array $call
* @return void
*/
protected function dynamicFlexField(array &$field, $property, array $call)
protected function dynamicFlexField(array &$field, $property, array $call): void
{
$params = (array)$call['params'];
$object = $call['object'] ?? null;
$method = array_shift($params);
$not = false;
if (str_starts_with($method, '!')) {
$method = substr($method, 1);
$not = true;
} elseif (str_starts_with($method, 'not ')) {
$method = substr($method, 4);
$not = true;
}
$method = trim($method);
if ($object && method_exists($object, $method)) {
$value = $object->{$method}(...$params);
if (is_array($value) && isset($field[$property]) && is_array($field[$property])) {
$field[$property] = array_merge_recursive($field[$property], $value);
$value = $this->mergeArrays($field[$property], $value);
}
$field[$property] = $not ? !$value : $value;
}
}
/**
* @param array $array1
* @param array $array2
* @return array
*/
protected function mergeArrays(array $array1, array $array2): array
{
foreach ($array2 as $key => $value) {
if (is_array($value) && isset($array1[$key]) && is_array($array1[$key])) {
$array1[$key] = $this->mergeArrays($array1[$key], $value);
} else {
$field[$property] = $value;
$array1[$key] = $value;
}
}
return $array1;
}
/**

View File

@@ -318,11 +318,11 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable
}
/**
* @param string $field
* @param string $filename
* @param string|null $field
* @param string|null $filename
* @return Route|null
*/
public function getFileDeleteAjaxRoute($field, $filename): ?Route
public function getFileDeleteAjaxRoute($field = null, $filename = null): ?Route
{
return null;
}
@@ -453,7 +453,9 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable
protected function doSerialize(): array
{
return $this->doTraitSerialize() + [
'form' => $this->form,
'directory' => $this->directory,
'flexName' => $this->flexName
];
}
@@ -465,7 +467,9 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable
{
$this->doTraitUnserialize($data);
$this->form = $data['form'];
$this->directory = $data['directory'];
$this->flexName = $data['flexName'];
}
/**

View File

@@ -103,7 +103,14 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
{
$this->name = $name;
$this->setObject($object);
$this->setName($object->getFlexType(), $name);
if (isset($options['form']['name'])) {
// Use custom form name.
$this->flexName = $options['form']['name'];
} else {
// Use standard form name.
$this->setName($object->getFlexType(), $name);
}
$this->setId($this->getName());
$uniqueId = $options['unique_id'] ?? null;
@@ -371,22 +378,28 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
{
$object = $this->getObject();
if (!method_exists($object, 'route')) {
return null;
/** @var Route $route */
$route = Grav::instance()['route'];
return $route->withExtension('json')->withGravParam('task', 'media.upload');
}
return $object->route('/edit.json/task:media.upload');
}
/**
* @param string $field
* @param string $filename
* @param string|null $field
* @param string|null $filename
* @return Route|null
*/
public function getFileDeleteAjaxRoute($field, $filename): ?Route
public function getFileDeleteAjaxRoute($field = null, $filename = null): ?Route
{
$object = $this->getObject();
if (!method_exists($object, 'route')) {
return null;
/** @var Route $route */
$route = Grav::instance()['route'];
return $route->withExtension('json')->withGravParam('task', 'media.delete');
}
return $object->route('/edit.json/task:media.delete');
@@ -536,7 +549,11 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
protected function doSerialize(): array
{
return $this->doTraitSerialize() + [
'items' => $this->items,
'form' => $this->form,
'object' => $this->object,
'flexName' => $this->flexName,
'submitMethod' => $this->submitMethod,
];
}
@@ -548,7 +565,11 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
{
$this->doTraitUnserialize($data);
$this->object = $data['object'];
$this->items = $data['items'] ?? null;
$this->form = $data['form'] ?? null;
$this->object = $data['object'] ?? null;
$this->flexName = $data['flexName'] ?? null;
$this->submitMethod = $data['submitMethod'] ?? null;
}
/**

View File

@@ -44,6 +44,7 @@ use function is_array;
use function is_object;
use function is_scalar;
use function is_string;
use function json_encode;
/**
* Class FlexObject
@@ -70,6 +71,8 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
/** @var array */
private $_meta;
/** @var array */
protected $_original;
/** @var array */
protected $_changes;
/** @var string */
protected $storage_key;
@@ -369,7 +372,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
*/
public function searchProperty(string $property, string $search, array $options = null): float
{
$options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []);
$options = $options ?? (array)$this->getFlexDirectory()->getConfig('data.search.options');
$value = $this->getProperty($property);
return $this->searchValue($property, $value, $search, $options);
@@ -383,7 +386,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
*/
public function searchNestedProperty(string $property, string $search, array $options = null): float
{
$options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []);
$options = $options ?? (array)$this->getFlexDirectory()->getConfig('data.search.options');
if ($property === 'key') {
$value = $this->getKey();
} else {
@@ -440,6 +443,16 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
return 0;
}
/**
* Get original data before update
*
* @return array
*/
public function getOriginalData(): array
{
return $this->_original ?? [];
}
/**
* Get any changes based on data sent to update
*
@@ -653,7 +666,8 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
}
// Store the changes
$this->_changes = Utils::arrayDiffMultidimensional($this->getElements(), $elements);
$this->_original = $this->getElements();
$this->_changes = Utils::arrayDiffMultidimensional($this->_original, $elements);
}
if ($files && method_exists($this, 'setUpdatedMedia')) {
@@ -691,6 +705,17 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
return $this->create($key);
}
/**
* @param UserInterface|null $user
*/
public function check(UserInterface $user = null): void
{
// If user has been provided, check if the user has permissions to save this object.
if ($user && !$this->isAuthorized('save', null, $user)) {
throw new \RuntimeException('Forbidden', 403);
}
}
/**
* {@inheritdoc}
* @see FlexObjectInterface::save()
@@ -809,11 +834,12 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
*/
public function getForm(string $name = '', array $options = null)
{
if (!isset($this->_forms[$name])) {
$this->_forms[$name] = $this->createFormObject($name, $options);
$hash = $name . '-' . md5(json_encode($options, JSON_THROW_ON_ERROR));
if (!isset($this->_forms[$hash])) {
$this->_forms[$hash] = $this->createFormObject($name, $options);
}
return $this->_forms[$name];
return $this->_forms[$hash];
}
/**
@@ -1063,6 +1089,17 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
return $action;
}
/**
* Method to reset blueprints if the type changes.
*
* @return void
* @since 1.7.18
*/
protected function resetBlueprints(): void
{
$this->_blueprint = [];
}
// DEPRECATED METHODS
/**

View File

@@ -17,7 +17,7 @@ use Grav\Framework\Cache\CacheInterface;
* Interface FlexDirectoryInterface
* @package Grav\Framework\Flex\Interfaces
*/
interface FlexDirectoryInterface
interface FlexDirectoryInterface extends FlexAuthorizeInterface
{
/**
* @return bool

View File

@@ -38,8 +38,8 @@ interface FlexFormInterface extends Serializable, FormInterface
/**
* Get route for deleting files by AJAX.
*
* @param string $field Field where the file is associated into.
* @param string $filename Filename for the file.
* @param string|null $field Field where the file is associated into.
* @param string|null $filename Filename for the file.
* @return Route|null Returns Route object or null if file uploads are not enabled.
*/
public function getFileDeleteAjaxRoute($field, $filename);

View File

@@ -40,6 +40,8 @@ class FolderStorage extends AbstractFilesystemStorage
protected $dataFolder;
/** @var string Pattern to access an object. */
protected $dataPattern = '{FOLDER}/{KEY}/{FILE}{EXT}';
/** @var string[] */
protected $variables = ['FOLDER' => '%1$s', 'KEY' => '%2$s', 'KEY:2' => '%3$s', 'FILE' => '%4$s', 'EXT' => '%5$s'];
/** @var string Filename for the object. */
protected $dataFile;
/** @var string File extension for the object. */
@@ -380,6 +382,12 @@ class FolderStorage extends AbstractFilesystemStorage
if (isset($data[0])) {
throw new RuntimeException('Broken object file');
}
// Add key field to the object.
$keyField = $this->keyField;
if ($keyField !== 'storage_key' && !isset($data[$keyField])) {
$data[$keyField] = $key;
}
} catch (RuntimeException $e) {
$data = ['__ERROR' => $e->getMessage()];
} finally {
@@ -692,9 +700,7 @@ class FolderStorage extends AbstractFilesystemStorage
$this->keyLen = (int)($options['key_len'] ?? 32);
$this->caseSensitive = (bool)($options['case_sensitive'] ?? true);
$variables = ['FOLDER' => '%1$s', 'KEY' => '%2$s', 'KEY:2' => '%3$s', 'FILE' => '%4$s', 'EXT' => '%5$s'];
$pattern = Utils::simpleTemplate($pattern, $variables);
$pattern = Utils::simpleTemplate($pattern, $this->variables);
if (!$pattern) {
throw new RuntimeException('Bad storage folder pattern');
}

View File

@@ -455,7 +455,7 @@ class SimpleStorage extends AbstractFilesystemStorage
$content = (array) $file->content();
if ($this->prefix) {
$data = new Data($content);
$content = $data->get($this->prefix);
$content = $data->get($this->prefix, []);
}
$file->free();

View File

@@ -120,7 +120,7 @@ trait FlexMediaTrait
// Load settings for the field.
$schema = $this->getBlueprint()->schema();
$settings = $field && is_object($schema) ? (array)$schema->getProperty($field) : null;
if (!isset($settings) || !is_array($settings)) {
if (!is_array($settings)) {
return null;
}

View File

@@ -120,7 +120,7 @@ class FormFlash implements FormFlashInterface
protected function loadStoredForm(): ?array
{
$file = $this->getTmpIndex();
$exists = $file->exists();
$exists = $file && $file->exists();
$data = null;
if ($exists) {
@@ -246,8 +246,10 @@ class FormFlash implements FormFlashInterface
if ($force || $this->data || $this->files) {
// Only save if there is data or files to be saved.
$file = $this->getTmpIndex();
$file->save($this->jsonSerialize());
$this->exists = true;
if ($file) {
$file->save($this->jsonSerialize());
$this->exists = true;
}
} elseif ($this->exists) {
// Delete empty form flash if it exists (it carries no information).
return $this->delete();
@@ -476,12 +478,14 @@ class FormFlash implements FormFlashInterface
}
/**
* @return YamlFile
* @return ?YamlFile
*/
protected function getTmpIndex(): YamlFile
protected function getTmpIndex(): ?YamlFile
{
$tmpDir = $this->getTmpDir();
// Do not use CompiledYamlFile as the file can change multiple times per second.
return YamlFile::instance($this->getTmpDir() . '/index.yaml');
return $tmpDir ? YamlFile::instance($tmpDir . '/index.yaml') : null;
}
/**
@@ -503,7 +507,9 @@ class FormFlash implements FormFlashInterface
{
// Make sure that index file cache gets always cleared.
$file = $this->getTmpIndex();
$file->free();
if ($file) {
$file->free();
}
$tmpDir = $this->getTmpDir();
if ($tmpDir && file_exists($tmpDir)) {

View File

@@ -0,0 +1,107 @@
<?php declare(strict_types=1);
/**
* @package Grav\Framework\Mime
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Mime;
use function in_array;
/**
* Class to handle mime-types.
*/
class MimeTypes
{
/** @var array */
protected $extensions;
/** @var array */
protected $mimes;
/**
* Create a new mime types instance with the given mappings.
*
* @param array $mimes An associative array containing ['ext' => ['mime/type', 'mime/type2']]
*/
public static function createFromMimes(array $mimes): self
{
$extensions = [];
foreach ($mimes as $ext => $list) {
foreach ($list as $mime) {
$list = $extensions[$mime] ?? [];
if (!in_array($ext, $list, true)) {
$list[] = $ext;
$extensions[$mime] = $list;
}
}
}
return new static($extensions, $mimes);
}
/**
* @param string $extension
* @return string|null
*/
public function getMimeType(string $extension): ?string
{
$extension = $this->cleanInput($extension);
return $this->mimes[$extension][0] ?? null;
}
/**
* @param string $mime
* @return string|null
*/
public function getExtension(string $mime): ?string
{
$mime = $this->cleanInput($mime);
return $this->extensions[$mime][0] ?? null;
}
/**
* @param string $extension
* @return array
*/
public function getMimeTypes(string $extension): array
{
$extension = $this->cleanInput($extension);
return $this->mimes[$extension] ?? [];
}
/**
* @param string $mime
* @return array
*/
public function getExtensions(string $mime): array
{
$mime = $this->cleanInput($mime);
return $this->extensions[$mime] ?? [];
}
/**
* @param string $input
* @return string
*/
protected function cleanInput(string $input): string
{
return strtolower(trim($input));
}
/**
* @param array $extensions
* @param array $mimes
*/
protected function __construct(array $extensions, array $mimes)
{
$this->extensions = $extensions;
$this->mimes = $mimes;
}
}

View File

@@ -9,7 +9,6 @@
namespace Grav\Framework\Object;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Grav\Framework\Collection\ArrayCollection;
use Grav\Framework\Object\Access\NestedPropertyCollectionTrait;

View File

@@ -23,6 +23,9 @@ class UploadedFile implements UploadedFileInterface
{
use UploadedFileDecoratorTrait;
/** @var array */
private $meta = [];
/**
* @param StreamInterface|string|resource $streamOrFile
* @param int $size
@@ -34,4 +37,34 @@ class UploadedFile implements UploadedFileInterface
{
$this->uploadedFile = new \Nyholm\Psr7\UploadedFile($streamOrFile, $size, $errorStatus, $clientFilename, $clientMediaType);
}
/**
* @param array $meta
* @return $this
*/
public function setMeta(array $meta)
{
$this->meta = $meta;
return $this;
}
/**
* @param array $meta
* @return $this
*/
public function addMeta(array $meta)
{
$this->meta = array_merge($this->meta, $meta);
return $this;
}
/**
* @return array
*/
public function getMeta(): array
{
return $this->meta;
}
}

View File

@@ -338,23 +338,12 @@ class Session implements SessionInterface
{
$name = $this->getName();
if (null !== $name) {
$params = session_get_cookie_params();
$cookie_options = array (
'expires' => time() - 42000,
'path' => $params['path'],
'domain' => $params['domain'],
'secure' => $params['secure'],
'httponly' => $params['httponly'],
'samesite' => $params['samesite']
);
$this->removeCookie();
setcookie(
session_name(),
'',
$cookie_options
$this->getCookieOptions(-42000)
);
}
@@ -463,27 +452,36 @@ class Session implements SessionInterface
}
/**
* @return void
* Store something in cookie temporarily.
*
* @param int|null $lifetime
* @return array
*/
protected function setCookie(): void
public function getCookieOptions(int $lifetime = null): array
{
$params = session_get_cookie_params();
$cookie_options = array (
'expires' => time() + $params['lifetime'],
return [
'expires' => time() + ($lifetime ?? $params['lifetime']),
'path' => $params['path'],
'domain' => $params['domain'],
'secure' => $params['secure'],
'httponly' => $params['httponly'],
'samesite' => $params['samesite']
);
];
}
/**
* @return void
*/
protected function setCookie(): void
{
$this->removeCookie();
setcookie(
session_name(),
session_id(),
$cookie_options
$this->getCookieOptions()
);
}