updated core to 1.7.15
This commit is contained in:
236
system/src/Grav/Framework/Acl/Access.php
Normal file
236
system/src/Grav/Framework/Acl/Access.php
Normal file
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Acl
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Acl;
|
||||
|
||||
use ArrayIterator;
|
||||
use Countable;
|
||||
use Grav\Common\Utils;
|
||||
use IteratorAggregate;
|
||||
use JsonSerializable;
|
||||
use RuntimeException;
|
||||
use Traversable;
|
||||
use function count;
|
||||
use function is_array;
|
||||
use function is_bool;
|
||||
use function is_string;
|
||||
use function strlen;
|
||||
|
||||
/**
|
||||
* Class Access
|
||||
* @package Grav\Framework\Acl
|
||||
*/
|
||||
class Access implements JsonSerializable, IteratorAggregate, Countable
|
||||
{
|
||||
/** @var string */
|
||||
private $name;
|
||||
/** @var array */
|
||||
private $rules;
|
||||
/** @var array */
|
||||
private $ops;
|
||||
/** @var array */
|
||||
private $acl = [];
|
||||
/** @var array */
|
||||
private $inherited = [];
|
||||
|
||||
/**
|
||||
* Access constructor.
|
||||
* @param string|array|null $acl
|
||||
* @param array|null $rules
|
||||
* @param string $name
|
||||
*/
|
||||
public function __construct($acl = null, array $rules = null, string $name = '')
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->rules = $rules ?? [];
|
||||
$this->ops = ['+' => true, '-' => false];
|
||||
if (is_string($acl)) {
|
||||
$this->acl = $this->resolvePermissions($acl);
|
||||
} elseif (is_array($acl)) {
|
||||
$this->acl = $this->normalizeAcl($acl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Access $parent
|
||||
* @param string|null $name
|
||||
* @return void
|
||||
*/
|
||||
public function inherit(Access $parent, string $name = null)
|
||||
{
|
||||
// Remove cached null actions from acl.
|
||||
$acl = $this->getAllActions();
|
||||
// Get only inherited actions.
|
||||
$inherited = array_diff_key($parent->getAllActions(), $acl);
|
||||
|
||||
$this->inherited += $parent->inherited + array_fill_keys(array_keys($inherited), $name ?? $parent->getName());
|
||||
$acl = array_replace($acl, $inherited);
|
||||
if (null === $acl) {
|
||||
throw new RuntimeException('Internal error');
|
||||
}
|
||||
|
||||
$this->acl = $acl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks user authorization to the action.
|
||||
*
|
||||
* @param string $action
|
||||
* @param string|null $scope
|
||||
* @return bool|null
|
||||
*/
|
||||
public function authorize(string $action, string $scope = null): ?bool
|
||||
{
|
||||
if (null !== $scope) {
|
||||
$action = $scope !== 'test' ? "{$scope}.{$action}" : $action;
|
||||
}
|
||||
|
||||
return $this->get($action);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return Utils::arrayUnflattenDotNotation($this->acl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getAllActions(): array
|
||||
{
|
||||
return array_filter($this->acl, static function($val) { return $val !== null; });
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $action
|
||||
* @return bool|null
|
||||
*/
|
||||
public function get(string $action)
|
||||
{
|
||||
// Get access value.
|
||||
if (isset($this->acl[$action])) {
|
||||
return $this->acl[$action];
|
||||
}
|
||||
|
||||
// If no value is defined, check the parent access (all true|false).
|
||||
$pos = strrpos($action, '.');
|
||||
$value = $pos ? $this->get(substr($action, 0, $pos)) : null;
|
||||
|
||||
// Cache result for faster lookup.
|
||||
$this->acl[$action] = $value;
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $action
|
||||
* @return bool
|
||||
*/
|
||||
public function isInherited(string $action): bool
|
||||
{
|
||||
return isset($this->inherited[$action]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $action
|
||||
* @return string|null
|
||||
*/
|
||||
public function getInherited(string $action): ?string
|
||||
{
|
||||
return $this->inherited[$action] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Traversable
|
||||
*/
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
return new ArrayIterator($this->acl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->acl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $acl
|
||||
* @return array
|
||||
*/
|
||||
protected function normalizeAcl(array $acl): array
|
||||
{
|
||||
if (empty($acl)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Normalize access control list.
|
||||
$list = [];
|
||||
foreach (Utils::arrayFlattenDotNotation($acl) as $key => $value) {
|
||||
if (is_bool($value)) {
|
||||
$list[$key] = $value;
|
||||
} elseif ($value === 0 || $value === 1) {
|
||||
$list[$key] = (bool)$value;
|
||||
} elseif($value === null) {
|
||||
continue;
|
||||
} elseif ($this->rules && is_string($value)) {
|
||||
$list[$key] = $this->resolvePermissions($value);
|
||||
} elseif (Utils::isPositive($value)) {
|
||||
$list[$key] = true;
|
||||
} elseif (Utils::isNegative($value)) {
|
||||
$list[$key] = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $access
|
||||
* @return array
|
||||
*/
|
||||
protected function resolvePermissions(string $access): array
|
||||
{
|
||||
$len = strlen($access);
|
||||
$op = true;
|
||||
$list = [];
|
||||
for($count = 0; $count < $len; $count++) {
|
||||
$letter = $access[$count];
|
||||
if (isset($this->rules[$letter])) {
|
||||
$list[$this->rules[$letter]] = $op;
|
||||
$op = true;
|
||||
} elseif (isset($this->ops[$letter])) {
|
||||
$op = $this->ops[$letter];
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
203
system/src/Grav/Framework/Acl/Action.php
Normal file
203
system/src/Grav/Framework/Acl/Action.php
Normal file
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Acl
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Acl;
|
||||
|
||||
use ArrayIterator;
|
||||
use Countable;
|
||||
use Grav\Common\Inflector;
|
||||
use IteratorAggregate;
|
||||
use RuntimeException;
|
||||
use Traversable;
|
||||
use function count;
|
||||
use function strlen;
|
||||
|
||||
/**
|
||||
* Class Action
|
||||
* @package Grav\Framework\Acl
|
||||
*/
|
||||
class Action implements IteratorAggregate, Countable
|
||||
{
|
||||
/** @var string */
|
||||
public $name;
|
||||
/** @var string */
|
||||
public $type;
|
||||
/** @var bool */
|
||||
public $visible;
|
||||
/** @var string|null */
|
||||
public $label;
|
||||
/** @var array */
|
||||
public $params;
|
||||
|
||||
/** @var Action|null */
|
||||
protected $parent;
|
||||
/** @var Action[] */
|
||||
protected $children = [];
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param array $action
|
||||
*/
|
||||
public function __construct(string $name, array $action = [])
|
||||
{
|
||||
$label = $action['label'] ?? null;
|
||||
if (!$label) {
|
||||
if ($pos = strrpos($name, '.')) {
|
||||
$label = substr($name, $pos + 1);
|
||||
} else {
|
||||
$label = $name;
|
||||
}
|
||||
$label = Inflector::humanize($label, 'all');
|
||||
}
|
||||
|
||||
$this->name = $name;
|
||||
$this->type = $action['type'] ?? 'action';
|
||||
$this->visible = (bool)($action['visible'] ?? true);
|
||||
$this->label = $label;
|
||||
unset($action['type'], $action['label']);
|
||||
$this->params = $action;
|
||||
|
||||
// Include compact rules.
|
||||
if (isset($action['letters'])) {
|
||||
foreach ($action['letters'] as $letter => $data) {
|
||||
$data['letter'] = $letter;
|
||||
$childName = $this->name . '.' . $data['action'];
|
||||
unset($data['action']);
|
||||
$child = new Action($childName, $data);
|
||||
$this->addChild($child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getParams(): array
|
||||
{
|
||||
return $this->params;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function getParam(string $name)
|
||||
{
|
||||
return $this->params[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Action|null
|
||||
*/
|
||||
public function getParent(): ?Action
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Action|null $parent
|
||||
* @return void
|
||||
*/
|
||||
public function setParent(?Action $parent): void
|
||||
{
|
||||
$this->parent = $parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getScope(): string
|
||||
{
|
||||
$pos = strpos($this->name, '.');
|
||||
if ($pos) {
|
||||
return substr($this->name, 0, $pos);
|
||||
}
|
||||
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getLevels(): int
|
||||
{
|
||||
return substr_count($this->name, '.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasChildren(): bool
|
||||
{
|
||||
return !empty($this->children);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Action[]
|
||||
*/
|
||||
public function getChildren(): array
|
||||
{
|
||||
return $this->children;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return Action|null
|
||||
*/
|
||||
public function getChild(string $name): ?Action
|
||||
{
|
||||
return $this->children[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Action $child
|
||||
* @return void
|
||||
*/
|
||||
public function addChild(Action $child): void
|
||||
{
|
||||
if (strpos($child->name, "{$this->name}.") !== 0) {
|
||||
throw new RuntimeException('Bad child');
|
||||
}
|
||||
|
||||
$child->setParent($this);
|
||||
$name = substr($child->name, strlen($this->name) + 1);
|
||||
|
||||
$this->children[$name] = $child;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Traversable
|
||||
*/
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
return new ArrayIterator($this->children);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->children);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo()
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'type' => $this->type,
|
||||
'label' => $this->label,
|
||||
'params' => $this->params,
|
||||
'actions' => $this->children
|
||||
];
|
||||
}
|
||||
}
|
||||
244
system/src/Grav/Framework/Acl/Permissions.php
Normal file
244
system/src/Grav/Framework/Acl/Permissions.php
Normal file
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Acl
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Acl;
|
||||
|
||||
use ArrayIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use RuntimeException;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* Class Permissions
|
||||
* @package Grav\Framework\Acl
|
||||
*/
|
||||
class Permissions implements \ArrayAccess, \Countable, \IteratorAggregate
|
||||
{
|
||||
/** @var Action[] */
|
||||
protected $instances = [];
|
||||
/** @var Action[] */
|
||||
protected $actions = [];
|
||||
/** @var array */
|
||||
protected $nested = [];
|
||||
/** @var array */
|
||||
protected $types = [];
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getInstances(): array
|
||||
{
|
||||
$iterator = new RecursiveActionIterator($this->actions);
|
||||
$recursive = new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::SELF_FIRST);
|
||||
|
||||
return iterator_to_array($recursive);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function hasAction(string $name): bool
|
||||
{
|
||||
return isset($this->instances[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return Action|null
|
||||
*/
|
||||
public function getAction(string $name): ?Action
|
||||
{
|
||||
return $this->instances[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Action $action
|
||||
* @return void
|
||||
*/
|
||||
public function addAction(Action $action): void
|
||||
{
|
||||
$name = $action->name;
|
||||
$parent = $this->getParent($name);
|
||||
if ($parent) {
|
||||
$parent->addChild($action);
|
||||
} else {
|
||||
$this->actions[$name] = $action;
|
||||
}
|
||||
|
||||
$this->instances[$name] = $action;
|
||||
|
||||
// If Action has children, add those, too.
|
||||
foreach ($action->getChildren() as $child) {
|
||||
$this->instances[$child->name] = $child;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getActions(): array
|
||||
{
|
||||
return $this->actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Action[] $actions
|
||||
* @return void
|
||||
*/
|
||||
public function addActions(array $actions): void
|
||||
{
|
||||
foreach ($actions as $action) {
|
||||
$this->addAction($action);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function hasType(string $name): bool
|
||||
{
|
||||
return isset($this->types[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return Action|null
|
||||
*/
|
||||
public function getType(string $name): ?Action
|
||||
{
|
||||
return $this->types[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param array $type
|
||||
* @return void
|
||||
*/
|
||||
public function addType(string $name, array $type): void
|
||||
{
|
||||
$this->types[$name] = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getTypes(): array
|
||||
{
|
||||
return $this->types;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $types
|
||||
* @return void
|
||||
*/
|
||||
public function addTypes(array $types): void
|
||||
{
|
||||
$types = array_replace($this->types, $types);
|
||||
if (null === $types) {
|
||||
throw new RuntimeException('Internal error');
|
||||
}
|
||||
|
||||
$this->types = $types;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|null $access
|
||||
* @return Access
|
||||
*/
|
||||
public function getAccess(array $access = null): Access
|
||||
{
|
||||
return new Access($access ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|string $offset
|
||||
* @return bool
|
||||
*/
|
||||
public function offsetExists($offset): bool
|
||||
{
|
||||
return isset($this->nested[$offset]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|string $offset
|
||||
* @return Action|null
|
||||
*/
|
||||
public function offsetGet($offset): ?Action
|
||||
{
|
||||
return $this->nested[$offset] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|string $offset
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
public function offsetSet($offset, $value): void
|
||||
{
|
||||
throw new RuntimeException(__METHOD__ . '(): Not Supported');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|string $offset
|
||||
* @return void
|
||||
*/
|
||||
public function offsetUnset($offset): void
|
||||
{
|
||||
throw new RuntimeException(__METHOD__ . '(): Not Supported');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayIterator|Traversable
|
||||
*/
|
||||
public function getIterator()
|
||||
{
|
||||
return new ArrayIterator($this->actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo()
|
||||
{
|
||||
return [
|
||||
'actions' => $this->actions
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return Action|null
|
||||
*/
|
||||
protected function getParent(string $name): ?Action
|
||||
{
|
||||
if ($pos = strrpos($name, '.')) {
|
||||
$parentName = substr($name, 0, $pos);
|
||||
|
||||
$parent = $this->getAction($parentName);
|
||||
if (!$parent) {
|
||||
$parent = new Action($parentName);
|
||||
$this->addAction($parent);
|
||||
}
|
||||
|
||||
return $parent;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
187
system/src/Grav/Framework/Acl/PermissionsReader.php
Normal file
187
system/src/Grav/Framework/Acl/PermissionsReader.php
Normal file
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Acl
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Acl;
|
||||
|
||||
use Grav\Common\File\CompiledYamlFile;
|
||||
use RuntimeException;
|
||||
use stdClass;
|
||||
use function is_array;
|
||||
|
||||
/**
|
||||
* Class PermissionsReader
|
||||
* @package Grav\Framework\Acl
|
||||
*/
|
||||
class PermissionsReader
|
||||
{
|
||||
/** @var array */
|
||||
private static $types;
|
||||
|
||||
/**
|
||||
* @param string $filename
|
||||
* @return Action[]
|
||||
*/
|
||||
public static function fromYaml(string $filename): array
|
||||
{
|
||||
$content = CompiledYamlFile::instance($filename)->content();
|
||||
$actions = $content['actions'] ?? [];
|
||||
$types = $content['types'] ?? [];
|
||||
|
||||
return static::fromArray($actions, $types);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $actions
|
||||
* @param array $types
|
||||
* @return Action[]
|
||||
*/
|
||||
public static function fromArray(array $actions, array $types): array
|
||||
{
|
||||
static::initTypes($types);
|
||||
|
||||
$list = [];
|
||||
foreach (static::read($actions) as $type => $data) {
|
||||
$list[$type] = new Action($type, $data);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $actions
|
||||
* @param string $prefix
|
||||
* @return array
|
||||
*/
|
||||
public static function read(array $actions, string $prefix = ''): array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($actions as $name => $action) {
|
||||
$prefixNname = $prefix . $name;
|
||||
$list[$prefixNname] = null;
|
||||
|
||||
// Support nested sets of actions.
|
||||
if (isset($action['actions']) && is_array($action['actions'])) {
|
||||
$list += static::read($action['actions'], "{$prefixNname}.");
|
||||
}
|
||||
|
||||
unset($action['actions']);
|
||||
|
||||
// Add defaults if they exist.
|
||||
$action = static::addDefaults($action);
|
||||
|
||||
// Build flat list of actions.
|
||||
$list[$prefixNname] = $action;
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $types
|
||||
* @return void
|
||||
*/
|
||||
protected static function initTypes(array $types)
|
||||
{
|
||||
static::$types = [];
|
||||
|
||||
$dependencies = [];
|
||||
foreach ($types as $type => $defaults) {
|
||||
$current = array_fill_keys((array)($defaults['use'] ?? null), null);
|
||||
$defType = $defaults['type'] ?? $type;
|
||||
if ($type !== $defType) {
|
||||
$current[$defaults['type']] = null;
|
||||
}
|
||||
|
||||
$dependencies[$type] = (object)$current;
|
||||
}
|
||||
|
||||
// Build dependency tree.
|
||||
foreach ($dependencies as $type => $dep) {
|
||||
foreach (get_object_vars($dep) as $k => &$val) {
|
||||
if (null === $val) {
|
||||
$val = $dependencies[$k] ?? new stdClass();
|
||||
}
|
||||
}
|
||||
unset($val);
|
||||
}
|
||||
|
||||
$encoded = json_encode($dependencies);
|
||||
if ($encoded === false) {
|
||||
throw new RuntimeException('json_encode(): failed to encode dependencies');
|
||||
}
|
||||
$dependencies = json_decode($encoded, true);
|
||||
|
||||
foreach (static::getDependencies($dependencies) as $type) {
|
||||
$defaults = $types[$type] ?? null;
|
||||
if ($defaults) {
|
||||
static::$types[$type] = static::addDefaults($defaults);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $dependencies
|
||||
* @return array
|
||||
*/
|
||||
protected static function getDependencies(array $dependencies): array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($dependencies as $name => $deps) {
|
||||
$current = $deps ? static::getDependencies($deps) : [];
|
||||
$current[] = $name;
|
||||
|
||||
$list[] = $current;
|
||||
}
|
||||
|
||||
return array_unique(array_merge(...$list));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $action
|
||||
* @return array
|
||||
*/
|
||||
protected static function addDefaults(array $action): array
|
||||
{
|
||||
$scopes = [];
|
||||
|
||||
// Add used properties.
|
||||
$use = (array)($action['use'] ?? null);
|
||||
foreach ($use as $type) {
|
||||
if (isset(static::$types[$type])) {
|
||||
$used = static::$types[$type];
|
||||
unset($used['type']);
|
||||
$scopes[] = $used;
|
||||
}
|
||||
}
|
||||
unset($action['use']);
|
||||
|
||||
// Add type defaults.
|
||||
$type = $action['type'] ?? 'default';
|
||||
$defaults = static::$types[$type] ?? null;
|
||||
if (is_array($defaults)) {
|
||||
$scopes[] = $defaults;
|
||||
}
|
||||
|
||||
if ($scopes) {
|
||||
$scopes[] = $action;
|
||||
|
||||
$action = array_replace_recursive(...$scopes);
|
||||
if (null === $action) {
|
||||
throw new RuntimeException('Internal error');
|
||||
}
|
||||
|
||||
$newType = $defaults['type'] ?? null;
|
||||
if ($newType && $newType !== $type) {
|
||||
$action['type'] = $newType;
|
||||
}
|
||||
}
|
||||
|
||||
return $action;
|
||||
}
|
||||
}
|
||||
60
system/src/Grav/Framework/Acl/RecursiveActionIterator.php
Normal file
60
system/src/Grav/Framework/Acl/RecursiveActionIterator.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Acl
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Acl;
|
||||
|
||||
use RecursiveIterator;
|
||||
use RocketTheme\Toolbox\ArrayTraits\Constructor;
|
||||
use RocketTheme\Toolbox\ArrayTraits\Countable;
|
||||
use RocketTheme\Toolbox\ArrayTraits\Iterator;
|
||||
|
||||
/**
|
||||
* Class Action
|
||||
* @package Grav\Framework\Acl
|
||||
*/
|
||||
class RecursiveActionIterator implements RecursiveIterator, \Countable
|
||||
{
|
||||
use Constructor, Iterator, Countable;
|
||||
|
||||
/**
|
||||
* @see \Iterator::key()
|
||||
* @return string
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
/** @var Action $current */
|
||||
$current = $this->current();
|
||||
|
||||
return $current->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see \RecursiveIterator::hasChildren()
|
||||
* @return bool
|
||||
*/
|
||||
public function hasChildren(): bool
|
||||
{
|
||||
/** @var Action $current */
|
||||
$current = $this->current();
|
||||
|
||||
return $current->hasChildren();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see \RecursiveIterator::getChildren()
|
||||
* @return RecursiveActionIterator
|
||||
*/
|
||||
public function getChildren(): self
|
||||
{
|
||||
/** @var Action $current */
|
||||
$current = $this->current();
|
||||
|
||||
return new static($current->getChildren());
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Cache
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Cache;
|
||||
|
||||
use Grav\Framework\Cache\Exception\InvalidArgumentException;
|
||||
use DateInterval;
|
||||
use Psr\SimpleCache\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Cache trait for PSR-16 compatible "Simple Cache" implementation
|
||||
@@ -20,7 +22,7 @@ abstract class AbstractCache implements CacheInterface
|
||||
|
||||
/**
|
||||
* @param string $namespace
|
||||
* @param null|int|\DateInterval $defaultLifetime
|
||||
* @param null|int|DateInterval $defaultLifetime
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct($namespace = '', $defaultLifetime = null)
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Cache
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Cache\Adapter;
|
||||
|
||||
use DateInterval;
|
||||
use Grav\Framework\Cache\AbstractCache;
|
||||
use Grav\Framework\Cache\CacheInterface;
|
||||
use Grav\Framework\Cache\Exception\InvalidArgumentException;
|
||||
use function count;
|
||||
use function get_class;
|
||||
|
||||
/**
|
||||
* Cache class for PSR-16 compatible "Simple Cache" implementation using chained cache adapters.
|
||||
@@ -19,21 +23,17 @@ use Grav\Framework\Cache\Exception\InvalidArgumentException;
|
||||
*/
|
||||
class ChainCache extends AbstractCache
|
||||
{
|
||||
/**
|
||||
* @var array|CacheInterface[]
|
||||
*/
|
||||
/** @var CacheInterface[] */
|
||||
protected $caches;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
/** @var int */
|
||||
protected $count;
|
||||
|
||||
/**
|
||||
* Chain Cache constructor.
|
||||
* @param array $caches
|
||||
* @param null|int|\DateInterval $defaultLifetime
|
||||
* @throws \Psr\SimpleCache\InvalidArgumentException
|
||||
* @param null|int|DateInterval $defaultLifetime
|
||||
* @throws \Psr\SimpleCache\InvalidArgumentException|InvalidArgumentException
|
||||
*/
|
||||
public function __construct(array $caches, $defaultLifetime = null)
|
||||
{
|
||||
@@ -120,6 +120,7 @@ class ChainCache extends AbstractCache
|
||||
while ($i--) {
|
||||
$success = $this->caches[$i]->doClear() && $success;
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
@@ -129,6 +130,10 @@ class ChainCache extends AbstractCache
|
||||
public function doGetMultiple($keys, $miss)
|
||||
{
|
||||
$list = [];
|
||||
/**
|
||||
* @var int $i
|
||||
* @var CacheInterface $cache
|
||||
*/
|
||||
foreach ($this->caches as $i => $cache) {
|
||||
$list[$i] = $cache->doGetMultiple($keys, $miss);
|
||||
|
||||
@@ -139,8 +144,12 @@ class ChainCache extends AbstractCache
|
||||
}
|
||||
}
|
||||
|
||||
$values = [];
|
||||
// Update all the previous caches with missing values.
|
||||
$values = [];
|
||||
/**
|
||||
* @var int $i
|
||||
* @var CacheInterface $items
|
||||
*/
|
||||
foreach (array_reverse($list) as $i => $items) {
|
||||
$values += $items;
|
||||
if ($i && $values) {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Cache
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Cache\Adapter;
|
||||
|
||||
use DateInterval;
|
||||
use Doctrine\Common\Cache\CacheProvider;
|
||||
use Grav\Framework\Cache\AbstractCache;
|
||||
use Grav\Framework\Cache\Exception\InvalidArgumentException;
|
||||
@@ -18,9 +20,7 @@ use Grav\Framework\Cache\Exception\InvalidArgumentException;
|
||||
*/
|
||||
class DoctrineCache extends AbstractCache
|
||||
{
|
||||
/**
|
||||
* @var CacheProvider
|
||||
*/
|
||||
/** @var CacheProvider */
|
||||
protected $driver;
|
||||
|
||||
/**
|
||||
@@ -28,8 +28,8 @@ class DoctrineCache extends AbstractCache
|
||||
*
|
||||
* @param CacheProvider $doctrineCache
|
||||
* @param string $namespace
|
||||
* @param null|int|\DateInterval $defaultLifetime
|
||||
* @throws \Psr\SimpleCache\InvalidArgumentException
|
||||
* @param null|int|DateInterval $defaultLifetime
|
||||
* @throws \Psr\SimpleCache\InvalidArgumentException|InvalidArgumentException
|
||||
*/
|
||||
public function __construct(CacheProvider $doctrineCache, $namespace = '', $defaultLifetime = null)
|
||||
{
|
||||
@@ -38,7 +38,9 @@ class DoctrineCache extends AbstractCache
|
||||
|
||||
// Set namespace to Doctrine Cache provider if it was given.
|
||||
$namespace = $this->getNamespace();
|
||||
$namespace && $doctrineCache->setNamespace($namespace);
|
||||
if ($namespace) {
|
||||
$doctrineCache->setNamespace($namespace);
|
||||
}
|
||||
|
||||
$this->driver = $doctrineCache;
|
||||
}
|
||||
@@ -96,20 +98,9 @@ class DoctrineCache extends AbstractCache
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @throws \Psr\SimpleCache\InvalidArgumentException
|
||||
*/
|
||||
public function doDeleteMultiple($keys)
|
||||
{
|
||||
// TODO: Remove when Doctrine Cache has been updated to support the feature.
|
||||
if (!method_exists($this->driver, 'deleteMultiple')) {
|
||||
$success = true;
|
||||
foreach ($keys as $key) {
|
||||
$success = $this->delete($key) && $success;
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
return $this->driver->deleteMultiple($keys);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Cache
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Cache\Adapter;
|
||||
|
||||
use ErrorException;
|
||||
use FilesystemIterator;
|
||||
use Grav\Framework\Cache\AbstractCache;
|
||||
use Grav\Framework\Cache\Exception\CacheException;
|
||||
use Grav\Framework\Cache\Exception\InvalidArgumentException;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use RuntimeException;
|
||||
use function strlen;
|
||||
|
||||
/**
|
||||
* Cache class for PSR-16 compatible "Simple Cache" implementation using file backend.
|
||||
@@ -21,11 +28,17 @@ use Grav\Framework\Cache\Exception\InvalidArgumentException;
|
||||
*/
|
||||
class FileCache extends AbstractCache
|
||||
{
|
||||
/** @var string */
|
||||
private $directory;
|
||||
/** @var string|null */
|
||||
private $tmp;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* FileCache constructor.
|
||||
* @param string $namespace
|
||||
* @param int|null $defaultLifetime
|
||||
* @param string|null $folder
|
||||
* @throws \Psr\SimpleCache\InvalidArgumentException|InvalidArgumentException
|
||||
*/
|
||||
public function __construct($namespace = '', $defaultLifetime = null, $folder = null)
|
||||
{
|
||||
@@ -50,12 +63,12 @@ class FileCache extends AbstractCache
|
||||
fclose($h);
|
||||
@unlink($file);
|
||||
} else {
|
||||
$i = rawurldecode(rtrim(fgets($h)));
|
||||
$value = stream_get_contents($h);
|
||||
$i = rawurldecode(rtrim((string)fgets($h)));
|
||||
$value = stream_get_contents($h) ?: '';
|
||||
fclose($h);
|
||||
|
||||
if ($i === $key) {
|
||||
return unserialize($value);
|
||||
return unserialize($value, ['allowed_classes' => true]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +77,7 @@ class FileCache extends AbstractCache
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @throws \Psr\SimpleCache\CacheException
|
||||
* @throws CacheException
|
||||
*/
|
||||
public function doSet($key, $value, $ttl)
|
||||
{
|
||||
@@ -99,7 +112,7 @@ class FileCache extends AbstractCache
|
||||
public function doClear()
|
||||
{
|
||||
$result = true;
|
||||
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->directory, \FilesystemIterator::SKIP_DOTS));
|
||||
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->directory, FilesystemIterator::SKIP_DOTS));
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
$result = ($file->isDir() || @unlink($file) || !file_exists($file)) && $result;
|
||||
@@ -128,8 +141,8 @@ class FileCache extends AbstractCache
|
||||
$hash = str_replace('/', '-', base64_encode(hash('sha256', static::class . $key, true)));
|
||||
$dir = $this->directory . $hash[0] . DIRECTORY_SEPARATOR . $hash[1] . DIRECTORY_SEPARATOR;
|
||||
|
||||
if ($mkdir && !file_exists($dir)) {
|
||||
@mkdir($dir, 0777, true);
|
||||
if ($mkdir) {
|
||||
$this->mkdir($dir);
|
||||
}
|
||||
|
||||
return $dir . substr($hash, 2, 20);
|
||||
@@ -138,7 +151,8 @@ class FileCache extends AbstractCache
|
||||
/**
|
||||
* @param string $namespace
|
||||
* @param string $directory
|
||||
* @throws \Psr\SimpleCache\InvalidArgumentException|InvalidArgumentException
|
||||
* @return void
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function initFileCache($namespace, $directory)
|
||||
{
|
||||
@@ -193,14 +207,45 @@ class FileCache extends AbstractCache
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $dir
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
private function mkdir($dir)
|
||||
{
|
||||
// Silence error for open_basedir; should fail in mkdir instead.
|
||||
if (@is_dir($dir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$success = @mkdir($dir, 0777, true);
|
||||
|
||||
if (!$success) {
|
||||
// Take yet another look, make sure that the folder doesn't exist.
|
||||
clearstatcache(true, $dir);
|
||||
if (!@is_dir($dir)) {
|
||||
throw new RuntimeException(sprintf('Unable to create directory: %s', $dir));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $type
|
||||
* @param string $message
|
||||
* @param string $file
|
||||
* @param int $line
|
||||
* @return bool
|
||||
* @internal
|
||||
* @throws \ErrorException
|
||||
* @throws ErrorException
|
||||
*/
|
||||
public static function throwError($type, $message, $file, $line)
|
||||
{
|
||||
throw new \ErrorException($message, 0, $type, $file, $line);
|
||||
throw new ErrorException($message, 0, $type, $file, $line);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->tmp !== null && file_exists($this->tmp)) {
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Cache
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Cache\Adapter;
|
||||
|
||||
use Grav\Framework\Cache\AbstractCache;
|
||||
use function array_key_exists;
|
||||
|
||||
/**
|
||||
* Cache class for PSR-16 compatible "Simple Cache" implementation using in memory backend.
|
||||
@@ -19,11 +21,14 @@ use Grav\Framework\Cache\AbstractCache;
|
||||
*/
|
||||
class MemoryCache extends AbstractCache
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
/** @var array */
|
||||
protected $cache = [];
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param mixed $miss
|
||||
* @return mixed
|
||||
*/
|
||||
public function doGet($key, $miss)
|
||||
{
|
||||
if (!array_key_exists($key, $this->cache)) {
|
||||
@@ -33,6 +38,12 @@ class MemoryCache extends AbstractCache
|
||||
return $this->cache[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param int $ttl
|
||||
* @return bool
|
||||
*/
|
||||
public function doSet($key, $value, $ttl)
|
||||
{
|
||||
$this->cache[$key] = $value;
|
||||
@@ -40,6 +51,10 @@ class MemoryCache extends AbstractCache
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function doDelete($key)
|
||||
{
|
||||
unset($this->cache[$key]);
|
||||
@@ -47,6 +62,9 @@ class MemoryCache extends AbstractCache
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function doClear()
|
||||
{
|
||||
$this->cache = [];
|
||||
@@ -54,6 +72,10 @@ class MemoryCache extends AbstractCache
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function doHas($key)
|
||||
{
|
||||
return array_key_exists($key, $this->cache);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Cache
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -17,9 +18,14 @@ use Grav\Framework\Cache\AbstractCache;
|
||||
*/
|
||||
class SessionCache extends AbstractCache
|
||||
{
|
||||
const VALUE = 0;
|
||||
const LIFETIME = 1;
|
||||
public const VALUE = 0;
|
||||
public const LIFETIME = 1;
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param mixed $miss
|
||||
* @return mixed
|
||||
*/
|
||||
public function doGet($key, $miss)
|
||||
{
|
||||
$stored = $this->doGetStored($key);
|
||||
@@ -27,12 +33,17 @@ class SessionCache extends AbstractCache
|
||||
return $stored ? $stored[self::VALUE] : $miss;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param int $ttl
|
||||
* @return bool
|
||||
*/
|
||||
public function doSet($key, $value, $ttl)
|
||||
{
|
||||
$stored = [self::VALUE => $value];
|
||||
if (null !== $ttl) {
|
||||
$stored[self::LIFETIME] = time() + $ttl;
|
||||
|
||||
}
|
||||
|
||||
$_SESSION[$this->getNamespace()][$key] = $stored;
|
||||
@@ -40,6 +51,10 @@ class SessionCache extends AbstractCache
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function doDelete($key)
|
||||
{
|
||||
unset($_SESSION[$this->getNamespace()][$key]);
|
||||
@@ -47,6 +62,9 @@ class SessionCache extends AbstractCache
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function doClear()
|
||||
{
|
||||
unset($_SESSION[$this->getNamespace()]);
|
||||
@@ -54,19 +72,30 @@ class SessionCache extends AbstractCache
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function doHas($key)
|
||||
{
|
||||
return $this->doGetStored($key) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getNamespace()
|
||||
{
|
||||
return 'cache-' . parent::getNamespace();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return mixed|null
|
||||
*/
|
||||
protected function doGetStored($key)
|
||||
{
|
||||
$stored = isset($_SESSION[$this->getNamespace()][$key]) ? $_SESSION[$this->getNamespace()][$key] : null;
|
||||
$stored = $_SESSION[$this->getNamespace()][$key] ?? null;
|
||||
|
||||
if (isset($stored[self::LIFETIME]) && $stored[self::LIFETIME] < time()) {
|
||||
unset($_SESSION[$this->getNamespace()][$key]);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Cache
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -16,12 +17,55 @@ use Psr\SimpleCache\CacheInterface as SimpleCacheInterface;
|
||||
*/
|
||||
interface CacheInterface extends SimpleCacheInterface
|
||||
{
|
||||
/**
|
||||
* @param string $key
|
||||
* @param mixed $miss
|
||||
* @return mixed
|
||||
*/
|
||||
public function doGet($key, $miss);
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param int|null $ttl
|
||||
* @return mixed
|
||||
*/
|
||||
public function doSet($key, $value, $ttl);
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function doDelete($key);
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function doClear();
|
||||
|
||||
/**
|
||||
* @param string[] $keys
|
||||
* @param mixed $miss
|
||||
* @return mixed
|
||||
*/
|
||||
public function doGetMultiple($keys, $miss);
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $values
|
||||
* @param int|null $ttl
|
||||
* @return mixed
|
||||
*/
|
||||
public function doSetMultiple($values, $ttl);
|
||||
|
||||
/**
|
||||
* @param string[] $keys
|
||||
* @return mixed
|
||||
*/
|
||||
public function doDeleteMultiple($keys);
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function doHas($key);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Cache
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Cache;
|
||||
|
||||
use DateInterval;
|
||||
use DateTime;
|
||||
use Grav\Framework\Cache\Exception\InvalidArgumentException;
|
||||
use stdClass;
|
||||
use Traversable;
|
||||
use function array_key_exists;
|
||||
use function get_class;
|
||||
use function gettype;
|
||||
use function is_array;
|
||||
use function is_int;
|
||||
use function is_object;
|
||||
use function is_string;
|
||||
use function strlen;
|
||||
|
||||
/**
|
||||
* Cache trait for PSR-16 compatible "Simple Cache" implementation
|
||||
@@ -18,13 +31,10 @@ trait CacheTrait
|
||||
{
|
||||
/** @var string */
|
||||
private $namespace = '';
|
||||
|
||||
/** @var int|null */
|
||||
private $defaultLifetime = null;
|
||||
|
||||
/** @var \stdClass */
|
||||
/** @var stdClass */
|
||||
private $miss;
|
||||
|
||||
/** @var bool */
|
||||
private $validation = true;
|
||||
|
||||
@@ -32,18 +42,20 @@ trait CacheTrait
|
||||
* Always call from constructor.
|
||||
*
|
||||
* @param string $namespace
|
||||
* @param null|int|\DateInterval $defaultLifetime
|
||||
* @throws \Psr\SimpleCache\InvalidArgumentException
|
||||
* @param null|int|DateInterval $defaultLifetime
|
||||
* @return void
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function init($namespace = '', $defaultLifetime = null)
|
||||
{
|
||||
$this->namespace = (string) $namespace;
|
||||
$this->defaultLifetime = $this->convertTtl($defaultLifetime);
|
||||
$this->miss = new \stdClass;
|
||||
$this->miss = new stdClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $validation
|
||||
* @param bool $validation
|
||||
* @return void
|
||||
*/
|
||||
public function setValidation($validation)
|
||||
{
|
||||
@@ -67,8 +79,10 @@ trait CacheTrait
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @throws \Psr\SimpleCache\InvalidArgumentException
|
||||
* @param string $key
|
||||
* @param mixed|null $default
|
||||
* @return mixed|null
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function get($key, $default = null)
|
||||
{
|
||||
@@ -80,8 +94,11 @@ trait CacheTrait
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @throws \Psr\SimpleCache\InvalidArgumentException
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param null|int|DateInterval $ttl
|
||||
* @return bool
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function set($key, $value, $ttl = null)
|
||||
{
|
||||
@@ -94,8 +111,9 @@ trait CacheTrait
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @throws \Psr\SimpleCache\InvalidArgumentException
|
||||
* @param string $key
|
||||
* @return bool
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function delete($key)
|
||||
{
|
||||
@@ -105,7 +123,7 @@ trait CacheTrait
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @return bool
|
||||
*/
|
||||
public function clear()
|
||||
{
|
||||
@@ -113,18 +131,21 @@ trait CacheTrait
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @throws \Psr\SimpleCache\InvalidArgumentException
|
||||
* @param iterable $keys
|
||||
* @param mixed|null $default
|
||||
* @return iterable
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function getMultiple($keys, $default = null)
|
||||
{
|
||||
if ($keys instanceof \Traversable) {
|
||||
if ($keys instanceof Traversable) {
|
||||
$keys = iterator_to_array($keys, false);
|
||||
} elseif (!is_array($keys)) {
|
||||
$isObject = is_object($keys);
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Cache keys must be array or Traversable, "%s" given',
|
||||
is_object($keys) ? get_class($keys) : gettype($keys)
|
||||
$isObject ? get_class($keys) : gettype($keys)
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -136,6 +157,9 @@ trait CacheTrait
|
||||
$this->validateKeys($keys);
|
||||
$keys = array_unique($keys);
|
||||
$keys = array_combine($keys, $keys);
|
||||
if (empty($keys)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$list = $this->doGetMultiple($keys, $this->miss);
|
||||
|
||||
@@ -153,18 +177,21 @@ trait CacheTrait
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @throws \Psr\SimpleCache\InvalidArgumentException
|
||||
* @param iterable $values
|
||||
* @param null|int|DateInterval $ttl
|
||||
* @return bool
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function setMultiple($values, $ttl = null)
|
||||
{
|
||||
if ($values instanceof \Traversable) {
|
||||
if ($values instanceof Traversable) {
|
||||
$values = iterator_to_array($values, true);
|
||||
} elseif (!is_array($values)) {
|
||||
$isObject = is_object($values);
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Cache values must be array or Traversable, "%s" given',
|
||||
is_object($values) ? get_class($values) : gettype($values)
|
||||
$isObject ? get_class($values) : gettype($values)
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -184,18 +211,20 @@ trait CacheTrait
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @throws \Psr\SimpleCache\InvalidArgumentException
|
||||
* @param iterable $keys
|
||||
* @return bool
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function deleteMultiple($keys)
|
||||
{
|
||||
if ($keys instanceof \Traversable) {
|
||||
if ($keys instanceof Traversable) {
|
||||
$keys = iterator_to_array($keys, false);
|
||||
} elseif (!is_array($keys)) {
|
||||
$isObject = is_object($keys);
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Cache keys must be array or Traversable, "%s" given',
|
||||
is_object($keys) ? get_class($keys) : gettype($keys)
|
||||
$isObject ? get_class($keys) : gettype($keys)
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -210,8 +239,9 @@ trait CacheTrait
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @throws \Psr\SimpleCache\InvalidArgumentException
|
||||
* @param string $key
|
||||
* @return bool
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function has($key)
|
||||
{
|
||||
@@ -220,11 +250,6 @@ trait CacheTrait
|
||||
return $this->doHas($key);
|
||||
}
|
||||
|
||||
abstract public function doGet($key, $miss);
|
||||
abstract public function doSet($key, $value, $ttl);
|
||||
abstract public function doDelete($key);
|
||||
abstract public function doClear();
|
||||
|
||||
/**
|
||||
* @param array $keys
|
||||
* @param mixed $miss
|
||||
@@ -246,7 +271,7 @@ trait CacheTrait
|
||||
|
||||
/**
|
||||
* @param array $values
|
||||
* @param int $ttl
|
||||
* @param int|null $ttl
|
||||
* @return bool
|
||||
*/
|
||||
public function doSetMultiple($values, $ttl)
|
||||
@@ -275,11 +300,10 @@ trait CacheTrait
|
||||
return $success;
|
||||
}
|
||||
|
||||
abstract public function doHas($key);
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @throws \Psr\SimpleCache\InvalidArgumentException
|
||||
* @param string|mixed $key
|
||||
* @return void
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function validateKey($key)
|
||||
{
|
||||
@@ -296,7 +320,7 @@ trait CacheTrait
|
||||
}
|
||||
if (strlen($key) > 64) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf('Cache key length must be less than 65 characters, key had %s characters', strlen($key))
|
||||
sprintf('Cache key length must be less than 65 characters, key had %d characters', strlen($key))
|
||||
);
|
||||
}
|
||||
if (strpbrk($key, '{}()/\@:') !== false) {
|
||||
@@ -308,7 +332,8 @@ trait CacheTrait
|
||||
|
||||
/**
|
||||
* @param array $keys
|
||||
* @throws \Psr\SimpleCache\InvalidArgumentException
|
||||
* @return void
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function validateKeys($keys)
|
||||
{
|
||||
@@ -322,9 +347,9 @@ trait CacheTrait
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|int|\DateInterval $ttl
|
||||
* @param null|int|DateInterval $ttl
|
||||
* @return int|null
|
||||
* @throws \Psr\SimpleCache\InvalidArgumentException
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function convertTtl($ttl)
|
||||
{
|
||||
@@ -336,8 +361,9 @@ trait CacheTrait
|
||||
return $ttl;
|
||||
}
|
||||
|
||||
if ($ttl instanceof \DateInterval) {
|
||||
$ttl = (int) \DateTime::createFromFormat('U', 0)->add($ttl)->format('U');
|
||||
if ($ttl instanceof DateInterval) {
|
||||
$date = DateTime::createFromFormat('U', '0');
|
||||
$ttl = $date ? (int)$date->add($ttl)->format('U') : 0;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Cache
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Cache\Exception;
|
||||
|
||||
use Exception;
|
||||
use Psr\SimpleCache\CacheException as SimpleCacheException;
|
||||
|
||||
/**
|
||||
* CacheException class for PSR-16 compatible "Simple Cache" implementation.
|
||||
* @package Grav\Framework\Cache\Exception
|
||||
*/
|
||||
class CacheException extends \Exception implements SimpleCacheException
|
||||
class CacheException extends Exception implements SimpleCacheException
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Cache
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Collection
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -10,45 +11,37 @@ namespace Grav\Framework\Collection;
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\Common\Collections\Expr\ClosureExpressionVisitor;
|
||||
use FilesystemIterator;
|
||||
use Grav\Common\Grav;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RocketTheme\Toolbox\ResourceLocator\RecursiveUniformResourceIterator;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
use RuntimeException;
|
||||
use SeekableIterator;
|
||||
use function array_slice;
|
||||
|
||||
/**
|
||||
* Collection of objects stored into a filesystem.
|
||||
*
|
||||
* @package Grav\Framework\Collection
|
||||
* @template TKey
|
||||
* @template T
|
||||
* @extends AbstractLazyCollection<TKey,T>
|
||||
* @mplements FileCollectionInterface<TKey,T>
|
||||
*/
|
||||
class AbstractFileCollection extends AbstractLazyCollection implements FileCollectionInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
/** @var string */
|
||||
protected $path;
|
||||
|
||||
/**
|
||||
* @var \RecursiveDirectoryIterator|RecursiveUniformResourceIterator
|
||||
*/
|
||||
/** @var RecursiveDirectoryIterator|RecursiveUniformResourceIterator */
|
||||
protected $iterator;
|
||||
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
/** @var callable */
|
||||
protected $createObjectFunction;
|
||||
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
/** @var callable|null */
|
||||
protected $filterFunction;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
/** @var int */
|
||||
protected $flags;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
/** @var int */
|
||||
protected $nestingLimit;
|
||||
|
||||
/**
|
||||
@@ -93,8 +86,15 @@ class AbstractFileCollection extends AbstractLazyCollection implements FileColle
|
||||
|
||||
if ($orderings = $criteria->getOrderings()) {
|
||||
$next = null;
|
||||
/**
|
||||
* @var string $field
|
||||
* @var string $ordering
|
||||
*/
|
||||
foreach (array_reverse($orderings) as $field => $ordering) {
|
||||
$next = ClosureExpressionVisitor::sortByField($field, $ordering == Criteria::DESC ? -1 : 1, $next);
|
||||
$next = ClosureExpressionVisitor::sortByField($field, $ordering === Criteria::DESC ? -1 : 1, $next);
|
||||
}
|
||||
if (null === $next) {
|
||||
throw new RuntimeException('Criteria is missing orderings');
|
||||
}
|
||||
|
||||
uasort($filtered, $next);
|
||||
@@ -112,17 +112,20 @@ class AbstractFileCollection extends AbstractLazyCollection implements FileColle
|
||||
return new ArrayCollection($filtered);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function setIterator()
|
||||
{
|
||||
$iteratorFlags = \RecursiveDirectoryIterator::SKIP_DOTS + \FilesystemIterator::UNIX_PATHS
|
||||
+ \FilesystemIterator::CURRENT_AS_SELF + \FilesystemIterator::FOLLOW_SYMLINKS;
|
||||
$iteratorFlags = RecursiveDirectoryIterator::SKIP_DOTS + FilesystemIterator::UNIX_PATHS
|
||||
+ FilesystemIterator::CURRENT_AS_SELF + FilesystemIterator::FOLLOW_SYMLINKS;
|
||||
|
||||
if (strpos($this->path, '://')) {
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
$this->iterator = $locator->getRecursiveIterator($this->path, $iteratorFlags);
|
||||
} else {
|
||||
$this->iterator = new \RecursiveDirectoryIterator($this->path, $iteratorFlags);
|
||||
$this->iterator = new RecursiveDirectoryIterator($this->path, $iteratorFlags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,14 +158,19 @@ class AbstractFileCollection extends AbstractLazyCollection implements FileColle
|
||||
$this->collection = new ArrayCollection($filtered);
|
||||
}
|
||||
|
||||
protected function doInitializeByIterator(\SeekableIterator $iterator, $nestingLimit)
|
||||
/**
|
||||
* @param SeekableIterator $iterator
|
||||
* @param int $nestingLimit
|
||||
* @return array
|
||||
*/
|
||||
protected function doInitializeByIterator(SeekableIterator $iterator, $nestingLimit)
|
||||
{
|
||||
$children = [];
|
||||
$objects = [];
|
||||
$filter = $this->filterFunction;
|
||||
$objectFunction = $this->createObjectFunction;
|
||||
|
||||
/** @var \RecursiveDirectoryIterator $file */
|
||||
/** @var RecursiveDirectoryIterator $file */
|
||||
foreach ($iterator as $file) {
|
||||
// Skip files if they shouldn't be included.
|
||||
if (!($this->flags & static::INCLUDE_FILES) && $file->isFile()) {
|
||||
@@ -196,7 +204,8 @@ class AbstractFileCollection extends AbstractLazyCollection implements FileColle
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \RecursiveDirectoryIterator[] $children
|
||||
* @param array $children
|
||||
* @param int $nestingLimit
|
||||
* @return array
|
||||
*/
|
||||
protected function doInitializeChildren(array $children, $nestingLimit)
|
||||
@@ -211,7 +220,7 @@ class AbstractFileCollection extends AbstractLazyCollection implements FileColle
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \RecursiveDirectoryIterator $file
|
||||
* @param RecursiveDirectoryIterator $file
|
||||
* @return object
|
||||
*/
|
||||
protected function createObject($file)
|
||||
|
||||
538
system/src/Grav/Framework/Collection/AbstractIndexCollection.php
Normal file
538
system/src/Grav/Framework/Collection/AbstractIndexCollection.php
Normal file
@@ -0,0 +1,538 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Collection
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Collection;
|
||||
|
||||
use ArrayIterator;
|
||||
use Closure;
|
||||
use Grav\Framework\Compat\Serializable;
|
||||
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
|
||||
use InvalidArgumentException;
|
||||
use function array_key_exists;
|
||||
use function array_slice;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* Abstract Index Collection.
|
||||
* @template TKey
|
||||
* @template T
|
||||
* @implements CollectionInterface<TKey,T>
|
||||
*/
|
||||
abstract class AbstractIndexCollection implements CollectionInterface
|
||||
{
|
||||
use Serializable;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
* @phpstan-var array<TKey,T>
|
||||
*/
|
||||
private $entries;
|
||||
|
||||
/**
|
||||
* Initializes a new IndexCollection.
|
||||
*
|
||||
* @param array $entries
|
||||
* @phpstan-param array<TKey,T> $entries
|
||||
*/
|
||||
public function __construct(array $entries = [])
|
||||
{
|
||||
$this->entries = $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
return $this->loadElements($this->entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function first()
|
||||
{
|
||||
$value = reset($this->entries);
|
||||
$key = (string)key($this->entries);
|
||||
|
||||
return $this->loadElement($key, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function last()
|
||||
{
|
||||
$value = end($this->entries);
|
||||
$key = (string)key($this->entries);
|
||||
|
||||
return $this->loadElement($key, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
/** @phpstan-var TKey $key */
|
||||
$key = (string)key($this->entries);
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
$value = next($this->entries);
|
||||
$key = (string)key($this->entries);
|
||||
|
||||
return $this->loadElement($key, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
$value = current($this->entries);
|
||||
$key = (string)key($this->entries);
|
||||
|
||||
return $this->loadElement($key, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function remove($key)
|
||||
{
|
||||
if (!array_key_exists($key, $this->entries)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = $this->entries[$key];
|
||||
unset($this->entries[$key]);
|
||||
|
||||
return $this->loadElement((string)$key, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function removeElement($element)
|
||||
{
|
||||
$key = $this->isAllowedElement($element) ? $this->getCurrentKey($element) : null;
|
||||
|
||||
if (!$key || !isset($this->entries[$key])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unset($this->entries[$key]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Required by interface ArrayAccess.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function offsetExists($offset)
|
||||
{
|
||||
return $this->containsKey($offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Required by interface ArrayAccess.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function offsetGet($offset)
|
||||
{
|
||||
return $this->get($offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Required by interface ArrayAccess.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function offsetSet($offset, $value)
|
||||
{
|
||||
if (null === $offset) {
|
||||
$this->add($value);
|
||||
}
|
||||
|
||||
$this->set($offset, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Required by interface ArrayAccess.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function offsetUnset($offset)
|
||||
{
|
||||
return $this->remove($offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function containsKey($key)
|
||||
{
|
||||
return isset($this->entries[$key]) || array_key_exists($key, $this->entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function contains($element)
|
||||
{
|
||||
$key = $this->isAllowedElement($element) ? $this->getCurrentKey($element) : null;
|
||||
|
||||
return $key && isset($this->entries[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function exists(Closure $p)
|
||||
{
|
||||
return $this->loadCollection($this->entries)->exists($p);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function indexOf($element)
|
||||
{
|
||||
$key = $this->isAllowedElement($element) ? $this->getCurrentKey($element) : null;
|
||||
|
||||
return $key && isset($this->entries[$key]) ? $key : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function get($key)
|
||||
{
|
||||
if (!isset($this->entries[$key])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->loadElement((string)$key, $this->entries[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getKeys()
|
||||
{
|
||||
return array_keys($this->entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getValues()
|
||||
{
|
||||
return array_values($this->loadElements($this->entries));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function count()
|
||||
{
|
||||
return count($this->entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function set($key, $value)
|
||||
{
|
||||
if (!$this->isAllowedElement($value)) {
|
||||
throw new InvalidArgumentException('Invalid argument $value');
|
||||
}
|
||||
|
||||
$this->entries[$key] = $this->getElementMeta($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function add($element)
|
||||
{
|
||||
if (!$this->isAllowedElement($element)) {
|
||||
throw new InvalidArgumentException('Invalid argument $element');
|
||||
}
|
||||
|
||||
$this->entries[$this->getCurrentKey($element)] = $this->getElementMeta($element);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function isEmpty()
|
||||
{
|
||||
return empty($this->entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Required by interface IteratorAggregate.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getIterator()
|
||||
{
|
||||
return new ArrayIterator($this->loadElements());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function map(Closure $func)
|
||||
{
|
||||
return $this->loadCollection($this->entries)->map($func);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function filter(Closure $p)
|
||||
{
|
||||
return $this->loadCollection($this->entries)->filter($p);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function forAll(Closure $p)
|
||||
{
|
||||
return $this->loadCollection($this->entries)->forAll($p);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function partition(Closure $p)
|
||||
{
|
||||
return $this->loadCollection($this->entries)->partition($p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of this object.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return __CLASS__ . '@' . spl_object_hash($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function clear()
|
||||
{
|
||||
$this->entries = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function slice($offset, $length = null)
|
||||
{
|
||||
return $this->loadElements(array_slice($this->entries, $offset, $length, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $start
|
||||
* @param int|null $limit
|
||||
* @return static
|
||||
*/
|
||||
public function limit($start, $limit = null)
|
||||
{
|
||||
return $this->createFrom(array_slice($this->entries, $start, $limit, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the order of the items.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function reverse()
|
||||
{
|
||||
return $this->createFrom(array_reverse($this->entries));
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffle items.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function shuffle()
|
||||
{
|
||||
$keys = $this->getKeys();
|
||||
shuffle($keys);
|
||||
|
||||
return $this->createFrom(array_replace(array_flip($keys), $this->entries) ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select items from collection.
|
||||
*
|
||||
* Collection is returned in the order of $keys given to the function.
|
||||
*
|
||||
* @param array $keys
|
||||
* @return static
|
||||
*/
|
||||
public function select(array $keys)
|
||||
{
|
||||
$list = [];
|
||||
foreach ($keys as $key) {
|
||||
if (isset($this->entries[$key])) {
|
||||
$list[$key] = $this->entries[$key];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createFrom($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Un-select items from collection.
|
||||
*
|
||||
* @param array $keys
|
||||
* @return static
|
||||
*/
|
||||
public function unselect(array $keys)
|
||||
{
|
||||
return $this->select(array_diff($this->getKeys(), $keys));
|
||||
}
|
||||
|
||||
/**
|
||||
* Split collection into chunks.
|
||||
*
|
||||
* @param int $size Size of each chunk.
|
||||
* @return array
|
||||
*/
|
||||
public function chunk($size)
|
||||
{
|
||||
return $this->loadCollection($this->entries)->chunk($size);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function __serialize(): array
|
||||
{
|
||||
return [
|
||||
'entries' => $this->entries
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
$this->entries = $data['entries'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements JsonSerializable interface.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->loadCollection()->jsonSerialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance from the specified elements.
|
||||
*
|
||||
* This method is provided for derived classes to specify how a new
|
||||
* instance should be created when constructor semantics have changed.
|
||||
*
|
||||
* @param array $entries Elements.
|
||||
* @return static
|
||||
*/
|
||||
protected function createFrom(array $entries)
|
||||
{
|
||||
return new static($entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function getEntries(): array
|
||||
{
|
||||
return $this->entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $entries
|
||||
* @return void
|
||||
* @phpstan-param array<TKey,T> $entries
|
||||
*/
|
||||
protected function setEntries(array $entries): void
|
||||
{
|
||||
$this->entries = $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FlexObjectInterface $element
|
||||
* @return string
|
||||
* @phpstan-param T $element
|
||||
* @phpstan-return TKey
|
||||
*/
|
||||
protected function getCurrentKey($element)
|
||||
{
|
||||
return $element->getKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return mixed|null
|
||||
*/
|
||||
abstract protected function loadElement($key, $value);
|
||||
|
||||
/**
|
||||
* @param array|null $entries
|
||||
* @return array
|
||||
* @phpstan-return array<TKey,T>
|
||||
*/
|
||||
abstract protected function loadElements(array $entries = null): array;
|
||||
|
||||
/**
|
||||
* @param array|null $entries
|
||||
* @return CollectionInterface
|
||||
*/
|
||||
abstract protected function loadCollection(array $entries = null): CollectionInterface;
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
abstract protected function isAllowedElement($value): bool;
|
||||
|
||||
/**
|
||||
* @param mixed $element
|
||||
* @return mixed
|
||||
*/
|
||||
abstract protected function getElementMeta($element);
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Collection
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -14,14 +15,14 @@ use Doctrine\Common\Collections\AbstractLazyCollection as BaseAbstractLazyCollec
|
||||
* General JSON serializable collection.
|
||||
*
|
||||
* @package Grav\Framework\Collection
|
||||
* @template TKey
|
||||
* @template T
|
||||
* @extends BaseAbstractLazyCollection<TKey,T>
|
||||
* @implements CollectionInterface<TKey,T>
|
||||
*/
|
||||
abstract class AbstractLazyCollection extends BaseAbstractLazyCollection implements CollectionInterface
|
||||
{
|
||||
/**
|
||||
* The backed collection to use
|
||||
*
|
||||
* @var ArrayCollection
|
||||
*/
|
||||
/** @var ArrayCollection The backed collection to use */
|
||||
protected $collection;
|
||||
|
||||
/**
|
||||
@@ -30,6 +31,7 @@ abstract class AbstractLazyCollection extends BaseAbstractLazyCollection impleme
|
||||
public function reverse()
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
return $this->collection->reverse();
|
||||
}
|
||||
|
||||
@@ -39,6 +41,7 @@ abstract class AbstractLazyCollection extends BaseAbstractLazyCollection impleme
|
||||
public function shuffle()
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
return $this->collection->shuffle();
|
||||
}
|
||||
|
||||
@@ -48,15 +51,37 @@ abstract class AbstractLazyCollection extends BaseAbstractLazyCollection impleme
|
||||
public function chunk($size)
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
return $this->collection->chunk($size);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function select(array $keys)
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
return $this->collection->select($keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function unselect(array $keys)
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
return $this->collection->unselect($keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
return $this->collection->jsonSerialize();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Collection
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -14,6 +15,10 @@ use Doctrine\Common\Collections\ArrayCollection as BaseArrayCollection;
|
||||
* General JSON serializable collection.
|
||||
*
|
||||
* @package Grav\Framework\Collection
|
||||
* @template TKey
|
||||
* @template T
|
||||
* @extends BaseArrayCollection<TKey,T>
|
||||
* @implements CollectionInterface<TKey,T>
|
||||
*/
|
||||
class ArrayCollection extends BaseArrayCollection implements CollectionInterface
|
||||
{
|
||||
@@ -21,6 +26,7 @@ class ArrayCollection extends BaseArrayCollection implements CollectionInterface
|
||||
* Reverse the order of the items.
|
||||
*
|
||||
* @return static
|
||||
* @phpstan-return static<TKey,T>
|
||||
*/
|
||||
public function reverse()
|
||||
{
|
||||
@@ -31,13 +37,14 @@ class ArrayCollection extends BaseArrayCollection implements CollectionInterface
|
||||
* Shuffle items.
|
||||
*
|
||||
* @return static
|
||||
* @phpstan-return static<TKey,T>
|
||||
*/
|
||||
public function shuffle()
|
||||
{
|
||||
$keys = $this->getKeys();
|
||||
shuffle($keys);
|
||||
|
||||
return $this->createFrom(array_replace(array_flip($keys), $this->toArray()));
|
||||
return $this->createFrom(array_replace(array_flip($keys), $this->toArray()) ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,7 +59,41 @@ class ArrayCollection extends BaseArrayCollection implements CollectionInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementes JsonSerializable interface.
|
||||
* Select items from collection.
|
||||
*
|
||||
* Collection is returned in the order of $keys given to the function.
|
||||
*
|
||||
* @param array<int|string> $keys
|
||||
* @return static
|
||||
* @phpstan-param array<TKey> $keys
|
||||
* @phpstan-return static<TKey,T>
|
||||
*/
|
||||
public function select(array $keys)
|
||||
{
|
||||
$list = [];
|
||||
foreach ($keys as $key) {
|
||||
if ($this->containsKey($key)) {
|
||||
$list[$key] = $this->get($key);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createFrom($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Un-select items from collection.
|
||||
*
|
||||
* @param array<int|string> $keys
|
||||
* @return static
|
||||
* @phpstan-return static<TKey,T>
|
||||
*/
|
||||
public function unselect(array $keys)
|
||||
{
|
||||
return $this->select(array_diff($this->getKeys(), $keys));
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements JsonSerializable interface.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
|
||||
@@ -1,33 +1,40 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Collection
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Collection;
|
||||
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use JsonSerializable;
|
||||
|
||||
/**
|
||||
* Collection Interface.
|
||||
*
|
||||
* @package Grav\Framework\Collection
|
||||
* @template TKey
|
||||
* @template T
|
||||
* @extends Collection<TKey,T>
|
||||
*/
|
||||
interface CollectionInterface extends Collection, \JsonSerializable
|
||||
interface CollectionInterface extends Collection, JsonSerializable
|
||||
{
|
||||
/**
|
||||
* Reverse the order of the items.
|
||||
*
|
||||
* @return static
|
||||
* @return CollectionInterface
|
||||
* @phpstan-return CollectionInterface<TKey,T>
|
||||
*/
|
||||
public function reverse();
|
||||
|
||||
/**
|
||||
* Shuffle items.
|
||||
*
|
||||
* @return static
|
||||
* @return CollectionInterface
|
||||
* @phpstan-return CollectionInterface<TKey,T>
|
||||
*/
|
||||
public function shuffle();
|
||||
|
||||
@@ -38,4 +45,24 @@ interface CollectionInterface extends Collection, \JsonSerializable
|
||||
* @return array
|
||||
*/
|
||||
public function chunk($size);
|
||||
|
||||
/**
|
||||
* Select items from collection.
|
||||
*
|
||||
* Collection is returned in the order of $keys given to the function.
|
||||
*
|
||||
* @param array<int|string> $keys
|
||||
* @return CollectionInterface
|
||||
* @phpstan-return CollectionInterface<TKey,T>
|
||||
*/
|
||||
public function select(array $keys);
|
||||
|
||||
/**
|
||||
* Un-select items from collection.
|
||||
*
|
||||
* @param array<int|string> $keys
|
||||
* @return CollectionInterface
|
||||
* @phpstan-return CollectionInterface<TKey,T>
|
||||
*/
|
||||
public function unselect(array $keys);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Collection
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -12,6 +13,9 @@ namespace Grav\Framework\Collection;
|
||||
* Collection of objects stored into a filesystem.
|
||||
*
|
||||
* @package Grav\Framework\Collection
|
||||
* @template TKey
|
||||
* @template T
|
||||
* @extends AbstractFileCollection<TKey,T>
|
||||
*/
|
||||
class FileCollection extends AbstractFileCollection
|
||||
{
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Collection
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -14,12 +15,16 @@ use Doctrine\Common\Collections\Selectable;
|
||||
* Collection of objects stored into a filesystem.
|
||||
*
|
||||
* @package Grav\Framework\Collection
|
||||
* @template TKey
|
||||
* @template T
|
||||
* @extends CollectionInterface<TKey,T>
|
||||
* @extends Selectable<TKey,T>
|
||||
*/
|
||||
interface FileCollectionInterface extends CollectionInterface, Selectable
|
||||
{
|
||||
const INCLUDE_FILES = 1;
|
||||
const INCLUDE_FOLDERS = 2;
|
||||
const RECURSIVE = 4;
|
||||
public const INCLUDE_FILES = 1;
|
||||
public const INCLUDE_FOLDERS = 2;
|
||||
public const RECURSIVE = 4;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
|
||||
47
system/src/Grav/Framework/Compat/Serializable.php
Normal file
47
system/src/Grav/Framework/Compat/Serializable.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Compat
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Compat;
|
||||
|
||||
/**
|
||||
* Serializable trait
|
||||
*
|
||||
* Adds backwards compatibility to PHP 7.3 Serializable interface.
|
||||
*
|
||||
* Note: Remember to add: `implements \Serializable` to the classes which use this trait.
|
||||
*
|
||||
* @package Grav\Framework\Traits
|
||||
*/
|
||||
trait Serializable
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
final public function serialize(): string
|
||||
{
|
||||
return serialize($this->__serialize());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $serialized
|
||||
* @return void
|
||||
*/
|
||||
final public function unserialize($serialized): void
|
||||
{
|
||||
$this->__unserialize(unserialize($serialized, ['allowed_classes' => $this->getUnserializeAllowedClasses()]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|bool
|
||||
*/
|
||||
protected function getUnserializeAllowedClasses()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,20 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\ContentBlock
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\ContentBlock;
|
||||
|
||||
use Exception;
|
||||
use Grav\Framework\Compat\Serializable;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
use function get_class;
|
||||
|
||||
/**
|
||||
* Class to create nested blocks of content.
|
||||
*
|
||||
@@ -22,15 +29,25 @@ namespace Grav\Framework\ContentBlock;
|
||||
*/
|
||||
class ContentBlock implements ContentBlockInterface
|
||||
{
|
||||
use Serializable;
|
||||
|
||||
/** @var int */
|
||||
protected $version = 1;
|
||||
/** @var string */
|
||||
protected $id;
|
||||
/** @var string */
|
||||
protected $tokenTemplate = '@@BLOCK-%s@@';
|
||||
/** @var string */
|
||||
protected $content = '';
|
||||
/** @var array */
|
||||
protected $blocks = [];
|
||||
/** @var string */
|
||||
protected $checksum;
|
||||
/** @var bool */
|
||||
protected $cached = true;
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @param string|null $id
|
||||
* @return static
|
||||
*/
|
||||
public static function create($id = null)
|
||||
@@ -41,23 +58,23 @@ class ContentBlock implements ContentBlockInterface
|
||||
/**
|
||||
* @param array $serialized
|
||||
* @return ContentBlockInterface
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public static function fromArray(array $serialized)
|
||||
{
|
||||
try {
|
||||
$type = isset($serialized['_type']) ? $serialized['_type'] : null;
|
||||
$id = isset($serialized['id']) ? $serialized['id'] : null;
|
||||
$type = $serialized['_type'] ?? null;
|
||||
$id = $serialized['id'] ?? null;
|
||||
|
||||
if (!$type || !$id || !is_a($type, 'Grav\Framework\ContentBlock\ContentBlockInterface', true)) {
|
||||
throw new \InvalidArgumentException('Bad data');
|
||||
if (!$type || !$id || !is_a($type, ContentBlockInterface::class, true)) {
|
||||
throw new InvalidArgumentException('Bad data');
|
||||
}
|
||||
|
||||
/** @var ContentBlockInterface $instance */
|
||||
$instance = new $type($id);
|
||||
$instance->build($serialized);
|
||||
} catch (\Exception $e) {
|
||||
throw new \InvalidArgumentException(sprintf('Cannot unserialize Block: %s', $e->getMessage()), $e->getCode(), $e);
|
||||
} catch (Exception $e) {
|
||||
throw new InvalidArgumentException(sprintf('Cannot unserialize Block: %s', $e->getMessage()), $e->getCode(), $e);
|
||||
}
|
||||
|
||||
return $instance;
|
||||
@@ -66,7 +83,7 @@ class ContentBlock implements ContentBlockInterface
|
||||
/**
|
||||
* Block constructor.
|
||||
*
|
||||
* @param string $id
|
||||
* @param string|null $id
|
||||
*/
|
||||
public function __construct($id = null)
|
||||
{
|
||||
@@ -95,10 +112,7 @@ class ContentBlock implements ContentBlockInterface
|
||||
public function toArray()
|
||||
{
|
||||
$blocks = [];
|
||||
/**
|
||||
* @var string $id
|
||||
* @var ContentBlockInterface $block
|
||||
*/
|
||||
/** @var ContentBlockInterface $block */
|
||||
foreach ($this->blocks as $block) {
|
||||
$blocks[$block->getId()] = $block->toArray();
|
||||
}
|
||||
@@ -106,7 +120,8 @@ class ContentBlock implements ContentBlockInterface
|
||||
$array = [
|
||||
'_type' => get_class($this),
|
||||
'_version' => $this->version,
|
||||
'id' => $this->id
|
||||
'id' => $this->id,
|
||||
'cached' => $this->cached
|
||||
];
|
||||
|
||||
if ($this->checksum) {
|
||||
@@ -150,21 +165,23 @@ class ContentBlock implements ContentBlockInterface
|
||||
{
|
||||
try {
|
||||
return $this->toString();
|
||||
} catch (\Exception $e) {
|
||||
} catch (Exception $e) {
|
||||
return sprintf('Error while rendering block: %s', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $serialized
|
||||
* @throws \RuntimeException
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function build(array $serialized)
|
||||
{
|
||||
$this->checkVersion($serialized);
|
||||
|
||||
$this->id = isset($serialized['id']) ? $serialized['id'] : $this->generateId();
|
||||
$this->checksum = isset($serialized['checksum']) ? $serialized['checksum'] : null;
|
||||
$this->id = $serialized['id'] ?? $this->generateId();
|
||||
$this->checksum = $serialized['checksum'] ?? null;
|
||||
$this->cached = $serialized['cached'] ?? null;
|
||||
|
||||
if (isset($serialized['content'])) {
|
||||
$this->setContent($serialized['content']);
|
||||
@@ -176,6 +193,34 @@ class ContentBlock implements ContentBlockInterface
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isCached()
|
||||
{
|
||||
if (!$this->cached) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($this->blocks as $block) {
|
||||
if (!$block->isCached()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function disableCache()
|
||||
{
|
||||
$this->cached = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $checksum
|
||||
* @return $this
|
||||
@@ -218,20 +263,20 @@ class ContentBlock implements ContentBlockInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @return array
|
||||
*/
|
||||
public function serialize()
|
||||
final public function __serialize(): array
|
||||
{
|
||||
return serialize($this->toArray());
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $serialized
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
public function unserialize($serialized)
|
||||
final public function __unserialize(array $data): void
|
||||
{
|
||||
$array = unserialize($serialized);
|
||||
$this->build($array);
|
||||
$this->build($data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -244,13 +289,14 @@ class ContentBlock implements ContentBlockInterface
|
||||
|
||||
/**
|
||||
* @param array $serialized
|
||||
* @throws \RuntimeException
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function checkVersion(array $serialized)
|
||||
{
|
||||
$version = isset($serialized['_version']) ? (int) $serialized['_version'] : 1;
|
||||
if ($version !== $this->version) {
|
||||
throw new \RuntimeException(sprintf('Unsupported version %s', $version));
|
||||
throw new RuntimeException(sprintf('Unsupported version %s', $version));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\ContentBlock
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\ContentBlock;
|
||||
|
||||
use Serializable;
|
||||
|
||||
/**
|
||||
* ContentBlock Interface
|
||||
* @package Grav\Framework\ContentBlock
|
||||
*/
|
||||
interface ContentBlockInterface extends \Serializable
|
||||
interface ContentBlockInterface extends Serializable
|
||||
{
|
||||
/**
|
||||
* @param string $id
|
||||
* @param string|null $id
|
||||
* @return static
|
||||
*/
|
||||
public static function create($id = null);
|
||||
@@ -27,7 +30,7 @@ interface ContentBlockInterface extends \Serializable
|
||||
public static function fromArray(array $serialized);
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @param string|null $id
|
||||
*/
|
||||
public function __construct($id = null);
|
||||
|
||||
@@ -58,6 +61,7 @@ interface ContentBlockInterface extends \Serializable
|
||||
|
||||
/**
|
||||
* @param array $serialized
|
||||
* @return void
|
||||
*/
|
||||
public function build(array $serialized);
|
||||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\ContentBlock
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\ContentBlock;
|
||||
|
||||
use RuntimeException;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* HtmlBlock
|
||||
*
|
||||
@@ -15,10 +20,15 @@ namespace Grav\Framework\ContentBlock;
|
||||
*/
|
||||
class HtmlBlock extends ContentBlock implements HtmlBlockInterface
|
||||
{
|
||||
/** @var int */
|
||||
protected $version = 1;
|
||||
/** @var array */
|
||||
protected $frameworks = [];
|
||||
/** @var array */
|
||||
protected $styles = [];
|
||||
/** @var array */
|
||||
protected $scripts = [];
|
||||
/** @var array */
|
||||
protected $html = [];
|
||||
|
||||
/**
|
||||
@@ -73,7 +83,7 @@ class HtmlBlock extends ContentBlock implements HtmlBlockInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
@@ -97,7 +107,8 @@ class HtmlBlock extends ContentBlock implements HtmlBlockInterface
|
||||
|
||||
/**
|
||||
* @param array $serialized
|
||||
* @throws \RuntimeException
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function build(array $serialized)
|
||||
{
|
||||
@@ -218,8 +229,8 @@ class HtmlBlock extends ContentBlock implements HtmlBlockInterface
|
||||
|
||||
$src = $element['src'];
|
||||
$type = !empty($element['type']) ? (string) $element['type'] : 'text/javascript';
|
||||
$defer = isset($element['defer']) ? true : false;
|
||||
$async = isset($element['async']) ? true : false;
|
||||
$defer = isset($element['defer']);
|
||||
$async = isset($element['async']);
|
||||
$handle = !empty($element['handle']) ? (string) $element['handle'] : '';
|
||||
|
||||
$this->scripts[$location][md5($src) . sha1($src)] = [
|
||||
@@ -302,7 +313,7 @@ class HtmlBlock extends ContentBlock implements HtmlBlockInterface
|
||||
];
|
||||
|
||||
foreach ($this->blocks as $block) {
|
||||
if ($block instanceof HtmlBlock) {
|
||||
if ($block instanceof self) {
|
||||
$blockAssets = $block->getAssetsFast();
|
||||
$assets['frameworks'] += $blockAssets['frameworks'];
|
||||
|
||||
@@ -356,6 +367,7 @@ class HtmlBlock extends ContentBlock implements HtmlBlockInterface
|
||||
|
||||
/**
|
||||
* @param array $items
|
||||
* @return void
|
||||
*/
|
||||
protected function sortAssetsInLocation(array &$items)
|
||||
{
|
||||
@@ -367,15 +379,15 @@ class HtmlBlock extends ContentBlock implements HtmlBlockInterface
|
||||
|
||||
uasort(
|
||||
$items,
|
||||
function ($a, $b) {
|
||||
return ($a[':priority'] === $b[':priority'])
|
||||
? $a[':order'] - $b[':order'] : $a[':priority'] - $b[':priority'];
|
||||
static function ($a, $b) {
|
||||
return $a[':priority'] <=> $b[':priority'] ?: $a[':order'] <=> $b[':order'];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $array
|
||||
* @return void
|
||||
*/
|
||||
protected function sortAssets(array &$array)
|
||||
{
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\ContentBlock
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Controller
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Grav\Framework\Controller\Traits;
|
||||
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Debugger;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Framework\Psr7\Response;
|
||||
use Grav\Framework\RequestHandler\Exception\RequestException;
|
||||
use Grav\Framework\Route\Route;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Throwable;
|
||||
use function get_class;
|
||||
use function in_array;
|
||||
|
||||
/**
|
||||
* Trait ControllerResponseTrait
|
||||
* @package Grav\Framework\Controller\Traits
|
||||
*/
|
||||
trait ControllerResponseTrait
|
||||
{
|
||||
/**
|
||||
* Display the current page.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
protected function createDisplayResponse(): ResponseInterface
|
||||
{
|
||||
return new Response(418);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $content
|
||||
* @param int|null $code
|
||||
* @param array|null $headers
|
||||
* @return Response
|
||||
*/
|
||||
protected function createHtmlResponse(string $content, int $code = null, array $headers = null): ResponseInterface
|
||||
{
|
||||
$code = $code ?? 200;
|
||||
if ($code < 100 || $code > 599) {
|
||||
$code = 500;
|
||||
}
|
||||
$headers = $headers ?? [];
|
||||
|
||||
return new Response($code, $headers, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $content
|
||||
* @param int|null $code
|
||||
* @param array|null $headers
|
||||
* @return Response
|
||||
*/
|
||||
protected function createJsonResponse(array $content, int $code = null, array $headers = null): ResponseInterface
|
||||
{
|
||||
$code = $code ?? $content['code'] ?? 200;
|
||||
if (null === $code || $code < 100 || $code > 599) {
|
||||
$code = 200;
|
||||
}
|
||||
$headers = ($headers ?? []) + [
|
||||
'Content-Type' => 'application/json',
|
||||
'Cache-Control' => 'no-store, max-age=0'
|
||||
];
|
||||
|
||||
return new Response($code, $headers, json_encode($content));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filename
|
||||
* @param string|resource|StreamInterface $resource
|
||||
* @param array|null $headers
|
||||
* @param array|null $options
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
protected function createDownloadResponse(string $filename, $resource, array $headers = null, array $options = null): ResponseInterface
|
||||
{
|
||||
// Required for IE, otherwise Content-Disposition may be ignored
|
||||
if (ini_get('zlib.output_compression')) {
|
||||
@ini_set('zlib.output_compression', 'Off');
|
||||
}
|
||||
|
||||
$headers = $headers ?? [];
|
||||
$options = $options ?? ['force_download' => true];
|
||||
|
||||
$file_parts = pathinfo($filename);
|
||||
|
||||
if (!isset($headers['Content-Type'])) {
|
||||
$mimetype = Utils::getMimeByExtension($file_parts['extension']);
|
||||
|
||||
$headers['Content-Type'] = $mimetype;
|
||||
}
|
||||
|
||||
// TODO: add multipart download support.
|
||||
//$headers['Accept-Ranges'] = 'bytes';
|
||||
|
||||
if (!empty($options['force_download'])) {
|
||||
$headers['Content-Disposition'] = 'attachment; filename="' . $file_parts['basename'] . '"';
|
||||
}
|
||||
|
||||
if (!isset($headers['Content-Length'])) {
|
||||
$realpath = realpath($filename);
|
||||
if ($realpath) {
|
||||
$headers['Content-Length'] = filesize($realpath);
|
||||
}
|
||||
}
|
||||
|
||||
$headers += [
|
||||
'Expires' => 'Mon, 26 Jul 1997 05:00:00 GMT',
|
||||
'Last-Modified' => gmdate('D, d M Y H:i:s') . ' GMT',
|
||||
'Cache-Control' => 'no-store, no-cache, must-revalidate',
|
||||
'Pragma' => 'no-cache'
|
||||
];
|
||||
|
||||
return new Response(200, $headers, $resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param int|null $code
|
||||
* @return Response
|
||||
*/
|
||||
protected function createRedirectResponse(string $url, int $code = null): ResponseInterface
|
||||
{
|
||||
if (null === $code || $code < 301 || $code > 307) {
|
||||
$code = (int)$this->getConfig()->get('system.pages.redirect_default_code', 302);
|
||||
}
|
||||
|
||||
$accept = $this->getAccept(['application/json', 'text/html']);
|
||||
|
||||
if ($accept === 'application/json') {
|
||||
return $this->createJsonResponse(['code' => $code, 'status' => 'redirect', 'redirect' => $url]);
|
||||
}
|
||||
|
||||
return new Response($code, ['Location' => $url]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Throwable $e
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
protected function createErrorResponse(Throwable $e): ResponseInterface
|
||||
{
|
||||
$response = $this->getErrorJson($e);
|
||||
$message = $response['message'];
|
||||
$code = $response['code'];
|
||||
$reason = $e instanceof RequestException ? $e->getHttpReason() : null;
|
||||
$accept = $this->getAccept(['application/json', 'text/html']);
|
||||
|
||||
$request = $this->getRequest();
|
||||
$context = $request->getAttributes();
|
||||
|
||||
/** @var Route $route */
|
||||
$route = $context['route'] ?? null;
|
||||
|
||||
$ext = $route ? $route->getExtension() : null;
|
||||
if ($ext !== 'json' && $accept === 'text/html') {
|
||||
$method = $request->getMethod();
|
||||
|
||||
// On POST etc, redirect back to the previous page.
|
||||
if ($method !== 'GET' && $method !== 'HEAD') {
|
||||
$this->setMessage($message, 'error');
|
||||
$referer = $request->getHeaderLine('Referer');
|
||||
|
||||
return $this->createRedirectResponse($referer, 303);
|
||||
}
|
||||
|
||||
// TODO: improve error page
|
||||
return $this->createHtmlResponse($response['message'], $code);
|
||||
}
|
||||
|
||||
return new Response($code, ['Content-Type' => 'application/json'], json_encode($response), '1.1', $reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Throwable $e
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
protected function createJsonErrorResponse(Throwable $e): ResponseInterface
|
||||
{
|
||||
$response = $this->getErrorJson($e);
|
||||
$reason = $e instanceof RequestException ? $e->getHttpReason() : null;
|
||||
|
||||
return new Response($response['code'], ['Content-Type' => 'application/json'], json_encode($response), '1.1', $reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Throwable $e
|
||||
* @return array
|
||||
*/
|
||||
protected function getErrorJson(Throwable $e): array
|
||||
{
|
||||
$code = $this->getErrorCode($e instanceof RequestException ? $e->getHttpCode() : $e->getCode());
|
||||
$message = $e->getMessage();
|
||||
$response = [
|
||||
'code' => $code,
|
||||
'status' => 'error',
|
||||
'message' => $message,
|
||||
'error' => [
|
||||
'code' => $code,
|
||||
'message' => $message
|
||||
]
|
||||
];
|
||||
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = Grav::instance()['debugger'];
|
||||
if ($debugger->enabled()) {
|
||||
$response['error'] += [
|
||||
'type' => get_class($e),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'trace' => explode("\n", $e->getTraceAsString())
|
||||
];
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $code
|
||||
* @return int
|
||||
*/
|
||||
protected function getErrorCode(int $code): int
|
||||
{
|
||||
static $errorCodes = [
|
||||
400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418,
|
||||
422, 423, 424, 425, 426, 428, 429, 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 511
|
||||
];
|
||||
|
||||
if (!in_array($code, $errorCodes, true)) {
|
||||
$code = 500;
|
||||
}
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $compare
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getAccept(array $compare)
|
||||
{
|
||||
$accepted = [];
|
||||
foreach ($this->getRequest()->getHeader('Accept') as $accept) {
|
||||
foreach (explode(',', $accept) as $item) {
|
||||
if (!$item) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$split = explode(';q=', $item);
|
||||
$mime = array_shift($split);
|
||||
$priority = array_shift($split) ?? 1.0;
|
||||
|
||||
$accepted[$mime] = $priority;
|
||||
}
|
||||
}
|
||||
|
||||
arsort($accepted);
|
||||
|
||||
// TODO: add support for image/* etc
|
||||
$list = array_intersect($compare, array_keys($accepted));
|
||||
if (!$list && (isset($accepted['*/*']) || isset($accepted['*']))) {
|
||||
return reset($compare);
|
||||
}
|
||||
|
||||
return reset($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ServerRequestInterface
|
||||
*/
|
||||
abstract protected function getRequest(): ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* @param string $message
|
||||
* @param string $type
|
||||
* @return $this
|
||||
*/
|
||||
abstract protected function setMessage(string $message, string $type = 'info');
|
||||
|
||||
/**
|
||||
* @return Config
|
||||
*/
|
||||
abstract protected function getConfig(): Config;
|
||||
}
|
||||
35
system/src/Grav/Framework/DI/Container.php
Normal file
35
system/src/Grav/Framework/DI/Container.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\DI
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Grav\Framework\DI;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
class Container extends \Pimple\Container implements ContainerInterface
|
||||
{
|
||||
/**
|
||||
* @param string $id
|
||||
* @return mixed
|
||||
*/
|
||||
public function get($id)
|
||||
{
|
||||
return $this->offsetGet($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @return bool
|
||||
*/
|
||||
public function has($id): bool
|
||||
{
|
||||
return $this->offsetExists($id);
|
||||
}
|
||||
}
|
||||
441
system/src/Grav/Framework/File/AbstractFile.php
Normal file
441
system/src/Grav/Framework/File/AbstractFile.php
Normal file
@@ -0,0 +1,441 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\File
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\File;
|
||||
|
||||
use Exception;
|
||||
use Grav\Framework\Compat\Serializable;
|
||||
use Grav\Framework\File\Interfaces\FileInterface;
|
||||
use Grav\Framework\Filesystem\Filesystem;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Class AbstractFile
|
||||
* @package Grav\Framework\File
|
||||
*/
|
||||
class AbstractFile implements FileInterface
|
||||
{
|
||||
use Serializable;
|
||||
|
||||
/** @var Filesystem */
|
||||
private $filesystem;
|
||||
/** @var string */
|
||||
private $filepath;
|
||||
/** @var string|null */
|
||||
private $filename;
|
||||
/** @var string|null */
|
||||
private $path;
|
||||
/** @var string|null */
|
||||
private $basename;
|
||||
/** @var string|null */
|
||||
private $extension;
|
||||
/** @var resource|null */
|
||||
private $handle;
|
||||
/** @var bool */
|
||||
private $locked = false;
|
||||
|
||||
/**
|
||||
* @param string $filepath
|
||||
* @param Filesystem|null $filesystem
|
||||
*/
|
||||
public function __construct(string $filepath, Filesystem $filesystem = null)
|
||||
{
|
||||
$this->filesystem = $filesystem ?? Filesystem::getInstance();
|
||||
$this->setFilepath($filepath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock file when the object gets destroyed.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->isLocked()) {
|
||||
$this->unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
$this->handle = null;
|
||||
$this->locked = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
final public function __serialize(): array
|
||||
{
|
||||
return ['filesystem_normalize' => $this->filesystem->getNormalization()] + $this->doSerialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
final public function __unserialize(array $data): void
|
||||
{
|
||||
$this->filesystem = Filesystem::getInstance($data['filesystem_normalize'] ?? null);
|
||||
|
||||
$this->doUnserialize($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileInterface::getFilePath()
|
||||
*/
|
||||
public function getFilePath(): string
|
||||
{
|
||||
return $this->filepath;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileInterface::getPath()
|
||||
*/
|
||||
public function getPath(): string
|
||||
{
|
||||
if (null === $this->path) {
|
||||
$this->setPathInfo();
|
||||
}
|
||||
|
||||
return $this->path ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileInterface::getFilename()
|
||||
*/
|
||||
public function getFilename(): string
|
||||
{
|
||||
if (null === $this->filename) {
|
||||
$this->setPathInfo();
|
||||
}
|
||||
|
||||
return $this->filename ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileInterface::getBasename()
|
||||
*/
|
||||
public function getBasename(): string
|
||||
{
|
||||
if (null === $this->basename) {
|
||||
$this->setPathInfo();
|
||||
}
|
||||
|
||||
return $this->basename ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileInterface::getExtension()
|
||||
*/
|
||||
public function getExtension(bool $withDot = false): string
|
||||
{
|
||||
if (null === $this->extension) {
|
||||
$this->setPathInfo();
|
||||
}
|
||||
|
||||
return ($withDot ? '.' : '') . $this->extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileInterface::exists()
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return is_file($this->filepath);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileInterface::getCreationTime()
|
||||
*/
|
||||
public function getCreationTime(): int
|
||||
{
|
||||
return is_file($this->filepath) ? (int)filectime($this->filepath) : time();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileInterface::getModificationTime()
|
||||
*/
|
||||
public function getModificationTime(): int
|
||||
{
|
||||
return is_file($this->filepath) ? (int)filemtime($this->filepath) : time();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileInterface::lock()
|
||||
*/
|
||||
public function lock(bool $block = true): bool
|
||||
{
|
||||
if (!$this->handle) {
|
||||
if (!$this->mkdir($this->getPath())) {
|
||||
throw new RuntimeException('Creating directory failed for ' . $this->filepath);
|
||||
}
|
||||
$this->handle = @fopen($this->filepath, 'cb+') ?: null;
|
||||
if (!$this->handle) {
|
||||
$error = error_get_last();
|
||||
|
||||
throw new RuntimeException("Opening file for writing failed on error {$error['message']}");
|
||||
}
|
||||
}
|
||||
|
||||
$lock = $block ? LOCK_EX : LOCK_EX | LOCK_NB;
|
||||
|
||||
// Some filesystems do not support file locks, only fail if another process holds the lock.
|
||||
$this->locked = flock($this->handle, $lock, $wouldblock) || !$wouldblock;
|
||||
|
||||
return $this->locked;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileInterface::unlock()
|
||||
*/
|
||||
public function unlock(): bool
|
||||
{
|
||||
if (!$this->handle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->locked) {
|
||||
flock($this->handle, LOCK_UN | LOCK_NB);
|
||||
$this->locked = false;
|
||||
}
|
||||
|
||||
fclose($this->handle);
|
||||
$this->handle = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileInterface::isLocked()
|
||||
*/
|
||||
public function isLocked(): bool
|
||||
{
|
||||
return $this->locked;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileInterface::isReadable()
|
||||
*/
|
||||
public function isReadable(): bool
|
||||
{
|
||||
return is_readable($this->filepath) && is_file($this->filepath);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileInterface::isWritable()
|
||||
*/
|
||||
public function isWritable(): bool
|
||||
{
|
||||
if (!file_exists($this->filepath)) {
|
||||
return $this->isWritablePath($this->getPath());
|
||||
}
|
||||
|
||||
return is_writable($this->filepath) && is_file($this->filepath);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileInterface::load()
|
||||
*/
|
||||
public function load()
|
||||
{
|
||||
return file_get_contents($this->filepath);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileInterface::save()
|
||||
*/
|
||||
public function save($data): void
|
||||
{
|
||||
$filepath = $this->filepath;
|
||||
$dir = $this->getPath();
|
||||
|
||||
if (!$this->mkdir($dir)) {
|
||||
throw new RuntimeException('Creating directory failed for ' . $filepath);
|
||||
}
|
||||
|
||||
try {
|
||||
if ($this->handle) {
|
||||
$tmp = true;
|
||||
// As we are using non-truncating locking, make sure that the file is empty before writing.
|
||||
if (@ftruncate($this->handle, 0) === false || @fwrite($this->handle, $data) === false) {
|
||||
// Writing file failed, throw an error.
|
||||
$tmp = false;
|
||||
}
|
||||
} else {
|
||||
// Support for symlinks.
|
||||
$realpath = is_link($filepath) ? realpath($filepath) : $filepath;
|
||||
if ($realpath === false) {
|
||||
throw new RuntimeException('Failed to save file ' . $filepath);
|
||||
}
|
||||
|
||||
// Create file with a temporary name and rename it to make the save action atomic.
|
||||
$tmp = $this->tempname($realpath);
|
||||
if (@file_put_contents($tmp, $data) === false) {
|
||||
$tmp = false;
|
||||
} elseif (@rename($tmp, $realpath) === false) {
|
||||
@unlink($tmp);
|
||||
$tmp = false;
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$tmp = false;
|
||||
}
|
||||
|
||||
if ($tmp === false) {
|
||||
throw new RuntimeException('Failed to save file ' . $filepath);
|
||||
}
|
||||
|
||||
// Touch the directory as well, thus marking it modified.
|
||||
@touch($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileInterface::rename()
|
||||
*/
|
||||
public function rename(string $path): bool
|
||||
{
|
||||
if ($this->exists() && !@rename($this->filepath, $path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->setFilepath($path);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileInterface::delete()
|
||||
*/
|
||||
public function delete(): bool
|
||||
{
|
||||
return @unlink($this->filepath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $dir
|
||||
* @return bool
|
||||
* @throws RuntimeException
|
||||
* @internal
|
||||
*/
|
||||
protected function mkdir(string $dir): bool
|
||||
{
|
||||
// Silence error for open_basedir; should fail in mkdir instead.
|
||||
if (@is_dir($dir)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$success = @mkdir($dir, 0777, true);
|
||||
|
||||
if (!$success) {
|
||||
// Take yet another look, make sure that the folder doesn't exist.
|
||||
clearstatcache(true, $dir);
|
||||
if (!@is_dir($dir)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function doSerialize(): array
|
||||
{
|
||||
return [
|
||||
'filepath' => $this->filepath
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $serialized
|
||||
* @return void
|
||||
*/
|
||||
protected function doUnserialize(array $serialized): void
|
||||
{
|
||||
$this->setFilepath($serialized['filepath']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filepath
|
||||
*/
|
||||
protected function setFilepath(string $filepath): void
|
||||
{
|
||||
$this->filepath = $filepath;
|
||||
$this->filename = null;
|
||||
$this->basename = null;
|
||||
$this->path = null;
|
||||
$this->extension = null;
|
||||
}
|
||||
|
||||
protected function setPathInfo(): void
|
||||
{
|
||||
/** @var array $pathInfo */
|
||||
$pathInfo = $this->filesystem->pathinfo($this->filepath);
|
||||
|
||||
$this->filename = $pathInfo['filename'] ?? null;
|
||||
$this->basename = $pathInfo['basename'] ?? null;
|
||||
$this->path = $pathInfo['dirname'] ?? null;
|
||||
$this->extension = $pathInfo['extension'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $dir
|
||||
* @return bool
|
||||
* @internal
|
||||
*/
|
||||
protected function isWritablePath(string $dir): bool
|
||||
{
|
||||
if ($dir === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!file_exists($dir)) {
|
||||
// Recursively look up in the directory tree.
|
||||
return $this->isWritablePath($this->filesystem->parent($dir));
|
||||
}
|
||||
|
||||
return is_dir($dir) && is_writable($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filename
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
protected function tempname(string $filename, int $length = 5)
|
||||
{
|
||||
do {
|
||||
$test = $filename . substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, $length);
|
||||
} while (file_exists($test));
|
||||
|
||||
return $test;
|
||||
}
|
||||
}
|
||||
31
system/src/Grav/Framework/File/CsvFile.php
Normal file
31
system/src/Grav/Framework/File/CsvFile.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\File
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\File;
|
||||
|
||||
use Grav\Framework\File\Formatter\CsvFormatter;
|
||||
|
||||
/**
|
||||
* Class IniFile
|
||||
* @package RocketTheme\Toolbox\File
|
||||
*/
|
||||
class CsvFile extends DataFile
|
||||
{
|
||||
/**
|
||||
* File constructor.
|
||||
* @param string $filepath
|
||||
* @param CsvFormatter $formatter
|
||||
*/
|
||||
public function __construct($filepath, CsvFormatter $formatter)
|
||||
{
|
||||
parent::__construct($filepath, $formatter);
|
||||
}
|
||||
}
|
||||
78
system/src/Grav/Framework/File/DataFile.php
Normal file
78
system/src/Grav/Framework/File/DataFile.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\File
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\File;
|
||||
|
||||
use Grav\Framework\File\Interfaces\FileFormatterInterface;
|
||||
use RuntimeException;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Class DataFile
|
||||
* @package Grav\Framework\File
|
||||
*/
|
||||
class DataFile extends AbstractFile
|
||||
{
|
||||
/** @var FileFormatterInterface */
|
||||
protected $formatter;
|
||||
|
||||
/**
|
||||
* File constructor.
|
||||
* @param string $filepath
|
||||
* @param FileFormatterInterface $formatter
|
||||
*/
|
||||
public function __construct($filepath, FileFormatterInterface $formatter)
|
||||
{
|
||||
parent::__construct($filepath);
|
||||
|
||||
$this->formatter = $formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileInterface::load()
|
||||
*/
|
||||
public function load()
|
||||
{
|
||||
$raw = parent::load();
|
||||
|
||||
try {
|
||||
if (!is_string($raw)) {
|
||||
throw new RuntimeException('Bad Data');
|
||||
}
|
||||
|
||||
return $this->formatter->decode($raw);
|
||||
} catch (RuntimeException $e) {
|
||||
throw new RuntimeException(sprintf("Failed to load file '%s': %s", $this->getFilePath(), $e->getMessage()), $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileInterface::save()
|
||||
*/
|
||||
public function save($data): void
|
||||
{
|
||||
if (is_string($data)) {
|
||||
// Make sure that the string is valid data.
|
||||
try {
|
||||
$this->formatter->decode($data);
|
||||
} catch (RuntimeException $e) {
|
||||
throw new RuntimeException(sprintf("Failed to save file '%s': %s", $this->getFilePath(), $e->getMessage()), $e->getCode(), $e);
|
||||
}
|
||||
$encoded = $data;
|
||||
} else {
|
||||
$encoded = $this->formatter->encode($data);
|
||||
}
|
||||
|
||||
parent::save($encoded);
|
||||
}
|
||||
}
|
||||
44
system/src/Grav/Framework/File/File.php
Normal file
44
system/src/Grav/Framework/File/File.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\File
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\File;
|
||||
|
||||
use RuntimeException;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Class File
|
||||
* @package Grav\Framework\File
|
||||
*/
|
||||
class File extends AbstractFile
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileInterface::load()
|
||||
*/
|
||||
public function load()
|
||||
{
|
||||
return parent::load();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileInterface::save()
|
||||
*/
|
||||
public function save($data): void
|
||||
{
|
||||
if (!is_string($data)) {
|
||||
throw new RuntimeException('Cannot save data, string required');
|
||||
}
|
||||
|
||||
parent::save($data);
|
||||
}
|
||||
}
|
||||
117
system/src/Grav/Framework/File/Formatter/AbstractFormatter.php
Normal file
117
system/src/Grav/Framework/File/Formatter/AbstractFormatter.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\File\Formatter
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\File\Formatter;
|
||||
|
||||
use Grav\Framework\Compat\Serializable;
|
||||
use Grav\Framework\File\Interfaces\FileFormatterInterface;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Abstract file formatter.
|
||||
*
|
||||
* @package Grav\Framework\File\Formatter
|
||||
*/
|
||||
abstract class AbstractFormatter implements FileFormatterInterface
|
||||
{
|
||||
use Serializable;
|
||||
|
||||
/** @var array */
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* IniFormatter constructor.
|
||||
* @param array $config
|
||||
*/
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getMimeType(): string
|
||||
{
|
||||
$mime = $this->getConfig('mime');
|
||||
|
||||
return is_string($mime) ? $mime : 'application/octet-stream';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileFormatterInterface::getDefaultFileExtension()
|
||||
*/
|
||||
public function getDefaultFileExtension(): string
|
||||
{
|
||||
$extensions = $this->getSupportedFileExtensions();
|
||||
|
||||
// Call fails on bad configuration.
|
||||
return reset($extensions) ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileFormatterInterface::getSupportedFileExtensions()
|
||||
*/
|
||||
public function getSupportedFileExtensions(): array
|
||||
{
|
||||
$extensions = $this->getConfig('file_extension');
|
||||
|
||||
// Call fails on bad configuration.
|
||||
return is_string($extensions) ? [$extensions] : $extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileFormatterInterface::encode()
|
||||
*/
|
||||
abstract public function encode($data): string;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileFormatterInterface::decode()
|
||||
*/
|
||||
abstract public function decode($data);
|
||||
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function __serialize(): array
|
||||
{
|
||||
return ['config' => $this->config];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
$this->config = $data['config'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get either full configuration or a single option.
|
||||
*
|
||||
* @param string|null $name Configuration option (optional)
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getConfig(string $name = null)
|
||||
{
|
||||
if (null !== $name) {
|
||||
return $this->config[$name] ?? null;
|
||||
}
|
||||
|
||||
return $this->config;
|
||||
}
|
||||
}
|
||||
169
system/src/Grav/Framework/File/Formatter/CsvFormatter.php
Normal file
169
system/src/Grav/Framework/File/Formatter/CsvFormatter.php
Normal file
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\File\Formatter
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\File\Formatter;
|
||||
|
||||
use Exception;
|
||||
use Grav\Framework\File\Interfaces\FileFormatterInterface;
|
||||
use JsonSerializable;
|
||||
use RuntimeException;
|
||||
use stdClass;
|
||||
use function is_array;
|
||||
use function is_object;
|
||||
use function is_scalar;
|
||||
|
||||
/**
|
||||
* Class CsvFormatter
|
||||
* @package Grav\Framework\File\Formatter
|
||||
*/
|
||||
class CsvFormatter extends AbstractFormatter
|
||||
{
|
||||
/**
|
||||
* IniFormatter constructor.
|
||||
* @param array $config
|
||||
*/
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$config += [
|
||||
'file_extension' => ['.csv', '.tsv'],
|
||||
'delimiter' => ',',
|
||||
'mime' => 'text/x-csv'
|
||||
];
|
||||
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns delimiter used to both encode and decode CSV.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDelimiter(): string
|
||||
{
|
||||
// Call fails on bad configuration.
|
||||
return $this->getConfig('delimiter');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param string|null $delimiter
|
||||
* @return string
|
||||
* @see FileFormatterInterface::encode()
|
||||
*/
|
||||
public function encode($data, $delimiter = null): string
|
||||
{
|
||||
if (count($data) === 0) {
|
||||
return '';
|
||||
}
|
||||
$delimiter = $delimiter ?? $this->getDelimiter();
|
||||
$header = array_keys(reset($data));
|
||||
|
||||
// Encode the field names
|
||||
$string = $this->encodeLine($header, $delimiter);
|
||||
|
||||
// Encode the data
|
||||
foreach ($data as $row) {
|
||||
$string .= $this->encodeLine($row, $delimiter);
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $data
|
||||
* @param string|null $delimiter
|
||||
* @return array
|
||||
* @see FileFormatterInterface::decode()
|
||||
*/
|
||||
public function decode($data, $delimiter = null): array
|
||||
{
|
||||
$delimiter = $delimiter ?? $this->getDelimiter();
|
||||
$lines = preg_split('/\r\n|\r|\n/', $data);
|
||||
if ($lines === false) {
|
||||
throw new RuntimeException('Decoding CSV failed');
|
||||
}
|
||||
|
||||
// Get the field names
|
||||
$headerStr = array_shift($lines);
|
||||
if (!$headerStr) {
|
||||
throw new RuntimeException('CSV header missing');
|
||||
}
|
||||
|
||||
$header = str_getcsv($headerStr, $delimiter);
|
||||
|
||||
// Allow for replacing a null string with null/empty value
|
||||
$null_replace = $this->getConfig('null');
|
||||
|
||||
// Get the data
|
||||
$list = [];
|
||||
$line = null;
|
||||
try {
|
||||
foreach ($lines as $line) {
|
||||
if (!empty($line)) {
|
||||
$csv_line = str_getcsv($line, $delimiter);
|
||||
|
||||
if ($null_replace) {
|
||||
array_walk($csv_line, static function (&$el) use ($null_replace) {
|
||||
$el = str_replace($null_replace, "\0", $el);
|
||||
});
|
||||
}
|
||||
|
||||
$list[] = array_combine($header, $csv_line);
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
throw new RuntimeException('Badly formatted CSV line: ' . $line);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $line
|
||||
* @param string $delimiter
|
||||
* @return string
|
||||
*/
|
||||
protected function encodeLine(array $line, string $delimiter): string
|
||||
{
|
||||
foreach ($line as $key => &$value) {
|
||||
// Oops, we need to convert the line to a string.
|
||||
if (!is_scalar($value)) {
|
||||
if (is_array($value) || $value instanceof JsonSerializable || $value instanceof stdClass) {
|
||||
$value = json_encode($value);
|
||||
} elseif (is_object($value)) {
|
||||
if (method_exists($value, 'toJson')) {
|
||||
$value = $value->toJson();
|
||||
} elseif (method_exists($value, 'toArray')) {
|
||||
$value = json_encode($value->toArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$value = $this->escape((string)$value);
|
||||
}
|
||||
unset($value);
|
||||
|
||||
return implode($delimiter, $line). "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
* @return string
|
||||
*/
|
||||
protected function escape(string $value)
|
||||
{
|
||||
if (preg_match('/[,"\r\n]/u', $value)) {
|
||||
$value = '"' . preg_replace('/"/', '""', $value) . '"';
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* @package Grav\Framework\File\Formatter
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\File\Formatter;
|
||||
|
||||
interface FormatterInterface
|
||||
use Grav\Framework\File\Interfaces\FileFormatterInterface;
|
||||
|
||||
/**
|
||||
* @deprecated 1.6 Use Grav\Framework\File\Interfaces\FileFormatterInterface instead
|
||||
*/
|
||||
interface FormatterInterface extends FileFormatterInterface
|
||||
{
|
||||
/**
|
||||
* Get default file extension from current formatter (with dot).
|
||||
*
|
||||
* Default file extension is the first defined extension.
|
||||
*
|
||||
* @return string File extension (can be empty).
|
||||
*/
|
||||
public function getDefaultFileExtension();
|
||||
|
||||
/**
|
||||
* Get file extensions supported by current formatter (with dot).
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSupportedFileExtensions();
|
||||
|
||||
/**
|
||||
* Encode data into a string.
|
||||
*
|
||||
* @param array $data
|
||||
* @return string
|
||||
*/
|
||||
public function encode($data);
|
||||
|
||||
/**
|
||||
* Decode a string into data.
|
||||
*
|
||||
* @param string $data
|
||||
* @return array
|
||||
*/
|
||||
public function decode($data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\File\Formatter
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\File\Formatter;
|
||||
|
||||
class IniFormatter implements FormatterInterface
|
||||
{
|
||||
/** @var array */
|
||||
private $config;
|
||||
use Grav\Framework\File\Interfaces\FileFormatterInterface;
|
||||
|
||||
/**
|
||||
* Class IniFormatter
|
||||
* @package Grav\Framework\File\Formatter
|
||||
*/
|
||||
class IniFormatter extends AbstractFormatter
|
||||
{
|
||||
/**
|
||||
* IniFormatter constructor.
|
||||
* @param array $config
|
||||
*/
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->config = $config + [
|
||||
'file_extension' => '.ini'
|
||||
];
|
||||
}
|
||||
$config += [
|
||||
'file_extension' => '.ini'
|
||||
];
|
||||
|
||||
/**
|
||||
* @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead.
|
||||
*/
|
||||
public function getFileExtension()
|
||||
{
|
||||
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getDefaultFileExtension() method instead', E_USER_DEPRECATED);
|
||||
|
||||
return $this->getDefaultFileExtension();
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileFormatterInterface::encode()
|
||||
*/
|
||||
public function getDefaultFileExtension()
|
||||
{
|
||||
$extensions = $this->getSupportedFileExtensions();
|
||||
|
||||
return (string) reset($extensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSupportedFileExtensions()
|
||||
{
|
||||
return (array) $this->config['file_extension'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function encode($data)
|
||||
public function encode($data): string
|
||||
{
|
||||
$string = '';
|
||||
foreach ($data as $key => $value) {
|
||||
$string .= $key . '="' . preg_replace(
|
||||
['/"/', '/\\\/', "/\t/", "/\n/", "/\r/"],
|
||||
['\"', '\\\\', '\t', '\n', '\r'],
|
||||
$value
|
||||
) . "\"\n";
|
||||
['/"/', '/\\\/', "/\t/", "/\n/", "/\r/"],
|
||||
['\"', '\\\\', '\t', '\n', '\r'],
|
||||
$value
|
||||
) . "\"\n";
|
||||
}
|
||||
|
||||
return $string;
|
||||
@@ -71,8 +52,9 @@ class IniFormatter implements FormatterInterface
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileFormatterInterface::decode()
|
||||
*/
|
||||
public function decode($data)
|
||||
public function decode($data): array
|
||||
{
|
||||
$decoded = @parse_ini_string($data);
|
||||
|
||||
|
||||
@@ -1,78 +1,163 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\File\Formatter
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\File\Formatter;
|
||||
|
||||
class JsonFormatter implements FormatterInterface
|
||||
use Grav\Framework\File\Interfaces\FileFormatterInterface;
|
||||
use RuntimeException;
|
||||
use function is_int;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Class JsonFormatter
|
||||
* @package Grav\Framework\File\Formatter
|
||||
*/
|
||||
class JsonFormatter extends AbstractFormatter
|
||||
{
|
||||
/** @var array */
|
||||
private $config;
|
||||
protected $encodeOptions = [
|
||||
'JSON_FORCE_OBJECT' => JSON_FORCE_OBJECT,
|
||||
'JSON_HEX_QUOT' => JSON_HEX_QUOT,
|
||||
'JSON_HEX_TAG' => JSON_HEX_TAG,
|
||||
'JSON_HEX_AMP' => JSON_HEX_AMP,
|
||||
'JSON_HEX_APOS' => JSON_HEX_APOS,
|
||||
'JSON_INVALID_UTF8_IGNORE' => JSON_INVALID_UTF8_IGNORE,
|
||||
'JSON_INVALID_UTF8_SUBSTITUTE' => JSON_INVALID_UTF8_SUBSTITUTE,
|
||||
'JSON_NUMERIC_CHECK' => JSON_NUMERIC_CHECK,
|
||||
'JSON_PARTIAL_OUTPUT_ON_ERROR' => JSON_PARTIAL_OUTPUT_ON_ERROR,
|
||||
'JSON_PRESERVE_ZERO_FRACTION' => JSON_PRESERVE_ZERO_FRACTION,
|
||||
'JSON_PRETTY_PRINT' => JSON_PRETTY_PRINT,
|
||||
'JSON_UNESCAPED_LINE_TERMINATORS' => JSON_UNESCAPED_LINE_TERMINATORS,
|
||||
'JSON_UNESCAPED_SLASHES' => JSON_UNESCAPED_SLASHES,
|
||||
'JSON_UNESCAPED_UNICODE' => JSON_UNESCAPED_UNICODE,
|
||||
//'JSON_THROW_ON_ERROR' => JSON_THROW_ON_ERROR // PHP 7.3
|
||||
];
|
||||
|
||||
/** @var array */
|
||||
protected $decodeOptions = [
|
||||
'JSON_BIGINT_AS_STRING' => JSON_BIGINT_AS_STRING,
|
||||
'JSON_INVALID_UTF8_IGNORE' => JSON_INVALID_UTF8_IGNORE,
|
||||
'JSON_INVALID_UTF8_SUBSTITUTE' => JSON_INVALID_UTF8_SUBSTITUTE,
|
||||
'JSON_OBJECT_AS_ARRAY' => JSON_OBJECT_AS_ARRAY,
|
||||
//'JSON_THROW_ON_ERROR' => JSON_THROW_ON_ERROR // PHP 7.3
|
||||
];
|
||||
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->config = $config + [
|
||||
$config += [
|
||||
'file_extension' => '.json',
|
||||
'encode_options' => 0,
|
||||
'decode_assoc' => true
|
||||
'decode_assoc' => true,
|
||||
'decode_depth' => 512,
|
||||
'decode_options' => 0
|
||||
];
|
||||
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead.
|
||||
* Returns options used in encode() function.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getFileExtension()
|
||||
public function getEncodeOptions(): int
|
||||
{
|
||||
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getDefaultFileExtension() method instead', E_USER_DEPRECATED);
|
||||
|
||||
return $this->getDefaultFileExtension();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDefaultFileExtension()
|
||||
{
|
||||
$extensions = $this->getSupportedFileExtensions();
|
||||
|
||||
return (string) reset($extensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSupportedFileExtensions()
|
||||
{
|
||||
return (array) $this->config['file_extension'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function encode($data)
|
||||
{
|
||||
$encoded = @json_encode($data, $this->config['encode_options']);
|
||||
|
||||
if ($encoded === false) {
|
||||
throw new \RuntimeException('Encoding JSON failed');
|
||||
$options = $this->getConfig('encode_options');
|
||||
if (!is_int($options)) {
|
||||
if (is_string($options)) {
|
||||
$list = preg_split('/[\s,|]+/', $options);
|
||||
$options = 0;
|
||||
foreach ($list as $option) {
|
||||
if (isset($this->encodeOptions[$option])) {
|
||||
$options += $this->encodeOptions[$option];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$options = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return $encoded;
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns options used in decode() function.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getDecodeOptions(): int
|
||||
{
|
||||
$options = $this->getConfig('decode_options');
|
||||
if (!is_int($options)) {
|
||||
if (is_string($options)) {
|
||||
$list = preg_split('/[\s,|]+/', $options);
|
||||
$options = 0;
|
||||
foreach ($list as $option) {
|
||||
if (isset($this->decodeOptions[$option])) {
|
||||
$options += $this->decodeOptions[$option];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$options = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns recursion depth used in decode() function.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getDecodeDepth(): int
|
||||
{
|
||||
return $this->getConfig('decode_depth');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if JSON objects will be converted into associative arrays.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getDecodeAssoc(): bool
|
||||
{
|
||||
return $this->getConfig('decode_assoc');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileFormatterInterface::encode()
|
||||
*/
|
||||
public function encode($data): string
|
||||
{
|
||||
$encoded = @json_encode($data, $this->getEncodeOptions());
|
||||
|
||||
if ($encoded === false && json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new RuntimeException('Encoding JSON failed: ' . json_last_error_msg());
|
||||
}
|
||||
|
||||
return $encoded ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileFormatterInterface::decode()
|
||||
*/
|
||||
public function decode($data)
|
||||
{
|
||||
$decoded = @json_decode($data, $this->config['decode_assoc']);
|
||||
$decoded = @json_decode($data, $this->getDecodeAssoc(), $this->getDecodeDepth(), $this->getDecodeOptions());
|
||||
|
||||
if ($decoded === false) {
|
||||
throw new \RuntimeException('Decoding JSON failed');
|
||||
if (null === $decoded && json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new RuntimeException('Decoding JSON failed: ' . json_last_error_msg());
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\File\Formatter
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\File\Formatter;
|
||||
|
||||
class MarkdownFormatter implements FormatterInterface
|
||||
use Grav\Framework\File\Interfaces\FileFormatterInterface;
|
||||
|
||||
/**
|
||||
* Class MarkdownFormatter
|
||||
* @package Grav\Framework\File\Formatter
|
||||
*/
|
||||
class MarkdownFormatter extends AbstractFormatter
|
||||
{
|
||||
/** @var array */
|
||||
private $config;
|
||||
/** @var FormatterInterface */
|
||||
/** @var FileFormatterInterface */
|
||||
private $headerFormatter;
|
||||
|
||||
public function __construct(array $config = [], FormatterInterface $headerFormatter = null)
|
||||
public function __construct(array $config = [], FileFormatterInterface $headerFormatter = null)
|
||||
{
|
||||
$this->config = $config + [
|
||||
$config += [
|
||||
'file_extension' => '.md',
|
||||
'header' => 'header',
|
||||
'body' => 'markdown',
|
||||
@@ -25,44 +32,59 @@ class MarkdownFormatter implements FormatterInterface
|
||||
'yaml' => ['inline' => 20]
|
||||
];
|
||||
|
||||
$this->headerFormatter = $headerFormatter ?: new YamlFormatter($this->config['yaml']);
|
||||
parent::__construct($config);
|
||||
|
||||
$this->headerFormatter = $headerFormatter ?? new YamlFormatter($config['yaml']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead.
|
||||
* Returns header field used in both encode() and decode().
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFileExtension()
|
||||
public function getHeaderField(): string
|
||||
{
|
||||
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getDefaultFileExtension() method instead', E_USER_DEPRECATED);
|
||||
return $this->getConfig('header');
|
||||
}
|
||||
|
||||
return $this->getDefaultFileExtension();
|
||||
/**
|
||||
* Returns body field used in both encode() and decode().
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBodyField(): string
|
||||
{
|
||||
return $this->getConfig('body');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns raw field used in both encode() and decode().
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRawField(): string
|
||||
{
|
||||
return $this->getConfig('raw');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns header formatter object used in both encode() and decode().
|
||||
*
|
||||
* @return FileFormatterInterface
|
||||
*/
|
||||
public function getHeaderFormatter(): FileFormatterInterface
|
||||
{
|
||||
return $this->headerFormatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileFormatterInterface::encode()
|
||||
*/
|
||||
public function getDefaultFileExtension()
|
||||
public function encode($data): string
|
||||
{
|
||||
$extensions = $this->getSupportedFileExtensions();
|
||||
|
||||
return (string) reset($extensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSupportedFileExtensions()
|
||||
{
|
||||
return (array) $this->config['file_extension'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function encode($data)
|
||||
{
|
||||
$headerVar = $this->config['header'];
|
||||
$bodyVar = $this->config['body'];
|
||||
$headerVar = $this->getHeaderField();
|
||||
$bodyVar = $this->getBodyField();
|
||||
|
||||
$header = isset($data[$headerVar]) ? (array) $data[$headerVar] : [];
|
||||
$body = isset($data[$bodyVar]) ? (string) $data[$bodyVar] : '';
|
||||
@@ -70,25 +92,30 @@ class MarkdownFormatter implements FormatterInterface
|
||||
// Create Markdown file with YAML header.
|
||||
$encoded = '';
|
||||
if ($header) {
|
||||
$encoded = "---\n" . trim($this->headerFormatter->encode($data['header'])) . "\n---\n\n";
|
||||
$encoded = "---\n" . trim($this->getHeaderFormatter()->encode($data['header'])) . "\n---\n\n";
|
||||
}
|
||||
$encoded .= $body;
|
||||
|
||||
// Normalize line endings to Unix style.
|
||||
$encoded = preg_replace("/(\r\n|\r)/", "\n", $encoded);
|
||||
$encoded = preg_replace("/(\r\n|\r)/u", "\n", $encoded);
|
||||
if (null === $encoded) {
|
||||
throw new \RuntimeException('Encoding markdown failed');
|
||||
}
|
||||
|
||||
return $encoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileFormatterInterface::decode()
|
||||
*/
|
||||
public function decode($data)
|
||||
public function decode($data): array
|
||||
{
|
||||
$headerVar = $this->config['header'];
|
||||
$bodyVar = $this->config['body'];
|
||||
$rawVar = $this->config['raw'];
|
||||
$headerVar = $this->getHeaderField();
|
||||
$bodyVar = $this->getBodyField();
|
||||
$rawVar = $this->getRawField();
|
||||
|
||||
// Define empty content
|
||||
$content = [
|
||||
$headerVar => [],
|
||||
$bodyVar => ''
|
||||
@@ -97,11 +124,14 @@ class MarkdownFormatter implements FormatterInterface
|
||||
$headerRegex = "/^---\n(.+?)\n---\n{0,}(.*)$/uis";
|
||||
|
||||
// Normalize line endings to Unix style.
|
||||
$data = preg_replace("/(\r\n|\r)/", "\n", $data);
|
||||
$data = preg_replace("/(\r\n|\r)/u", "\n", $data);
|
||||
if (null === $data) {
|
||||
throw new \RuntimeException('Decoding markdown failed');
|
||||
}
|
||||
|
||||
// Parse header.
|
||||
preg_match($headerRegex, ltrim($data), $matches);
|
||||
if(empty($matches)) {
|
||||
if (empty($matches)) {
|
||||
$content[$bodyVar] = $data;
|
||||
} else {
|
||||
// Normalize frontmatter.
|
||||
@@ -109,10 +139,22 @@ class MarkdownFormatter implements FormatterInterface
|
||||
if ($rawVar) {
|
||||
$content[$rawVar] = $frontmatter;
|
||||
}
|
||||
$content[$headerVar] = $this->headerFormatter->decode($frontmatter);
|
||||
$content[$headerVar] = $this->getHeaderFormatter()->decode($frontmatter);
|
||||
$content[$bodyVar] = $matches[2];
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
public function __serialize(): array
|
||||
{
|
||||
return parent::__serialize() + ['headerFormatter' => $this->headerFormatter];
|
||||
}
|
||||
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
parent::__unserialize($data);
|
||||
|
||||
$this->headerFormatter = $data['headerFormatter'] ?? new YamlFormatter(['inline' => 20]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\File\Formatter
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\File\Formatter;
|
||||
|
||||
class SerializeFormatter implements FormatterInterface
|
||||
{
|
||||
/** @var array */
|
||||
private $config;
|
||||
use Grav\Framework\File\Interfaces\FileFormatterInterface;
|
||||
use RuntimeException;
|
||||
use stdClass;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Class SerializeFormatter
|
||||
* @package Grav\Framework\File\Formatter
|
||||
*/
|
||||
class SerializeFormatter extends AbstractFormatter
|
||||
{
|
||||
/**
|
||||
* IniFormatter constructor.
|
||||
* @param array $config
|
||||
*/
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->config = $config + [
|
||||
'file_extension' => '.ser'
|
||||
];
|
||||
$config += [
|
||||
'file_extension' => '.ser',
|
||||
'decode_options' => ['allowed_classes' => [stdClass::class]]
|
||||
];
|
||||
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead.
|
||||
* Returns options used in decode().
|
||||
*
|
||||
* By default only allow stdClass class.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFileExtension()
|
||||
public function getOptions()
|
||||
{
|
||||
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getDefaultFileExtension() method instead', E_USER_DEPRECATED);
|
||||
|
||||
return $this->getDefaultFileExtension();
|
||||
return $this->getConfig('decode_options');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileFormatterInterface::encode()
|
||||
*/
|
||||
public function getDefaultFileExtension()
|
||||
{
|
||||
$extensions = $this->getSupportedFileExtensions();
|
||||
|
||||
return (string) reset($extensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSupportedFileExtensions()
|
||||
{
|
||||
return (array) $this->config['file_extension'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function encode($data)
|
||||
public function encode($data): string
|
||||
{
|
||||
return serialize($this->preserveLines($data, ["\n", "\r"], ['\\n', '\\r']));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileFormatterInterface::decode()
|
||||
*/
|
||||
public function decode($data)
|
||||
{
|
||||
$decoded = @unserialize($data);
|
||||
$classes = $this->getOptions()['allowed_classes'] ?? false;
|
||||
$decoded = @unserialize($data, ['allowed_classes' => $classes]);
|
||||
|
||||
if ($decoded === false) {
|
||||
throw new \RuntimeException('Decoding serialized data failed');
|
||||
if ($decoded === false && $data !== serialize(false)) {
|
||||
throw new RuntimeException('Decoding serialized data failed');
|
||||
}
|
||||
|
||||
return $this->preserveLines($decoded, ['\\n', '\\r'], ["\n", "\r"]);
|
||||
@@ -82,7 +82,7 @@ class SerializeFormatter implements FormatterInterface
|
||||
* @param array $replace
|
||||
* @return mixed
|
||||
*/
|
||||
protected function preserveLines($data, $search, $replace)
|
||||
protected function preserveLines($data, array $search, array $replace)
|
||||
{
|
||||
if (is_string($data)) {
|
||||
$data = str_replace($search, $replace, $data);
|
||||
@@ -95,4 +95,4 @@ class SerializeFormatter implements FormatterInterface
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,89 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\File\Formatter
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\File\Formatter;
|
||||
|
||||
use Grav\Framework\File\Interfaces\FileFormatterInterface;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Yaml\Exception\DumpException;
|
||||
use Symfony\Component\Yaml\Exception\ParseException;
|
||||
use Symfony\Component\Yaml\Yaml as YamlParser;
|
||||
use RocketTheme\Toolbox\Compat\Yaml\Yaml as FallbackYamlParser;
|
||||
use function function_exists;
|
||||
|
||||
class YamlFormatter implements FormatterInterface
|
||||
/**
|
||||
* Class YamlFormatter
|
||||
* @package Grav\Framework\File\Formatter
|
||||
*/
|
||||
class YamlFormatter extends AbstractFormatter
|
||||
{
|
||||
/** @var array */
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* YamlFormatter constructor.
|
||||
* @param array $config
|
||||
*/
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->config = $config + [
|
||||
$config += [
|
||||
'file_extension' => '.yaml',
|
||||
'inline' => 5,
|
||||
'indent' => 2,
|
||||
'native' => true,
|
||||
'compat' => true
|
||||
];
|
||||
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead.
|
||||
* @return int
|
||||
*/
|
||||
public function getFileExtension()
|
||||
public function getInlineOption(): int
|
||||
{
|
||||
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getDefaultFileExtension() method instead', E_USER_DEPRECATED);
|
||||
|
||||
return $this->getDefaultFileExtension();
|
||||
return $this->getConfig('inline');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @return int
|
||||
*/
|
||||
public function getDefaultFileExtension()
|
||||
public function getIndentOption(): int
|
||||
{
|
||||
$extensions = $this->getSupportedFileExtensions();
|
||||
|
||||
return (string) reset($extensions);
|
||||
return $this->getConfig('indent');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @return bool
|
||||
*/
|
||||
public function getSupportedFileExtensions()
|
||||
public function useNativeDecoder(): bool
|
||||
{
|
||||
return (array) $this->config['file_extension'];
|
||||
return $this->getConfig('native');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @return bool
|
||||
*/
|
||||
public function encode($data, $inline = null, $indent = null)
|
||||
public function useCompatibleDecoder(): bool
|
||||
{
|
||||
return $this->getConfig('compat');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param int|null $inline
|
||||
* @param int|null $indent
|
||||
* @return string
|
||||
* @see FileFormatterInterface::encode()
|
||||
*/
|
||||
public function encode($data, $inline = null, $indent = null): string
|
||||
{
|
||||
try {
|
||||
return (string) YamlParser::dump(
|
||||
return YamlParser::dump(
|
||||
$data,
|
||||
$inline ? (int) $inline : $this->config['inline'],
|
||||
$indent ? (int) $indent : $this->config['indent'],
|
||||
$inline ? (int) $inline : $this->getInlineOption(),
|
||||
$indent ? (int) $indent : $this->getIndentOption(),
|
||||
YamlParser::DUMP_EXCEPTION_ON_INVALID_TYPE
|
||||
);
|
||||
} catch (DumpException $e) {
|
||||
throw new \RuntimeException('Encoding YAML failed: ' . $e->getMessage(), 0, $e);
|
||||
throw new RuntimeException('Encoding YAML failed: ' . $e->getMessage(), 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FileFormatterInterface::decode()
|
||||
*/
|
||||
public function decode($data)
|
||||
public function decode($data): array
|
||||
{
|
||||
// Try native PECL YAML PHP extension first if available.
|
||||
if ($this->config['native'] && function_exists('yaml_parse')) {
|
||||
if (function_exists('yaml_parse') && $this->useNativeDecoder()) {
|
||||
// Safely decode YAML.
|
||||
$saved = @ini_get('yaml.decode_php');
|
||||
@ini_set('yaml.decode_php', 0);
|
||||
@ini_set('yaml.decode_php', '0');
|
||||
$decoded = @yaml_parse($data);
|
||||
@ini_set('yaml.decode_php', $saved);
|
||||
|
||||
@@ -95,11 +117,11 @@ class YamlFormatter implements FormatterInterface
|
||||
try {
|
||||
return (array) YamlParser::parse($data);
|
||||
} catch (ParseException $e) {
|
||||
if ($this->config['compat']) {
|
||||
if ($this->useCompatibleDecoder()) {
|
||||
return (array) FallbackYamlParser::parse($data);
|
||||
}
|
||||
|
||||
throw new \RuntimeException('Decoding YAML failed: ' . $e->getMessage(), 0, $e);
|
||||
throw new RuntimeException('Decoding YAML failed: ' . $e->getMessage(), 0, $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
31
system/src/Grav/Framework/File/IniFile.php
Normal file
31
system/src/Grav/Framework/File/IniFile.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\File
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\File;
|
||||
|
||||
use Grav\Framework\File\Formatter\IniFormatter;
|
||||
|
||||
/**
|
||||
* Class IniFile
|
||||
* @package RocketTheme\Toolbox\File
|
||||
*/
|
||||
class IniFile extends DataFile
|
||||
{
|
||||
/**
|
||||
* File constructor.
|
||||
* @param string $filepath
|
||||
* @param IniFormatter $formatter
|
||||
*/
|
||||
public function __construct($filepath, IniFormatter $formatter)
|
||||
{
|
||||
parent::__construct($filepath, $formatter);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\File
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\File\Interfaces;
|
||||
|
||||
use Serializable;
|
||||
|
||||
/**
|
||||
* Defines common interface for all file formatters.
|
||||
*
|
||||
* File formatters allow you to read and optionally write various file formats, such as:
|
||||
*
|
||||
* @used-by \Grav\Framework\File\Formatter\CsvFormatter CVS
|
||||
* @used-by \Grav\Framework\File\Formatter\JsonFormatter JSON
|
||||
* @used-by \Grav\Framework\File\Formatter\MarkdownFormatter Markdown
|
||||
* @used-by \Grav\Framework\File\Formatter\SerializeFormatter Serialized PHP
|
||||
* @used-by \Grav\Framework\File\Formatter\YamlFormatter YAML
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
interface FileFormatterInterface extends Serializable
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
* @since 1.7
|
||||
*/
|
||||
public function getMimeType(): string;
|
||||
|
||||
/**
|
||||
* Get default file extension from current formatter (with dot).
|
||||
*
|
||||
* Default file extension is the first defined extension.
|
||||
*
|
||||
* @return string Returns file extension (can be empty).
|
||||
* @api
|
||||
*/
|
||||
public function getDefaultFileExtension(): string;
|
||||
|
||||
/**
|
||||
* Get file extensions supported by current formatter (with dot).
|
||||
*
|
||||
* @return string[] Returns list of all supported file extensions.
|
||||
* @api
|
||||
*/
|
||||
public function getSupportedFileExtensions(): array;
|
||||
|
||||
/**
|
||||
* Encode data into a string.
|
||||
*
|
||||
* @param mixed $data Data to be encoded.
|
||||
* @return string Returns encoded data as a string.
|
||||
* @api
|
||||
*/
|
||||
public function encode($data): string;
|
||||
|
||||
/**
|
||||
* Decode a string into data.
|
||||
*
|
||||
* @param string $data String to be decoded.
|
||||
* @return mixed Returns decoded data.
|
||||
* @api
|
||||
*/
|
||||
public function decode($data);
|
||||
}
|
||||
180
system/src/Grav/Framework/File/Interfaces/FileInterface.php
Normal file
180
system/src/Grav/Framework/File/Interfaces/FileInterface.php
Normal file
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\File
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\File\Interfaces;
|
||||
|
||||
use RuntimeException;
|
||||
use Serializable;
|
||||
|
||||
/**
|
||||
* Defines common interface for all file readers.
|
||||
*
|
||||
* File readers allow you to read and optionally write files of various file formats, such as:
|
||||
*
|
||||
* @used-by \Grav\Framework\File\CsvFile CVS
|
||||
* @used-by \Grav\Framework\File\JsonFile JSON
|
||||
* @used-by \Grav\Framework\File\MarkdownFile Markdown
|
||||
* @used-by \Grav\Framework\File\SerializeFile Serialized PHP
|
||||
* @used-by \Grav\Framework\File\YamlFile YAML
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
interface FileInterface extends Serializable
|
||||
{
|
||||
/**
|
||||
* Get both path and filename of the file.
|
||||
*
|
||||
* @return string Returns path and filename in the filesystem. Can also be URI.
|
||||
* @api
|
||||
*/
|
||||
public function getFilePath(): string;
|
||||
|
||||
/**
|
||||
* Get path of the file.
|
||||
*
|
||||
* @return string Returns path in the filesystem. Can also be URI.
|
||||
* @api
|
||||
*/
|
||||
public function getPath(): string;
|
||||
|
||||
/**
|
||||
* Get filename of the file.
|
||||
*
|
||||
* @return string Returns name of the file.
|
||||
* @api
|
||||
*/
|
||||
public function getFilename(): string;
|
||||
|
||||
/**
|
||||
* Get basename of the file (filename without the associated file extension).
|
||||
*
|
||||
* @return string Returns basename of the file.
|
||||
* @api
|
||||
*/
|
||||
public function getBasename(): string;
|
||||
|
||||
/**
|
||||
* Get file extension of the file.
|
||||
*
|
||||
* @param bool $withDot If true, return file extension with beginning dot (.json).
|
||||
*
|
||||
* @return string Returns file extension of the file (can be empty).
|
||||
* @api
|
||||
*/
|
||||
public function getExtension(bool $withDot = false): string;
|
||||
|
||||
/**
|
||||
* Check if the file exits in the filesystem.
|
||||
*
|
||||
* @return bool Returns `true` if the filename exists and is a regular file, `false` otherwise.
|
||||
* @api
|
||||
*/
|
||||
public function exists(): bool;
|
||||
|
||||
/**
|
||||
* Get file creation time.
|
||||
*
|
||||
* @return int Returns Unix timestamp. If file does not exist, method returns current time.
|
||||
* @api
|
||||
*/
|
||||
public function getCreationTime(): int;
|
||||
|
||||
/**
|
||||
* Get file modification time.
|
||||
*
|
||||
* @return int Returns Unix timestamp. If file does not exist, method returns current time.
|
||||
* @api
|
||||
*/
|
||||
public function getModificationTime(): int;
|
||||
|
||||
/**
|
||||
* Lock file for writing. You need to manually call unlock().
|
||||
*
|
||||
* @param bool $block For non-blocking lock, set the parameter to `false`.
|
||||
*
|
||||
* @return bool Returns `true` if the file was successfully locked, `false` otherwise.
|
||||
* @throws RuntimeException
|
||||
* @api
|
||||
*/
|
||||
public function lock(bool $block = true): bool;
|
||||
|
||||
/**
|
||||
* Unlock file after writing.
|
||||
*
|
||||
* @return bool Returns `true` if the file was successfully unlocked, `false` otherwise.
|
||||
* @api
|
||||
*/
|
||||
public function unlock(): bool;
|
||||
|
||||
/**
|
||||
* Returns true if file has been locked by you for writing.
|
||||
*
|
||||
* @return bool Returns `true` if the file is locked, `false` otherwise.
|
||||
* @api
|
||||
*/
|
||||
public function isLocked(): bool;
|
||||
|
||||
/**
|
||||
* Check if file exists and can be read.
|
||||
*
|
||||
* @return bool Returns `true` if the file can be read, `false` otherwise.
|
||||
* @api
|
||||
*/
|
||||
public function isReadable(): bool;
|
||||
|
||||
/**
|
||||
* Check if file can be written.
|
||||
*
|
||||
* @return bool Returns `true` if the file can be written, `false` otherwise.
|
||||
* @api
|
||||
*/
|
||||
public function isWritable(): bool;
|
||||
|
||||
/**
|
||||
* (Re)Load a file and return file contents.
|
||||
*
|
||||
* @return string|array|object|false Returns file content or `false` if file couldn't be read.
|
||||
* @api
|
||||
*/
|
||||
public function load();
|
||||
|
||||
/**
|
||||
* Save file.
|
||||
*
|
||||
* See supported data format for each of the file format.
|
||||
*
|
||||
* @param mixed $data Data to be saved.
|
||||
*
|
||||
* @throws RuntimeException
|
||||
* @api
|
||||
*/
|
||||
public function save($data): void;
|
||||
|
||||
/**
|
||||
* Rename file in the filesystem if it exists.
|
||||
*
|
||||
* Target folder will be created if if did not exist.
|
||||
*
|
||||
* @param string $path New path and filename for the file. Can also be URI.
|
||||
*
|
||||
* @return bool Returns `true` if the file was successfully renamed, `false` otherwise.
|
||||
* @api
|
||||
*/
|
||||
public function rename(string $path): bool;
|
||||
|
||||
/**
|
||||
* Delete file from filesystem.
|
||||
*
|
||||
* @return bool Returns `true` if the file was successfully deleted, `false` otherwise.
|
||||
* @api
|
||||
*/
|
||||
public function delete(): bool;
|
||||
}
|
||||
31
system/src/Grav/Framework/File/JsonFile.php
Normal file
31
system/src/Grav/Framework/File/JsonFile.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\File
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\File;
|
||||
|
||||
use Grav\Framework\File\Formatter\JsonFormatter;
|
||||
|
||||
/**
|
||||
* Class JsonFile
|
||||
* @package Grav\Framework\File
|
||||
*/
|
||||
class JsonFile extends DataFile
|
||||
{
|
||||
/**
|
||||
* File constructor.
|
||||
* @param string $filepath
|
||||
* @param JsonFormatter $formatter
|
||||
*/
|
||||
public function __construct($filepath, JsonFormatter $formatter)
|
||||
{
|
||||
parent::__construct($filepath, $formatter);
|
||||
}
|
||||
}
|
||||
31
system/src/Grav/Framework/File/MarkdownFile.php
Normal file
31
system/src/Grav/Framework/File/MarkdownFile.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\File
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\File;
|
||||
|
||||
use Grav\Framework\File\Formatter\MarkdownFormatter;
|
||||
|
||||
/**
|
||||
* Class MarkdownFile
|
||||
* @package Grav\Framework\File
|
||||
*/
|
||||
class MarkdownFile extends DataFile
|
||||
{
|
||||
/**
|
||||
* File constructor.
|
||||
* @param string $filepath
|
||||
* @param MarkdownFormatter $formatter
|
||||
*/
|
||||
public function __construct($filepath, MarkdownFormatter $formatter)
|
||||
{
|
||||
parent::__construct($filepath, $formatter);
|
||||
}
|
||||
}
|
||||
31
system/src/Grav/Framework/File/YamlFile.php
Normal file
31
system/src/Grav/Framework/File/YamlFile.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\File
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\File;
|
||||
|
||||
use Grav\Framework\File\Formatter\YamlFormatter;
|
||||
|
||||
/**
|
||||
* Class YamlFile
|
||||
* @package Grav\Framework\File
|
||||
*/
|
||||
class YamlFile extends DataFile
|
||||
{
|
||||
/**
|
||||
* File constructor.
|
||||
* @param string $filepath
|
||||
* @param YamlFormatter $formatter
|
||||
*/
|
||||
public function __construct($filepath, YamlFormatter $formatter)
|
||||
{
|
||||
parent::__construct($filepath, $formatter);
|
||||
}
|
||||
}
|
||||
340
system/src/Grav/Framework/Filesystem/Filesystem.php
Normal file
340
system/src/Grav/Framework/Filesystem/Filesystem.php
Normal file
@@ -0,0 +1,340 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Filesystem
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Filesystem;
|
||||
|
||||
use Grav\Framework\Filesystem\Interfaces\FilesystemInterface;
|
||||
use RuntimeException;
|
||||
use function count;
|
||||
use function dirname;
|
||||
use function pathinfo;
|
||||
|
||||
/**
|
||||
* Class Filesystem
|
||||
* @package Grav\Framework\Filesystem
|
||||
*/
|
||||
class Filesystem implements FilesystemInterface
|
||||
{
|
||||
/** @var bool|null */
|
||||
private $normalize;
|
||||
|
||||
/** @var static|null */
|
||||
protected static $default;
|
||||
|
||||
/** @var static|null */
|
||||
protected static $unsafe;
|
||||
|
||||
/** @var static|null */
|
||||
protected static $safe;
|
||||
|
||||
/**
|
||||
* @param bool|null $normalize See $this->setNormalization()
|
||||
* @return Filesystem
|
||||
*/
|
||||
public static function getInstance(bool $normalize = null): Filesystem
|
||||
{
|
||||
if ($normalize === true) {
|
||||
$instance = &static::$safe;
|
||||
} elseif ($normalize === false) {
|
||||
$instance = &static::$unsafe;
|
||||
} else {
|
||||
$instance = &static::$default;
|
||||
}
|
||||
|
||||
if (null === $instance) {
|
||||
$instance = new static($normalize);
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Always use Filesystem::getInstance() instead.
|
||||
*
|
||||
* @param bool|null $normalize
|
||||
* @internal
|
||||
*/
|
||||
protected function __construct(bool $normalize = null)
|
||||
{
|
||||
$this->normalize = $normalize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set path normalization.
|
||||
*
|
||||
* Default option enables normalization for the streams only, but you can force the normalization to be either
|
||||
* on or off for every path. Disabling path normalization speeds up the calls, but may cause issues if paths were
|
||||
* not normalized.
|
||||
*
|
||||
* @param bool|null $normalize
|
||||
* @return Filesystem
|
||||
*/
|
||||
public function setNormalization(bool $normalize = null): self
|
||||
{
|
||||
return static::getInstance($normalize);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|null
|
||||
*/
|
||||
public function getNormalization(): ?bool
|
||||
{
|
||||
return $this->normalize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force all paths to be normalized.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function unsafe(): self
|
||||
{
|
||||
return static::getInstance(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force all paths not to be normalized (speeds up the calls if given paths are known to be normalized).
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function safe(): self
|
||||
{
|
||||
return static::getInstance(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FilesystemInterface::parent()
|
||||
*/
|
||||
public function parent(string $path, int $levels = 1): string
|
||||
{
|
||||
[$scheme, $path] = $this->getSchemeAndHierarchy($path);
|
||||
|
||||
if ($this->normalize !== false) {
|
||||
$path = $this->normalizePathPart($path);
|
||||
}
|
||||
|
||||
if ($path === '' || $path === '.') {
|
||||
return '';
|
||||
}
|
||||
|
||||
[$scheme, $parent] = $this->dirnameInternal($scheme, $path, $levels);
|
||||
|
||||
return $parent !== $path ? $this->toString($scheme, $parent) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FilesystemInterface::normalize()
|
||||
*/
|
||||
public function normalize(string $path): string
|
||||
{
|
||||
[$scheme, $path] = $this->getSchemeAndHierarchy($path);
|
||||
|
||||
$path = $this->normalizePathPart($path);
|
||||
|
||||
return $this->toString($scheme, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FilesystemInterface::basename()
|
||||
*/
|
||||
public function basename(string $path, ?string $suffix = null): string
|
||||
{
|
||||
return $suffix ? basename($path, $suffix) : basename($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FilesystemInterface::dirname()
|
||||
*/
|
||||
public function dirname(string $path, int $levels = 1): string
|
||||
{
|
||||
[$scheme, $path] = $this->getSchemeAndHierarchy($path);
|
||||
|
||||
if ($this->normalize || ($scheme && null === $this->normalize)) {
|
||||
$path = $this->normalizePathPart($path);
|
||||
}
|
||||
|
||||
[$scheme, $path] = $this->dirnameInternal($scheme, $path, $levels);
|
||||
|
||||
return $this->toString($scheme, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets full path with trailing slash.
|
||||
*
|
||||
* @param string $path
|
||||
* @param int $levels
|
||||
* @return string
|
||||
*/
|
||||
public function pathname(string $path, int $levels = 1): string
|
||||
{
|
||||
$path = $this->dirname($path, $levels);
|
||||
|
||||
return $path !== '.' ? $path . '/' : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FilesystemInterface::pathinfo()
|
||||
*/
|
||||
public function pathinfo(string $path, ?int $options = null)
|
||||
{
|
||||
[$scheme, $path] = $this->getSchemeAndHierarchy($path);
|
||||
|
||||
if ($this->normalize || ($scheme && null === $this->normalize)) {
|
||||
$path = $this->normalizePathPart($path);
|
||||
}
|
||||
|
||||
return $this->pathinfoInternal($scheme, $path, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $scheme
|
||||
* @param string $path
|
||||
* @param int $levels
|
||||
* @return array
|
||||
*/
|
||||
protected function dirnameInternal(?string $scheme, string $path, int $levels = 1): array
|
||||
{
|
||||
$path = dirname($path, $levels);
|
||||
|
||||
if (null !== $scheme && $path === '.') {
|
||||
return [$scheme, ''];
|
||||
}
|
||||
|
||||
// In Windows dirname() may return backslashes, fix that.
|
||||
if (DIRECTORY_SEPARATOR !== '/') {
|
||||
$path = str_replace('\\', '/', $path);
|
||||
}
|
||||
|
||||
return [$scheme, $path];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $scheme
|
||||
* @param string $path
|
||||
* @param int|null $options
|
||||
* @return array|string
|
||||
*/
|
||||
protected function pathinfoInternal(?string $scheme, string $path, ?int $options = null)
|
||||
{
|
||||
if ($options) {
|
||||
return pathinfo($path, $options);
|
||||
}
|
||||
|
||||
$info = pathinfo($path);
|
||||
|
||||
if (null !== $scheme) {
|
||||
$info['scheme'] = $scheme;
|
||||
$dirname = isset($info['dirname']) && $info['dirname'] !== '.' ? $info['dirname'] : null;
|
||||
|
||||
if (null !== $dirname) {
|
||||
// In Windows dirname may be using backslashes, fix that.
|
||||
if (DIRECTORY_SEPARATOR !== '/') {
|
||||
$dirname = str_replace('\\', '/', $dirname);
|
||||
}
|
||||
|
||||
$info['dirname'] = $scheme . '://' . $dirname;
|
||||
} else {
|
||||
$info = ['dirname' => $scheme . '://'] + $info;
|
||||
}
|
||||
}
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a 2-tuple of scheme (may be null) and hierarchical part of a filename (e.g. file:///tmp -> array(file, tmp)).
|
||||
*
|
||||
* @param string $filename
|
||||
* @return array
|
||||
*/
|
||||
protected function getSchemeAndHierarchy(string $filename): array
|
||||
{
|
||||
$components = explode('://', $filename, 2);
|
||||
|
||||
return 2 === count($components) ? $components : [null, $components[0]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $scheme
|
||||
* @param string $path
|
||||
* @return string
|
||||
*/
|
||||
protected function toString(?string $scheme, string $path): string
|
||||
{
|
||||
if ($scheme) {
|
||||
return $scheme . '://' . $path;
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @return string
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function normalizePathPart(string $path): string
|
||||
{
|
||||
// Quick check for empty path.
|
||||
if ($path === '' || $path === '.') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Quick check for root.
|
||||
if ($path === '/') {
|
||||
return '/';
|
||||
}
|
||||
|
||||
// If the last character is not '/' or any of '\', './', '//' and '..' are not found, path is clean and we're done.
|
||||
if ($path[-1] !== '/' && !preg_match('`(\\\\|\./|//|\.\.)`', $path)) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
// Convert backslashes
|
||||
$path = strtr($path, ['\\' => '/']);
|
||||
|
||||
$parts = explode('/', $path);
|
||||
|
||||
// Keep absolute paths.
|
||||
$root = '';
|
||||
if ($parts[0] === '') {
|
||||
$root = '/';
|
||||
array_shift($parts);
|
||||
}
|
||||
|
||||
$list = [];
|
||||
foreach ($parts as $i => $part) {
|
||||
// Remove empty parts: // and /./
|
||||
if ($part === '' || $part === '.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Resolve /../ by removing path part.
|
||||
if ($part === '..') {
|
||||
$test = array_pop($list);
|
||||
if ($test === null) {
|
||||
// Oops, user tried to access something outside of our root folder.
|
||||
throw new RuntimeException("Bad path {$path}");
|
||||
}
|
||||
} else {
|
||||
$list[] = $part;
|
||||
}
|
||||
}
|
||||
|
||||
// Build path back together.
|
||||
return $root . implode('/', $list);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Filesystem
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Filesystem\Interfaces;
|
||||
|
||||
use Grav\Framework\Filesystem\Filesystem;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Defines several stream-save filesystem actions.
|
||||
*
|
||||
* @used-by Filesystem
|
||||
* @since 1.6
|
||||
*/
|
||||
interface FilesystemInterface
|
||||
{
|
||||
/**
|
||||
* Get parent path. Empty path is returned if there are no segments remaining.
|
||||
*
|
||||
* Can be used recursively to get towards the root directory.
|
||||
*
|
||||
* @param string $path A filename or path, does not need to exist as a file.
|
||||
* @param int $levels The number of parent directories to go up (>= 1).
|
||||
* @return string Returns parent path.
|
||||
* @throws RuntimeException
|
||||
* @api
|
||||
*/
|
||||
public function parent(string $path, int $levels = 1): string;
|
||||
|
||||
/**
|
||||
* Normalize path by cleaning up `\`, `/./`, `//` and `/../`.
|
||||
*
|
||||
* @param string $path A filename or path, does not need to exist as a file.
|
||||
* @return string Returns normalized path.
|
||||
* @throws RuntimeException
|
||||
* @api
|
||||
*/
|
||||
public function normalize(string $path): string;
|
||||
|
||||
/**
|
||||
* Returns filename component of path.
|
||||
*
|
||||
* @param string $path A filename or path, does not need to exist as a file.
|
||||
* @param string|null $suffix If the filename ends in suffix this will also be cut off.
|
||||
* @return string
|
||||
* @api
|
||||
*/
|
||||
public function basename(string $path, ?string $suffix = null): string;
|
||||
|
||||
/**
|
||||
* Stream-safe `\dirname()` replacement.
|
||||
*
|
||||
* @see http://php.net/manual/en/function.dirname.php
|
||||
*
|
||||
* @param string $path A filename or path, does not need to exist as a file.
|
||||
* @param int $levels The number of parent directories to go up (>= 1).
|
||||
* @return string Returns path to the directory.
|
||||
* @throws RuntimeException
|
||||
* @api
|
||||
*/
|
||||
public function dirname(string $path, int $levels = 1): string;
|
||||
|
||||
/**
|
||||
* Stream-safe `\pathinfo()` replacement.
|
||||
*
|
||||
* @see http://php.net/manual/en/function.pathinfo.php
|
||||
*
|
||||
* @param string $path A filename or path, does not need to exist as a file.
|
||||
* @param int|null $options A PATHINFO_* constant.
|
||||
* @return array|string
|
||||
* @api
|
||||
*/
|
||||
public function pathinfo(string $path, ?int $options = null);
|
||||
}
|
||||
332
system/src/Grav/Framework/Flex/Flex.php
Normal file
332
system/src/Grav/Framework/Flex/Flex.php
Normal file
@@ -0,0 +1,332 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex;
|
||||
|
||||
use Grav\Common\Debugger;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
|
||||
use Grav\Framework\Object\ObjectCollection;
|
||||
use RuntimeException;
|
||||
use function count;
|
||||
use function is_array;
|
||||
|
||||
/**
|
||||
* Class Flex
|
||||
* @package Grav\Framework\Flex
|
||||
*/
|
||||
class Flex implements FlexInterface
|
||||
{
|
||||
/** @var array */
|
||||
protected $config;
|
||||
/** @var FlexDirectory[] */
|
||||
protected $types;
|
||||
|
||||
/**
|
||||
* Flex constructor.
|
||||
* @param array $types List of [type => blueprint file, ...]
|
||||
* @param array $config
|
||||
*/
|
||||
public function __construct(array $types, array $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->types = [];
|
||||
|
||||
foreach ($types as $type => $blueprint) {
|
||||
if (!file_exists($blueprint)) {
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = Grav::instance()['debugger'];
|
||||
$debugger->addMessage(sprintf('Flex: blueprint for flex type %s is missing', $type), 'error');
|
||||
|
||||
continue;
|
||||
}
|
||||
$this->addDirectoryType($type, $blueprint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param string $blueprint
|
||||
* @param array $config
|
||||
* @return $this
|
||||
*/
|
||||
public function addDirectoryType(string $type, string $blueprint, array $config = [])
|
||||
{
|
||||
$config = array_replace_recursive(['enabled' => true], $this->config ?? [], $config);
|
||||
|
||||
$this->types[$type] = new FlexDirectory($type, $blueprint, $config);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FlexDirectory $directory
|
||||
* @return $this
|
||||
*/
|
||||
public function addDirectory(FlexDirectory $directory)
|
||||
{
|
||||
$this->types[$directory->getFlexType()] = $directory;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @return bool
|
||||
*/
|
||||
public function hasDirectory(string $type): bool
|
||||
{
|
||||
return isset($this->types[$type]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string[]|null $types
|
||||
* @param bool $keepMissing
|
||||
* @return array<FlexDirectory|null>
|
||||
*/
|
||||
public function getDirectories(array $types = null, bool $keepMissing = false): array
|
||||
{
|
||||
if ($types === null) {
|
||||
return $this->types;
|
||||
}
|
||||
|
||||
// Return the directories in the given order.
|
||||
$directories = [];
|
||||
foreach ($types as $type) {
|
||||
$directories[$type] = $this->types[$type] ?? null;
|
||||
}
|
||||
|
||||
return $keepMissing ? $directories : array_filter($directories);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @return FlexDirectory|null
|
||||
*/
|
||||
public function getDirectory(string $type): ?FlexDirectory
|
||||
{
|
||||
return $this->types[$type] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param array|null $keys
|
||||
* @param string|null $keyField
|
||||
* @return FlexCollectionInterface|null
|
||||
*/
|
||||
public function getCollection(string $type, array $keys = null, string $keyField = null): ?FlexCollectionInterface
|
||||
{
|
||||
$directory = $type ? $this->getDirectory($type) : null;
|
||||
|
||||
return $directory ? $directory->getCollection($keys, $keyField) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $keys
|
||||
* @param array $options In addition to the options in getObjects(), following options can be passed:
|
||||
* collection_class: Class to be used to create the collection. Defaults to ObjectCollection.
|
||||
* @return FlexCollectionInterface
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function getMixedCollection(array $keys, array $options = []): FlexCollectionInterface
|
||||
{
|
||||
$collectionClass = $options['collection_class'] ?? ObjectCollection::class;
|
||||
if (!class_exists($collectionClass)) {
|
||||
throw new RuntimeException(sprintf('Cannot create collection: Class %s does not exist', $collectionClass));
|
||||
}
|
||||
|
||||
$objects = $this->getObjects($keys, $options);
|
||||
|
||||
return new $collectionClass($objects);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $keys
|
||||
* @param array $options Following optional options can be passed:
|
||||
* types: List of allowed types.
|
||||
* type: Allowed type if types isn't defined, otherwise acts as default_type.
|
||||
* default_type: Set default type for objects given without type (only used if key_field isn't set).
|
||||
* keep_missing: Set to true if you want to return missing objects as null.
|
||||
* key_field: Key field which is used to match the objects.
|
||||
* @return array
|
||||
*/
|
||||
public function getObjects(array $keys, array $options = []): array
|
||||
{
|
||||
$type = $options['type'] ?? null;
|
||||
$defaultType = $options['default_type'] ?? $type ?? null;
|
||||
$keyField = $options['key_field'] ?? 'flex_key';
|
||||
|
||||
// Prepare empty result lists for all requested Flex types.
|
||||
$types = $options['types'] ?? (array)$type ?: null;
|
||||
if ($types) {
|
||||
$types = array_fill_keys($types, []);
|
||||
}
|
||||
$strict = isset($types);
|
||||
|
||||
$guessed = [];
|
||||
if ($keyField === 'flex_key') {
|
||||
// We need to split Flex key lookups into individual directories.
|
||||
$undefined = [];
|
||||
$keyFieldFind = 'storage_key';
|
||||
|
||||
foreach ($keys as $flexKey) {
|
||||
if (!$flexKey) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$flexKey = (string)$flexKey;
|
||||
// Normalize key and type using fallback to default type if it was set.
|
||||
[$key, $type, $guess] = $this->resolveKeyAndType($flexKey, $defaultType);
|
||||
|
||||
if ($type === '' && $types) {
|
||||
// Add keys which are not associated to any Flex type. They will be included to every Flex type.
|
||||
foreach ($types as $type => &$array) {
|
||||
$array[] = $key;
|
||||
$guessed[$key][] = "{$type}.obj:{$key}";
|
||||
}
|
||||
unset($array);
|
||||
} elseif (!$strict || isset($types[$type])) {
|
||||
// Collect keys by their Flex type. If allowed types are defined, only include values from those types.
|
||||
$types[$type][] = $key;
|
||||
if ($guess) {
|
||||
$guessed[$key][] = "{$type}.obj:{$key}";
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We are using a specific key field, make every key undefined.
|
||||
$undefined = $keys;
|
||||
$keyFieldFind = $keyField;
|
||||
}
|
||||
|
||||
if (!$types) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$list = [[]];
|
||||
foreach ($types as $type => $typeKeys) {
|
||||
// Also remember to look up keys from undefined Flex types.
|
||||
$lookupKeys = $undefined ? array_merge($typeKeys, $undefined) : $typeKeys;
|
||||
|
||||
$collection = $this->getCollection($type, $lookupKeys, $keyFieldFind);
|
||||
if ($collection && $keyFieldFind !== $keyField) {
|
||||
$collection = $collection->withKeyField($keyField);
|
||||
}
|
||||
|
||||
$list[] = $collection ? $collection->toArray() : [];
|
||||
}
|
||||
|
||||
// Merge objects from individual types back together.
|
||||
$list = array_merge(...$list);
|
||||
|
||||
// Use the original key ordering.
|
||||
if (!$guessed) {
|
||||
$list = array_replace(array_fill_keys($keys, null), $list) ?? [];
|
||||
} else {
|
||||
// We have mixed keys, we need to map flex keys back to storage keys.
|
||||
$results = [];
|
||||
foreach ($keys as $key) {
|
||||
$flexKey = $guessed[$key] ?? $key;
|
||||
if (is_array($flexKey)) {
|
||||
$result = null;
|
||||
foreach ($flexKey as $tryKey) {
|
||||
if ($result = $list[$tryKey] ?? null) {
|
||||
// Use the first matching object (conflicting objects will be ignored for now).
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$result = $list[$flexKey] ?? null;
|
||||
}
|
||||
|
||||
$results[$key] = $result;
|
||||
}
|
||||
|
||||
$list = $results;
|
||||
}
|
||||
|
||||
// Remove missing objects if not asked to keep them.
|
||||
if (empty($option['keep_missing'])) {
|
||||
$list = array_filter($list);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param string|null $type
|
||||
* @param string|null $keyField
|
||||
* @return FlexObjectInterface|null
|
||||
*/
|
||||
public function getObject(string $key, string $type = null, string $keyField = null): ?FlexObjectInterface
|
||||
{
|
||||
if (null === $type && null === $keyField) {
|
||||
// Special handling for quick Flex key lookups.
|
||||
$keyField = 'storage_key';
|
||||
[$key, $type] = $this->resolveKeyAndType($key, $type);
|
||||
} else {
|
||||
$type = $this->resolveType($type);
|
||||
}
|
||||
|
||||
if ($type === '' || $key === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$directory = $this->getDirectory($type);
|
||||
|
||||
return $directory ? $directory->getObject($key, $keyField) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->types);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $flexKey
|
||||
* @param string|null $type
|
||||
* @return array
|
||||
*/
|
||||
protected function resolveKeyAndType(string $flexKey, string $type = null): array
|
||||
{
|
||||
$guess = false;
|
||||
if (strpos($flexKey, ':') !== false) {
|
||||
[$type, $key] = explode(':', $flexKey, 2);
|
||||
|
||||
$type = $this->resolveType($type);
|
||||
} else {
|
||||
$key = $flexKey;
|
||||
$type = (string)$type;
|
||||
$guess = true;
|
||||
}
|
||||
|
||||
return [$key, $type, $guess];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $type
|
||||
* @return string
|
||||
*/
|
||||
protected function resolveType(string $type = null): string
|
||||
{
|
||||
if (null !== $type && strpos($type, '.') !== false) {
|
||||
return preg_replace('|\.obj$|', '', $type) ?? $type;
|
||||
}
|
||||
|
||||
return $type ?? '';
|
||||
}
|
||||
}
|
||||
712
system/src/Grav/Framework/Flex/FlexCollection.php
Normal file
712
system/src/Grav/Framework/Flex/FlexCollection.php
Normal file
@@ -0,0 +1,712 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex;
|
||||
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Grav\Common\Debugger;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Inflector;
|
||||
use Grav\Common\Twig\Twig;
|
||||
use Grav\Common\User\Interfaces\UserInterface;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Framework\Cache\CacheInterface;
|
||||
use Grav\Framework\ContentBlock\HtmlBlock;
|
||||
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
|
||||
use Grav\Framework\Object\ObjectCollection;
|
||||
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
|
||||
use Psr\SimpleCache\InvalidArgumentException;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Template;
|
||||
use Twig\TemplateWrapper;
|
||||
use function array_filter;
|
||||
use function get_class;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function is_scalar;
|
||||
|
||||
/**
|
||||
* Class FlexCollection
|
||||
* @package Grav\Framework\Flex
|
||||
* @template T of FlexObjectInterface
|
||||
* @extends ObjectCollection<string,T>
|
||||
* @implements FlexCollectionInterface<T>
|
||||
*/
|
||||
class FlexCollection extends ObjectCollection implements FlexCollectionInterface
|
||||
{
|
||||
/** @var FlexDirectory */
|
||||
private $_flexDirectory;
|
||||
|
||||
/** @var string */
|
||||
private $_keyField;
|
||||
|
||||
/**
|
||||
* Get list of cached methods.
|
||||
*
|
||||
* @return array Returns a list of methods with their caching information.
|
||||
*/
|
||||
public static function getCachedMethods(): array
|
||||
{
|
||||
return [
|
||||
'getTypePrefix' => true,
|
||||
'getType' => true,
|
||||
'getFlexDirectory' => true,
|
||||
'hasFlexFeature' => true,
|
||||
'getFlexFeatures' => true,
|
||||
'getCacheKey' => true,
|
||||
'getCacheChecksum' => false,
|
||||
'getTimestamp' => true,
|
||||
'hasProperty' => true,
|
||||
'getProperty' => true,
|
||||
'hasNestedProperty' => true,
|
||||
'getNestedProperty' => true,
|
||||
'orderBy' => true,
|
||||
|
||||
'render' => false,
|
||||
'isAuthorized' => 'session',
|
||||
'search' => true,
|
||||
'sort' => true,
|
||||
'getDistinctValues' => true
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::createFromArray()
|
||||
*/
|
||||
public static function createFromArray(array $entries, FlexDirectory $directory, string $keyField = null)
|
||||
{
|
||||
$instance = new static($entries, $directory);
|
||||
$instance->setKeyField($keyField);
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::__construct()
|
||||
*/
|
||||
public function __construct(array $entries = [], FlexDirectory $directory = null)
|
||||
{
|
||||
// @phpstan-ignore-next-line
|
||||
if (get_class($this) === __CLASS__) {
|
||||
user_error('Using ' . __CLASS__ . ' directly is deprecated since Grav 1.7, use \Grav\Common\Flex\Types\Generic\GenericCollection or your own class instead', E_USER_DEPRECATED);
|
||||
}
|
||||
|
||||
parent::__construct($entries);
|
||||
|
||||
if ($directory) {
|
||||
$this->setFlexDirectory($directory)->setKey($directory->getFlexType());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCommonInterface::hasFlexFeature()
|
||||
*/
|
||||
public function hasFlexFeature(string $name): bool
|
||||
{
|
||||
return in_array($name, $this->getFlexFeatures(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCommonInterface::hasFlexFeature()
|
||||
*/
|
||||
public function getFlexFeatures(): array
|
||||
{
|
||||
$implements = class_implements($this);
|
||||
|
||||
$list = [];
|
||||
foreach ($implements as $interface) {
|
||||
if ($pos = strrpos($interface, '\\')) {
|
||||
$interface = substr($interface, $pos+1);
|
||||
}
|
||||
|
||||
$list[] = Inflector::hyphenize(str_replace('Interface', '', $interface));
|
||||
}
|
||||
|
||||
return $list;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::search()
|
||||
*/
|
||||
public function search(string $search, $properties = null, array $options = null)
|
||||
{
|
||||
$matching = $this->call('search', [$search, $properties, $options]);
|
||||
$matching = array_filter($matching);
|
||||
|
||||
if ($matching) {
|
||||
arsort($matching, SORT_NUMERIC);
|
||||
}
|
||||
|
||||
return $this->select(array_keys($matching));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::sort()
|
||||
*/
|
||||
public function sort(array $order)
|
||||
{
|
||||
$criteria = Criteria::create()->orderBy($order);
|
||||
|
||||
/** @var FlexCollectionInterface $matching */
|
||||
$matching = $this->matching($criteria);
|
||||
|
||||
return $matching;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $filters
|
||||
* @return FlexCollectionInterface|Collection
|
||||
*/
|
||||
public function filterBy(array $filters)
|
||||
{
|
||||
$expr = Criteria::expr();
|
||||
$criteria = Criteria::create();
|
||||
|
||||
foreach ($filters as $key => $value) {
|
||||
$criteria->andWhere($expr->eq($key, $value));
|
||||
}
|
||||
|
||||
return $this->matching($criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getFlexType()
|
||||
*/
|
||||
public function getFlexType(): string
|
||||
{
|
||||
return $this->_flexDirectory->getFlexType();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getFlexDirectory()
|
||||
*/
|
||||
public function getFlexDirectory(): FlexDirectory
|
||||
{
|
||||
return $this->_flexDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getTimestamp()
|
||||
*/
|
||||
public function getTimestamp(): int
|
||||
{
|
||||
$timestamps = $this->getTimestamps();
|
||||
|
||||
return $timestamps ? max($timestamps) : time();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getFlexDirectory()
|
||||
*/
|
||||
public function getCacheKey(): string
|
||||
{
|
||||
return $this->getTypePrefix() . $this->getFlexType() . '.' . sha1((string)json_encode($this->call('getKey')));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getFlexDirectory()
|
||||
*/
|
||||
public function getCacheChecksum(): string
|
||||
{
|
||||
$list = [];
|
||||
/**
|
||||
* @var string $key
|
||||
* @var FlexObjectInterface $object
|
||||
*/
|
||||
foreach ($this as $key => $object) {
|
||||
$list[$key] = $object->getCacheChecksum();
|
||||
}
|
||||
|
||||
return sha1((string)json_encode($list));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getFlexDirectory()
|
||||
*/
|
||||
public function getTimestamps(): array
|
||||
{
|
||||
/** @var int[] $timestamps */
|
||||
$timestamps = $this->call('getTimestamp');
|
||||
|
||||
return $timestamps;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getFlexDirectory()
|
||||
*/
|
||||
public function getStorageKeys(): array
|
||||
{
|
||||
/** @var string[] $keys */
|
||||
$keys = $this->call('getStorageKey');
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getFlexDirectory()
|
||||
*/
|
||||
public function getFlexKeys(): array
|
||||
{
|
||||
/** @var string[] $keys */
|
||||
$keys = $this->call('getFlexKey');
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the values in property.
|
||||
*
|
||||
* Supports either single scalar values or array of scalar values.
|
||||
*
|
||||
* @param string $property Object property to be used to make groups.
|
||||
* @param string|null $separator Separator, defaults to '.'
|
||||
* @return array
|
||||
*/
|
||||
public function getDistinctValues(string $property, string $separator = null): array
|
||||
{
|
||||
$list = [];
|
||||
|
||||
/** @var FlexObjectInterface $element */
|
||||
foreach ($this->getIterator() as $element) {
|
||||
$value = (array)$element->getNestedProperty($property, null, $separator);
|
||||
foreach ($value as $v) {
|
||||
if (is_scalar($v)) {
|
||||
$t = gettype($v) . (string)$v;
|
||||
$list[$t] = $v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::withKeyField()
|
||||
*/
|
||||
public function withKeyField(string $keyField = null)
|
||||
{
|
||||
$keyField = $keyField ?: 'key';
|
||||
if ($keyField === $this->getKeyField()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$entries = [];
|
||||
foreach ($this as $key => $object) {
|
||||
// TODO: remove hardcoded logic
|
||||
if ($keyField === 'storage_key') {
|
||||
$entries[$object->getStorageKey()] = $object;
|
||||
} elseif ($keyField === 'flex_key') {
|
||||
$entries[$object->getFlexKey()] = $object;
|
||||
} elseif ($keyField === 'key') {
|
||||
$entries[$object->getKey()] = $object;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createFrom($entries, $keyField);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getIndex()
|
||||
*/
|
||||
public function getIndex()
|
||||
{
|
||||
return $this->getFlexDirectory()->getIndex($this->getKeys(), $this->getKeyField());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc}
|
||||
* @see FlexCollectionInterface::getCollection()
|
||||
* @return $this
|
||||
*/
|
||||
public function getCollection()
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::render()
|
||||
*/
|
||||
public function render(string $layout = null, array $context = [])
|
||||
{
|
||||
if (!$layout) {
|
||||
$config = $this->getTemplateConfig();
|
||||
$layout = $config['collection']['defaults']['layout'] ?? 'default';
|
||||
}
|
||||
|
||||
$type = $this->getFlexType();
|
||||
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = $grav['debugger'];
|
||||
$debugger->startTimer('flex-collection-' . ($debugKey = uniqid($type, false)), 'Render Collection ' . $type . ' (' . $layout . ')');
|
||||
|
||||
$key = null;
|
||||
foreach ($context as $value) {
|
||||
if (!is_scalar($value)) {
|
||||
$key = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($key !== false) {
|
||||
$key = md5($this->getCacheKey() . '.' . $layout . json_encode($context));
|
||||
$cache = $this->getCache('render');
|
||||
} else {
|
||||
$cache = null;
|
||||
}
|
||||
|
||||
try {
|
||||
$data = $cache && $key ? $cache->get($key) : null;
|
||||
|
||||
$block = $data ? HtmlBlock::fromArray($data) : null;
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$debugger->addException($e);
|
||||
$block = null;
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$debugger->addException($e);
|
||||
$block = null;
|
||||
}
|
||||
|
||||
$checksum = $this->getCacheChecksum();
|
||||
if ($block && $checksum !== $block->getChecksum()) {
|
||||
$block = null;
|
||||
}
|
||||
|
||||
if (!$block) {
|
||||
$block = HtmlBlock::create($key ?: null);
|
||||
$block->setChecksum($checksum);
|
||||
if (!$key) {
|
||||
$block->disableCache();
|
||||
}
|
||||
|
||||
$event = new Event([
|
||||
'type' => 'flex',
|
||||
'directory' => $this->getFlexDirectory(),
|
||||
'collection' => $this,
|
||||
'layout' => &$layout,
|
||||
'context' => &$context
|
||||
]);
|
||||
$this->triggerEvent('onRender', $event);
|
||||
|
||||
$output = $this->getTemplate($layout)->render(
|
||||
[
|
||||
'grav' => $grav,
|
||||
'config' => $grav['config'],
|
||||
'block' => $block,
|
||||
'directory' => $this->getFlexDirectory(),
|
||||
'collection' => $this,
|
||||
'layout' => $layout
|
||||
] + $context
|
||||
);
|
||||
|
||||
if ($debugger->enabled()) {
|
||||
$output = "\n<!–– START {$type} collection ––>\n{$output}\n<!–– END {$type} collection ––>\n";
|
||||
}
|
||||
|
||||
$block->setContent($output);
|
||||
|
||||
try {
|
||||
$cache && $key && $block->isCached() && $cache->set($key, $block->toArray());
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$debugger->addException($e);
|
||||
}
|
||||
}
|
||||
|
||||
$debugger->stopTimer('flex-collection-' . $debugKey);
|
||||
|
||||
return $block;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FlexDirectory $type
|
||||
* @return $this
|
||||
*/
|
||||
public function setFlexDirectory(FlexDirectory $type)
|
||||
{
|
||||
$this->_flexDirectory = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return array
|
||||
*/
|
||||
public function getMetaData(string $key): array
|
||||
{
|
||||
$object = $this->get($key);
|
||||
|
||||
return $object instanceof FlexObjectInterface ? $object->getMetaData() : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $namespace
|
||||
* @return CacheInterface
|
||||
*/
|
||||
public function getCache(string $namespace = null)
|
||||
{
|
||||
return $this->_flexDirectory->getCache($namespace);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getKeyField(): string
|
||||
{
|
||||
return $this->_keyField ?? 'storage_key';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $action
|
||||
* @param string|null $scope
|
||||
* @param UserInterface|null $user
|
||||
* @return static
|
||||
* @phpstan-return static<T>
|
||||
*/
|
||||
public function isAuthorized(string $action, string $scope = null, UserInterface $user = null)
|
||||
{
|
||||
$list = $this->call('isAuthorized', [$action, $scope, $user]);
|
||||
$list = array_filter($list);
|
||||
|
||||
return $this->select(array_keys($list));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
* @param string $field
|
||||
* @return T|null
|
||||
*/
|
||||
public function find($value, $field = 'id')
|
||||
{
|
||||
if ($value) {
|
||||
foreach ($this as $element) {
|
||||
if (mb_strtolower($element->getProperty($field)) === mb_strtolower($value)) {
|
||||
return $element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
$elements = [];
|
||||
|
||||
/**
|
||||
* @var string $key
|
||||
* @var array|FlexObject $object
|
||||
*/
|
||||
foreach ($this->getElements() as $key => $object) {
|
||||
$elements[$key] = is_array($object) ? $object : $object->jsonSerialize();
|
||||
}
|
||||
|
||||
return $elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo()
|
||||
{
|
||||
return [
|
||||
'type:private' => $this->getFlexType(),
|
||||
'key:private' => $this->getKey(),
|
||||
'objects_key:private' => $this->getKeyField(),
|
||||
'objects:private' => $this->getElements()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance from the specified elements.
|
||||
*
|
||||
* This method is provided for derived classes to specify how a new
|
||||
* instance should be created when constructor semantics have changed.
|
||||
*
|
||||
* @param array $elements Elements.
|
||||
* @param string|null $keyField
|
||||
* @return static
|
||||
* @phpstan-return static<T>
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function createFrom(array $elements, $keyField = null)
|
||||
{
|
||||
$collection = new static($elements, $this->_flexDirectory);
|
||||
$collection->setKeyField($keyField ?: $this->_keyField);
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getTypePrefix(): string
|
||||
{
|
||||
return 'c.';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function getTemplateConfig(): array
|
||||
{
|
||||
$config = $this->getFlexDirectory()->getConfig('site.templates', []);
|
||||
$defaults = array_replace($config['defaults'] ?? [], $config['collection']['defaults'] ?? []);
|
||||
$config['collection']['defaults'] = $defaults;
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $layout
|
||||
* @return array
|
||||
*/
|
||||
protected function getTemplatePaths(string $layout): array
|
||||
{
|
||||
$config = $this->getTemplateConfig();
|
||||
$type = $this->getFlexType();
|
||||
$defaults = $config['collection']['defaults'] ?? [];
|
||||
|
||||
$ext = $defaults['ext'] ?? '.html.twig';
|
||||
$types = array_unique(array_merge([$type], (array)($defaults['type'] ?? null)));
|
||||
$paths = $config['collection']['paths'] ?? [
|
||||
'flex/{TYPE}/collection/{LAYOUT}{EXT}',
|
||||
'flex-objects/layouts/{TYPE}/collection/{LAYOUT}{EXT}'
|
||||
];
|
||||
$table = ['TYPE' => '%1$s', 'LAYOUT' => '%2$s', 'EXT' => '%3$s'];
|
||||
|
||||
$lookups = [];
|
||||
foreach ($paths as $path) {
|
||||
$path = Utils::simpleTemplate($path, $table);
|
||||
foreach ($types as $type) {
|
||||
$lookups[] = sprintf($path, $type, $layout, $ext);
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique($lookups);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $layout
|
||||
* @return Template|TemplateWrapper
|
||||
* @throws LoaderError
|
||||
* @throws SyntaxError
|
||||
*/
|
||||
protected function getTemplate($layout)
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Twig $twig */
|
||||
$twig = $grav['twig'];
|
||||
|
||||
try {
|
||||
return $twig->twig()->resolveTemplate($this->getTemplatePaths($layout));
|
||||
} catch (LoaderError $e) {
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = Grav::instance()['debugger'];
|
||||
$debugger->addException($e);
|
||||
|
||||
return $twig->twig()->resolveTemplate(['flex/404.html.twig']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @return FlexDirectory
|
||||
*/
|
||||
protected function getRelatedDirectory($type): ?FlexDirectory
|
||||
{
|
||||
/** @var Flex $flex */
|
||||
$flex = Grav::instance()['flex'];
|
||||
|
||||
return $flex->getDirectory($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $keyField
|
||||
* @return void
|
||||
*/
|
||||
protected function setKeyField($keyField = null): void
|
||||
{
|
||||
$this->_keyField = $keyField ?? 'storage_key';
|
||||
}
|
||||
|
||||
// DEPRECATED METHODS
|
||||
|
||||
/**
|
||||
* @param bool $prefix
|
||||
* @return string
|
||||
* @deprecated 1.6 Use `->getFlexType()` instead.
|
||||
*/
|
||||
public function getType($prefix = false)
|
||||
{
|
||||
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getFlexType() method instead', E_USER_DEPRECATED);
|
||||
|
||||
$type = $prefix ? $this->getTypePrefix() : '';
|
||||
|
||||
return $type . $this->getFlexType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param object|null $event
|
||||
* @return $this
|
||||
* @deprecated 1.7, moved to \Grav\Common\Flex\Traits\FlexObjectTrait
|
||||
*/
|
||||
public function triggerEvent(string $name, $event = null)
|
||||
{
|
||||
user_error(__METHOD__ . '() is deprecated since Grav 1.7, moved to \Grav\Common\Flex\Traits\FlexObjectTrait', E_USER_DEPRECATED);
|
||||
|
||||
if (null === $event) {
|
||||
$event = new Event([
|
||||
'type' => 'flex',
|
||||
'directory' => $this->getFlexDirectory(),
|
||||
'collection' => $this
|
||||
]);
|
||||
}
|
||||
if (strpos($name, 'onFlexCollection') !== 0 && strpos($name, 'on') === 0) {
|
||||
$name = 'onFlexCollection' . substr($name, 2);
|
||||
}
|
||||
|
||||
$grav = Grav::instance();
|
||||
if ($event instanceof Event) {
|
||||
$grav->fireEvent($name, $event);
|
||||
} else {
|
||||
$grav->dispatchEvent($event);
|
||||
}
|
||||
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
1041
system/src/Grav/Framework/Flex/FlexDirectory.php
Normal file
1041
system/src/Grav/Framework/Flex/FlexDirectory.php
Normal file
File diff suppressed because it is too large
Load Diff
482
system/src/Grav/Framework/Flex/FlexDirectoryForm.php
Normal file
482
system/src/Grav/Framework/Flex/FlexDirectoryForm.php
Normal file
@@ -0,0 +1,482 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex;
|
||||
|
||||
use ArrayAccess;
|
||||
use Exception;
|
||||
use Grav\Common\Data\Blueprint;
|
||||
use Grav\Common\Data\Data;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Twig\Twig;
|
||||
use Grav\Framework\Flex\Interfaces\FlexDirectoryFormInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexFormInterface;
|
||||
use Grav\Framework\Form\Interfaces\FormFlashInterface;
|
||||
use Grav\Framework\Form\Traits\FormTrait;
|
||||
use Grav\Framework\Route\Route;
|
||||
use JsonSerializable;
|
||||
use RocketTheme\Toolbox\ArrayTraits\NestedArrayAccessWithGetters;
|
||||
use RuntimeException;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Template;
|
||||
use Twig\TemplateWrapper;
|
||||
|
||||
/**
|
||||
* Class FlexForm
|
||||
* @package Grav\Framework\Flex
|
||||
*/
|
||||
class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable
|
||||
{
|
||||
use NestedArrayAccessWithGetters {
|
||||
NestedArrayAccessWithGetters::get as private traitGet;
|
||||
NestedArrayAccessWithGetters::set as private traitSet;
|
||||
}
|
||||
use FormTrait {
|
||||
FormTrait::doSerialize as doTraitSerialize;
|
||||
FormTrait::doUnserialize as doTraitUnserialize;
|
||||
}
|
||||
|
||||
/** @var array|null */
|
||||
private $form;
|
||||
/** @var FlexDirectory */
|
||||
private $directory;
|
||||
/** @var string */
|
||||
private $flexName;
|
||||
|
||||
/**
|
||||
* @param array $options Options to initialize the form instance:
|
||||
* (string) name: Form name, allows you to use custom form.
|
||||
* (string) unique_id: Unique id for this form instance.
|
||||
* (array) form: Custom form fields.
|
||||
* (FlexDirectory) directory: Flex Directory, mandatory.
|
||||
*
|
||||
* @return FlexFormInterface
|
||||
*/
|
||||
public static function instance(array $options = []): FlexFormInterface
|
||||
{
|
||||
if (isset($options['directory'])) {
|
||||
$directory = $options['directory'];
|
||||
if (!$directory instanceof FlexDirectory) {
|
||||
throw new RuntimeException(__METHOD__ . "(): 'directory' should be instance of FlexDirectory", 400);
|
||||
}
|
||||
unset($options['directory']);
|
||||
} else {
|
||||
throw new RuntimeException(__METHOD__ . "(): You need to pass option 'directory'", 400);
|
||||
}
|
||||
|
||||
$name = $options['name'] ?? '';
|
||||
|
||||
return $directory->getDirectoryForm($name, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* FlexForm constructor.
|
||||
* @param string $name
|
||||
* @param FlexDirectory $directory
|
||||
* @param array|null $options
|
||||
*/
|
||||
public function __construct(string $name, FlexDirectory $directory, array $options = null)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->setDirectory($directory);
|
||||
$this->setName($directory->getFlexType(), $name);
|
||||
$this->setId($this->getName());
|
||||
|
||||
$uniqueId = $options['unique_id'] ?? null;
|
||||
if (!$uniqueId) {
|
||||
$uniqueId = md5($directory->getFlexType() . '-directory-' . $this->name);
|
||||
}
|
||||
$this->setUniqueId($uniqueId);
|
||||
$this->setFlashLookupFolder($directory->getDirectoryBlueprint()->get('form/flash_folder') ?? 'tmp://forms/[SESSIONID]');
|
||||
$this->form = $options['form'] ?? null;
|
||||
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function initialize()
|
||||
{
|
||||
$this->messages = [];
|
||||
$this->submitted = false;
|
||||
$this->data = new Data($this->directory->loadDirectoryConfig($this->name), $this->getBlueprint());
|
||||
$this->files = [];
|
||||
$this->unsetFlash();
|
||||
|
||||
/** @var FlexFormFlash $flash */
|
||||
$flash = $this->getFlash();
|
||||
if ($flash->exists()) {
|
||||
$data = $flash->getData();
|
||||
$includeOriginal = (bool)($this->getBlueprint()->form()['images']['original'] ?? null);
|
||||
|
||||
$directory = $flash->getDirectory();
|
||||
if (null === $directory) {
|
||||
throw new RuntimeException('Flash has no directory');
|
||||
}
|
||||
$this->directory = $directory;
|
||||
$this->data = $data ? new Data($data, $this->getBlueprint()) : null;
|
||||
$this->files = $flash->getFilesByFields($includeOriginal);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param mixed $default
|
||||
* @param string|null $separator
|
||||
* @return mixed
|
||||
*/
|
||||
public function get($name, $default = null, $separator = null)
|
||||
{
|
||||
switch (strtolower($name)) {
|
||||
case 'id':
|
||||
case 'uniqueid':
|
||||
case 'name':
|
||||
case 'noncename':
|
||||
case 'nonceaction':
|
||||
case 'action':
|
||||
case 'data':
|
||||
case 'files':
|
||||
case 'errors';
|
||||
case 'fields':
|
||||
case 'blueprint':
|
||||
case 'page':
|
||||
$method = 'get' . $name;
|
||||
return $this->{$method}();
|
||||
}
|
||||
|
||||
return $this->traitGet($name, $default, $separator);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
* @param string|null $separator
|
||||
* @return $this
|
||||
*/
|
||||
public function set($name, $value, $separator = null)
|
||||
{
|
||||
switch (strtolower($name)) {
|
||||
case 'id':
|
||||
case 'uniqueid':
|
||||
$method = 'set' . $name;
|
||||
return $this->{$method}();
|
||||
}
|
||||
|
||||
return $this->traitSet($name, $value, $separator);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->flexName;
|
||||
}
|
||||
|
||||
protected function setName(string $type, string $name): void
|
||||
{
|
||||
// Make sure that both type and name do not have dash (convert dashes to underscores).
|
||||
$type = str_replace('-', '_', $type);
|
||||
$name = str_replace('-', '_', $name);
|
||||
$this->flexName = $name ? "flex_conf-{$type}-{$name}" : "flex_conf-{$type}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Data|object
|
||||
*/
|
||||
public function getData()
|
||||
{
|
||||
if (null === $this->data) {
|
||||
$this->data = new Data([], $this->getBlueprint());
|
||||
}
|
||||
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value from the form.
|
||||
*
|
||||
* Note: Used in form fields.
|
||||
*
|
||||
* @param string $name
|
||||
* @return mixed
|
||||
*/
|
||||
public function getValue(string $name)
|
||||
{
|
||||
// Attempt to get value from the form data.
|
||||
$value = $this->data ? $this->data[$name] : null;
|
||||
|
||||
// Return the form data or fall back to the object property.
|
||||
return $value ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return array|mixed|null
|
||||
*/
|
||||
public function getDefaultValue(string $name)
|
||||
{
|
||||
return $this->getBlueprint()->getDefaultValue($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getDefaultValues(): array
|
||||
{
|
||||
return $this->getBlueprint()->getDefaults();
|
||||
}
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFlexType(): string
|
||||
{
|
||||
return $this->directory->getFlexType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get form flash object.
|
||||
*
|
||||
* @return FormFlashInterface|FlexFormFlash
|
||||
*/
|
||||
public function getFlash()
|
||||
{
|
||||
if (null === $this->flash) {
|
||||
$grav = Grav::instance();
|
||||
$config = [
|
||||
'session_id' => $this->getSessionId(),
|
||||
'unique_id' => $this->getUniqueId(),
|
||||
'form_name' => $this->getName(),
|
||||
'folder' => $this->getFlashFolder(),
|
||||
'directory' => $this->getDirectory()
|
||||
];
|
||||
|
||||
$this->flash = new FlexFormFlash($config);
|
||||
$this->flash
|
||||
->setUrl($grav['uri']->url)
|
||||
->setUser($grav['user'] ?? null);
|
||||
}
|
||||
|
||||
return $this->flash;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FlexDirectory
|
||||
*/
|
||||
public function getDirectory(): FlexDirectory
|
||||
{
|
||||
return $this->directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Blueprint
|
||||
*/
|
||||
public function getBlueprint(): Blueprint
|
||||
{
|
||||
if (null === $this->blueprint) {
|
||||
try {
|
||||
$blueprint = $this->getDirectory()->getDirectoryBlueprint();
|
||||
if ($this->form) {
|
||||
// We have field overrides available.
|
||||
$blueprint->extend(['form' => $this->form], true);
|
||||
$blueprint->init();
|
||||
}
|
||||
} catch (RuntimeException $e) {
|
||||
if (!isset($this->form['fields'])) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Blueprint is not defined, but we have custom form fields available.
|
||||
$blueprint = new Blueprint(null, ['form' => $this->form]);
|
||||
$blueprint->load();
|
||||
$blueprint->setScope('directory');
|
||||
$blueprint->init();
|
||||
}
|
||||
|
||||
$this->blueprint = $blueprint;
|
||||
}
|
||||
|
||||
return $this->blueprint;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Route|null
|
||||
*/
|
||||
public function getFileUploadAjaxRoute(): ?Route
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
* @param string $filename
|
||||
* @return Route|null
|
||||
*/
|
||||
public function getFileDeleteAjaxRoute($field, $filename): ?Route
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $params
|
||||
* @param string|null $extension
|
||||
* @return string
|
||||
*/
|
||||
public function getMediaTaskRoute(array $params = [], string $extension = null): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function __get($name)
|
||||
{
|
||||
$method = "get{$name}";
|
||||
if (method_exists($this, $method)) {
|
||||
return $this->{$method}();
|
||||
}
|
||||
|
||||
$form = $this->getBlueprint()->form();
|
||||
|
||||
return $form[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
public function __set($name, $value)
|
||||
{
|
||||
$method = "set{$name}";
|
||||
if (method_exists($this, $method)) {
|
||||
$this->{$method}($value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function __isset($name)
|
||||
{
|
||||
$method = "get{$name}";
|
||||
if (method_exists($this, $method)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$form = $this->getBlueprint()->form();
|
||||
|
||||
return isset($form[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return void
|
||||
*/
|
||||
public function __unset($name)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|bool
|
||||
*/
|
||||
protected function getUnserializeAllowedClasses()
|
||||
{
|
||||
return [FlexObject::class];
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: this method clones the object.
|
||||
*
|
||||
* @param FlexDirectory $directory
|
||||
* @return $this
|
||||
*/
|
||||
protected function setDirectory(FlexDirectory $directory): self
|
||||
{
|
||||
$this->directory = $directory;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $layout
|
||||
* @return Template|TemplateWrapper
|
||||
* @throws LoaderError
|
||||
* @throws SyntaxError
|
||||
*/
|
||||
protected function getTemplate($layout)
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Twig $twig */
|
||||
$twig = $grav['twig'];
|
||||
|
||||
return $twig->twig()->resolveTemplate(
|
||||
[
|
||||
"flex-objects/layouts/{$this->getFlexType()}/form/{$layout}.html.twig",
|
||||
"flex-objects/layouts/_default/form/{$layout}.html.twig",
|
||||
"forms/{$layout}/form.html.twig",
|
||||
'forms/default/form.html.twig'
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param array $files
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function doSubmit(array $data, array $files)
|
||||
{
|
||||
$this->directory->saveDirectoryConfig($this->name, $data);
|
||||
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function doSerialize(): array
|
||||
{
|
||||
return $this->doTraitSerialize() + [
|
||||
'directory' => $this->directory,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
protected function doUnserialize(array $data): void
|
||||
{
|
||||
$this->doTraitUnserialize($data);
|
||||
|
||||
$this->directory = $data['directory'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter validated data.
|
||||
*
|
||||
* @param ArrayAccess|Data|null $data
|
||||
*/
|
||||
protected function filterData($data = null): void
|
||||
{
|
||||
if ($data instanceof Data) {
|
||||
$data->filter(false, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
566
system/src/Grav/Framework/Flex/FlexForm.php
Normal file
566
system/src/Grav/Framework/Flex/FlexForm.php
Normal file
@@ -0,0 +1,566 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex;
|
||||
|
||||
use ArrayAccess;
|
||||
use Exception;
|
||||
use Grav\Common\Data\Blueprint;
|
||||
use Grav\Common\Data\Data;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Twig\Twig;
|
||||
use Grav\Framework\Flex\Interfaces\FlexFormInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexObjectFormInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
|
||||
use Grav\Framework\Form\Interfaces\FormFlashInterface;
|
||||
use Grav\Framework\Form\Traits\FormTrait;
|
||||
use Grav\Framework\Route\Route;
|
||||
use JsonSerializable;
|
||||
use RocketTheme\Toolbox\ArrayTraits\NestedArrayAccessWithGetters;
|
||||
use RuntimeException;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Template;
|
||||
use Twig\TemplateWrapper;
|
||||
|
||||
/**
|
||||
* Class FlexForm
|
||||
* @package Grav\Framework\Flex
|
||||
*/
|
||||
class FlexForm implements FlexObjectFormInterface, JsonSerializable
|
||||
{
|
||||
use NestedArrayAccessWithGetters {
|
||||
NestedArrayAccessWithGetters::get as private traitGet;
|
||||
NestedArrayAccessWithGetters::set as private traitSet;
|
||||
}
|
||||
use FormTrait {
|
||||
FormTrait::doSerialize as doTraitSerialize;
|
||||
FormTrait::doUnserialize as doTraitUnserialize;
|
||||
}
|
||||
|
||||
/** @var array */
|
||||
private $items = [];
|
||||
|
||||
/** @var array|null */
|
||||
private $form;
|
||||
/** @var FlexObjectInterface */
|
||||
private $object;
|
||||
/** @var string */
|
||||
private $flexName;
|
||||
/** @var callable|null */
|
||||
private $submitMethod;
|
||||
|
||||
/**
|
||||
* @param array $options Options to initialize the form instance:
|
||||
* (string) name: Form name, allows you to use custom form.
|
||||
* (string) unique_id: Unique id for this form instance.
|
||||
* (array) form: Custom form fields.
|
||||
* (FlexObjectInterface) object: Object instance.
|
||||
* (string) key: Object key, used only if object instance isn't given.
|
||||
* (FlexDirectory) directory: Flex Directory, mandatory if object isn't given.
|
||||
*
|
||||
* @return FlexFormInterface
|
||||
*/
|
||||
public static function instance(array $options = [])
|
||||
{
|
||||
if (isset($options['object'])) {
|
||||
$object = $options['object'];
|
||||
if (!$object instanceof FlexObjectInterface) {
|
||||
throw new RuntimeException(__METHOD__ . "(): 'object' should be instance of FlexObjectInterface", 400);
|
||||
}
|
||||
} elseif (isset($options['directory'])) {
|
||||
$directory = $options['directory'];
|
||||
if (!$directory instanceof FlexDirectory) {
|
||||
throw new RuntimeException(__METHOD__ . "(): 'directory' should be instance of FlexDirectory", 400);
|
||||
}
|
||||
$key = $options['key'] ?? '';
|
||||
$object = $directory->getObject($key) ?? $directory->createObject([], $key);
|
||||
} else {
|
||||
throw new RuntimeException(__METHOD__ . "(): You need to pass option 'directory' or 'object'", 400);
|
||||
}
|
||||
|
||||
$name = $options['name'] ?? '';
|
||||
|
||||
// There is no reason to pass object and directory.
|
||||
unset($options['object'], $options['directory']);
|
||||
|
||||
return $object->getForm($name, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* FlexForm constructor.
|
||||
* @param string $name
|
||||
* @param FlexObjectInterface $object
|
||||
* @param array|null $options
|
||||
*/
|
||||
public function __construct(string $name, FlexObjectInterface $object, array $options = null)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->setObject($object);
|
||||
$this->setName($object->getFlexType(), $name);
|
||||
$this->setId($this->getName());
|
||||
|
||||
$uniqueId = $options['unique_id'] ?? null;
|
||||
if (!$uniqueId) {
|
||||
if ($object->exists()) {
|
||||
$uniqueId = $object->getStorageKey();
|
||||
} elseif ($object->hasKey()) {
|
||||
$uniqueId = "{$object->getKey()}:new";
|
||||
} else {
|
||||
$uniqueId = "{$object->getFlexType()}:new";
|
||||
}
|
||||
$uniqueId = md5($uniqueId);
|
||||
}
|
||||
$this->setUniqueId($uniqueId);
|
||||
$directory = $object->getFlexDirectory();
|
||||
$this->setFlashLookupFolder($options['flash_folder'] ?? $directory->getBlueprint()->get('form/flash_folder') ?? 'tmp://forms/[SESSIONID]');
|
||||
$this->form = $options['form'] ?? null;
|
||||
|
||||
if (!empty($options['reset'])) {
|
||||
$this->getFlash()->delete();
|
||||
}
|
||||
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function initialize()
|
||||
{
|
||||
$this->messages = [];
|
||||
$this->submitted = false;
|
||||
$this->data = null;
|
||||
$this->files = [];
|
||||
$this->unsetFlash();
|
||||
|
||||
/** @var FlexFormFlash $flash */
|
||||
$flash = $this->getFlash();
|
||||
if ($flash->exists()) {
|
||||
$data = $flash->getData();
|
||||
if (null !== $data) {
|
||||
$data = new Data($data, $this->getBlueprint());
|
||||
$data->setKeepEmptyValues(true);
|
||||
$data->setMissingValuesAsNull(true);
|
||||
}
|
||||
|
||||
$object = $flash->getObject();
|
||||
if (null === $object) {
|
||||
throw new RuntimeException('Flash has no object');
|
||||
}
|
||||
|
||||
$this->object = $object;
|
||||
$this->data = $data;
|
||||
|
||||
$includeOriginal = (bool)($this->getBlueprint()->form()['images']['original'] ?? null);
|
||||
$this->files = $flash->getFilesByFields($includeOriginal);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param mixed $default
|
||||
* @param string|null $separator
|
||||
* @return mixed
|
||||
*/
|
||||
public function get($name, $default = null, $separator = null)
|
||||
{
|
||||
switch (strtolower($name)) {
|
||||
case 'id':
|
||||
case 'uniqueid':
|
||||
case 'name':
|
||||
case 'noncename':
|
||||
case 'nonceaction':
|
||||
case 'action':
|
||||
case 'data':
|
||||
case 'files':
|
||||
case 'errors';
|
||||
case 'fields':
|
||||
case 'blueprint':
|
||||
case 'page':
|
||||
$method = 'get' . $name;
|
||||
return $this->{$method}();
|
||||
}
|
||||
|
||||
return $this->traitGet($name, $default, $separator);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
* @param string|null $separator
|
||||
* @return FlexForm
|
||||
*/
|
||||
public function set($name, $value, $separator = null)
|
||||
{
|
||||
switch (strtolower($name)) {
|
||||
case 'id':
|
||||
case 'uniqueid':
|
||||
$method = 'set' . $name;
|
||||
return $this->{$method}();
|
||||
}
|
||||
|
||||
return $this->traitSet($name, $value, $separator);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->flexName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable|null $submitMethod
|
||||
*/
|
||||
public function setSubmitMethod(?callable $submitMethod): void
|
||||
{
|
||||
$this->submitMethod = $submitMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param string $name
|
||||
*/
|
||||
protected function setName(string $type, string $name): void
|
||||
{
|
||||
// Make sure that both type and name do not have dash (convert dashes to underscores).
|
||||
$type = str_replace('-', '_', $type);
|
||||
$name = str_replace('-', '_', $name);
|
||||
$this->flexName = $name ? "flex-{$type}-{$name}" : "flex-{$type}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Data|FlexObjectInterface|object
|
||||
*/
|
||||
public function getData()
|
||||
{
|
||||
return $this->data ?? $this->getObject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value from the form.
|
||||
*
|
||||
* Note: Used in form fields.
|
||||
*
|
||||
* @param string $name
|
||||
* @return mixed
|
||||
*/
|
||||
public function getValue(string $name)
|
||||
{
|
||||
// Attempt to get value from the form data.
|
||||
$value = $this->data ? $this->data[$name] : null;
|
||||
|
||||
// Return the form data or fall back to the object property.
|
||||
return $value ?? $this->getObject()->getFormValue($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return array|mixed|null
|
||||
*/
|
||||
public function getDefaultValue(string $name)
|
||||
{
|
||||
return $this->object->getDefaultValue($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getDefaultValues(): array
|
||||
{
|
||||
return $this->object->getDefaultValues();
|
||||
}
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFlexType(): string
|
||||
{
|
||||
return $this->object->getFlexType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get form flash object.
|
||||
*
|
||||
* @return FormFlashInterface|FlexFormFlash
|
||||
*/
|
||||
public function getFlash()
|
||||
{
|
||||
if (null === $this->flash) {
|
||||
$grav = Grav::instance();
|
||||
$config = [
|
||||
'session_id' => $this->getSessionId(),
|
||||
'unique_id' => $this->getUniqueId(),
|
||||
'form_name' => $this->getName(),
|
||||
'folder' => $this->getFlashFolder(),
|
||||
'object' => $this->getObject()
|
||||
];
|
||||
|
||||
$this->flash = new FlexFormFlash($config);
|
||||
$this->flash
|
||||
->setUrl($grav['uri']->url)
|
||||
->setUser($grav['user'] ?? null);
|
||||
}
|
||||
|
||||
return $this->flash;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FlexObjectInterface
|
||||
*/
|
||||
public function getObject(): FlexObjectInterface
|
||||
{
|
||||
return $this->object;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FlexObjectInterface
|
||||
*/
|
||||
public function updateObject(): FlexObjectInterface
|
||||
{
|
||||
$data = $this->data instanceof Data ? $this->data->toArray() : [];
|
||||
$files = $this->files;
|
||||
|
||||
return $this->getObject()->update($data, $files);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Blueprint
|
||||
*/
|
||||
public function getBlueprint(): Blueprint
|
||||
{
|
||||
if (null === $this->blueprint) {
|
||||
try {
|
||||
$blueprint = $this->getObject()->getBlueprint($this->name);
|
||||
if ($this->form) {
|
||||
// We have field overrides available.
|
||||
$blueprint->extend(['form' => $this->form], true);
|
||||
$blueprint->init();
|
||||
}
|
||||
} catch (RuntimeException $e) {
|
||||
if (!isset($this->form['fields'])) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Blueprint is not defined, but we have custom form fields available.
|
||||
$blueprint = new Blueprint(null, ['form' => $this->form]);
|
||||
$blueprint->load();
|
||||
$blueprint->setScope('object');
|
||||
$blueprint->init();
|
||||
}
|
||||
|
||||
$this->blueprint = $blueprint;
|
||||
}
|
||||
|
||||
return $this->blueprint;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Route|null
|
||||
*/
|
||||
public function getFileUploadAjaxRoute(): ?Route
|
||||
{
|
||||
$object = $this->getObject();
|
||||
if (!method_exists($object, 'route')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $object->route('/edit.json/task:media.upload');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
* @param string $filename
|
||||
* @return Route|null
|
||||
*/
|
||||
public function getFileDeleteAjaxRoute($field, $filename): ?Route
|
||||
{
|
||||
$object = $this->getObject();
|
||||
if (!method_exists($object, 'route')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $object->route('/edit.json/task:media.delete');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $params
|
||||
* @param string|null $extension
|
||||
* @return string
|
||||
*/
|
||||
public function getMediaTaskRoute(array $params = [], string $extension = null): string
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
/** @var Flex $flex */
|
||||
$flex = $grav['flex_objects'];
|
||||
|
||||
if (method_exists($flex, 'adminRoute')) {
|
||||
return $flex->adminRoute($this->getObject(), $params, $extension ?? 'json');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function __get($name)
|
||||
{
|
||||
$method = "get{$name}";
|
||||
if (method_exists($this, $method)) {
|
||||
return $this->{$method}();
|
||||
}
|
||||
|
||||
$form = $this->getBlueprint()->form();
|
||||
|
||||
return $form[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
public function __set($name, $value)
|
||||
{
|
||||
$method = "set{$name}";
|
||||
if (method_exists($this, $method)) {
|
||||
$this->{$method}($value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function __isset($name)
|
||||
{
|
||||
$method = "get{$name}";
|
||||
if (method_exists($this, $method)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$form = $this->getBlueprint()->form();
|
||||
|
||||
return isset($form[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return void
|
||||
*/
|
||||
public function __unset($name)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|bool
|
||||
*/
|
||||
protected function getUnserializeAllowedClasses()
|
||||
{
|
||||
return [FlexObject::class];
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: this method clones the object.
|
||||
*
|
||||
* @param FlexObjectInterface $object
|
||||
* @return $this
|
||||
*/
|
||||
protected function setObject(FlexObjectInterface $object): self
|
||||
{
|
||||
$this->object = clone $object;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $layout
|
||||
* @return Template|TemplateWrapper
|
||||
* @throws LoaderError
|
||||
* @throws SyntaxError
|
||||
*/
|
||||
protected function getTemplate($layout)
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Twig $twig */
|
||||
$twig = $grav['twig'];
|
||||
|
||||
return $twig->twig()->resolveTemplate(
|
||||
[
|
||||
"flex-objects/layouts/{$this->getFlexType()}/form/{$layout}.html.twig",
|
||||
"flex-objects/layouts/_default/form/{$layout}.html.twig",
|
||||
"forms/{$layout}/form.html.twig",
|
||||
'forms/default/form.html.twig'
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param array $files
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function doSubmit(array $data, array $files)
|
||||
{
|
||||
/** @var FlexObject $object */
|
||||
$object = clone $this->getObject();
|
||||
|
||||
$method = $this->submitMethod;
|
||||
if ($method) {
|
||||
$method($data, $files, $object);
|
||||
} else {
|
||||
$object->update($data, $files);
|
||||
$object->save();
|
||||
}
|
||||
|
||||
$this->setObject($object);
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function doSerialize(): array
|
||||
{
|
||||
return $this->doTraitSerialize() + [
|
||||
'object' => $this->object,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
protected function doUnserialize(array $data): void
|
||||
{
|
||||
$this->doTraitUnserialize($data);
|
||||
|
||||
$this->object = $data['object'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter validated data.
|
||||
*
|
||||
* @param ArrayAccess|Data|null $data
|
||||
* @return void
|
||||
*/
|
||||
protected function filterData($data = null): void
|
||||
{
|
||||
if ($data instanceof Data) {
|
||||
$data->filter(true, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
130
system/src/Grav/Framework/Flex/FlexFormFlash.php
Normal file
130
system/src/Grav/Framework/Flex/FlexFormFlash.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex;
|
||||
|
||||
use Grav\Framework\Flex\Interfaces\FlexInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
|
||||
use Grav\Framework\Form\FormFlash;
|
||||
|
||||
/**
|
||||
* Class FlexFormFlash
|
||||
* @package Grav\Framework\Flex
|
||||
*/
|
||||
class FlexFormFlash extends FormFlash
|
||||
{
|
||||
/** @var FlexDirectory|null */
|
||||
protected $directory;
|
||||
/** @var FlexObjectInterface|null */
|
||||
protected $object;
|
||||
|
||||
/** @var FlexInterface */
|
||||
static protected $flex;
|
||||
|
||||
public static function setFlex(FlexInterface $flex): void
|
||||
{
|
||||
static::$flex = $flex;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FlexObjectInterface $object
|
||||
* @return void
|
||||
*/
|
||||
public function setObject(FlexObjectInterface $object): void
|
||||
{
|
||||
$this->object = $object;
|
||||
$this->directory = $object->getFlexDirectory();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FlexObjectInterface|null
|
||||
*/
|
||||
public function getObject(): ?FlexObjectInterface
|
||||
{
|
||||
return $this->object;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FlexDirectory $directory
|
||||
*/
|
||||
public function setDirectory(FlexDirectory $directory): void
|
||||
{
|
||||
$this->directory = $directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FlexDirectory|null
|
||||
*/
|
||||
public function getDirectory(): ?FlexDirectory
|
||||
{
|
||||
return $this->directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$serialized = parent::jsonSerialize();
|
||||
|
||||
$object = $this->getObject();
|
||||
if ($object instanceof FlexObjectInterface) {
|
||||
$serialized['object'] = [
|
||||
'type' => $object->getFlexType(),
|
||||
'key' => $object->getKey() ?: null,
|
||||
'storage_key' => $object->getStorageKey(),
|
||||
'timestamp' => $object->getTimestamp(),
|
||||
'serialized' => $object->prepareStorage()
|
||||
];
|
||||
} else {
|
||||
$directory = $this->getDirectory();
|
||||
if ($directory instanceof FlexDirectory) {
|
||||
$serialized['directory'] = [
|
||||
'type' => $directory->getFlexType()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $serialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|null $data
|
||||
* @param array $config
|
||||
* @return void
|
||||
*/
|
||||
protected function init(?array $data, array $config): void
|
||||
{
|
||||
parent::init($data, $config);
|
||||
|
||||
$data = $data ?? [];
|
||||
/** @var FlexObjectInterface|null $object */
|
||||
$object = $config['object'] ?? null;
|
||||
$create = true;
|
||||
if ($object) {
|
||||
$directory = $object->getFlexDirectory();
|
||||
$create = !$object->exists();
|
||||
} elseif (null === ($directory = $config['directory'] ?? null)) {
|
||||
$flex = $config['flex'] ?? static::$flex;
|
||||
$type = $data['object']['type'] ?? $data['directory']['type'] ?? null;
|
||||
$directory = $flex && $type ? $flex->getDirectory($type) : null;
|
||||
}
|
||||
|
||||
if ($directory && $create && isset($data['object']['serialized'])) {
|
||||
// TODO: update instead of create new.
|
||||
$object = $directory->createObject($data['object']['serialized'], $data['object']['key'] ?? '');
|
||||
}
|
||||
|
||||
if ($object) {
|
||||
$this->setObject($object);
|
||||
} elseif ($directory) {
|
||||
$this->setDirectory($directory);
|
||||
}
|
||||
}
|
||||
}
|
||||
901
system/src/Grav/Framework/Flex/FlexIndex.php
Normal file
901
system/src/Grav/Framework/Flex/FlexIndex.php
Normal file
@@ -0,0 +1,901 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex;
|
||||
|
||||
use Exception;
|
||||
use Grav\Common\Debugger;
|
||||
use Grav\Common\File\CompiledYamlFile;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Inflector;
|
||||
use Grav\Common\Session;
|
||||
use Grav\Framework\Cache\CacheInterface;
|
||||
use Grav\Framework\Collection\CollectionInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexIndexInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
|
||||
use Grav\Framework\Object\Interfaces\ObjectInterface;
|
||||
use Grav\Framework\Object\ObjectIndex;
|
||||
use Monolog\Logger;
|
||||
use Psr\SimpleCache\InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
use function count;
|
||||
use function get_class;
|
||||
use function in_array;
|
||||
|
||||
/**
|
||||
* Class FlexIndex
|
||||
* @package Grav\Framework\Flex
|
||||
* @template T of FlexObjectInterface
|
||||
* @template C of FlexCollectionInterface
|
||||
* @extends ObjectIndex<string,T>
|
||||
* @implements FlexIndexInterface<T>
|
||||
* @mixin C
|
||||
*/
|
||||
class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexIndexInterface
|
||||
{
|
||||
const VERSION = 1;
|
||||
|
||||
/** @var FlexDirectory|null */
|
||||
private $_flexDirectory;
|
||||
/** @var string */
|
||||
private $_keyField;
|
||||
/** @var array */
|
||||
private $_indexKeys;
|
||||
|
||||
/**
|
||||
* @param FlexDirectory $directory
|
||||
* @return static
|
||||
* @phpstan-return static<T,C>
|
||||
*/
|
||||
public static function createFromStorage(FlexDirectory $directory)
|
||||
{
|
||||
return static::createFromArray(static::loadEntriesFromStorage($directory->getStorage()), $directory);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::createFromArray()
|
||||
*/
|
||||
public static function createFromArray(array $entries, FlexDirectory $directory, string $keyField = null)
|
||||
{
|
||||
$instance = new static($entries, $directory);
|
||||
$instance->setKeyField($keyField);
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FlexStorageInterface $storage
|
||||
* @return array
|
||||
*/
|
||||
public static function loadEntriesFromStorage(FlexStorageInterface $storage): array
|
||||
{
|
||||
return $storage->getExistingKeys();
|
||||
}
|
||||
|
||||
/**
|
||||
* You can define indexes for fast lookup.
|
||||
*
|
||||
* Primary key: $meta['key']
|
||||
* Secondary keys: $meta['my_field']
|
||||
*
|
||||
* @param array $meta
|
||||
* @param array $data
|
||||
* @param FlexStorageInterface $storage
|
||||
* @return void
|
||||
*/
|
||||
public static function updateObjectMeta(array &$meta, array $data, FlexStorageInterface $storage)
|
||||
{
|
||||
// For backwards compatibility, no need to call this method when you override this method.
|
||||
static::updateIndexData($meta, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new FlexIndex.
|
||||
*
|
||||
* @param array $entries
|
||||
* @param FlexDirectory|null $directory
|
||||
*/
|
||||
public function __construct(array $entries = [], FlexDirectory $directory = null)
|
||||
{
|
||||
// @phpstan-ignore-next-line
|
||||
if (get_class($this) === __CLASS__) {
|
||||
user_error('Using ' . __CLASS__ . ' directly is deprecated since Grav 1.7, use \Grav\Common\Flex\Types\Generic\GenericIndex or your own class instead', E_USER_DEPRECATED);
|
||||
}
|
||||
|
||||
parent::__construct($entries);
|
||||
|
||||
$this->_flexDirectory = $directory;
|
||||
$this->setKeyField(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCommonInterface::hasFlexFeature()
|
||||
*/
|
||||
public function hasFlexFeature(string $name): bool
|
||||
{
|
||||
return in_array($name, $this->getFlexFeatures(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCommonInterface::hasFlexFeature()
|
||||
*/
|
||||
public function getFlexFeatures(): array
|
||||
{
|
||||
$implements = class_implements($this->getFlexDirectory()->getCollectionClass());
|
||||
|
||||
$list = [];
|
||||
foreach ($implements as $interface) {
|
||||
if ($pos = strrpos($interface, '\\')) {
|
||||
$interface = substr($interface, $pos+1);
|
||||
}
|
||||
|
||||
$list[] = Inflector::hyphenize(str_replace('Interface', '', $interface));
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::search()
|
||||
*/
|
||||
public function search(string $search, $properties = null, array $options = null)
|
||||
{
|
||||
return $this->__call('search', [$search, $properties, $options]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::sort()
|
||||
*/
|
||||
public function sort(array $orderings)
|
||||
{
|
||||
return $this->orderBy($orderings);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::filterBy()
|
||||
*/
|
||||
public function filterBy(array $filters)
|
||||
{
|
||||
return $this->__call('filterBy', [$filters]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getFlexType()
|
||||
*/
|
||||
public function getFlexType(): string
|
||||
{
|
||||
return $this->getFlexDirectory()->getFlexType();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getFlexDirectory()
|
||||
*/
|
||||
public function getFlexDirectory(): FlexDirectory
|
||||
{
|
||||
if (null === $this->_flexDirectory) {
|
||||
throw new RuntimeException('Flex Directory not defined, object is not fully defined');
|
||||
}
|
||||
|
||||
return $this->_flexDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getTimestamp()
|
||||
*/
|
||||
public function getTimestamp(): int
|
||||
{
|
||||
$timestamps = $this->getTimestamps();
|
||||
|
||||
return $timestamps ? max($timestamps) : time();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getCacheKey()
|
||||
*/
|
||||
public function getCacheKey(): string
|
||||
{
|
||||
return $this->getTypePrefix() . $this->getFlexType() . '.' . sha1(json_encode($this->getKeys()) . $this->_keyField);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getCacheChecksum()
|
||||
*/
|
||||
public function getCacheChecksum(): string
|
||||
{
|
||||
$list = [];
|
||||
foreach ($this->getEntries() as $key => $value) {
|
||||
$list[$key] = $value['checksum'] ?? $value['storage_timestamp'];
|
||||
}
|
||||
|
||||
return sha1((string)json_encode($list));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getTimestamps()
|
||||
*/
|
||||
public function getTimestamps(): array
|
||||
{
|
||||
return $this->getIndexMap('storage_timestamp');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getStorageKeys()
|
||||
*/
|
||||
public function getStorageKeys(): array
|
||||
{
|
||||
return $this->getIndexMap('storage_key');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getFlexKeys()
|
||||
*/
|
||||
public function getFlexKeys(): array
|
||||
{
|
||||
// Get storage keys for the objects.
|
||||
$keys = [];
|
||||
$type = $this->getFlexDirectory()->getFlexType() . '.obj:';
|
||||
|
||||
foreach ($this->getEntries() as $key => $value) {
|
||||
$keys[$key] = $value['flex_key'] ?? $type . $value['storage_key'];
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexIndexInterface::withKeyField()
|
||||
*/
|
||||
public function withKeyField(string $keyField = null)
|
||||
{
|
||||
$keyField = $keyField ?: 'key';
|
||||
if ($keyField === $this->getKeyField()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$type = $keyField === 'flex_key' ? $this->getFlexDirectory()->getFlexType() . '.obj:' : '';
|
||||
$entries = [];
|
||||
foreach ($this->getEntries() as $key => $value) {
|
||||
if (!isset($value['key'])) {
|
||||
$value['key'] = $key;
|
||||
}
|
||||
|
||||
if (isset($value[$keyField])) {
|
||||
$entries[$value[$keyField]] = $value;
|
||||
} elseif ($keyField === 'flex_key') {
|
||||
$entries[$type . $value['storage_key']] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createFrom($entries, $keyField);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::getIndex()
|
||||
*/
|
||||
public function getIndex()
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FlexCollectionInterface
|
||||
* @phpstan-return C
|
||||
*/
|
||||
public function getCollection()
|
||||
{
|
||||
return $this->loadCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexCollectionInterface::render()
|
||||
*/
|
||||
public function render(string $layout = null, array $context = [])
|
||||
{
|
||||
return $this->__call('render', [$layout, $context]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexIndexInterface::getFlexKeys()
|
||||
*/
|
||||
public function getIndexMap(string $indexKey = null)
|
||||
{
|
||||
if (null === $indexKey) {
|
||||
return $this->getEntries();
|
||||
}
|
||||
|
||||
// Get storage keys for the objects.
|
||||
$index = [];
|
||||
foreach ($this->getEntries() as $key => $value) {
|
||||
$index[$key] = $value[$indexKey] ?? null;
|
||||
}
|
||||
|
||||
return $index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return array
|
||||
*/
|
||||
public function getMetaData($key): array
|
||||
{
|
||||
return $this->getEntries()[$key] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getKeyField(): string
|
||||
{
|
||||
return $this->_keyField ?? 'storage_key';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $namespace
|
||||
* @return CacheInterface
|
||||
*/
|
||||
public function getCache(string $namespace = null)
|
||||
{
|
||||
return $this->getFlexDirectory()->getCache($namespace);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $orderings
|
||||
* @return static
|
||||
* @phpstan-return static<T,C>
|
||||
*/
|
||||
public function orderBy(array $orderings)
|
||||
{
|
||||
if (!$orderings || !$this->count()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Handle primary key alias.
|
||||
$keyField = $this->getFlexDirectory()->getStorage()->getKeyField();
|
||||
if ($keyField !== 'key' && $keyField !== 'storage_key' && isset($orderings[$keyField])) {
|
||||
$orderings['key'] = $orderings[$keyField];
|
||||
unset($orderings[$keyField]);
|
||||
}
|
||||
|
||||
// Check if ordering needs to load the objects.
|
||||
if (array_diff_key($orderings, $this->getIndexKeys())) {
|
||||
return $this->__call('orderBy', [$orderings]);
|
||||
}
|
||||
|
||||
// Ordering can be done by using index only.
|
||||
$previous = null;
|
||||
foreach (array_reverse($orderings) as $field => $ordering) {
|
||||
$field = (string)$field;
|
||||
if ($this->getKeyField() === $field) {
|
||||
$keys = $this->getKeys();
|
||||
$search = array_combine($keys, $keys) ?: [];
|
||||
} elseif ($field === 'flex_key') {
|
||||
$search = $this->getFlexKeys();
|
||||
} else {
|
||||
$search = $this->getIndexMap($field);
|
||||
}
|
||||
|
||||
// Update current search to match the previous ordering.
|
||||
if (null !== $previous) {
|
||||
$search = array_replace($previous, $search);
|
||||
}
|
||||
|
||||
// Order by current field.
|
||||
if (strtoupper($ordering) === 'DESC') {
|
||||
arsort($search, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
} else {
|
||||
asort($search, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
}
|
||||
|
||||
$previous = $search;
|
||||
}
|
||||
|
||||
return $this->createFrom(array_replace($previous ?? [], $this->getEntries()) ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function call($method, array $arguments = [])
|
||||
{
|
||||
return $this->__call('call', [$method, $arguments]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param array $arguments
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call($name, $arguments)
|
||||
{
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = Grav::instance()['debugger'];
|
||||
|
||||
/** @var FlexCollection $className */
|
||||
$className = $this->getFlexDirectory()->getCollectionClass();
|
||||
$cachedMethods = $className::getCachedMethods();
|
||||
|
||||
$flexType = $this->getFlexType();
|
||||
|
||||
if (!empty($cachedMethods[$name])) {
|
||||
$type = $cachedMethods[$name];
|
||||
if ($type === 'session') {
|
||||
/** @var Session $session */
|
||||
$session = Grav::instance()['session'];
|
||||
$cacheKey = $session->getId() . ($session->user->username ?? '');
|
||||
} else {
|
||||
$cacheKey = '';
|
||||
}
|
||||
$key = "{$flexType}.idx." . sha1($name . '.' . $cacheKey . json_encode($arguments) . $this->getCacheKey());
|
||||
$checksum = $this->getCacheChecksum();
|
||||
|
||||
$cache = $this->getCache('object');
|
||||
|
||||
try {
|
||||
$cached = $cache->get($key);
|
||||
$test = $cached[0] ?? null;
|
||||
$result = $test === $checksum ? ($cached[1] ?? null) : null;
|
||||
|
||||
// Make sure the keys aren't changed if the returned type is the same index type.
|
||||
if ($result instanceof self && $flexType === $result->getFlexType()) {
|
||||
$result = $result->withKeyField($this->getKeyField());
|
||||
}
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$debugger->addException($e);
|
||||
}
|
||||
|
||||
if (!isset($result)) {
|
||||
$collection = $this->loadCollection();
|
||||
$result = $collection->{$name}(...$arguments);
|
||||
$debugger->addMessage("Cache miss: '{$flexType}::{$name}()'", 'debug');
|
||||
|
||||
try {
|
||||
// If flex collection is returned, convert it back to flex index.
|
||||
if ($result instanceof FlexCollection) {
|
||||
$cached = $result->getFlexDirectory()->getIndex($result->getKeys(), $this->getKeyField());
|
||||
} else {
|
||||
$cached = $result;
|
||||
}
|
||||
|
||||
$cache->set($key, [$checksum, $cached]);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$debugger->addException($e);
|
||||
|
||||
// TODO: log error.
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$collection = $this->loadCollection();
|
||||
$result = $collection->{$name}(...$arguments);
|
||||
if (!isset($cachedMethods[$name])) {
|
||||
$debugger->addMessage("Call '{$flexType}:{$name}()' isn't cached", 'debug');
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function __serialize(): array
|
||||
{
|
||||
return ['type' => $this->getFlexType(), 'entries' => $this->getEntries()];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
$this->_flexDirectory = Grav::instance()['flex']->getDirectory($data['type']);
|
||||
$this->setEntries($data['entries']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo()
|
||||
{
|
||||
return [
|
||||
'type:private' => $this->getFlexType(),
|
||||
'key:private' => $this->getKey(),
|
||||
'entries_key:private' => $this->getKeyField(),
|
||||
'entries:private' => $this->getEntries()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $entries
|
||||
* @param string|null $keyField
|
||||
* @return static
|
||||
* @phpstan-return static<T,C>
|
||||
*/
|
||||
protected function createFrom(array $entries, string $keyField = null)
|
||||
{
|
||||
$index = new static($entries, $this->getFlexDirectory());
|
||||
$index->setKeyField($keyField ?? $this->_keyField);
|
||||
|
||||
return $index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $keyField
|
||||
* @return void
|
||||
*/
|
||||
protected function setKeyField(string $keyField = null)
|
||||
{
|
||||
$this->_keyField = $keyField ?? 'storage_key';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function getIndexKeys()
|
||||
{
|
||||
if (null === $this->_indexKeys) {
|
||||
$entries = $this->getEntries();
|
||||
$first = reset($entries);
|
||||
if ($first) {
|
||||
$keys = array_keys($first);
|
||||
$keys = array_combine($keys, $keys) ?: [];
|
||||
} else {
|
||||
$keys = [];
|
||||
}
|
||||
|
||||
$this->setIndexKeys($keys);
|
||||
}
|
||||
|
||||
return $this->_indexKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $indexKeys
|
||||
* @return void
|
||||
*/
|
||||
protected function setIndexKeys(array $indexKeys)
|
||||
{
|
||||
// Add defaults.
|
||||
$indexKeys += [
|
||||
'key' => 'key',
|
||||
'storage_key' => 'storage_key',
|
||||
'storage_timestamp' => 'storage_timestamp',
|
||||
'flex_key' => 'flex_key'
|
||||
];
|
||||
|
||||
|
||||
$this->_indexKeys = $indexKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getTypePrefix()
|
||||
{
|
||||
return 'i.';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return ObjectInterface|null
|
||||
*/
|
||||
protected function loadElement($key, $value): ?ObjectInterface
|
||||
{
|
||||
$objects = $this->getFlexDirectory()->loadObjects([$key => $value]);
|
||||
|
||||
return $objects ? reset($objects): null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|null $entries
|
||||
* @return ObjectInterface[]
|
||||
*/
|
||||
protected function loadElements(array $entries = null): array
|
||||
{
|
||||
return $this->getFlexDirectory()->loadObjects($entries ?? $this->getEntries());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|null $entries
|
||||
* @return CollectionInterface
|
||||
* @phpstan-return C
|
||||
*/
|
||||
protected function loadCollection(array $entries = null): CollectionInterface
|
||||
{
|
||||
return $this->getFlexDirectory()->loadCollection($entries ?? $this->getEntries(), $this->_keyField);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
protected function isAllowedElement($value): bool
|
||||
{
|
||||
return $value instanceof FlexObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FlexObjectInterface $object
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getElementMeta($object)
|
||||
{
|
||||
return $object->getMetaData();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FlexObjectInterface $element
|
||||
* @return string
|
||||
*/
|
||||
protected function getCurrentKey($element)
|
||||
{
|
||||
$keyField = $this->getKeyField();
|
||||
if ($keyField === 'storage_key') {
|
||||
return $element->getStorageKey();
|
||||
}
|
||||
if ($keyField === 'flex_key') {
|
||||
return $element->getFlexKey();
|
||||
}
|
||||
if ($keyField === 'key') {
|
||||
return $element->getKey();
|
||||
}
|
||||
|
||||
return $element->getKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FlexStorageInterface $storage
|
||||
* @param array $index Saved index
|
||||
* @param array $entries Updated index
|
||||
* @param array $options
|
||||
* @return array Compiled list of entries
|
||||
*/
|
||||
protected static function updateIndexFile(FlexStorageInterface $storage, array $index, array $entries, array $options = []): array
|
||||
{
|
||||
$indexFile = static::getIndexFile($storage);
|
||||
if (null === $indexFile) {
|
||||
return $entries;
|
||||
}
|
||||
|
||||
// Calculate removed objects.
|
||||
$removed = array_diff_key($index, $entries);
|
||||
|
||||
// First get rid of all removed objects.
|
||||
if ($removed) {
|
||||
$index = array_diff_key($index, $removed);
|
||||
}
|
||||
|
||||
if ($entries && empty($options['force_update'])) {
|
||||
// Calculate difference between saved index and current data.
|
||||
foreach ($index as $key => $entry) {
|
||||
$storage_key = $entry['storage_key'] ?? null;
|
||||
if (isset($entries[$storage_key]) && $entries[$storage_key]['storage_timestamp'] === $entry['storage_timestamp']) {
|
||||
// Entry is up to date, no update needed.
|
||||
unset($entries[$storage_key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($entries) && empty($removed)) {
|
||||
// No objects were added, updated or removed.
|
||||
return $index;
|
||||
}
|
||||
} elseif (!$removed) {
|
||||
// There are no objects and nothing was removed.
|
||||
return [];
|
||||
}
|
||||
|
||||
// Index should be updated, lock the index file for saving.
|
||||
$indexFile->lock();
|
||||
|
||||
// Read all the data rows into an array using chunks of 100.
|
||||
$keys = array_fill_keys(array_keys($entries), null);
|
||||
$chunks = array_chunk($keys, 100, true);
|
||||
$updated = $added = [];
|
||||
foreach ($chunks as $keys) {
|
||||
$rows = $storage->readRows($keys);
|
||||
|
||||
$keyField = $storage->getKeyField();
|
||||
|
||||
// Go through all the updated objects and refresh their index data.
|
||||
foreach ($rows as $key => $row) {
|
||||
if (null !== $row || !empty($options['include_missing'])) {
|
||||
$entry = $entries[$key] + ['key' => $key];
|
||||
if ($keyField !== 'storage_key' && isset($row[$keyField])) {
|
||||
$entry['key'] = $row[$keyField];
|
||||
}
|
||||
static::updateObjectMeta($entry, $row ?? [], $storage);
|
||||
if (isset($row['__ERROR'])) {
|
||||
$entry['__ERROR'] = true;
|
||||
static::onException(new RuntimeException(sprintf('Object failed to load: %s (%s)', $key,
|
||||
$row['__ERROR'])));
|
||||
}
|
||||
if (isset($index[$key])) {
|
||||
// Update object in the index.
|
||||
$updated[$key] = $entry;
|
||||
} else {
|
||||
// Add object into the index.
|
||||
$added[$key] = $entry;
|
||||
}
|
||||
|
||||
// Either way, update the entry.
|
||||
$index[$key] = $entry;
|
||||
} elseif (isset($index[$key])) {
|
||||
// Remove object from the index.
|
||||
$removed[$key] = $index[$key];
|
||||
unset($index[$key]);
|
||||
}
|
||||
}
|
||||
unset($rows);
|
||||
}
|
||||
|
||||
// Sort the index before saving it.
|
||||
ksort($index, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
|
||||
static::onChanges($index, $added, $updated, $removed);
|
||||
|
||||
$indexFile->save(['version' => static::VERSION, 'timestamp' => time(), 'count' => count($index), 'index' => $index]);
|
||||
$indexFile->unlock();
|
||||
|
||||
return $index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $entry
|
||||
* @param array $data
|
||||
* @return void
|
||||
* @deprecated 1.7 Use static ::updateObjectMeta() method instead.
|
||||
*/
|
||||
protected static function updateIndexData(array &$entry, array $data)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FlexStorageInterface $storage
|
||||
* @return array
|
||||
*/
|
||||
protected static function loadIndex(FlexStorageInterface $storage)
|
||||
{
|
||||
$indexFile = static::getIndexFile($storage);
|
||||
|
||||
if ($indexFile) {
|
||||
$data = [];
|
||||
try {
|
||||
$data = (array)$indexFile->content();
|
||||
$version = $data['version'] ?? null;
|
||||
if ($version !== static::VERSION) {
|
||||
$data = [];
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$e = new RuntimeException(sprintf('Index failed to load: %s', $e->getMessage()), $e->getCode(), $e);
|
||||
|
||||
static::onException($e);
|
||||
}
|
||||
|
||||
if ($data) {
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
return ['version' => static::VERSION, 'timestamp' => 0, 'count' => 0, 'index' => []];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FlexStorageInterface $storage
|
||||
* @return array
|
||||
*/
|
||||
protected static function loadEntriesFromIndex(FlexStorageInterface $storage)
|
||||
{
|
||||
$data = static::loadIndex($storage);
|
||||
|
||||
return $data['index'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FlexStorageInterface $storage
|
||||
* @return CompiledYamlFile|null
|
||||
*/
|
||||
protected static function getIndexFile(FlexStorageInterface $storage)
|
||||
{
|
||||
if (!method_exists($storage, 'isIndexed') || !$storage->isIndexed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$path = $storage->getStoragePath();
|
||||
if (!$path) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Load saved index file.
|
||||
$grav = Grav::instance();
|
||||
$locator = $grav['locator'];
|
||||
$filename = $locator->findResource("{$path}/index.yaml", true, true);
|
||||
|
||||
return CompiledYamlFile::instance($filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Exception $e
|
||||
* @return void
|
||||
*/
|
||||
protected static function onException(Exception $e)
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Logger $logger */
|
||||
$logger = $grav['log'];
|
||||
$logger->addAlert($e->getMessage());
|
||||
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = $grav['debugger'];
|
||||
$debugger->addException($e);
|
||||
$debugger->addMessage($e, 'error');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $entries
|
||||
* @param array $added
|
||||
* @param array $updated
|
||||
* @param array $removed
|
||||
* @return void
|
||||
*/
|
||||
protected static function onChanges(array $entries, array $added, array $updated, array $removed)
|
||||
{
|
||||
$addedCount = count($added);
|
||||
$updatedCount = count($updated);
|
||||
$removedCount = count($removed);
|
||||
|
||||
if ($addedCount + $updatedCount + $removedCount) {
|
||||
$message = sprintf('Index updated, %d objects (%d added, %d updated, %d removed).', count($entries), $addedCount, $updatedCount, $removedCount);
|
||||
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = $grav['debugger'];
|
||||
$debugger->addMessage($message, 'debug');
|
||||
}
|
||||
}
|
||||
|
||||
// DEPRECATED METHODS
|
||||
|
||||
/**
|
||||
* @param bool $prefix
|
||||
* @return string
|
||||
* @deprecated 1.6 Use `->getFlexType()` instead.
|
||||
*/
|
||||
public function getType($prefix = false)
|
||||
{
|
||||
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getFlexType() method instead', E_USER_DEPRECATED);
|
||||
|
||||
$type = $prefix ? $this->getTypePrefix() : '';
|
||||
|
||||
return $type . $this->getFlexType();
|
||||
}
|
||||
}
|
||||
1211
system/src/Grav/Framework/Flex/FlexObject.php
Normal file
1211
system/src/Grav/Framework/Flex/FlexObject.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Interfaces;
|
||||
|
||||
use Grav\Common\User\Interfaces\UserInterface;
|
||||
|
||||
/**
|
||||
* Defines authorization checks for Flex Objects.
|
||||
*/
|
||||
interface FlexAuthorizeInterface
|
||||
{
|
||||
/**
|
||||
* Check if user is authorized for the action.
|
||||
*
|
||||
* Note: There are two deny values: denied (false), not set (null). This allows chaining multiple rules together
|
||||
* when the previous rules were not matched.
|
||||
*
|
||||
* @param string $action
|
||||
* @param string|null $scope
|
||||
* @param UserInterface|null $user
|
||||
* @return bool|null
|
||||
*/
|
||||
public function isAuthorized(string $action, string $scope = null, UserInterface $user = null): ?bool;
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Interfaces;
|
||||
|
||||
use Grav\Framework\Flex\Flex;
|
||||
use Grav\Framework\Object\Interfaces\NestedObjectInterface;
|
||||
use Grav\Framework\Object\Interfaces\ObjectCollectionInterface;
|
||||
use Grav\Framework\Flex\FlexDirectory;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Defines a collection of Flex Objects.
|
||||
*
|
||||
* @used-by \Grav\Framework\Flex\FlexCollection
|
||||
* @since 1.6
|
||||
* @template T
|
||||
* @extends ObjectCollectionInterface<string,T>
|
||||
*/
|
||||
interface FlexCollectionInterface extends FlexCommonInterface, ObjectCollectionInterface, NestedObjectInterface
|
||||
{
|
||||
/**
|
||||
* Creates a Flex Collection from an array.
|
||||
*
|
||||
* @used-by FlexDirectory::createCollection() Official method to create a Flex Collection.
|
||||
*
|
||||
* @param FlexObjectInterface[] $entries Associated array of Flex Objects to be included in the collection.
|
||||
* @param FlexDirectory $directory Flex Directory where all the objects belong into.
|
||||
* @param string|null $keyField Key field used to index the collection.
|
||||
* @return static Returns a new Flex Collection.
|
||||
*/
|
||||
public static function createFromArray(array $entries, FlexDirectory $directory, string $keyField = null);
|
||||
|
||||
/**
|
||||
* Creates a new Flex Collection.
|
||||
*
|
||||
* @used-by FlexDirectory::createCollection() Official method to create Flex Collection.
|
||||
*
|
||||
* @param FlexObjectInterface[] $entries Associated array of Flex Objects to be included in the collection.
|
||||
* @param FlexDirectory|null $directory Flex Directory where all the objects belong into.
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct(array $entries = [], FlexDirectory $directory = null);
|
||||
|
||||
/**
|
||||
* Search a string from the collection.
|
||||
*
|
||||
* @param string $search Search string.
|
||||
* @param string|string[]|null $properties Properties to search for, defaults to configured properties.
|
||||
* @param array|null $options Search options, defaults to configured options.
|
||||
* @return FlexCollectionInterface Returns a Flex Collection with only matching objects.
|
||||
* @phpstan-return static<T>
|
||||
* @api
|
||||
*/
|
||||
public function search(string $search, $properties = null, array $options = null);
|
||||
|
||||
/**
|
||||
* Sort the collection.
|
||||
*
|
||||
* @param array $orderings Pair of [property => 'ASC'|'DESC', ...].
|
||||
*
|
||||
* @return FlexCollectionInterface Returns a sorted version from the collection.
|
||||
* @phpstan-return static<T>
|
||||
*/
|
||||
public function sort(array $orderings);
|
||||
|
||||
/**
|
||||
* Filter collection by filter array with keys and values.
|
||||
*
|
||||
* @param array $filters
|
||||
* @return FlexCollectionInterface
|
||||
* @phpstan-return static<T>
|
||||
*/
|
||||
public function filterBy(array $filters);
|
||||
|
||||
/**
|
||||
* Get timestamps from all the objects in the collection.
|
||||
*
|
||||
* This method can be used for example in caching.
|
||||
*
|
||||
* @return int[] Returns [key => timestamp, ...] pairs.
|
||||
*/
|
||||
public function getTimestamps(): array;
|
||||
|
||||
/**
|
||||
* Get storage keys from all the objects in the collection.
|
||||
*
|
||||
* @see FlexDirectory::getObject() If you want to get Flex Object from the Flex Directory.
|
||||
*
|
||||
* @return string[] Returns [key => storage_key, ...] pairs.
|
||||
*/
|
||||
public function getStorageKeys(): array;
|
||||
|
||||
/**
|
||||
* Get Flex keys from all the objects in the collection.
|
||||
*
|
||||
* @see Flex::getObjects() If you want to get list of Flex Objects from any Flex Directory.
|
||||
*
|
||||
* @return string[] Returns[key => flex_key, ...] pairs.
|
||||
*/
|
||||
public function getFlexKeys(): array;
|
||||
|
||||
/**
|
||||
* Return new collection with a different key.
|
||||
*
|
||||
* @param string|null $keyField Switch key field of the collection.
|
||||
* @return FlexCollectionInterface Returns a new Flex Collection with new key field.
|
||||
* @phpstan-return static<T>
|
||||
* @api
|
||||
*/
|
||||
public function withKeyField(string $keyField = null);
|
||||
|
||||
/**
|
||||
* Get Flex Index from the Flex Collection.
|
||||
*
|
||||
* @return FlexIndexInterface Returns a Flex Index from the current collection.
|
||||
* @phpstan-return FlexIndexInterface<T>
|
||||
*/
|
||||
public function getIndex();
|
||||
|
||||
/**
|
||||
* Load all the objects into memory,
|
||||
*
|
||||
* @return FlexCollectionInterface
|
||||
* @phpstan-return static<T>
|
||||
*/
|
||||
public function getCollection();
|
||||
|
||||
/**
|
||||
* Get metadata associated to the object
|
||||
*
|
||||
* @param string $key Key.
|
||||
* @return array
|
||||
*/
|
||||
public function getMetaData(string $key): array;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Interfaces;
|
||||
|
||||
use Grav\Framework\Flex\FlexDirectory;
|
||||
use Grav\Framework\Interfaces\RenderInterface;
|
||||
|
||||
/**
|
||||
* Defines common interface shared with both Flex Objects and Collections.
|
||||
*
|
||||
* @used-by \Grav\Framework\Flex\FlexObject
|
||||
* @since 1.6
|
||||
*/
|
||||
interface FlexCommonInterface extends RenderInterface
|
||||
{
|
||||
/**
|
||||
* Get Flex Type of the object / collection.
|
||||
*
|
||||
* @return string Returns Flex Type of the collection.
|
||||
* @api
|
||||
*/
|
||||
public function getFlexType(): string;
|
||||
|
||||
/**
|
||||
* Get Flex Directory for the object / collection.
|
||||
*
|
||||
* @return FlexDirectory Returns associated Flex Directory.
|
||||
* @api
|
||||
*/
|
||||
public function getFlexDirectory(): FlexDirectory;
|
||||
|
||||
/**
|
||||
* Test whether the feature is implemented in the object / collection.
|
||||
*
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function hasFlexFeature(string $name): bool;
|
||||
|
||||
/**
|
||||
* Get full list of features the object / collection implements.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFlexFeatures(): array;
|
||||
|
||||
/**
|
||||
* Get last updated timestamp for the object / collection.
|
||||
*
|
||||
* @return int Returns Unix timestamp.
|
||||
* @api
|
||||
*/
|
||||
public function getTimestamp(): int;
|
||||
|
||||
/**
|
||||
* Get a cache key which is used for caching the object / collection.
|
||||
*
|
||||
* @return string Returns cache key.
|
||||
*/
|
||||
public function getCacheKey(): string;
|
||||
|
||||
/**
|
||||
* Get cache checksum for the object / collection.
|
||||
*
|
||||
* If checksum changes, cache gets invalided.
|
||||
*
|
||||
* @return string Returns cache checksum.
|
||||
*/
|
||||
public function getCacheChecksum(): string;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Interfaces;
|
||||
|
||||
/**
|
||||
* Defines Forms for Flex Objects.
|
||||
*
|
||||
* @used-by \Grav\Framework\Flex\FlexForm
|
||||
* @since 1.7
|
||||
*/
|
||||
interface FlexDirectoryFormInterface extends FlexFormInterface
|
||||
{
|
||||
/**
|
||||
* Get object associated to the form.
|
||||
*
|
||||
* @return FlexObjectInterface Returns Flex Object associated to the form.
|
||||
* @api
|
||||
*/
|
||||
public function getDirectory();
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Interfaces;
|
||||
|
||||
use Exception;
|
||||
use Grav\Common\Data\Blueprint;
|
||||
use Grav\Framework\Cache\CacheInterface;
|
||||
|
||||
/**
|
||||
* Interface FlexDirectoryInterface
|
||||
* @package Grav\Framework\Flex\Interfaces
|
||||
*/
|
||||
interface FlexDirectoryInterface
|
||||
{
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isListed(): bool;
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isEnabled(): bool;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFlexType(): string;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle(): string;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription(): string;
|
||||
|
||||
/**
|
||||
* @param string|null $name
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function getConfig(string $name = null, $default = null);
|
||||
|
||||
/**
|
||||
* @param string|null $name
|
||||
* @param array $options
|
||||
* @return FlexFormInterface
|
||||
* @internal
|
||||
*/
|
||||
public function getDirectoryForm(string $name = null, array $options = []);
|
||||
|
||||
/**
|
||||
* @return Blueprint
|
||||
* @internal
|
||||
*/
|
||||
public function getDirectoryBlueprint();
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param array $data
|
||||
* @return void
|
||||
* @throws Exception
|
||||
* @internal
|
||||
*/
|
||||
public function saveDirectoryConfig(string $name, array $data);
|
||||
|
||||
/**
|
||||
* @param string|null $name
|
||||
* @return string
|
||||
*/
|
||||
public function getDirectoryConfigUri(string $name = null): string;
|
||||
|
||||
/**
|
||||
* Returns a new uninitialized instance of blueprint.
|
||||
*
|
||||
* Always use $object->getBlueprint() or $object->getForm()->getBlueprint() instead.
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $context
|
||||
* @return Blueprint
|
||||
*/
|
||||
public function getBlueprint(string $type = '', string $context = '');
|
||||
|
||||
/**
|
||||
* @param string $view
|
||||
* @return string
|
||||
*/
|
||||
public function getBlueprintFile(string $view = ''): string;
|
||||
|
||||
/**
|
||||
* Get collection. In the site this will be filtered by the default filters (published etc).
|
||||
*
|
||||
* Use $directory->getIndex() if you want unfiltered collection.
|
||||
*
|
||||
* @param array|null $keys Array of keys.
|
||||
* @param string|null $keyField Field to be used as the key.
|
||||
* @return FlexCollectionInterface
|
||||
*/
|
||||
public function getCollection(array $keys = null, string $keyField = null): FlexCollectionInterface;
|
||||
|
||||
/**
|
||||
* Get the full collection of all stored objects.
|
||||
*
|
||||
* Use $directory->getCollection() if you want a filtered collection.
|
||||
*
|
||||
* @param array|null $keys Array of keys.
|
||||
* @param string|null $keyField Field to be used as the key.
|
||||
* @return FlexIndexInterface
|
||||
*/
|
||||
public function getIndex(array $keys = null, string $keyField = null): FlexIndexInterface;
|
||||
|
||||
/**
|
||||
* Returns an object if it exists. If no arguments are passed (or both of them are null), method creates a new empty object.
|
||||
*
|
||||
* Note: It is not safe to use the object without checking if the user can access it.
|
||||
*
|
||||
* @param string|null $key
|
||||
* @param string|null $keyField Field to be used as the key.
|
||||
* @return FlexObjectInterface|null
|
||||
*/
|
||||
public function getObject($key = null, string $keyField = null): ?FlexObjectInterface;
|
||||
|
||||
/**
|
||||
* @param string|null $namespace
|
||||
* @return CacheInterface
|
||||
*/
|
||||
public function getCache(string $namespace = null);
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function clearCache();
|
||||
|
||||
/**
|
||||
* @param string|null $key
|
||||
* @return string|null
|
||||
*/
|
||||
public function getStorageFolder(string $key = null): ?string;
|
||||
|
||||
/**
|
||||
* @param string|null $key
|
||||
* @return string|null
|
||||
*/
|
||||
public function getMediaFolder(string $key = null): ?string;
|
||||
|
||||
/**
|
||||
* @return FlexStorageInterface
|
||||
*/
|
||||
public function getStorage(): FlexStorageInterface;
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param string $key
|
||||
* @param bool $validate
|
||||
* @return FlexObjectInterface
|
||||
*/
|
||||
public function createObject(array $data, string $key = '', bool $validate = false): FlexObjectInterface;
|
||||
|
||||
/**
|
||||
* @param array $entries
|
||||
* @param string|null $keyField
|
||||
* @return FlexCollectionInterface
|
||||
*/
|
||||
public function createCollection(array $entries, string $keyField = null): FlexCollectionInterface;
|
||||
|
||||
/**
|
||||
* @param array $entries
|
||||
* @param string|null $keyField
|
||||
* @return FlexIndexInterface
|
||||
*/
|
||||
public function createIndex(array $entries, string $keyField = null): FlexIndexInterface;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getObjectClass(): string;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCollectionClass(): string;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getIndexClass(): string;
|
||||
|
||||
/**
|
||||
* @param array $entries
|
||||
* @param string|null $keyField
|
||||
* @return FlexCollectionInterface
|
||||
*/
|
||||
public function loadCollection(array $entries, string $keyField = null): FlexCollectionInterface;
|
||||
|
||||
/**
|
||||
* @param array $entries
|
||||
* @return FlexObjectInterface[]
|
||||
* @internal
|
||||
*/
|
||||
public function loadObjects(array $entries): array;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function reloadIndex(): void;
|
||||
|
||||
/**
|
||||
* @param string $scope
|
||||
* @param string $action
|
||||
* @return string
|
||||
*/
|
||||
public function getAuthorizeRule(string $scope, string $action): string;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Interfaces;
|
||||
|
||||
use Grav\Framework\Form\Interfaces\FormInterface;
|
||||
use Grav\Framework\Route\Route;
|
||||
use Serializable;
|
||||
|
||||
/**
|
||||
* Defines Forms for Flex Objects.
|
||||
*
|
||||
* @used-by \Grav\Framework\Flex\FlexForm
|
||||
* @since 1.6
|
||||
*/
|
||||
interface FlexFormInterface extends Serializable, FormInterface
|
||||
{
|
||||
/**
|
||||
* Get media task route.
|
||||
*
|
||||
* @return string Returns admin route for media tasks.
|
||||
*/
|
||||
public function getMediaTaskRoute(): string;
|
||||
|
||||
/**
|
||||
* Get route for uploading files by AJAX.
|
||||
*
|
||||
* @return Route|null Returns Route object or null if file uploads are not enabled.
|
||||
*/
|
||||
public function getFileUploadAjaxRoute();
|
||||
|
||||
/**
|
||||
* Get route for deleting files by AJAX.
|
||||
*
|
||||
* @param string $field Field where the file is associated into.
|
||||
* @param string $filename Filename for the file.
|
||||
* @return Route|null Returns Route object or null if file uploads are not enabled.
|
||||
*/
|
||||
public function getFileDeleteAjaxRoute($field, $filename);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Interfaces;
|
||||
|
||||
use Grav\Framework\Flex\FlexDirectory;
|
||||
|
||||
/**
|
||||
* Defines Indexes for Flex Objects.
|
||||
*
|
||||
* Flex indexes are similar to database indexes, they contain indexed fields which can be used to quickly look up or
|
||||
* find the objects without loading them.
|
||||
*
|
||||
* @used-by \Grav\Framework\Flex\FlexIndex
|
||||
* @since 1.6
|
||||
* @template T
|
||||
* @extends FlexCollectionInterface<T>
|
||||
*/
|
||||
interface FlexIndexInterface extends FlexCollectionInterface
|
||||
{
|
||||
/**
|
||||
* Helper method to create Flex Index.
|
||||
*
|
||||
* @used-by FlexDirectory::getIndex() Official method to get Index from a Flex Directory.
|
||||
*
|
||||
* @param FlexDirectory $directory Flex directory.
|
||||
* @return static Returns a new Flex Index.
|
||||
*/
|
||||
public static function createFromStorage(FlexDirectory $directory);
|
||||
|
||||
/**
|
||||
* Method to load index from the object storage, usually filesystem.
|
||||
*
|
||||
* @used-by FlexDirectory::getIndex() Official method to get Index from a Flex Directory.
|
||||
*
|
||||
* @param FlexStorageInterface $storage Flex Storage associated to the directory.
|
||||
* @return array Returns a list of existing objects [storage_key => [storage_key => xxx, storage_timestamp => 123456, ...]]
|
||||
*/
|
||||
public static function loadEntriesFromStorage(FlexStorageInterface $storage): array;
|
||||
|
||||
/**
|
||||
* Return new collection with a different key.
|
||||
*
|
||||
* @param string|null $keyField Switch key field of the collection.
|
||||
* @return static Returns a new Flex Collection with new key field.
|
||||
* @api
|
||||
*/
|
||||
public function withKeyField(string $keyField = null);
|
||||
|
||||
/**
|
||||
* @param string|null $indexKey
|
||||
* @return array
|
||||
*/
|
||||
public function getIndexMap(string $indexKey = null);
|
||||
}
|
||||
98
system/src/Grav/Framework/Flex/Interfaces/FlexInterface.php
Normal file
98
system/src/Grav/Framework/Flex/Interfaces/FlexInterface.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Interfaces;
|
||||
|
||||
use Countable;
|
||||
use Grav\Framework\Flex\FlexDirectory;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Interface FlexInterface
|
||||
* @package Grav\Framework\Flex\Interfaces
|
||||
*/
|
||||
interface FlexInterface extends Countable
|
||||
{
|
||||
/**
|
||||
* @param string $type
|
||||
* @param string $blueprint
|
||||
* @param array $config
|
||||
* @return $this
|
||||
*/
|
||||
public function addDirectoryType(string $type, string $blueprint, array $config = []);
|
||||
|
||||
/**
|
||||
* @param FlexDirectory $directory
|
||||
* @return $this
|
||||
*/
|
||||
public function addDirectory(FlexDirectory $directory);
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @return bool
|
||||
*/
|
||||
public function hasDirectory(string $type): bool;
|
||||
|
||||
/**
|
||||
* @param array|string[]|null $types
|
||||
* @param bool $keepMissing
|
||||
* @return array<FlexDirectory|null>
|
||||
*/
|
||||
public function getDirectories(array $types = null, bool $keepMissing = false): array;
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @return FlexDirectory|null
|
||||
*/
|
||||
public function getDirectory(string $type): ?FlexDirectory;
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param array|null $keys
|
||||
* @param string|null $keyField
|
||||
* @return FlexCollectionInterface|null
|
||||
*/
|
||||
public function getCollection(string $type, array $keys = null, string $keyField = null): ?FlexCollectionInterface;
|
||||
|
||||
/**
|
||||
* @param array $keys
|
||||
* @param array $options In addition to the options in getObjects(), following options can be passed:
|
||||
* collection_class: Class to be used to create the collection. Defaults to ObjectCollection.
|
||||
* @return FlexCollectionInterface
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function getMixedCollection(array $keys, array $options = []): FlexCollectionInterface;
|
||||
|
||||
/**
|
||||
* @param array $keys
|
||||
* @param array $options Following optional options can be passed:
|
||||
* types: List of allowed types.
|
||||
* type: Allowed type if types isn't defined, otherwise acts as default_type.
|
||||
* default_type: Set default type for objects given without type (only used if key_field isn't set).
|
||||
* keep_missing: Set to true if you want to return missing objects as null.
|
||||
* key_field: Key field which is used to match the objects.
|
||||
* @return array
|
||||
*/
|
||||
public function getObjects(array $keys, array $options = []): array;
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param string|null $type
|
||||
* @param string|null $keyField
|
||||
* @return FlexObjectInterface|null
|
||||
*/
|
||||
public function getObject(string $key, string $type = null, string $keyField = null): ?FlexObjectInterface;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Interfaces;
|
||||
|
||||
/**
|
||||
* Defines Forms for Flex Objects.
|
||||
*
|
||||
* @used-by \Grav\Framework\Flex\FlexForm
|
||||
* @since 1.7
|
||||
*/
|
||||
interface FlexObjectFormInterface extends FlexFormInterface
|
||||
{
|
||||
/**
|
||||
* Get object associated to the form.
|
||||
*
|
||||
* @return FlexObjectInterface Returns Flex Object associated to the form.
|
||||
* @api
|
||||
*/
|
||||
public function getObject();
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Interfaces;
|
||||
|
||||
use ArrayAccess;
|
||||
use Grav\Common\Data\Blueprint;
|
||||
use Grav\Framework\Flex\Flex;
|
||||
use Grav\Framework\Object\Interfaces\NestedObjectInterface;
|
||||
use Grav\Framework\Flex\FlexDirectory;
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Defines Flex Objects.
|
||||
*
|
||||
* @used-by \Grav\Framework\Flex\FlexObject
|
||||
* @since 1.6
|
||||
*/
|
||||
interface FlexObjectInterface extends FlexCommonInterface, NestedObjectInterface, ArrayAccess
|
||||
{
|
||||
/**
|
||||
* Construct a new Flex Object instance.
|
||||
*
|
||||
* @used-by FlexDirectory::createObject() Method to create Flex Object.
|
||||
*
|
||||
* @param array $elements Array of object properties.
|
||||
* @param string $key Identifier key for the new object.
|
||||
* @param FlexDirectory $directory Flex Directory the object belongs into.
|
||||
* @param bool $validate True if the object should be validated against blueprint.
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct(array $elements, $key, FlexDirectory $directory, bool $validate = false);
|
||||
|
||||
/**
|
||||
* Search a string from the object, returns weight between 0 and 1.
|
||||
*
|
||||
* Note: If you override this function, make sure you return value in range 0...1!
|
||||
*
|
||||
* @used-by FlexCollectionInterface::search() If you want to search a string from a Flex Collection.
|
||||
*
|
||||
* @param string $search Search string.
|
||||
* @param string|string[]|null $properties Properties to search for, defaults to configured properties.
|
||||
* @param array|null $options Search options, defaults to configured options.
|
||||
* @return float Returns a weight between 0 and 1.
|
||||
* @api
|
||||
*/
|
||||
public function search(string $search, $properties = null, array $options = null): float;
|
||||
|
||||
/**
|
||||
* Returns true if object has a key.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasKey();
|
||||
|
||||
/**
|
||||
* Get a unique key for the object.
|
||||
*
|
||||
* Flex Keys can be used without knowing the Directory the Object belongs into.
|
||||
*
|
||||
* @see Flex::getObject() If you want to get Flex Object from any Flex Directory.
|
||||
* @see Flex::getObjects() If you want to get list of Flex Objects from any Flex Directory.
|
||||
*
|
||||
* NOTE: Please do not override the method!
|
||||
*
|
||||
* @return string Returns Flex Key of the object.
|
||||
* @api
|
||||
*/
|
||||
public function getFlexKey(): string;
|
||||
|
||||
/**
|
||||
* Get an unique storage key (within the directory) which is used for figuring out the filename or database id.
|
||||
*
|
||||
* @see FlexDirectory::getObject() If you want to get Flex Object from the Flex Directory.
|
||||
* @see FlexDirectory::getCollection() If you want to get Flex Collection with selected keys from the Flex Directory.
|
||||
*
|
||||
* @return string Returns storage key of the Object.
|
||||
* @api
|
||||
*/
|
||||
public function getStorageKey(): string;
|
||||
|
||||
/**
|
||||
* Get index data associated to the object.
|
||||
*
|
||||
* @return array Returns metadata of the object.
|
||||
*/
|
||||
public function getMetaData(): array;
|
||||
|
||||
/**
|
||||
* Returns true if the object exists in the storage.
|
||||
*
|
||||
* @return bool Returns `true` if the object exists, `false` otherwise.
|
||||
* @api
|
||||
*/
|
||||
public function exists(): bool;
|
||||
|
||||
/**
|
||||
* Prepare object for saving into the storage.
|
||||
*
|
||||
* @return array Returns an array of object properties containing only scalars and arrays.
|
||||
*/
|
||||
public function prepareStorage(): array;
|
||||
|
||||
/**
|
||||
* Updates object in the memory.
|
||||
*
|
||||
* @see FlexObjectInterface::save() You need to save the object after calling this method.
|
||||
*
|
||||
* @param array $data Data containing updated properties with their values. To unset a value, use `null`.
|
||||
* @param array|UploadedFileInterface[] $files List of uploaded files to be saved within the object.
|
||||
* @return static
|
||||
* @throws RuntimeException
|
||||
* @api
|
||||
*/
|
||||
public function update(array $data, array $files = []);
|
||||
|
||||
/**
|
||||
* Create new object into the storage.
|
||||
*
|
||||
* @see FlexDirectory::createObject() If you want to create a new object instance.
|
||||
* @see FlexObjectInterface::update() If you want to update properties of the object.
|
||||
*
|
||||
* @param string|null $key Optional new key. If key isn't given, random key will be associated to the object.
|
||||
* @return static
|
||||
* @throws RuntimeException if object already exists.
|
||||
* @api
|
||||
*/
|
||||
public function create(string $key = null);
|
||||
|
||||
/**
|
||||
* Save object into the storage.
|
||||
*
|
||||
* @see FlexObjectInterface::update() If you want to update properties of the object.
|
||||
*
|
||||
* @return static
|
||||
* @api
|
||||
*/
|
||||
public function save();
|
||||
|
||||
/**
|
||||
* Delete object from the storage.
|
||||
*
|
||||
* @return static
|
||||
* @api
|
||||
*/
|
||||
public function delete();
|
||||
|
||||
/**
|
||||
* Returns the blueprint of the object.
|
||||
*
|
||||
* @see FlexObjectInterface::getForm()
|
||||
* @used-by FlexForm::getBlueprint()
|
||||
*
|
||||
* @param string $name Name of the Blueprint form. Used to create customized forms for different use cases.
|
||||
* @return Blueprint Returns a Blueprint.
|
||||
*/
|
||||
public function getBlueprint(string $name = '');
|
||||
|
||||
/**
|
||||
* Returns a form instance for the object.
|
||||
*
|
||||
* @param string $name Name of the form. Can be used to create customized forms for different use cases.
|
||||
* @param array|null $options Options can be used to further customize the form.
|
||||
* @return FlexFormInterface Returns a Form.
|
||||
* @api
|
||||
*/
|
||||
public function getForm(string $name = '', array $options = null);
|
||||
|
||||
/**
|
||||
* Returns default value suitable to be used in a form for the given property.
|
||||
*
|
||||
* @see FlexObjectInterface::getForm()
|
||||
*
|
||||
* @param string $name Property name.
|
||||
* @param string|null $separator Optional nested property separator.
|
||||
* @return mixed|null Returns default value of the field, null if there is no default value.
|
||||
*/
|
||||
public function getDefaultValue(string $name, string $separator = null);
|
||||
|
||||
/**
|
||||
* Returns default values suitable to be used in a form for the given property.
|
||||
*
|
||||
* @see FlexObjectInterface::getForm()
|
||||
*
|
||||
* @return array Returns default values.
|
||||
*/
|
||||
public function getDefaultValues(): array;
|
||||
|
||||
/**
|
||||
* Returns raw value suitable to be used in a form for the given property.
|
||||
*
|
||||
* @see FlexObjectInterface::getForm()
|
||||
*
|
||||
* @param string $name Property name.
|
||||
* @param mixed $default Default value.
|
||||
* @param string|null $separator Optional nested property separator.
|
||||
* @return mixed Returns value of the field.
|
||||
*/
|
||||
public function getFormValue(string $name, $default = null, string $separator = null);
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Interfaces;
|
||||
|
||||
/**
|
||||
* Defines Flex Storage layer.
|
||||
*
|
||||
* @since 1.6
|
||||
*/
|
||||
interface FlexStorageInterface
|
||||
{
|
||||
/**
|
||||
* StorageInterface constructor.
|
||||
* @param array $options
|
||||
*/
|
||||
public function __construct(array $options);
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getKeyField(): string;
|
||||
|
||||
/**
|
||||
* @param string[] $keys
|
||||
* @param bool $reload
|
||||
* @return array
|
||||
*/
|
||||
public function getMetaData(array $keys, bool $reload = false): array;
|
||||
|
||||
/**
|
||||
* Returns associated array of all existing storage keys with a timestamp.
|
||||
*
|
||||
* @return array Returns all existing keys as `[key => [storage_key => key, storage_timestamp => timestamp], ...]`.
|
||||
*/
|
||||
public function getExistingKeys(): array;
|
||||
|
||||
/**
|
||||
* Check if the key exists in the storage.
|
||||
*
|
||||
* @param string $key Storage key of an object.
|
||||
* @return bool Returns `true` if the key exists in the storage, `false` otherwise.
|
||||
*/
|
||||
public function hasKey(string $key): bool;
|
||||
|
||||
/**
|
||||
* Check if the key exists in the storage.
|
||||
*
|
||||
* @param string[] $keys Storage key of an object.
|
||||
* @return bool[] Returns keys with `true` if the key exists in the storage, `false` otherwise.
|
||||
*/
|
||||
public function hasKeys(array $keys): array;
|
||||
|
||||
/**
|
||||
* Create new rows into the storage.
|
||||
*
|
||||
* New keys will be assigned when the objects are created.
|
||||
*
|
||||
* @param array $rows List of rows as `[row, ...]`.
|
||||
* @return array Returns created rows as `[key => row, ...] pairs.
|
||||
*/
|
||||
public function createRows(array $rows): array;
|
||||
|
||||
/**
|
||||
* Read rows from the storage.
|
||||
*
|
||||
* If you pass object or array as value, that value will be used to save I/O.
|
||||
*
|
||||
* @param array $rows Array of `[key => row, ...]` pairs.
|
||||
* @param array|null $fetched Optional reference to store only fetched items.
|
||||
* @return array Returns rows. Note that non-existing rows will have `null` as their value.
|
||||
*/
|
||||
public function readRows(array $rows, array &$fetched = null): array;
|
||||
|
||||
/**
|
||||
* Update existing rows in the storage.
|
||||
*
|
||||
* @param array $rows Array of `[key => row, ...]` pairs.
|
||||
* @return array Returns updated rows. Note that non-existing rows will not be saved and have `null` as their value.
|
||||
*/
|
||||
public function updateRows(array $rows): array;
|
||||
|
||||
/**
|
||||
* Delete rows from the storage.
|
||||
*
|
||||
* @param array $rows Array of `[key => row, ...]` pairs.
|
||||
* @return array Returns deleted rows. Note that non-existing rows have `null` as their value.
|
||||
*/
|
||||
public function deleteRows(array $rows): array;
|
||||
|
||||
/**
|
||||
* Replace rows regardless if they exist or not.
|
||||
*
|
||||
* All rows should have a specified key for replace to work properly.
|
||||
*
|
||||
* @param array $rows Array of `[key => row, ...]` pairs.
|
||||
* @return array Returns both created and updated rows.
|
||||
*/
|
||||
public function replaceRows(array $rows): array;
|
||||
|
||||
/**
|
||||
* @param string $src
|
||||
* @param string $dst
|
||||
* @return bool
|
||||
*/
|
||||
public function copyRow(string $src, string $dst): bool;
|
||||
|
||||
/**
|
||||
* @param string $src
|
||||
* @param string $dst
|
||||
* @return bool
|
||||
*/
|
||||
public function renameRow(string $src, string $dst): bool;
|
||||
|
||||
/**
|
||||
* Get filesystem path for the collection or object storage.
|
||||
*
|
||||
* @param string|null $key Optional storage key.
|
||||
* @return string|null Path in the filesystem. Can be URI or null if storage is not filesystem based.
|
||||
*/
|
||||
public function getStoragePath(string $key = null): ?string;
|
||||
|
||||
/**
|
||||
* Get filesystem path for the collection or object media.
|
||||
*
|
||||
* @param string|null $key Optional storage key.
|
||||
* @return string|null Path in the filesystem. Can be URI or null if media isn't supported.
|
||||
*/
|
||||
public function getMediaPath(string $key = null): ?string;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Interfaces;
|
||||
|
||||
/**
|
||||
* Implements PageTranslateInterface
|
||||
*/
|
||||
interface FlexTranslateInterface
|
||||
{
|
||||
/**
|
||||
* Returns true if object has a translation in given language (or any of its fallback languages).
|
||||
*
|
||||
* @param string|null $languageCode
|
||||
* @param bool|null $fallback
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTranslation(string $languageCode = null, bool $fallback = null): bool;
|
||||
|
||||
/**
|
||||
* Get translation.
|
||||
*
|
||||
* @param string|null $languageCode
|
||||
* @param bool|null $fallback
|
||||
* @return static|null
|
||||
*/
|
||||
public function getTranslation(string $languageCode = null, bool $fallback = null);
|
||||
|
||||
/**
|
||||
* Returns all translated languages.
|
||||
*
|
||||
* @param bool $includeDefault If set to true, return separate entries for '' and 'en' (default) language.
|
||||
* @return array
|
||||
*/
|
||||
public function getLanguages(bool $includeDefault = false): array;
|
||||
|
||||
/**
|
||||
* Get used language.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLanguage(): string;
|
||||
}
|
||||
203
system/src/Grav/Framework/Flex/Pages/FlexPageCollection.php
Normal file
203
system/src/Grav/Framework/Flex/Pages/FlexPageCollection.php
Normal file
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Pages;
|
||||
|
||||
use Grav\Common\Page\Interfaces\PageInterface;
|
||||
use Grav\Framework\Flex\FlexCollection;
|
||||
use function array_search;
|
||||
use function assert;
|
||||
use function is_int;
|
||||
|
||||
/**
|
||||
* Class FlexPageCollection
|
||||
* @package Grav\Plugin\FlexObjects\Types\FlexPages
|
||||
* @template T of \Grav\Framework\Flex\Interfaces\FlexObjectInterface
|
||||
* @extends FlexCollection<T>
|
||||
*/
|
||||
class FlexPageCollection extends FlexCollection
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getCachedMethods(): array
|
||||
{
|
||||
return [
|
||||
// Collection filtering
|
||||
'withPublished' => true,
|
||||
'withVisible' => true,
|
||||
'withRoutable' => true,
|
||||
|
||||
'isFirst' => true,
|
||||
'isLast' => true,
|
||||
|
||||
// Find objects
|
||||
'prevSibling' => false,
|
||||
'nextSibling' => false,
|
||||
'adjacentSibling' => false,
|
||||
'currentPosition' => true,
|
||||
|
||||
'getNextOrder' => false,
|
||||
] + parent::getCachedMethods();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $bool
|
||||
* @return static
|
||||
* @phpstan-return static<T>
|
||||
*/
|
||||
public function withPublished(bool $bool = true)
|
||||
{
|
||||
$list = array_keys(array_filter($this->call('isPublished', [$bool])));
|
||||
|
||||
return $this->select($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $bool
|
||||
* @return static
|
||||
* @phpstan-return static<T>
|
||||
*/
|
||||
public function withVisible(bool $bool = true)
|
||||
{
|
||||
$list = array_keys(array_filter($this->call('isVisible', [$bool])));
|
||||
|
||||
return $this->select($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $bool
|
||||
* @return static
|
||||
* @phpstan-return static<T>
|
||||
*/
|
||||
public function withRoutable(bool $bool = true)
|
||||
{
|
||||
$list = array_keys(array_filter($this->call('isRoutable', [$bool])));
|
||||
|
||||
return $this->select($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if this item is the first in the collection.
|
||||
*
|
||||
* @param string $path
|
||||
* @return bool True if item is first.
|
||||
*/
|
||||
public function isFirst($path): bool
|
||||
{
|
||||
$keys = $this->getKeys();
|
||||
$first = reset($keys);
|
||||
|
||||
return $path === $first;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if this item is the last in the collection.
|
||||
*
|
||||
* @param string $path
|
||||
* @return bool True if item is last.
|
||||
*/
|
||||
public function isLast($path): bool
|
||||
{
|
||||
$keys = $this->getKeys();
|
||||
$last = end($keys);
|
||||
|
||||
return $path === $last;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the previous sibling based on current position.
|
||||
*
|
||||
* @param string $path
|
||||
* @return PageInterface|false The previous item.
|
||||
* @phpstan-return T|false
|
||||
*/
|
||||
public function prevSibling($path)
|
||||
{
|
||||
return $this->adjacentSibling($path, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next sibling based on current position.
|
||||
*
|
||||
* @param string $path
|
||||
* @return PageInterface|false The next item.
|
||||
* @phpstan-return T|false
|
||||
*/
|
||||
public function nextSibling($path)
|
||||
{
|
||||
return $this->adjacentSibling($path, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the adjacent sibling based on a direction.
|
||||
*
|
||||
* @param string $path
|
||||
* @param int $direction either -1 or +1
|
||||
* @return PageInterface|false The sibling item.
|
||||
* @phpstan-return T|false
|
||||
*/
|
||||
public function adjacentSibling($path, $direction = 1)
|
||||
{
|
||||
$keys = $this->getKeys();
|
||||
$pos = array_search($path, $keys, true);
|
||||
|
||||
if ($pos !== false) {
|
||||
$pos += $direction;
|
||||
if (isset($keys[$pos])) {
|
||||
return $this[$keys[$pos]];
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the item in the current position.
|
||||
*
|
||||
* @param string $path the path the item
|
||||
* @return int|null The index of the current page, null if not found.
|
||||
*/
|
||||
public function currentPosition($path): ?int
|
||||
{
|
||||
$pos = array_search($path, $this->getKeys(), true);
|
||||
|
||||
return $pos !== false ? $pos : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getNextOrder()
|
||||
{
|
||||
$directory = $this->getFlexDirectory();
|
||||
|
||||
$collection = $directory->getIndex();
|
||||
$keys = $collection->getStorageKeys();
|
||||
|
||||
// Assign next free order.
|
||||
/** @var FlexPageObject|null $last */
|
||||
$last = null;
|
||||
$order = 0;
|
||||
foreach ($keys as $folder => $key) {
|
||||
preg_match(FlexPageIndex::ORDER_PREFIX_REGEX, $folder, $test);
|
||||
$test = $test[0] ?? null;
|
||||
if ($test && $test > $order) {
|
||||
$order = $test;
|
||||
$last = $key;
|
||||
}
|
||||
}
|
||||
|
||||
$last = $collection[$last];
|
||||
|
||||
return sprintf('%d.', $last ? $last->value('order') + 1 : 1);
|
||||
}
|
||||
}
|
||||
48
system/src/Grav/Framework/Flex/Pages/FlexPageIndex.php
Normal file
48
system/src/Grav/Framework/Flex/Pages/FlexPageIndex.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Pages;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Framework\Flex\FlexIndex;
|
||||
|
||||
/**
|
||||
* Class FlexPageObject
|
||||
* @package Grav\Plugin\FlexObjects\Types\FlexPages
|
||||
*
|
||||
* @method FlexPageIndex withRoutable(bool $bool = true)
|
||||
* @method FlexPageIndex withPublished(bool $bool = true)
|
||||
* @method FlexPageIndex withVisible(bool $bool = true)
|
||||
*
|
||||
* @template T of FlexPageObject
|
||||
* @template C of FlexPageCollection
|
||||
* @extends FlexIndex<T,C>
|
||||
*/
|
||||
class FlexPageIndex extends FlexIndex
|
||||
{
|
||||
public const ORDER_PREFIX_REGEX = '/^\d+\./u';
|
||||
|
||||
/**
|
||||
* @param string $route
|
||||
* @return string
|
||||
* @internal
|
||||
*/
|
||||
public static function normalizeRoute(string $route)
|
||||
{
|
||||
static $case_insensitive;
|
||||
|
||||
if (null === $case_insensitive) {
|
||||
$case_insensitive = Grav::instance()['config']->get('system.force_lowercase_urls', false);
|
||||
}
|
||||
|
||||
return $case_insensitive ? mb_strtolower($route) : $route;
|
||||
}
|
||||
}
|
||||
495
system/src/Grav/Framework/Flex/Pages/FlexPageObject.php
Normal file
495
system/src/Grav/Framework/Flex/Pages/FlexPageObject.php
Normal file
@@ -0,0 +1,495 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Pages;
|
||||
|
||||
use DateTime;
|
||||
use Exception;
|
||||
use Grav\Common\Debugger;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Page\Interfaces\PageInterface;
|
||||
use Grav\Common\Page\Traits\PageFormTrait;
|
||||
use Grav\Common\User\Interfaces\UserCollectionInterface;
|
||||
use Grav\Framework\Flex\FlexObject;
|
||||
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexTranslateInterface;
|
||||
use Grav\Framework\Flex\Pages\Traits\PageAuthorsTrait;
|
||||
use Grav\Framework\Flex\Pages\Traits\PageContentTrait;
|
||||
use Grav\Framework\Flex\Pages\Traits\PageLegacyTrait;
|
||||
use Grav\Framework\Flex\Pages\Traits\PageRoutableTrait;
|
||||
use Grav\Framework\Flex\Pages\Traits\PageTranslateTrait;
|
||||
use Grav\Framework\Flex\Traits\FlexMediaTrait;
|
||||
use RuntimeException;
|
||||
use stdClass;
|
||||
use function array_key_exists;
|
||||
use function is_array;
|
||||
|
||||
/**
|
||||
* Class FlexPageObject
|
||||
* @package Grav\Plugin\FlexObjects\Types\FlexPages
|
||||
*/
|
||||
class FlexPageObject extends FlexObject implements PageInterface, FlexTranslateInterface
|
||||
{
|
||||
use PageAuthorsTrait;
|
||||
use PageContentTrait;
|
||||
use PageFormTrait;
|
||||
use PageLegacyTrait;
|
||||
use PageRoutableTrait;
|
||||
use PageTranslateTrait;
|
||||
use FlexMediaTrait;
|
||||
|
||||
public const PAGE_ORDER_REGEX = '/^(\d+)\.(.*)$/u';
|
||||
public const PAGE_ORDER_PREFIX_REGEX = '/^[0-9]+\./u';
|
||||
|
||||
/** @var array|null */
|
||||
protected $_reorder;
|
||||
/** @var FlexPageObject|null */
|
||||
protected $_original;
|
||||
|
||||
/**
|
||||
* Clone page.
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
parent::__clone();
|
||||
|
||||
if (isset($this->header)) {
|
||||
$this->header = clone($this->header);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getCachedMethods(): array
|
||||
{
|
||||
return [
|
||||
// Page Content Interface
|
||||
'header' => false,
|
||||
'summary' => true,
|
||||
'content' => true,
|
||||
'value' => false,
|
||||
'media' => false,
|
||||
'title' => true,
|
||||
'menu' => true,
|
||||
'visible' => true,
|
||||
'published' => true,
|
||||
'publishDate' => true,
|
||||
'unpublishDate' => true,
|
||||
'process' => true,
|
||||
'slug' => true,
|
||||
'order' => true,
|
||||
'id' => true,
|
||||
'modified' => true,
|
||||
'lastModified' => true,
|
||||
'folder' => true,
|
||||
'date' => true,
|
||||
'dateformat' => true,
|
||||
'taxonomy' => true,
|
||||
'shouldProcess' => true,
|
||||
'isPage' => true,
|
||||
'isDir' => true,
|
||||
'folderExists' => true,
|
||||
|
||||
// Page
|
||||
'isPublished' => true,
|
||||
'isOrdered' => true,
|
||||
'isVisible' => true,
|
||||
'isRoutable' => true,
|
||||
'getCreated_Timestamp' => true,
|
||||
'getPublish_Timestamp' => true,
|
||||
'getUnpublish_Timestamp' => true,
|
||||
'getUpdated_Timestamp' => true,
|
||||
] + parent::getCachedMethods();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $test
|
||||
* @return bool
|
||||
*/
|
||||
public function isPublished(bool $test = true): bool
|
||||
{
|
||||
$time = time();
|
||||
$start = $this->getPublish_Timestamp();
|
||||
$stop = $this->getUnpublish_Timestamp();
|
||||
|
||||
return $this->published() && $start <= $time && (!$stop || $time <= $stop) === $test;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $test
|
||||
* @return bool
|
||||
*/
|
||||
public function isOrdered(bool $test = true): bool
|
||||
{
|
||||
return ($this->order() !== false) === $test;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $test
|
||||
* @return bool
|
||||
*/
|
||||
public function isVisible(bool $test = true): bool
|
||||
{
|
||||
return $this->visible() === $test;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $test
|
||||
* @return bool
|
||||
*/
|
||||
public function isRoutable(bool $test = true): bool
|
||||
{
|
||||
return $this->routable() === $test;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getCreated_Timestamp(): int
|
||||
{
|
||||
return $this->getFieldTimestamp('created_date') ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getPublish_Timestamp(): int
|
||||
{
|
||||
return $this->getFieldTimestamp('publish_date') ?? $this->getCreated_Timestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getUnpublish_Timestamp(): ?int
|
||||
{
|
||||
return $this->getFieldTimestamp('unpublish_date');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getUpdated_Timestamp(): int
|
||||
{
|
||||
return $this->getFieldTimestamp('updated_date') ?? $this->getPublish_Timestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getFormValue(string $name, $default = null, string $separator = null)
|
||||
{
|
||||
$test = new stdClass();
|
||||
|
||||
$value = $this->pageContentValue($name, $test);
|
||||
if ($value !== $test) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
switch ($name) {
|
||||
case 'name':
|
||||
return $this->getProperty('template');
|
||||
case 'route':
|
||||
return $this->hasKey() ? '/' . $this->getKey() : null;
|
||||
case 'header.permissions.groups':
|
||||
$encoded = json_encode($this->getPermissions());
|
||||
if ($encoded === false) {
|
||||
throw new RuntimeException('json_encode(): failed to encode group permissions');
|
||||
}
|
||||
|
||||
return json_decode($encoded, true);
|
||||
}
|
||||
|
||||
return parent::getFormValue($name, $default, $separator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get master storage key.
|
||||
*
|
||||
* @return string
|
||||
* @see FlexObjectInterface::getStorageKey()
|
||||
*/
|
||||
public function getMasterKey(): string
|
||||
{
|
||||
$key = (string)($this->storage_key ?? $this->getMetaData()['storage_key'] ?? null);
|
||||
if (($pos = strpos($key, '|')) !== false) {
|
||||
$key = substr($key, 0, $pos);
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexObjectInterface::getCacheKey()
|
||||
*/
|
||||
public function getCacheKey(): string
|
||||
{
|
||||
return $this->hasKey() ? $this->getTypePrefix() . $this->getFlexType() . '.' . $this->getKey() . '.' . $this->getLanguage() : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $key
|
||||
* @return FlexObjectInterface
|
||||
*/
|
||||
public function createCopy(string $key = null)
|
||||
{
|
||||
$this->copy();
|
||||
|
||||
return parent::createCopy($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|bool $reorder
|
||||
* @return FlexObject|FlexObjectInterface
|
||||
*/
|
||||
public function save($reorder = true)
|
||||
{
|
||||
return parent::save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Page Unmodified (original) version of the page.
|
||||
*
|
||||
* Assumes that object has been cloned before modifying it.
|
||||
*
|
||||
* @return FlexPageObject|null The original version of the page.
|
||||
*/
|
||||
public function getOriginal()
|
||||
{
|
||||
return $this->_original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the Page Unmodified (original) version of the page.
|
||||
*
|
||||
* Can be called multiple times, only the first call matters.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function storeOriginal(): void
|
||||
{
|
||||
if (null === $this->_original) {
|
||||
$this->_original = clone $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display order for the associated media.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getMediaOrder(): array
|
||||
{
|
||||
$order = $this->getNestedProperty('header.media_order');
|
||||
|
||||
if (is_array($order)) {
|
||||
return $order;
|
||||
}
|
||||
|
||||
if (!$order) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_map('trim', explode(',', $order));
|
||||
}
|
||||
|
||||
// Overrides for header properties.
|
||||
|
||||
/**
|
||||
* Common logic to load header properties.
|
||||
*
|
||||
* @param string $property
|
||||
* @param mixed $var
|
||||
* @param callable $filter
|
||||
* @return mixed|null
|
||||
*/
|
||||
protected function loadHeaderProperty(string $property, $var, callable $filter)
|
||||
{
|
||||
// We have to use parent methods in order to avoid loops.
|
||||
$value = null === $var ? parent::getProperty($property) : null;
|
||||
if (null === $value) {
|
||||
$value = $filter($var ?? $this->getProperty('header')->get($property));
|
||||
|
||||
parent::setProperty($property, $value);
|
||||
if ($this->doHasProperty($property)) {
|
||||
$value = parent::getProperty($property);
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common logic to load header properties.
|
||||
*
|
||||
* @param string $property
|
||||
* @param mixed $var
|
||||
* @param callable $filter
|
||||
* @return mixed|null
|
||||
*/
|
||||
protected function loadProperty(string $property, $var, callable $filter)
|
||||
{
|
||||
// We have to use parent methods in order to avoid loops.
|
||||
$value = null === $var ? parent::getProperty($property) : null;
|
||||
if (null === $value) {
|
||||
$value = $filter($var);
|
||||
|
||||
parent::setProperty($property, $value);
|
||||
if ($this->doHasProperty($property)) {
|
||||
$value = parent::getProperty($property);
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $property
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function getProperty($property, $default = null)
|
||||
{
|
||||
$method = static::$headerProperties[$property] ?? static::$calculatedProperties[$property] ?? null;
|
||||
if ($method && method_exists($this, $method)) {
|
||||
return $this->{$method}();
|
||||
}
|
||||
|
||||
return parent::getProperty($property, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $property
|
||||
* @param mixed $value
|
||||
* @return $this
|
||||
*/
|
||||
public function setProperty($property, $value)
|
||||
{
|
||||
$method = static::$headerProperties[$property] ?? static::$calculatedProperties[$property] ?? null;
|
||||
if ($method && method_exists($this, $method)) {
|
||||
$this->{$method}($value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
parent::setProperty($property, $value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $property
|
||||
* @param mixed $value
|
||||
* @param string|null $separator
|
||||
* @return $this
|
||||
*/
|
||||
public function setNestedProperty($property, $value, $separator = null)
|
||||
{
|
||||
$separator = $separator ?: '.';
|
||||
if (strpos($property, 'header' . $separator) === 0) {
|
||||
$this->getProperty('header')->set(str_replace('header' . $separator, '', $property), $value, $separator);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
parent::setNestedProperty($property, $value, $separator);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $property
|
||||
* @param string|null $separator
|
||||
* @return $this
|
||||
*/
|
||||
public function unsetNestedProperty($property, $separator = null)
|
||||
{
|
||||
$separator = $separator ?: '.';
|
||||
if (strpos($property, 'header' . $separator) === 0) {
|
||||
$this->getProperty('header')->undef(str_replace('header' . $separator, '', $property), $separator);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
parent::unsetNestedProperty($property, $separator);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $elements
|
||||
* @param bool $extended
|
||||
* @return void
|
||||
*/
|
||||
protected function filterElements(array &$elements, bool $extended = false): void
|
||||
{
|
||||
// Markdown storage conversion to page structure.
|
||||
if (array_key_exists('content', $elements)) {
|
||||
$elements['markdown'] = $elements['content'];
|
||||
unset($elements['content']);
|
||||
}
|
||||
|
||||
if (!$extended) {
|
||||
$folder = !empty($elements['folder']) ? trim($elements['folder']) : '';
|
||||
|
||||
if ($folder) {
|
||||
$order = !empty($elements['order']) ? (int)$elements['order'] : null;
|
||||
// TODO: broken
|
||||
$elements['storage_key'] = $order ? sprintf('%02d.%s', $order, $folder) : $folder;
|
||||
}
|
||||
}
|
||||
|
||||
parent::filterElements($elements);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
* @return int|null
|
||||
*/
|
||||
protected function getFieldTimestamp(string $field): ?int
|
||||
{
|
||||
$date = $this->getFieldDateTime($field);
|
||||
|
||||
return $date ? $date->getTimestamp() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
* @return DateTime|null
|
||||
*/
|
||||
protected function getFieldDateTime(string $field): ?DateTime
|
||||
{
|
||||
try {
|
||||
$value = $this->getProperty($field);
|
||||
if (is_numeric($value)) {
|
||||
$value = '@' . $value;
|
||||
}
|
||||
$date = $value ? new DateTime($value) : null;
|
||||
} catch (Exception $e) {
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = Grav::instance()['debugger'];
|
||||
$debugger->addException($e);
|
||||
|
||||
$date = null;
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UserCollectionInterface|null
|
||||
* @internal
|
||||
*/
|
||||
protected function loadAccounts()
|
||||
{
|
||||
return Grav::instance()['accounts'] ?? null;
|
||||
}
|
||||
}
|
||||
249
system/src/Grav/Framework/Flex/Pages/Traits/PageAuthorsTrait.php
Normal file
249
system/src/Grav/Framework/Flex/Pages/Traits/PageAuthorsTrait.php
Normal file
@@ -0,0 +1,249 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Pages\Traits;
|
||||
|
||||
use Grav\Common\User\Interfaces\UserInterface;
|
||||
use Grav\Framework\Acl\Access;
|
||||
use InvalidArgumentException;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function is_bool;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Trait PageAuthorsTrait
|
||||
* @package Grav\Framework\Flex\Pages\Traits
|
||||
*/
|
||||
trait PageAuthorsTrait
|
||||
{
|
||||
/** @var array<int,UserInterface> */
|
||||
private $_authors;
|
||||
/** @var array|null */
|
||||
private $_permissionsCache;
|
||||
|
||||
/**
|
||||
* Returns true if object has the named author.
|
||||
*
|
||||
* @param string $username
|
||||
* @return bool
|
||||
*/
|
||||
public function hasAuthor(string $username): bool
|
||||
{
|
||||
$authors = (array)$this->getNestedProperty('header.permissions.authors');
|
||||
if (empty($authors)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($authors as $author) {
|
||||
if ($username === $author) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of all author objects.
|
||||
*
|
||||
* @return array<int,UserInterface>
|
||||
*/
|
||||
public function getAuthors(): array
|
||||
{
|
||||
if (null === $this->_authors) {
|
||||
$this->_authors = $this->loadAuthors($this->getNestedProperty('header.permissions.authors', []));
|
||||
}
|
||||
|
||||
return $this->_authors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $inherit
|
||||
* @return array
|
||||
*/
|
||||
public function getPermissions(bool $inherit = false)
|
||||
{
|
||||
if (null === $this->_permissionsCache) {
|
||||
$permissions = [];
|
||||
if ($inherit && $this->getNestedProperty('header.permissions.inherit', true)) {
|
||||
$parent = $this->parent();
|
||||
if ($parent && method_exists($parent, 'getPermissions')) {
|
||||
$permissions = $parent->getPermissions($inherit);
|
||||
}
|
||||
}
|
||||
|
||||
$this->_permissionsCache = $this->loadPermissions($permissions);
|
||||
}
|
||||
|
||||
return $this->_permissionsCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param iterable $authors
|
||||
* @return array<int,UserInterface>
|
||||
*/
|
||||
protected function loadAuthors(iterable $authors): array
|
||||
{
|
||||
$accounts = $this->loadAccounts();
|
||||
if (null === $accounts || empty($authors)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$list = [];
|
||||
foreach ($authors as $username) {
|
||||
if (!is_string($username)) {
|
||||
throw new InvalidArgumentException('Iterable should return username (string).', 500);
|
||||
}
|
||||
$list[] = $accounts->load($username);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $action
|
||||
* @param string|null $scope
|
||||
* @param UserInterface|null $user
|
||||
* @param bool $isAuthor
|
||||
* @return bool|null
|
||||
*/
|
||||
public function isParentAuthorized(string $action, string $scope = null, UserInterface $user = null, bool $isAuthor = false): ?bool
|
||||
{
|
||||
$scope = $scope ?? $this->getAuthorizeScope();
|
||||
|
||||
$isMe = null === $user;
|
||||
if ($isMe) {
|
||||
$user = $this->getActiveUser();
|
||||
}
|
||||
|
||||
if (null === $user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->isAuthorizedByGroup($user, $action, $scope, $isMe, $isAuthor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UserInterface $user
|
||||
* @param string $action
|
||||
* @param string $scope
|
||||
* @param bool $isMe
|
||||
* @return bool|null
|
||||
*/
|
||||
protected function isAuthorizedOverride(UserInterface $user, string $action, string $scope, bool $isMe): ?bool
|
||||
{
|
||||
if ($action === 'delete' && $this->root()) {
|
||||
// Do not allow deleting root.
|
||||
return false;
|
||||
}
|
||||
|
||||
$isAuthor = !$isMe || $user->authorized ? $this->hasAuthor($user->username) : false;
|
||||
|
||||
return $this->isAuthorizedByGroup($user, $action, $scope, $isMe, $isAuthor) ?? parent::isAuthorizedOverride($user, $action, $scope, $isMe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Group authorization works as follows:
|
||||
*
|
||||
* 1. if any of the groups deny access, return false
|
||||
* 2. else if any of the groups allow access, return true
|
||||
* 3. else return null
|
||||
*
|
||||
* @param UserInterface $user
|
||||
* @param string $action
|
||||
* @param string $scope
|
||||
* @param bool $isMe
|
||||
* @param bool $isAuthor
|
||||
* @return bool|null
|
||||
*/
|
||||
protected function isAuthorizedByGroup(UserInterface $user, string $action, string $scope, bool $isMe, bool $isAuthor): ?bool
|
||||
{
|
||||
$authorized = null;
|
||||
|
||||
// In admin we want to check against group permissions.
|
||||
$pageGroups = $this->getPermissions();
|
||||
$userGroups = (array)$user->groups;
|
||||
|
||||
/** @var Access $access */
|
||||
foreach ($pageGroups as $group => $access) {
|
||||
if ($group === 'defaults') {
|
||||
// Special defaults permissions group does not apply to guest.
|
||||
if ($isMe && !$user->authorized) {
|
||||
continue;
|
||||
}
|
||||
} elseif ($group === 'authors') {
|
||||
if (!$isAuthor) {
|
||||
continue;
|
||||
}
|
||||
} elseif (!in_array($group, $userGroups, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$auth = $access->authorize($action);
|
||||
if (is_bool($auth)) {
|
||||
if ($auth === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$authorized = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $authorized && $this->getNestedProperty('header.permissions.inherit', true)) {
|
||||
// Authorize against parent page.
|
||||
$parent = $this->parent();
|
||||
if ($parent && method_exists($parent, 'isParentAuthorized')) {
|
||||
$authorized = $parent->isParentAuthorized($action, $scope, !$isMe ? $user : null, $isAuthor);
|
||||
}
|
||||
}
|
||||
|
||||
return $authorized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $parent
|
||||
* @return array
|
||||
*/
|
||||
protected function loadPermissions(array $parent = []): array
|
||||
{
|
||||
static $rules = [
|
||||
'c' => 'create',
|
||||
'r' => 'read',
|
||||
'u' => 'update',
|
||||
'd' => 'delete',
|
||||
'p' => 'publish',
|
||||
'l' => 'list'
|
||||
];
|
||||
|
||||
$permissions = $this->getNestedProperty('header.permissions.groups');
|
||||
$name = $this->root() ? '<root>' : '/' . $this->getKey();
|
||||
|
||||
$list = [];
|
||||
if (is_array($permissions)) {
|
||||
foreach ($permissions as $group => $access) {
|
||||
$list[$group] = new Access($access, $rules, $name);
|
||||
}
|
||||
}
|
||||
foreach ($parent as $group => $access) {
|
||||
if (isset($list[$group])) {
|
||||
$object = $list[$group];
|
||||
} else {
|
||||
$object = new Access([], $rules, $name);
|
||||
$list[$group] = $object;
|
||||
}
|
||||
|
||||
$object->inherit($access);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
840
system/src/Grav/Framework/Flex/Pages/Traits/PageContentTrait.php
Normal file
840
system/src/Grav/Framework/Flex/Pages/Traits/PageContentTrait.php
Normal file
@@ -0,0 +1,840 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Pages\Traits;
|
||||
|
||||
use Exception;
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Markdown\Parsedown;
|
||||
use Grav\Common\Markdown\ParsedownExtra;
|
||||
use Grav\Common\Page\Header;
|
||||
use Grav\Common\Page\Interfaces\PageInterface;
|
||||
use Grav\Common\Page\Markdown\Excerpts;
|
||||
use Grav\Common\Page\Media;
|
||||
use Grav\Common\Twig\Twig;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Framework\File\Formatter\YamlFormatter;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
use stdClass;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Implements PageContentInterface.
|
||||
*/
|
||||
trait PageContentTrait
|
||||
{
|
||||
/** @var array */
|
||||
protected static $headerProperties = [
|
||||
'slug' => 'slug',
|
||||
'routes' => false,
|
||||
'title' => 'title',
|
||||
'language' => 'language',
|
||||
'template' => 'template',
|
||||
'menu' => 'menu',
|
||||
'routable' => 'routable',
|
||||
'visible' => 'visible',
|
||||
'redirect' => 'redirect',
|
||||
'external_url' => false,
|
||||
'order_dir' => 'orderDir',
|
||||
'order_by' => 'orderBy',
|
||||
'order_manual' => 'orderManual',
|
||||
'dateformat' => 'dateformat',
|
||||
'date' => 'date',
|
||||
'markdown_extra' => false,
|
||||
'taxonomy' => 'taxonomy',
|
||||
'max_count' => 'maxCount',
|
||||
'process' => 'process',
|
||||
'published' => 'published',
|
||||
'publish_date' => 'publishDate',
|
||||
'unpublish_date' => 'unpublishDate',
|
||||
'expires' => 'expires',
|
||||
'cache_control' => 'cacheControl',
|
||||
'etag' => 'eTag',
|
||||
'last_modified' => 'lastModified',
|
||||
'ssl' => 'ssl',
|
||||
'template_format' => 'templateFormat',
|
||||
'debugger' => false,
|
||||
];
|
||||
|
||||
/** @var array */
|
||||
protected static $calculatedProperties = [
|
||||
'name' => 'name',
|
||||
'parent' => 'parent',
|
||||
'parent_key' => 'parentStorageKey',
|
||||
'folder' => 'folder',
|
||||
'order' => 'order',
|
||||
'template' => 'template',
|
||||
];
|
||||
|
||||
/** @var object */
|
||||
protected $header;
|
||||
|
||||
/** @var string */
|
||||
protected $_summary;
|
||||
|
||||
/** @var string */
|
||||
protected $_content;
|
||||
|
||||
/**
|
||||
* Method to normalize the route.
|
||||
*
|
||||
* @param string $route
|
||||
* @return string
|
||||
* @internal
|
||||
*/
|
||||
public static function normalizeRoute($route): string
|
||||
{
|
||||
$case_insensitive = Grav::instance()['config']->get('system.force_lowercase_urls');
|
||||
|
||||
return $case_insensitive ? mb_strtolower($route) : $route;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function header($var = null)
|
||||
{
|
||||
if (null !== $var) {
|
||||
$this->setProperty('header', $var);
|
||||
}
|
||||
|
||||
return $this->getProperty('header');
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function summary($size = null, $textOnly = false): string
|
||||
{
|
||||
return $this->processSummary($size, $textOnly);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function setSummary($summary): void
|
||||
{
|
||||
$this->_summary = $summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @throws Exception
|
||||
*/
|
||||
public function content($var = null): string
|
||||
{
|
||||
if (null !== $var) {
|
||||
$this->_content = $var;
|
||||
}
|
||||
|
||||
return $this->_content ?? $this->processContent($this->getRawContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRawContent(): string
|
||||
{
|
||||
return $this->_content ?? $this->getArrayProperty('markdown') ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function setRawContent($content): void
|
||||
{
|
||||
$this->_content = $content ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function rawMarkdown($var = null): string
|
||||
{
|
||||
if ($var !== null) {
|
||||
$this->setProperty('markdown', $var);
|
||||
}
|
||||
|
||||
return $this->getProperty('markdown') ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* Implement by calling:
|
||||
*
|
||||
* $test = new \stdClass();
|
||||
* $value = $this->pageContentValue($name, $test);
|
||||
* if ($value !== $test) {
|
||||
* return $value;
|
||||
* }
|
||||
* return parent::value($name, $default);
|
||||
*/
|
||||
abstract public function value($name, $default = null, $separator = null);
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function media($var = null): Media
|
||||
{
|
||||
if ($var instanceof Media) {
|
||||
$this->setProperty('media', $var);
|
||||
}
|
||||
|
||||
return $this->getProperty('media');
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function title($var = null): string
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'title',
|
||||
$var,
|
||||
function ($value) {
|
||||
return trim($value ?? ($this->root() ? '<root>' : ucfirst($this->slug())));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function menu($var = null): string
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'menu',
|
||||
$var,
|
||||
function ($value) {
|
||||
return trim($value ?: $this->title());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function visible($var = null): bool
|
||||
{
|
||||
$value = $this->loadHeaderProperty(
|
||||
'visible',
|
||||
$var,
|
||||
function ($value) {
|
||||
return ($value ?? $this->order() !== false) && !$this->isModule();
|
||||
}
|
||||
);
|
||||
|
||||
return $value && $this->published();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function published($var = null): bool
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'published',
|
||||
$var,
|
||||
static function ($value) {
|
||||
return (bool)($value ?? true);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function publishDate($var = null): ?int
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'publish_date',
|
||||
$var,
|
||||
function ($value) {
|
||||
return $value ? Utils::date2timestamp($value, $this->getProperty('dateformat')) : null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function unpublishDate($var = null): ?int
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'unpublish_date',
|
||||
$var,
|
||||
function ($value) {
|
||||
return $value ? Utils::date2timestamp($value, $this->getProperty('dateformat')) : null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function process($var = null): array
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'process',
|
||||
$var,
|
||||
function ($value) {
|
||||
$value = array_replace(Grav::instance()['config']->get('system.pages.process', []), is_array($value) ? $value : []) ?? [];
|
||||
foreach ($value as $process => $status) {
|
||||
$value[$process] = (bool)$status;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function slug($var = null)
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'slug',
|
||||
$var,
|
||||
function ($value) {
|
||||
if (is_string($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$folder = $this->folder();
|
||||
if (null === $folder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$folder = preg_replace(static::PAGE_ORDER_PREFIX_REGEX, '', $folder);
|
||||
if (null === $folder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return static::normalizeRoute($folder);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function order($var = null)
|
||||
{
|
||||
$property = $this->loadProperty(
|
||||
'order',
|
||||
$var,
|
||||
function ($value) {
|
||||
if (null === $value) {
|
||||
$folder = $this->folder();
|
||||
if (null !== $folder) {
|
||||
preg_match(static::PAGE_ORDER_REGEX, $folder, $order);
|
||||
}
|
||||
|
||||
$value = $order[1] ?? false;
|
||||
}
|
||||
|
||||
if ($value === '') {
|
||||
$value = false;
|
||||
}
|
||||
if ($value !== false) {
|
||||
$value = (int)$value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
);
|
||||
|
||||
return $property !== false ? sprintf('%02d.', $property) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function id($var = null): string
|
||||
{
|
||||
$property = 'id';
|
||||
$value = null === $var ? $this->getProperty($property) : null;
|
||||
if (null === $value) {
|
||||
$value = $this->language() . ($var ?? ($this->modified() . md5('flex-' . $this->getFlexType() . '-' . $this->getKey())));
|
||||
|
||||
$this->setProperty($property, $value);
|
||||
if ($this->doHasProperty($property)) {
|
||||
$value = $this->getProperty($property);
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function modified($var = null): int
|
||||
{
|
||||
$property = 'modified';
|
||||
$value = null === $var ? $this->getProperty($property) : null;
|
||||
if (null === $value) {
|
||||
$value = (int)($var ?: $this->getTimestamp());
|
||||
|
||||
$this->setProperty($property, $value);
|
||||
if ($this->doHasProperty($property)) {
|
||||
$value = $this->getProperty($property);
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function lastModified($var = null): bool
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'last_modified',
|
||||
$var,
|
||||
static function ($value) {
|
||||
return (bool)($value ?? Grav::instance()['config']->get('system.pages.last_modified'));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function date($var = null): int
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'date',
|
||||
$var,
|
||||
function ($value) {
|
||||
$value = $value ? Utils::date2timestamp($value, $this->getProperty('dateformat')) : false;
|
||||
|
||||
return $value ?: $this->modified();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function dateformat($var = null): ?string
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'dateformat',
|
||||
$var,
|
||||
static function ($value) {
|
||||
return $value;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function taxonomy($var = null): array
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'taxonomy',
|
||||
$var,
|
||||
static function ($value) {
|
||||
if (is_array($value)) {
|
||||
// make sure first level are arrays
|
||||
array_walk($value, static function (&$val) {
|
||||
$val = (array) $val;
|
||||
});
|
||||
// make sure all values are strings
|
||||
array_walk_recursive($value, static function (&$val) {
|
||||
$val = (string) $val;
|
||||
});
|
||||
}
|
||||
|
||||
return $value ?? [];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function shouldProcess($process): bool
|
||||
{
|
||||
$test = $this->process();
|
||||
|
||||
return !empty($test[$process]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function isPage(): bool
|
||||
{
|
||||
return !in_array($this->template(), ['', 'folder'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function isDir(): bool
|
||||
{
|
||||
return !$this->isPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isModule(): bool
|
||||
{
|
||||
return $this->modularTwig();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Header|stdClass|array|null $value
|
||||
* @return Header
|
||||
*/
|
||||
protected function offsetLoad_header($value)
|
||||
{
|
||||
if ($value instanceof Header) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (null === $value) {
|
||||
$value = [];
|
||||
} elseif ($value instanceof stdClass) {
|
||||
$value = (array)$value;
|
||||
}
|
||||
|
||||
return new Header($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Header|stdClass|array|null $value
|
||||
* @return Header
|
||||
*/
|
||||
protected function offsetPrepare_header($value)
|
||||
{
|
||||
return $this->offsetLoad_header($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Header|null $value
|
||||
* @return array
|
||||
*/
|
||||
protected function offsetSerialize_header(?Header $value)
|
||||
{
|
||||
return $value ? $value->toArray() : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param mixed|null $default
|
||||
* @return mixed
|
||||
*/
|
||||
protected function pageContentValue($name, $default = null)
|
||||
{
|
||||
switch ($name) {
|
||||
case 'frontmatter':
|
||||
$frontmatter = $this->getArrayProperty('frontmatter');
|
||||
if ($frontmatter === null) {
|
||||
$header = $this->prepareStorage()['header'] ?? null;
|
||||
if ($header) {
|
||||
$formatter = new YamlFormatter();
|
||||
$frontmatter = $formatter->encode($header);
|
||||
} else {
|
||||
$frontmatter = '';
|
||||
}
|
||||
}
|
||||
return $frontmatter;
|
||||
case 'content':
|
||||
return $this->getProperty('markdown');
|
||||
case 'order':
|
||||
return (string)$this->order();
|
||||
case 'menu':
|
||||
return $this->menu();
|
||||
case 'ordering':
|
||||
return $this->order() !== false ? '1' : '0';
|
||||
case 'folder':
|
||||
$folder = $this->folder();
|
||||
|
||||
return null !== $folder ? preg_replace(static::PAGE_ORDER_PREFIX_REGEX, '', $folder) : '';
|
||||
case 'slug':
|
||||
return $this->slug();
|
||||
case 'published':
|
||||
return $this->published();
|
||||
case 'visible':
|
||||
return $this->visible();
|
||||
case 'media':
|
||||
return $this->media()->all();
|
||||
case 'media.file':
|
||||
return $this->media()->files();
|
||||
case 'media.video':
|
||||
return $this->media()->videos();
|
||||
case 'media.image':
|
||||
return $this->media()->images();
|
||||
case 'media.audio':
|
||||
return $this->media()->audios();
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $size
|
||||
* @param bool $textOnly
|
||||
* @return string
|
||||
*/
|
||||
protected function processSummary($size = null, $textOnly = false): string
|
||||
{
|
||||
$config = (array)Grav::instance()['config']->get('site.summary');
|
||||
$config_page = (array)$this->getNestedProperty('header.summary');
|
||||
if ($config_page) {
|
||||
$config = array_merge($config, $config_page);
|
||||
}
|
||||
|
||||
// Summary is not enabled, return the whole content.
|
||||
if (empty($config['enabled'])) {
|
||||
return $this->content();
|
||||
}
|
||||
|
||||
$content = $this->_summary ?? $this->content();
|
||||
if ($textOnly) {
|
||||
$content = strip_tags($content);
|
||||
}
|
||||
$content_size = mb_strwidth($content, 'utf-8');
|
||||
$summary_size = $this->_summary !== null ? $content_size : $this->getProperty('summary_size');
|
||||
|
||||
// Return calculated summary based on summary divider's position.
|
||||
$format = $config['format'] ?? '';
|
||||
|
||||
// Return entire page content on wrong/unknown format.
|
||||
if ($format !== 'long' && $format !== 'short') {
|
||||
return $content;
|
||||
}
|
||||
|
||||
if ($format === 'short' && null !== $summary_size) {
|
||||
// Slice the string on breakpoint.
|
||||
if ($content_size > $summary_size) {
|
||||
return mb_substr($content, 0, $summary_size);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
// If needed, get summary size from the config.
|
||||
$size = $size ?? $config['size'] ?? null;
|
||||
|
||||
// Return calculated summary based on defaults.
|
||||
$size = is_numeric($size) ? (int)$size : -1;
|
||||
if ($size < 0) {
|
||||
$size = 300;
|
||||
}
|
||||
|
||||
// If the size is zero or smaller than the summary limit, return the entire page content.
|
||||
if ($size === 0 || $content_size <= $size) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
// Only return string but not html, wrap whatever html tag you want when using.
|
||||
if ($textOnly) {
|
||||
return mb_strimwidth($content, 0, $size, '...', 'UTF-8');
|
||||
}
|
||||
|
||||
$summary = Utils::truncateHTML($content, $size);
|
||||
|
||||
return html_entity_decode($summary, ENT_COMPAT | ENT_HTML5, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and Sets the content based on content portion of the .md file
|
||||
*
|
||||
* @param string $content
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function processContent($content): string
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Config $config */
|
||||
$config = $grav['config'];
|
||||
|
||||
$process_markdown = $this->shouldProcess('markdown');
|
||||
$process_twig = $this->shouldProcess('twig') || $this->isModule();
|
||||
$cache_enable = $this->getNestedProperty('header.cache_enable') ?? $config->get('system.cache.enabled', true);
|
||||
|
||||
$twig_first = $this->getNestedProperty('header.twig_first') ?? $config->get('system.pages.twig_first', false);
|
||||
$never_cache_twig = $this->getNestedProperty('header.never_cache_twig') ?? $config->get('system.pages.never_cache_twig', false);
|
||||
|
||||
$cached = null;
|
||||
if ($cache_enable) {
|
||||
$cache = $this->getCache('render');
|
||||
$key = md5($this->getCacheKey() . '-content');
|
||||
$cached = $cache->get($key);
|
||||
if ($cached && $cached['checksum'] === $this->getCacheChecksum()) {
|
||||
$this->_content = $cached['content'] ?? '';
|
||||
$this->_content_meta = $cached['content_meta'] ?? null;
|
||||
|
||||
if ($process_twig && $never_cache_twig) {
|
||||
$this->_content = $this->processTwig($this->_content);
|
||||
}
|
||||
} else {
|
||||
$cached = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$cached) {
|
||||
$markdown_options = [];
|
||||
if ($process_markdown) {
|
||||
// Build markdown options.
|
||||
$markdown_options = (array)$config->get('system.pages.markdown');
|
||||
$markdown_page_options = (array)$this->getNestedProperty('header.markdown');
|
||||
if ($markdown_page_options) {
|
||||
$markdown_options = array_merge($markdown_options, $markdown_page_options);
|
||||
}
|
||||
|
||||
// pages.markdown_extra is deprecated, but still check it...
|
||||
if (!isset($markdown_options['extra'])) {
|
||||
$extra = $this->getNestedProperty('header.markdown_extra') ?? $config->get('system.pages.markdown_extra');
|
||||
if (null !== $extra) {
|
||||
user_error('Configuration option \'system.pages.markdown_extra\' is deprecated since Grav 1.5, use \'system.pages.markdown.extra\' instead', E_USER_DEPRECATED);
|
||||
|
||||
$markdown_options['extra'] = $extra;
|
||||
}
|
||||
}
|
||||
}
|
||||
$options = [
|
||||
'markdown' => $markdown_options,
|
||||
'images' => $config->get('system.images', [])
|
||||
];
|
||||
|
||||
$this->_content = $content;
|
||||
$grav->fireEvent('onPageContentRaw', new Event(['page' => $this]));
|
||||
|
||||
if ($twig_first && !$never_cache_twig) {
|
||||
if ($process_twig) {
|
||||
$this->_content = $this->processTwig($this->_content);
|
||||
}
|
||||
|
||||
if ($process_markdown) {
|
||||
$this->_content = $this->processMarkdown($this->_content, $options);
|
||||
}
|
||||
|
||||
// Content Processed but not cached yet
|
||||
$grav->fireEvent('onPageContentProcessed', new Event(['page' => $this]));
|
||||
} else {
|
||||
if ($process_markdown) {
|
||||
$options['keep_twig'] = $process_twig;
|
||||
$this->_content = $this->processMarkdown($this->_content, $options);
|
||||
}
|
||||
|
||||
// Content Processed but not cached yet
|
||||
$grav->fireEvent('onPageContentProcessed', new Event(['page' => $this]));
|
||||
|
||||
if ($cache_enable && $never_cache_twig) {
|
||||
$this->cachePageContent();
|
||||
}
|
||||
|
||||
if ($process_twig) {
|
||||
$this->_content = $this->processTwig($this->_content);
|
||||
}
|
||||
}
|
||||
|
||||
if ($cache_enable && !$never_cache_twig) {
|
||||
$this->cachePageContent();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle summary divider
|
||||
$delimiter = $config->get('site.summary.delimiter', '===');
|
||||
$divider_pos = mb_strpos($this->_content, "<p>{$delimiter}</p>");
|
||||
if ($divider_pos !== false) {
|
||||
$this->setProperty('summary_size', $divider_pos);
|
||||
$this->_content = str_replace("<p>{$delimiter}</p>", '', $this->_content);
|
||||
}
|
||||
|
||||
// Fire event when Page::content() is called
|
||||
$grav->fireEvent('onPageContent', new Event(['page' => $this]));
|
||||
|
||||
return $this->_content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the Twig page content.
|
||||
*
|
||||
* @param string $content
|
||||
* @return string
|
||||
*/
|
||||
protected function processTwig($content): string
|
||||
{
|
||||
/** @var Twig $twig */
|
||||
$twig = Grav::instance()['twig'];
|
||||
|
||||
/** @var PageInterface $this */
|
||||
return $twig->processPage($this, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the Markdown content.
|
||||
*
|
||||
* Uses Parsedown or Parsedown Extra depending on configuration.
|
||||
*
|
||||
* @param string $content
|
||||
* @param array $options
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function processMarkdown($content, array $options = []): string
|
||||
{
|
||||
/** @var PageInterface $self */
|
||||
$self = $this;
|
||||
|
||||
$excerpts = new Excerpts($self, $options);
|
||||
|
||||
// Initialize the preferred variant of markdown parser.
|
||||
if (isset($options['extra'])) {
|
||||
$parsedown = new ParsedownExtra($excerpts);
|
||||
} else {
|
||||
$parsedown = new Parsedown($excerpts);
|
||||
}
|
||||
|
||||
$keepTwig = (bool)($options['keep_twig'] ?? false);
|
||||
if ($keepTwig) {
|
||||
$token = [
|
||||
'/' . Utils::generateRandomString(3),
|
||||
Utils::generateRandomString(3) . '/'
|
||||
];
|
||||
// Base64 encode any twig.
|
||||
$content = preg_replace_callback(
|
||||
['/({#.*?#})/mu', '/({{.*?}})/mu', '/({%.*?%})/mu'],
|
||||
static function ($matches) use ($token) { return $token[0] . base64_encode($matches[1]) . $token[1]; },
|
||||
$content
|
||||
);
|
||||
}
|
||||
|
||||
$content = $parsedown->text($content);
|
||||
|
||||
if ($keepTwig) {
|
||||
// Base64 decode the encoded twig.
|
||||
$content = preg_replace_callback(
|
||||
['`' . $token[0] . '([A-Za-z0-9+/]+={0,2})' . $token[1] . '`mu'],
|
||||
static function ($matches) { return base64_decode($matches[1]); },
|
||||
$content
|
||||
);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
abstract protected function loadHeaderProperty(string $property, $var, callable $filter);
|
||||
}
|
||||
1119
system/src/Grav/Framework/Flex/Pages/Traits/PageLegacyTrait.php
Normal file
1119
system/src/Grav/Framework/Flex/Pages/Traits/PageLegacyTrait.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,550 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Pages\Traits;
|
||||
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Page\Interfaces\PageCollectionInterface;
|
||||
use Grav\Common\Page\Interfaces\PageInterface;
|
||||
use Grav\Common\Page\Pages;
|
||||
use Grav\Common\Uri;
|
||||
use Grav\Framework\Filesystem\Filesystem;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
use RuntimeException;
|
||||
use function dirname;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Implements PageRoutableInterface
|
||||
*/
|
||||
trait PageRoutableTrait
|
||||
{
|
||||
/** @var bool */
|
||||
protected $root = false;
|
||||
|
||||
/** @var string|null */
|
||||
private $_route;
|
||||
/** @var string|null */
|
||||
private $_path;
|
||||
/** @var PageInterface|null */
|
||||
private $_parentCache;
|
||||
|
||||
/**
|
||||
* Returns the page extension, got from the page `url_extension` config and falls back to the
|
||||
* system config `system.pages.append_url_extension`.
|
||||
*
|
||||
* @return string The extension of this page. For example `.html`
|
||||
*/
|
||||
public function urlExtension(): string
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'url_extension',
|
||||
null,
|
||||
function ($value) {
|
||||
if ($this->home()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $value ?? Grav::instance()['config']->get('system.pages.append_url_extension', '');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and Sets whether or not this Page is routable, ie you can reach it via a URL.
|
||||
* The page must be *routable* and *published*
|
||||
*
|
||||
* @param bool|null $var true if the page is routable
|
||||
* @return bool true if the page is routable
|
||||
*/
|
||||
public function routable($var = null): bool
|
||||
{
|
||||
$value = $this->loadHeaderProperty(
|
||||
'routable',
|
||||
$var,
|
||||
static function ($value) {
|
||||
return $value ?? true;
|
||||
}
|
||||
);
|
||||
|
||||
return $value && $this->published() && !$this->isModule() && !$this->root() && $this->getLanguages(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the URL for a page - alias of url().
|
||||
*
|
||||
* @param bool $include_host
|
||||
* @return string the permalink
|
||||
*/
|
||||
public function link($include_host = false): string
|
||||
{
|
||||
return $this->url($include_host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the URL with host information, aka Permalink.
|
||||
* @return string The permalink.
|
||||
*/
|
||||
public function permalink(): string
|
||||
{
|
||||
return $this->url(true, false, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the canonical URL for a page
|
||||
*
|
||||
* @param bool $include_lang
|
||||
* @return string
|
||||
*/
|
||||
public function canonical($include_lang = true): string
|
||||
{
|
||||
return $this->url(true, true, $include_lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the url for the Page.
|
||||
*
|
||||
* @param bool $include_host Defaults false, but true would include http://yourhost.com
|
||||
* @param bool $canonical true to return the canonical URL
|
||||
* @param bool $include_base
|
||||
* @param bool $raw_route
|
||||
* @return string The url.
|
||||
*/
|
||||
public function url($include_host = false, $canonical = false, $include_base = true, $raw_route = false): string
|
||||
{
|
||||
// Override any URL when external_url is set
|
||||
$external = $this->getNestedProperty('header.external_url');
|
||||
if ($external) {
|
||||
return $external;
|
||||
}
|
||||
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Pages $pages */
|
||||
$pages = $grav['pages'];
|
||||
|
||||
/** @var Config $config */
|
||||
$config = $grav['config'];
|
||||
|
||||
// get base route (multi-site base and language)
|
||||
$route = $include_base ? $pages->baseRoute() : '';
|
||||
|
||||
// add full route if configured to do so
|
||||
if (!$include_host && $config->get('system.absolute_urls', false)) {
|
||||
$include_host = true;
|
||||
}
|
||||
|
||||
if ($canonical) {
|
||||
$route .= $this->routeCanonical();
|
||||
} elseif ($raw_route) {
|
||||
$route .= $this->rawRoute();
|
||||
} else {
|
||||
$route .= $this->route();
|
||||
}
|
||||
|
||||
/** @var Uri $uri */
|
||||
$uri = $grav['uri'];
|
||||
$url = $uri->rootUrl($include_host) . '/' . trim($route, '/') . $this->urlExtension();
|
||||
|
||||
return Uri::filterPath($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the route for the page based on the route headers if available, else from
|
||||
* the parents route and the current Page's slug.
|
||||
*
|
||||
* @param string $var Set new default route.
|
||||
* @return string|null The route for the Page.
|
||||
*/
|
||||
public function route($var = null): ?string
|
||||
{
|
||||
if (null !== $var) {
|
||||
// TODO: not the best approach, but works...
|
||||
$this->setNestedProperty('header.routes.default', $var);
|
||||
}
|
||||
|
||||
// Return default route if given.
|
||||
$default = $this->getNestedProperty('header.routes.default');
|
||||
if (is_string($default)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return $this->routeInternal();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
protected function routeInternal(): ?string
|
||||
{
|
||||
$route = $this->_route;
|
||||
if (null !== $route) {
|
||||
return $route;
|
||||
}
|
||||
|
||||
if ($this->root()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Root and orphan nodes have no route.
|
||||
$parent = $this->parent();
|
||||
if (!$parent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($parent->home()) {
|
||||
/** @var Config $config */
|
||||
$config = Grav::instance()['config'];
|
||||
$hide = (bool)$config->get('system.home.hide_in_urls', false);
|
||||
$route = '/' . ($hide ? '' : $parent->slug());
|
||||
} else {
|
||||
$route = $parent->route();
|
||||
}
|
||||
|
||||
if ($route !== '' && $route !== '/') {
|
||||
$route .= '/';
|
||||
}
|
||||
|
||||
if (!$this->home()) {
|
||||
$route .= $this->slug();
|
||||
}
|
||||
|
||||
$this->_route = $route;
|
||||
|
||||
return $route;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to clear the route out so it regenerates next time you use it
|
||||
*/
|
||||
public function unsetRouteSlug(): void
|
||||
{
|
||||
// TODO:
|
||||
throw new RuntimeException(__METHOD__ . '(): Not Implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and Sets the page raw route
|
||||
*
|
||||
* @param string|null $var
|
||||
* @return string|null
|
||||
*/
|
||||
public function rawRoute($var = null): ?string
|
||||
{
|
||||
if (null !== $var) {
|
||||
// TODO:
|
||||
throw new RuntimeException(__METHOD__ . '(string): Not Implemented');
|
||||
}
|
||||
|
||||
if ($this->root()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return '/' . $this->getKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the route aliases for the page based on page headers.
|
||||
*
|
||||
* @param array|null $var list of route aliases
|
||||
* @return array The route aliases for the Page.
|
||||
*/
|
||||
public function routeAliases($var = null): array
|
||||
{
|
||||
if (null !== $var) {
|
||||
$this->setNestedProperty('header.routes.aliases', (array)$var);
|
||||
}
|
||||
|
||||
$aliases = (array)$this->getNestedProperty('header.routes.aliases');
|
||||
$default = $this->getNestedProperty('header.routes.default');
|
||||
if ($default) {
|
||||
$aliases[] = $default;
|
||||
}
|
||||
|
||||
return $aliases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the canonical route for this page if its set. If provided it will use
|
||||
* that value, else if it's `true` it will use the default route.
|
||||
*
|
||||
* @param string|null $var
|
||||
* @return string|null
|
||||
*/
|
||||
public function routeCanonical($var = null): ?string
|
||||
{
|
||||
if (null !== $var) {
|
||||
$this->setNestedProperty('header.routes.canonical', (array)$var);
|
||||
}
|
||||
|
||||
$canonical = $this->getNestedProperty('header.routes.canonical');
|
||||
|
||||
return is_string($canonical) ? $canonical : $this->route();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the redirect set in the header.
|
||||
*
|
||||
* @param string|null $var redirect url
|
||||
* @return string|null
|
||||
*/
|
||||
public function redirect($var = null): ?string
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'redirect',
|
||||
$var,
|
||||
static function ($value) {
|
||||
return trim($value) ?: null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the clean path to the page file
|
||||
*
|
||||
* Needed in admin for Page Media.
|
||||
*/
|
||||
public function relativePagePath(): ?string
|
||||
{
|
||||
$folder = $this->getMediaFolder();
|
||||
if (!$folder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
$path = $locator->isStream($folder) ? $locator->findResource($folder, false) : $folder;
|
||||
|
||||
return is_string($path) ? $path : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and sets the path to the folder where the .md for this Page object resides.
|
||||
* This is equivalent to the filePath but without the filename.
|
||||
*
|
||||
* @param string|null $var the path
|
||||
* @return string|null the path
|
||||
*/
|
||||
public function path($var = null): ?string
|
||||
{
|
||||
if (null !== $var) {
|
||||
// TODO:
|
||||
throw new RuntimeException(__METHOD__ . '(string): Not Implemented');
|
||||
}
|
||||
|
||||
$path = $this->_path;
|
||||
if ($path) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
if ($this->root()) {
|
||||
$folder = $this->getFlexDirectory()->getStorageFolder();
|
||||
} else {
|
||||
$folder = $this->getStorageFolder();
|
||||
}
|
||||
|
||||
if ($folder) {
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
$folder = $locator->isStream($folder) ? $locator->getResource($folder) : GRAV_ROOT . "/{$folder}";
|
||||
}
|
||||
|
||||
return $this->_path = is_string($folder) ? $folder : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get/set the folder.
|
||||
*
|
||||
* @param string|null $var Optional path, including numeric prefix.
|
||||
* @return string|null
|
||||
*/
|
||||
public function folder($var = null): ?string
|
||||
{
|
||||
return $this->loadProperty(
|
||||
'folder',
|
||||
$var,
|
||||
function ($value) {
|
||||
if (null === $value) {
|
||||
$value = $this->getMasterKey() ?: $this->getKey();
|
||||
}
|
||||
|
||||
return basename($value) ?: null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get/set the folder.
|
||||
*
|
||||
* @param string|null $var Optional path, including numeric prefix.
|
||||
* @return string|null
|
||||
*/
|
||||
public function parentStorageKey($var = null): ?string
|
||||
{
|
||||
return $this->loadProperty(
|
||||
'parent_key',
|
||||
$var,
|
||||
function ($value) {
|
||||
if (null === $value) {
|
||||
$filesystem = Filesystem::getInstance(false);
|
||||
$value = $this->getMasterKey() ?: $this->getKey();
|
||||
$value = ltrim($filesystem->dirname("/{$value}"), '/') ?: '';
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and Sets the parent object for this page
|
||||
*
|
||||
* @param PageInterface|null $var the parent page object
|
||||
* @return PageInterface|null the parent page object if it exists.
|
||||
*/
|
||||
public function parent(PageInterface $var = null)
|
||||
{
|
||||
if (null !== $var) {
|
||||
// TODO:
|
||||
throw new RuntimeException(__METHOD__ . '(PageInterface): Not Implemented');
|
||||
}
|
||||
|
||||
if ($this->_parentCache || $this->root()) {
|
||||
return $this->_parentCache;
|
||||
}
|
||||
|
||||
// Use filesystem as \dirname() does not work in Windows because of '/foo' becomes '\'.
|
||||
$filesystem = Filesystem::getInstance(false);
|
||||
$directory = $this->getFlexDirectory();
|
||||
$parentKey = ltrim($filesystem->dirname("/{$this->getKey()}"), '/');
|
||||
if ('' !== $parentKey) {
|
||||
$parent = $directory->getObject($parentKey);
|
||||
$language = $this->getLanguage();
|
||||
if ($language && $parent && method_exists($parent, 'getTranslation')) {
|
||||
$parent = $parent->getTranslation($language) ?? $parent;
|
||||
}
|
||||
|
||||
$this->_parentCache = $parent;
|
||||
} else {
|
||||
$index = $directory->getIndex();
|
||||
|
||||
$this->_parentCache = \is_callable([$index, 'getRoot']) ? $index->getRoot() : null;
|
||||
}
|
||||
|
||||
return $this->_parentCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the top parent object for this page. Can return page itself.
|
||||
*
|
||||
* @return PageInterface The top parent page object.
|
||||
*/
|
||||
public function topParent()
|
||||
{
|
||||
$topParent = $this;
|
||||
while ($topParent) {
|
||||
$parent = $topParent->parent();
|
||||
if (!$parent || !$parent->parent()) {
|
||||
break;
|
||||
}
|
||||
$topParent = $parent;
|
||||
}
|
||||
|
||||
return $topParent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the item in the current position.
|
||||
*
|
||||
* @return int|null the index of the current page.
|
||||
*/
|
||||
public function currentPosition(): ?int
|
||||
{
|
||||
$parent = $this->parent();
|
||||
$collection = $parent ? $parent->collection('content', false) : null;
|
||||
if ($collection instanceof PageCollectionInterface && $path = $this->path()) {
|
||||
return $collection->currentPosition($path);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not this page is the currently active page requested via the URL.
|
||||
*
|
||||
* @return bool True if it is active
|
||||
*/
|
||||
public function active(): bool
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
$uri_path = rtrim(urldecode($grav['uri']->path()), '/') ?: '/';
|
||||
$routes = $grav['pages']->routes();
|
||||
|
||||
return isset($routes[$uri_path]) && $routes[$uri_path] === $this->path();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not this URI's URL contains the URL of the active page.
|
||||
* Or in other words, is this page's URL in the current URL
|
||||
*
|
||||
* @return bool True if active child exists
|
||||
*/
|
||||
public function activeChild(): bool
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
/** @var Uri $uri */
|
||||
$uri = $grav['uri'];
|
||||
/** @var Pages $pages */
|
||||
$pages = $grav['pages'];
|
||||
$uri_path = rtrim(urldecode($uri->path()), '/');
|
||||
$routes = $pages->routes();
|
||||
|
||||
if (isset($routes[$uri_path])) {
|
||||
$page = $pages->find($uri->route());
|
||||
/** @var PageInterface|null $child_page */
|
||||
$child_page = $page ? $page->parent() : null;
|
||||
while ($child_page && !$child_page->root()) {
|
||||
if ($this->path() === $child_page->path()) {
|
||||
return true;
|
||||
}
|
||||
$child_page = $child_page->parent();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not this page is the currently configured home page.
|
||||
*
|
||||
* @return bool True if it is the homepage
|
||||
*/
|
||||
public function home(): bool
|
||||
{
|
||||
$home = Grav::instance()['config']->get('system.home.alias');
|
||||
|
||||
return '/' . $this->getKey() === $home;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not this page is the root node of the pages tree.
|
||||
*
|
||||
* @param bool|null $var
|
||||
* @return bool True if it is the root
|
||||
*/
|
||||
public function root($var = null): bool
|
||||
{
|
||||
if (null !== $var) {
|
||||
$this->root = (bool)$var;
|
||||
}
|
||||
|
||||
return $this->root === true || $this->getKey() === '/';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Pages\Traits;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Language\Language;
|
||||
use Grav\Common\Page\Interfaces\PageInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
|
||||
use function is_bool;
|
||||
|
||||
/**
|
||||
* Implements PageTranslateInterface
|
||||
*/
|
||||
trait PageTranslateTrait
|
||||
{
|
||||
/** @var array|null */
|
||||
private $_languages;
|
||||
|
||||
/** @var PageInterface[] */
|
||||
private $_translations = [];
|
||||
|
||||
/**
|
||||
* @param string|null $languageCode
|
||||
* @param bool|null $fallback
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTranslation(string $languageCode = null, bool $fallback = null): bool
|
||||
{
|
||||
$code = $this->findTranslation($languageCode, $fallback);
|
||||
|
||||
return null !== $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $languageCode
|
||||
* @param bool|null $fallback
|
||||
* @return FlexObjectInterface|PageInterface|null
|
||||
*/
|
||||
public function getTranslation(string $languageCode = null, bool $fallback = null)
|
||||
{
|
||||
if ($this->root()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$code = $this->findTranslation($languageCode, $fallback);
|
||||
if (null === $code) {
|
||||
$object = null;
|
||||
} elseif ('' === $code) {
|
||||
$object = $this->getLanguage() ? $this->getFlexDirectory()->getObject($this->getMasterKey(), 'storage_key') : $this;
|
||||
} else {
|
||||
$meta = $this->getMetaData();
|
||||
$meta['template'] = $this->getLanguageTemplates()[$code] ?? $meta['template'];
|
||||
$key = $this->getStorageKey() . '|' . $meta['template'] . '.' . $code;
|
||||
$meta['storage_key'] = $key;
|
||||
$meta['lang'] = $code;
|
||||
$object = $this->getFlexDirectory()->loadObjects([$key => $meta])[$key] ?? null;
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $includeDefault If set to true, return separate entries for '' and 'en' (default) language.
|
||||
* @return array
|
||||
*/
|
||||
public function getAllLanguages(bool $includeDefault = false): array
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Language $language */
|
||||
$language = $grav['language'];
|
||||
$languages = $language->getLanguages();
|
||||
if (!$languages) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$translated = $this->getLanguageTemplates();
|
||||
|
||||
if ($includeDefault) {
|
||||
$languages[] = '';
|
||||
} elseif (isset($translated[''])) {
|
||||
$default = $language->getDefault();
|
||||
if (is_bool($default)) {
|
||||
$default = '';
|
||||
}
|
||||
$translated[$default] = $translated[''];
|
||||
unset($translated['']);
|
||||
}
|
||||
|
||||
$languages = array_fill_keys($languages, false);
|
||||
$translated = array_fill_keys(array_keys($translated), true);
|
||||
|
||||
return array_replace($languages, $translated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all translated languages.
|
||||
*
|
||||
* @param bool $includeDefault If set to true, return separate entries for '' and 'en' (default) language.
|
||||
* @return array
|
||||
*/
|
||||
public function getLanguages(bool $includeDefault = false): array
|
||||
{
|
||||
$languages = $this->getLanguageTemplates();
|
||||
|
||||
if (!$includeDefault && isset($languages[''])) {
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Language $language */
|
||||
$language = $grav['language'];
|
||||
$default = $language->getDefault();
|
||||
if (is_bool($default)) {
|
||||
$default = '';
|
||||
}
|
||||
$languages[$default] = $languages[''];
|
||||
unset($languages['']);
|
||||
}
|
||||
|
||||
return array_keys($languages);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getLanguage(): string
|
||||
{
|
||||
return $this->language() ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $languageCode
|
||||
* @param bool|null $fallback
|
||||
* @return string|null
|
||||
*/
|
||||
public function findTranslation(string $languageCode = null, bool $fallback = null): ?string
|
||||
{
|
||||
$translated = $this->getLanguageTemplates();
|
||||
|
||||
// If there's no translations (including default), we have an empty folder.
|
||||
if (!$translated) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// FIXME: only published is not implemented...
|
||||
$languages = $this->getFallbackLanguages($languageCode, $fallback);
|
||||
|
||||
$language = null;
|
||||
foreach ($languages as $code) {
|
||||
if (isset($translated[$code])) {
|
||||
$language = $code;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $language;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array with the routes of other translated languages
|
||||
*
|
||||
* @param bool $onlyPublished only return published translations
|
||||
* @return array the page translated languages
|
||||
*/
|
||||
public function translatedLanguages($onlyPublished = false): array
|
||||
{
|
||||
// FIXME: only published is not implemented...
|
||||
$translated = $this->getLanguageTemplates();
|
||||
if (!$translated) {
|
||||
return $translated;
|
||||
}
|
||||
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Language $language */
|
||||
$language = $grav['language'];
|
||||
$languages = $language->getLanguages();
|
||||
$languages[] = '';
|
||||
|
||||
$translated = array_intersect_key($translated, array_flip($languages));
|
||||
$list = array_fill_keys($languages, null);
|
||||
foreach ($translated as $languageCode => $languageFile) {
|
||||
$path = ($languageCode ? '/' : '') . $languageCode;
|
||||
$list[$languageCode] = "{$path}/{$this->getKey()}";
|
||||
}
|
||||
|
||||
return array_filter($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array listing untranslated languages available
|
||||
*
|
||||
* @param bool $includeUnpublished also list unpublished translations
|
||||
* @return array the page untranslated languages
|
||||
*/
|
||||
public function untranslatedLanguages($includeUnpublished = false): array
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Language $language */
|
||||
$language = $grav['language'];
|
||||
|
||||
$languages = $language->getLanguages();
|
||||
$translated = array_keys($this->translatedLanguages(!$includeUnpublished));
|
||||
|
||||
return array_values(array_diff($languages, $translated));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page language
|
||||
*
|
||||
* @param string|null $var
|
||||
* @return string|null
|
||||
*/
|
||||
public function language($var = null): ?string
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'lang',
|
||||
$var,
|
||||
function ($value) {
|
||||
$value = $value ?? $this->getMetaData()['lang'] ?? '';
|
||||
|
||||
return trim($value) ?: null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function getLanguageTemplates(): array
|
||||
{
|
||||
if (null === $this->_languages) {
|
||||
$template = $this->getProperty('template');
|
||||
$meta = $this->getMetaData();
|
||||
$translations = $meta['markdown'] ?? [];
|
||||
$list = [];
|
||||
foreach ($translations as $code => $search) {
|
||||
if (isset($search[$template])) {
|
||||
// Use main template if possible.
|
||||
$list[$code] = $template;
|
||||
} elseif (!empty($search)) {
|
||||
// Fall back to first matching template.
|
||||
$list[$code] = key($search);
|
||||
}
|
||||
}
|
||||
|
||||
$this->_languages = $list;
|
||||
}
|
||||
|
||||
return $this->_languages;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $languageCode
|
||||
* @param bool|null $fallback
|
||||
* @return array
|
||||
*/
|
||||
protected function getFallbackLanguages(string $languageCode = null, bool $fallback = null): array
|
||||
{
|
||||
$fallback = $fallback ?? true;
|
||||
if (!$fallback && null !== $languageCode) {
|
||||
return [$languageCode];
|
||||
}
|
||||
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Language $language */
|
||||
$language = $grav['language'];
|
||||
$languageCode = $languageCode ?? ($language->getLanguage() ?: '');
|
||||
if ($languageCode === '' && $fallback) {
|
||||
return $language->getFallbackLanguages(null, true);
|
||||
}
|
||||
|
||||
return $fallback ? $language->getFallbackLanguages($languageCode, true) : [$languageCode];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Storage;
|
||||
|
||||
use Grav\Common\File\CompiledJsonFile;
|
||||
use Grav\Common\File\CompiledMarkdownFile;
|
||||
use Grav\Common\File\CompiledYamlFile;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Framework\File\Formatter\JsonFormatter;
|
||||
use Grav\Framework\File\Formatter\MarkdownFormatter;
|
||||
use Grav\Framework\File\Formatter\YamlFormatter;
|
||||
use Grav\Framework\File\Interfaces\FileFormatterInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
use RuntimeException;
|
||||
use function is_array;
|
||||
|
||||
/**
|
||||
* Class AbstractFilesystemStorage
|
||||
* @package Grav\Framework\Flex\Storage
|
||||
*/
|
||||
abstract class AbstractFilesystemStorage implements FlexStorageInterface
|
||||
{
|
||||
/** @var FileFormatterInterface */
|
||||
protected $dataFormatter;
|
||||
/** @var string */
|
||||
protected $keyField = 'storage_key';
|
||||
/** @var int */
|
||||
protected $keyLen = 32;
|
||||
/** @var bool */
|
||||
protected $caseSensitive = true;
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isIndexed(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::hasKeys()
|
||||
*/
|
||||
public function hasKeys(array $keys): array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($keys as $key) {
|
||||
$list[$key] = $this->hasKey((string)$key);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* @see FlexStorageInterface::getKeyField()
|
||||
*/
|
||||
public function getKeyField(): string
|
||||
{
|
||||
return $this->keyField;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $keys
|
||||
* @param bool $includeParams
|
||||
* @return string
|
||||
*/
|
||||
public function buildStorageKey(array $keys, bool $includeParams = true): string
|
||||
{
|
||||
$key = $keys['key'] ?? '';
|
||||
$params = $includeParams ? $this->buildStorageKeyParams($keys) : '';
|
||||
|
||||
return $params ? "{$key}|{$params}" : $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $keys
|
||||
* @return string
|
||||
*/
|
||||
public function buildStorageKeyParams(array $keys): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $row
|
||||
* @return array
|
||||
*/
|
||||
public function extractKeysFromRow(array $row): array
|
||||
{
|
||||
return [
|
||||
'key' => $this->normalizeKey($row[$this->keyField] ?? '')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return array
|
||||
*/
|
||||
public function extractKeysFromStorageKey(string $key): array
|
||||
{
|
||||
return [
|
||||
'key' => $key
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|array $formatter
|
||||
* @return void
|
||||
*/
|
||||
protected function initDataFormatter($formatter): void
|
||||
{
|
||||
// Initialize formatter.
|
||||
if (!is_array($formatter)) {
|
||||
$formatter = ['class' => $formatter];
|
||||
}
|
||||
$formatterClassName = $formatter['class'] ?? JsonFormatter::class;
|
||||
$formatterOptions = $formatter['options'] ?? [];
|
||||
|
||||
$this->dataFormatter = new $formatterClassName($formatterOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filename
|
||||
* @return string|null
|
||||
*/
|
||||
protected function detectDataFormatter(string $filename): ?string
|
||||
{
|
||||
if (preg_match('|(\.[a-z0-9]*)$|ui', $filename, $matches)) {
|
||||
switch ($matches[1]) {
|
||||
case '.json':
|
||||
return JsonFormatter::class;
|
||||
case '.yaml':
|
||||
return YamlFormatter::class;
|
||||
case '.md':
|
||||
return MarkdownFormatter::class;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filename
|
||||
* @return CompiledJsonFile|CompiledYamlFile|CompiledMarkdownFile
|
||||
*/
|
||||
protected function getFile(string $filename)
|
||||
{
|
||||
$filename = $this->resolvePath($filename);
|
||||
|
||||
// TODO: start using the new file classes.
|
||||
switch ($this->dataFormatter->getDefaultFileExtension()) {
|
||||
case '.json':
|
||||
$file = CompiledJsonFile::instance($filename);
|
||||
break;
|
||||
case '.yaml':
|
||||
$file = CompiledYamlFile::instance($filename);
|
||||
break;
|
||||
case '.md':
|
||||
$file = CompiledMarkdownFile::instance($filename);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException('Unknown extension type ' . $this->dataFormatter->getDefaultFileExtension());
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @return string
|
||||
*/
|
||||
protected function resolvePath(string $path): string
|
||||
{
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
|
||||
if (!$locator->isStream($path)) {
|
||||
return GRAV_ROOT . "/{$path}";
|
||||
}
|
||||
|
||||
return $locator->getResource($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random, unique key for the row.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function generateKey(): string
|
||||
{
|
||||
return substr(hash('sha256', random_bytes($this->keyLen)), 0, $this->keyLen);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return string
|
||||
*/
|
||||
public function normalizeKey(string $key): string
|
||||
{
|
||||
if ($this->caseSensitive === true) {
|
||||
return $key;
|
||||
}
|
||||
|
||||
return mb_strtolower($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a key is valid.
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
protected function validateKey(string $key): bool
|
||||
{
|
||||
return $key && (bool) preg_match('/^[^\\/?*:;{}\\\\\\n]+$/u', $key);
|
||||
}
|
||||
}
|
||||
159
system/src/Grav/Framework/Flex/Storage/FileStorage.php
Normal file
159
system/src/Grav/Framework/Flex/Storage/FileStorage.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Storage;
|
||||
|
||||
use FilesystemIterator;
|
||||
use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
|
||||
use RuntimeException;
|
||||
use SplFileInfo;
|
||||
|
||||
/**
|
||||
* Class FileStorage
|
||||
* @package Grav\Framework\Flex\Storage
|
||||
*/
|
||||
class FileStorage extends FolderStorage
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::__construct()
|
||||
*/
|
||||
public function __construct(array $options)
|
||||
{
|
||||
$this->dataPattern = '{FOLDER}/{KEY}{EXT}';
|
||||
|
||||
if (!isset($options['formatter']) && isset($options['pattern'])) {
|
||||
$options['formatter'] = $this->detectDataFormatter($options['pattern']);
|
||||
}
|
||||
|
||||
parent::__construct($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::getMediaPath()
|
||||
*/
|
||||
public function getMediaPath(string $key = null): ?string
|
||||
{
|
||||
$path = $this->getStoragePath();
|
||||
if (!$path) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $key ? "{$path}/{$key}" : $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $src
|
||||
* @param string $dst
|
||||
* @return bool
|
||||
*/
|
||||
public function copyRow(string $src, string $dst): bool
|
||||
{
|
||||
if ($this->hasKey($dst)) {
|
||||
throw new RuntimeException("Cannot copy object: key '{$dst}' is already taken");
|
||||
}
|
||||
|
||||
if (!$this->hasKey($src)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::renameRow()
|
||||
*/
|
||||
public function renameRow(string $src, string $dst): bool
|
||||
{
|
||||
if (!$this->hasKey($src)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove old file.
|
||||
$path = $this->getPathFromKey($src);
|
||||
$file = $this->getFile($path);
|
||||
$file->delete();
|
||||
$file->free();
|
||||
unset($file);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $src
|
||||
* @param string $dst
|
||||
* @return bool
|
||||
*/
|
||||
protected function copyFolder(string $src, string $dst): bool
|
||||
{
|
||||
// Nothing to copy.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $src
|
||||
* @param string $dst
|
||||
* @return bool
|
||||
*/
|
||||
protected function moveFolder(string $src, string $dst): bool
|
||||
{
|
||||
// Nothing to move.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
protected function canDeleteFolder(string $key): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getKeyFromPath(string $path): string
|
||||
{
|
||||
return basename($path, $this->dataFormatter->getDefaultFileExtension());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function buildIndex(): array
|
||||
{
|
||||
$this->clearCache();
|
||||
|
||||
$path = $this->getStoragePath();
|
||||
if (!$path || !file_exists($path)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$flags = FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS | FilesystemIterator::UNIX_PATHS;
|
||||
$iterator = new FilesystemIterator($path, $flags);
|
||||
$list = [];
|
||||
/** @var SplFileInfo $info */
|
||||
foreach ($iterator as $filename => $info) {
|
||||
if (!$info->isFile() || !($key = $this->getKeyFromPath($filename)) || strpos($info->getFilename(), '.') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$list[$key] = $this->getObjectMeta($key);
|
||||
}
|
||||
|
||||
ksort($list, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
704
system/src/Grav/Framework/Flex/Storage/FolderStorage.php
Normal file
704
system/src/Grav/Framework/Flex/Storage/FolderStorage.php
Normal file
@@ -0,0 +1,704 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Storage;
|
||||
|
||||
use FilesystemIterator;
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Framework\Filesystem\Filesystem;
|
||||
use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
|
||||
use RocketTheme\Toolbox\File\File;
|
||||
use InvalidArgumentException;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
use RuntimeException;
|
||||
use SplFileInfo;
|
||||
use function array_key_exists;
|
||||
use function basename;
|
||||
use function count;
|
||||
use function is_scalar;
|
||||
use function is_string;
|
||||
use function mb_strpos;
|
||||
use function mb_substr;
|
||||
|
||||
/**
|
||||
* Class FolderStorage
|
||||
* @package Grav\Framework\Flex\Storage
|
||||
*/
|
||||
class FolderStorage extends AbstractFilesystemStorage
|
||||
{
|
||||
/** @var string Folder where all the data is stored. */
|
||||
protected $dataFolder;
|
||||
/** @var string Pattern to access an object. */
|
||||
protected $dataPattern = '{FOLDER}/{KEY}/{FILE}{EXT}';
|
||||
/** @var string Filename for the object. */
|
||||
protected $dataFile;
|
||||
/** @var string File extension for the object. */
|
||||
protected $dataExt;
|
||||
/** @var bool */
|
||||
protected $prefixed;
|
||||
/** @var bool */
|
||||
protected $indexed;
|
||||
/** @var array */
|
||||
protected $meta = [];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct(array $options)
|
||||
{
|
||||
if (!isset($options['folder'])) {
|
||||
throw new InvalidArgumentException("Argument \$options is missing 'folder'");
|
||||
}
|
||||
|
||||
$this->initDataFormatter($options['formatter'] ?? []);
|
||||
$this->initOptions($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isIndexed(): bool
|
||||
{
|
||||
return $this->indexed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function clearCache(): void
|
||||
{
|
||||
$this->meta = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $keys
|
||||
* @param bool $reload
|
||||
* @return array
|
||||
*/
|
||||
public function getMetaData(array $keys, bool $reload = false): array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($keys as $key) {
|
||||
$list[$key] = $this->getObjectMeta((string)$key, $reload);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::getExistingKeys()
|
||||
*/
|
||||
public function getExistingKeys(): array
|
||||
{
|
||||
return $this->buildIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::hasKey()
|
||||
*/
|
||||
public function hasKey(string $key): bool
|
||||
{
|
||||
$meta = $this->getObjectMeta($key);
|
||||
|
||||
return array_key_exists('exists', $meta) ? $meta['exists'] : !empty($meta['storage_timestamp']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::createRows()
|
||||
*/
|
||||
public function createRows(array $rows): array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
$list[$key] = $this->saveRow('@@', $row);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::readRows()
|
||||
*/
|
||||
public function readRows(array $rows, array &$fetched = null): array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
if (null === $row || is_scalar($row)) {
|
||||
// Only load rows which haven't been loaded before.
|
||||
$key = (string)$key;
|
||||
$list[$key] = $this->loadRow($key);
|
||||
|
||||
if (null !== $fetched) {
|
||||
$fetched[$key] = $list[$key];
|
||||
}
|
||||
} else {
|
||||
// Keep the row if it has been loaded.
|
||||
$list[$key] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::updateRows()
|
||||
*/
|
||||
public function updateRows(array $rows): array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
$key = (string)$key;
|
||||
$list[$key] = $this->hasKey($key) ? $this->saveRow($key, $row) : null;
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::deleteRows()
|
||||
*/
|
||||
public function deleteRows(array $rows): array
|
||||
{
|
||||
$list = [];
|
||||
$baseMediaPath = $this->getMediaPath();
|
||||
foreach ($rows as $key => $row) {
|
||||
$key = (string)$key;
|
||||
if (!$this->hasKey($key)) {
|
||||
$list[$key] = null;
|
||||
} else {
|
||||
$path = $this->getPathFromKey($key);
|
||||
$file = $this->getFile($path);
|
||||
$list[$key] = $this->deleteFile($file);
|
||||
|
||||
if ($this->canDeleteFolder($key)) {
|
||||
$storagePath = $this->getStoragePath($key);
|
||||
$mediaPath = $this->getMediaPath($key);
|
||||
|
||||
if ($storagePath) {
|
||||
$this->deleteFolder($storagePath, true);
|
||||
}
|
||||
if ($mediaPath && $mediaPath !== $storagePath && $mediaPath !== $baseMediaPath) {
|
||||
$this->deleteFolder($mediaPath, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::replaceRows()
|
||||
*/
|
||||
public function replaceRows(array $rows): array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
$key = (string)$key;
|
||||
$list[$key] = $this->saveRow($key, $row);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $src
|
||||
* @param string $dst
|
||||
* @return bool
|
||||
*/
|
||||
public function copyRow(string $src, string $dst): bool
|
||||
{
|
||||
if ($this->hasKey($dst)) {
|
||||
throw new RuntimeException("Cannot copy object: key '{$dst}' is already taken");
|
||||
}
|
||||
|
||||
if (!$this->hasKey($src)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$srcPath = $this->getStoragePath($src);
|
||||
$dstPath = $this->getStoragePath($dst);
|
||||
if (!$srcPath || !$dstPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->copyFolder($srcPath, $dstPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::renameRow()
|
||||
*/
|
||||
public function renameRow(string $src, string $dst): bool
|
||||
{
|
||||
if (!$this->hasKey($src)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$srcPath = $this->getStoragePath($src);
|
||||
$dstPath = $this->getStoragePath($dst);
|
||||
if (!$srcPath || !$dstPath) {
|
||||
throw new RuntimeException("Destination path '{$dst}' is empty");
|
||||
}
|
||||
|
||||
if ($srcPath === $dstPath) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->hasKey($dst)) {
|
||||
throw new RuntimeException("Cannot rename object '{$src}': key '{$dst}' is already taken $srcPath $dstPath");
|
||||
}
|
||||
|
||||
return $this->moveFolder($srcPath, $dstPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::getStoragePath()
|
||||
*/
|
||||
public function getStoragePath(string $key = null): ?string
|
||||
{
|
||||
if (null === $key || $key === '') {
|
||||
$path = $this->dataFolder;
|
||||
} else {
|
||||
$parts = $this->parseKey($key, false);
|
||||
$options = [
|
||||
$this->dataFolder, // {FOLDER}
|
||||
$parts['key'], // {KEY}
|
||||
$parts['key:2'], // {KEY:2}
|
||||
'***', // {FILE}
|
||||
'***' // {EXT}
|
||||
];
|
||||
|
||||
$path = rtrim(explode('***', sprintf($this->dataPattern, ...$options))[0], '/');
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::getMediaPath()
|
||||
*/
|
||||
public function getMediaPath(string $key = null): ?string
|
||||
{
|
||||
return $this->getStoragePath($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filesystem path from the key.
|
||||
*
|
||||
* @param string $key
|
||||
* @return string
|
||||
*/
|
||||
public function getPathFromKey(string $key): string
|
||||
{
|
||||
$parts = $this->parseKey($key);
|
||||
$options = [
|
||||
$this->dataFolder, // {FOLDER}
|
||||
$parts['key'], // {KEY}
|
||||
$parts['key:2'], // {KEY:2}
|
||||
$parts['file'], // {FILE}
|
||||
$this->dataExt // {EXT}
|
||||
];
|
||||
|
||||
return sprintf($this->dataPattern, ...$options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param bool $variations
|
||||
* @return array
|
||||
*/
|
||||
public function parseKey(string $key, bool $variations = true): array
|
||||
{
|
||||
$keys = [
|
||||
'key' => $key,
|
||||
'key:2' => mb_substr($key, 0, 2),
|
||||
];
|
||||
if ($variations) {
|
||||
$keys['file'] = $this->dataFile;
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get key from the filesystem path.
|
||||
*
|
||||
* @param string $path
|
||||
* @return string
|
||||
*/
|
||||
protected function getKeyFromPath(string $path): string
|
||||
{
|
||||
return basename($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the row for saving and returns the storage key for the record.
|
||||
*
|
||||
* @param array $row
|
||||
* @return void
|
||||
*/
|
||||
protected function prepareRow(array &$row): void
|
||||
{
|
||||
if (array_key_exists($this->keyField, $row)) {
|
||||
$key = $row[$this->keyField];
|
||||
if ($key === $this->normalizeKey($key)) {
|
||||
unset($row[$this->keyField]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return array
|
||||
*/
|
||||
protected function loadRow(string $key): ?array
|
||||
{
|
||||
$path = $this->getPathFromKey($key);
|
||||
$file = $this->getFile($path);
|
||||
try {
|
||||
$data = (array)$file->content();
|
||||
if (isset($data[0])) {
|
||||
throw new RuntimeException('Broken object file');
|
||||
}
|
||||
} catch (RuntimeException $e) {
|
||||
$data = ['__ERROR' => $e->getMessage()];
|
||||
} finally {
|
||||
$file->free();
|
||||
unset($file);
|
||||
}
|
||||
|
||||
$data['__META'] = $this->getObjectMeta($key);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param array $row
|
||||
* @return array
|
||||
*/
|
||||
protected function saveRow(string $key, array $row): array
|
||||
{
|
||||
try {
|
||||
if (isset($row[$this->keyField])) {
|
||||
$key = $row[$this->keyField];
|
||||
}
|
||||
if (strpos($key, '@@') !== false) {
|
||||
$key = $this->getNewKey();
|
||||
}
|
||||
|
||||
$key = $this->normalizeKey($key);
|
||||
|
||||
// Check if the row already exists and if the key has been changed.
|
||||
$oldKey = $row['__META']['storage_key'] ?? null;
|
||||
if (is_string($oldKey) && $oldKey !== $key) {
|
||||
$isCopy = $row['__META']['copy'] ?? false;
|
||||
if ($isCopy) {
|
||||
$this->copyRow($oldKey, $key);
|
||||
} else {
|
||||
$this->renameRow($oldKey, $key);
|
||||
}
|
||||
}
|
||||
|
||||
$this->prepareRow($row);
|
||||
unset($row['__META'], $row['__ERROR']);
|
||||
|
||||
$path = $this->getPathFromKey($key);
|
||||
$file = $this->getFile($path);
|
||||
|
||||
$file->save($row);
|
||||
|
||||
} catch (RuntimeException $e) {
|
||||
throw new RuntimeException(sprintf('Flex saveFile(%s): %s', $path ?? $key, $e->getMessage()));
|
||||
} finally {
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
$locator->clearCache();
|
||||
|
||||
if (isset($file)) {
|
||||
$file->free();
|
||||
unset($file);
|
||||
}
|
||||
}
|
||||
|
||||
$row['__META'] = $this->getObjectMeta($key, true);
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param File $file
|
||||
* @return array|string
|
||||
*/
|
||||
protected function deleteFile(File $file)
|
||||
{
|
||||
$filename = $file->filename();
|
||||
try {
|
||||
$data = $file->content();
|
||||
if ($file->exists()) {
|
||||
$file->delete();
|
||||
}
|
||||
} catch (RuntimeException $e) {
|
||||
throw new RuntimeException(sprintf('Flex deleteFile(%s): %s', $filename, $e->getMessage()));
|
||||
} finally {
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
$locator->clearCache();
|
||||
|
||||
$file->free();
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $src
|
||||
* @param string $dst
|
||||
* @return bool
|
||||
*/
|
||||
protected function copyFolder(string $src, string $dst): bool
|
||||
{
|
||||
try {
|
||||
Folder::copy($this->resolvePath($src), $this->resolvePath($dst));
|
||||
} catch (RuntimeException $e) {
|
||||
throw new RuntimeException(sprintf('Flex copyFolder(%s, %s): %s', $src, $dst, $e->getMessage()));
|
||||
} finally {
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
$locator->clearCache();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $src
|
||||
* @param string $dst
|
||||
* @return bool
|
||||
*/
|
||||
protected function moveFolder(string $src, string $dst): bool
|
||||
{
|
||||
try {
|
||||
Folder::move($this->resolvePath($src), $this->resolvePath($dst));
|
||||
} catch (RuntimeException $e) {
|
||||
throw new RuntimeException(sprintf('Flex moveFolder(%s, %s): %s', $src, $dst, $e->getMessage()));
|
||||
} finally {
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
$locator->clearCache();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @param bool $include_target
|
||||
* @return bool
|
||||
*/
|
||||
protected function deleteFolder(string $path, bool $include_target = false): bool
|
||||
{
|
||||
try {
|
||||
return Folder::delete($this->resolvePath($path), $include_target);
|
||||
} catch (RuntimeException $e) {
|
||||
throw new RuntimeException(sprintf('Flex deleteFolder(%s): %s', $path, $e->getMessage()));
|
||||
} finally {
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
$locator->clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
protected function canDeleteFolder(string $key): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of all stored keys in [key => timestamp] pairs.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function buildIndex(): array
|
||||
{
|
||||
$this->clearCache();
|
||||
|
||||
$path = $this->getStoragePath();
|
||||
if (!$path || !file_exists($path)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($this->prefixed) {
|
||||
$list = $this->buildPrefixedIndexFromFilesystem($path);
|
||||
} else {
|
||||
$list = $this->buildIndexFromFilesystem($path);
|
||||
}
|
||||
|
||||
ksort($list, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param bool $reload
|
||||
* @return array
|
||||
*/
|
||||
protected function getObjectMeta(string $key, bool $reload = false): array
|
||||
{
|
||||
if (!$reload && isset($this->meta[$key])) {
|
||||
return $this->meta[$key];
|
||||
}
|
||||
|
||||
if ($key && strpos($key, '@@') === false) {
|
||||
$filename = $this->getPathFromKey($key);
|
||||
$modified = is_file($filename) ? filemtime($filename) : 0;
|
||||
} else {
|
||||
$modified = 0;
|
||||
}
|
||||
|
||||
$meta = [
|
||||
'storage_key' => $key,
|
||||
'storage_timestamp' => $modified
|
||||
];
|
||||
|
||||
$this->meta[$key] = $meta;
|
||||
|
||||
return $meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @return array
|
||||
*/
|
||||
protected function buildIndexFromFilesystem($path)
|
||||
{
|
||||
$flags = FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS | FilesystemIterator::UNIX_PATHS;
|
||||
|
||||
$iterator = new FilesystemIterator($path, $flags);
|
||||
$list = [];
|
||||
/** @var SplFileInfo $info */
|
||||
foreach ($iterator as $filename => $info) {
|
||||
if (!$info->isDir() || strpos($info->getFilename(), '.') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = $this->getKeyFromPath($filename);
|
||||
$meta = $this->getObjectMeta($key);
|
||||
if ($meta['storage_timestamp']) {
|
||||
$list[$key] = $meta;
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @return array
|
||||
*/
|
||||
protected function buildPrefixedIndexFromFilesystem($path)
|
||||
{
|
||||
$flags = FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS | FilesystemIterator::UNIX_PATHS;
|
||||
|
||||
$iterator = new FilesystemIterator($path, $flags);
|
||||
$list = [];
|
||||
/** @var SplFileInfo $info */
|
||||
foreach ($iterator as $filename => $info) {
|
||||
if (!$info->isDir() || strpos($info->getFilename(), '.') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$list[] = $this->buildIndexFromFilesystem($filename);
|
||||
}
|
||||
|
||||
if (!$list) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return count($list) > 1 ? array_merge(...$list) : $list[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getNewKey(): string
|
||||
{
|
||||
// Make sure that the file doesn't exist.
|
||||
do {
|
||||
$key = $this->generateKey();
|
||||
} while (file_exists($this->getPathFromKey($key)));
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $options
|
||||
* @return void
|
||||
*/
|
||||
protected function initOptions(array $options): void
|
||||
{
|
||||
$extension = $this->dataFormatter->getDefaultFileExtension();
|
||||
|
||||
/** @var string $pattern */
|
||||
$pattern = !empty($options['pattern']) ? $options['pattern'] : $this->dataPattern;
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
$folder = $options['folder'];
|
||||
if ($locator->isStream($folder)) {
|
||||
$folder = $locator->getResource($folder, false);
|
||||
}
|
||||
|
||||
$this->dataFolder = $folder;
|
||||
$this->dataFile = $options['file'] ?? 'item';
|
||||
$this->dataExt = $extension;
|
||||
if (mb_strpos($pattern, '{FILE}') === false && mb_strpos($pattern, '{EXT}') === false) {
|
||||
if (isset($options['file'])) {
|
||||
$pattern .= '/{FILE}{EXT}';
|
||||
} else {
|
||||
$filesystem = Filesystem::getInstance(true);
|
||||
$this->dataFile = basename($pattern, $extension);
|
||||
$pattern = $filesystem->dirname($pattern) . '/{FILE}{EXT}';
|
||||
}
|
||||
}
|
||||
$this->prefixed = (bool)($options['prefixed'] ?? strpos($pattern, '/{KEY:2}/'));
|
||||
$this->indexed = (bool)($options['indexed'] ?? false);
|
||||
$this->keyField = $options['key'] ?? 'storage_key';
|
||||
$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);
|
||||
|
||||
if (!$pattern) {
|
||||
throw new RuntimeException('Bad storage folder pattern');
|
||||
}
|
||||
|
||||
$this->dataPattern = $pattern;
|
||||
}
|
||||
}
|
||||
506
system/src/Grav/Framework/Flex/Storage/SimpleStorage.php
Normal file
506
system/src/Grav/Framework/Flex/Storage/SimpleStorage.php
Normal file
@@ -0,0 +1,506 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Storage;
|
||||
|
||||
use Grav\Common\Data\Data;
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
use Grav\Framework\Filesystem\Filesystem;
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
use RuntimeException;
|
||||
use function is_scalar;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Class SimpleStorage
|
||||
* @package Grav\Framework\Flex\Storage
|
||||
*/
|
||||
class SimpleStorage extends AbstractFilesystemStorage
|
||||
{
|
||||
/** @var string */
|
||||
protected $dataFolder;
|
||||
/** @var string */
|
||||
protected $dataPattern;
|
||||
/** @var string */
|
||||
protected $prefix;
|
||||
/** @var array|null */
|
||||
protected $data;
|
||||
/** @var int */
|
||||
protected $modified = 0;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::__construct()
|
||||
*/
|
||||
public function __construct(array $options)
|
||||
{
|
||||
if (!isset($options['folder'])) {
|
||||
throw new InvalidArgumentException("Argument \$options is missing 'folder'");
|
||||
}
|
||||
|
||||
$formatter = $options['formatter'] ?? $this->detectDataFormatter($options['folder']);
|
||||
$this->initDataFormatter($formatter);
|
||||
|
||||
$filesystem = Filesystem::getInstance(true);
|
||||
|
||||
$extension = $this->dataFormatter->getDefaultFileExtension();
|
||||
$pattern = basename($options['folder']);
|
||||
|
||||
$this->dataPattern = basename($pattern, $extension) . $extension;
|
||||
$this->dataFolder = $filesystem->dirname($options['folder']);
|
||||
$this->keyField = $options['key'] ?? 'storage_key';
|
||||
$this->keyLen = (int)($options['key_len'] ?? 32);
|
||||
$this->prefix = $options['prefix'] ?? null;
|
||||
|
||||
// Make sure that the data folder exists.
|
||||
if (!file_exists($this->dataFolder)) {
|
||||
try {
|
||||
Folder::create($this->dataFolder);
|
||||
} catch (RuntimeException $e) {
|
||||
throw new RuntimeException(sprintf('Flex: %s', $e->getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function clearCache(): void
|
||||
{
|
||||
$this->data = null;
|
||||
$this->modified = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $keys
|
||||
* @param bool $reload
|
||||
* @return array
|
||||
*/
|
||||
public function getMetaData(array $keys, bool $reload = false): array
|
||||
{
|
||||
if (null === $this->data || $reload) {
|
||||
$this->buildIndex();
|
||||
}
|
||||
|
||||
$list = [];
|
||||
foreach ($keys as $key) {
|
||||
$list[$key] = $this->getObjectMeta((string)$key);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::getExistingKeys()
|
||||
*/
|
||||
public function getExistingKeys(): array
|
||||
{
|
||||
return $this->buildIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::hasKey()
|
||||
*/
|
||||
public function hasKey(string $key): bool
|
||||
{
|
||||
if (null === $this->data) {
|
||||
$this->buildIndex();
|
||||
}
|
||||
|
||||
return $key && strpos($key, '@@') === false && isset($this->data[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::createRows()
|
||||
*/
|
||||
public function createRows(array $rows): array
|
||||
{
|
||||
if (null === $this->data) {
|
||||
$this->buildIndex();
|
||||
}
|
||||
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
$list[$key] = $this->saveRow('@@', $rows);
|
||||
}
|
||||
|
||||
if ($list) {
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::readRows()
|
||||
*/
|
||||
public function readRows(array $rows, array &$fetched = null): array
|
||||
{
|
||||
if (null === $this->data) {
|
||||
$this->buildIndex();
|
||||
}
|
||||
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
if (null === $row || is_scalar($row)) {
|
||||
// Only load rows which haven't been loaded before.
|
||||
$key = (string)$key;
|
||||
$list[$key] = $this->hasKey($key) ? $this->loadRow($key) : null;
|
||||
if (null !== $fetched) {
|
||||
$fetched[$key] = $list[$key];
|
||||
}
|
||||
} else {
|
||||
// Keep the row if it has been loaded.
|
||||
$list[$key] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::updateRows()
|
||||
*/
|
||||
public function updateRows(array $rows): array
|
||||
{
|
||||
if (null === $this->data) {
|
||||
$this->buildIndex();
|
||||
}
|
||||
|
||||
$save = false;
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
$key = (string)$key;
|
||||
if ($this->hasKey($key)) {
|
||||
$list[$key] = $this->saveRow($key, $row);
|
||||
$save = true;
|
||||
} else {
|
||||
$list[$key] = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($save) {
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::deleteRows()
|
||||
*/
|
||||
public function deleteRows(array $rows): array
|
||||
{
|
||||
if (null === $this->data) {
|
||||
$this->buildIndex();
|
||||
}
|
||||
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
$key = (string)$key;
|
||||
if ($this->hasKey($key)) {
|
||||
unset($this->data[$key]);
|
||||
$list[$key] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
if ($list) {
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::replaceRows()
|
||||
*/
|
||||
public function replaceRows(array $rows): array
|
||||
{
|
||||
if (null === $this->data) {
|
||||
$this->buildIndex();
|
||||
}
|
||||
|
||||
$list = [];
|
||||
foreach ($rows as $key => $row) {
|
||||
$list[$key] = $this->saveRow((string)$key, $row);
|
||||
}
|
||||
|
||||
if ($list) {
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $src
|
||||
* @param string $dst
|
||||
* @return bool
|
||||
*/
|
||||
public function copyRow(string $src, string $dst): bool
|
||||
{
|
||||
if ($this->hasKey($dst)) {
|
||||
throw new RuntimeException("Cannot copy object: key '{$dst}' is already taken");
|
||||
}
|
||||
|
||||
if (!$this->hasKey($src)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->data[$dst] = $this->data[$src];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::renameRow()
|
||||
*/
|
||||
public function renameRow(string $src, string $dst): bool
|
||||
{
|
||||
if (null === $this->data) {
|
||||
$this->buildIndex();
|
||||
}
|
||||
|
||||
if ($this->hasKey($dst)) {
|
||||
throw new RuntimeException("Cannot rename object: key '{$dst}' is already taken");
|
||||
}
|
||||
|
||||
if (!$this->hasKey($src)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Change single key in the array without changing the order or value.
|
||||
$keys = array_keys($this->data);
|
||||
$keys[array_search($src, $keys, true)] = $dst;
|
||||
|
||||
$data = array_combine($keys, $this->data);
|
||||
if (false === $data) {
|
||||
throw new LogicException('Bad data');
|
||||
}
|
||||
|
||||
$this->data = $data;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::getStoragePath()
|
||||
*/
|
||||
public function getStoragePath(string $key = null): ?string
|
||||
{
|
||||
return $this->dataFolder . '/' . $this->dataPattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexStorageInterface::getMediaPath()
|
||||
*/
|
||||
public function getMediaPath(string $key = null): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the row for saving and returns the storage key for the record.
|
||||
*
|
||||
* @param array $row
|
||||
*/
|
||||
protected function prepareRow(array &$row): void
|
||||
{
|
||||
unset($row[$this->keyField]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return array
|
||||
*/
|
||||
protected function loadRow(string $key): ?array
|
||||
{
|
||||
$data = $this->data[$key] ?? [];
|
||||
if ($this->keyField !== 'storage_key') {
|
||||
$data[$this->keyField] = $key;
|
||||
}
|
||||
$data['__META'] = $this->getObjectMeta($key);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param array $row
|
||||
* @return array
|
||||
*/
|
||||
protected function saveRow(string $key, array $row): array
|
||||
{
|
||||
try {
|
||||
if (isset($row[$this->keyField])) {
|
||||
$key = $row[$this->keyField];
|
||||
}
|
||||
if (strpos($key, '@@') !== false) {
|
||||
$key = $this->getNewKey();
|
||||
}
|
||||
|
||||
// Check if the row already exists and if the key has been changed.
|
||||
$oldKey = $row['__META']['storage_key'] ?? null;
|
||||
if (is_string($oldKey) && $oldKey !== $key) {
|
||||
$isCopy = $row['__META']['copy'] ?? false;
|
||||
if ($isCopy) {
|
||||
$this->copyRow($oldKey, $key);
|
||||
} else {
|
||||
$this->renameRow($oldKey, $key);
|
||||
}
|
||||
}
|
||||
|
||||
$this->prepareRow($row);
|
||||
unset($row['__META'], $row['__ERROR']);
|
||||
|
||||
$this->data[$key] = $row;
|
||||
} catch (RuntimeException $e) {
|
||||
throw new RuntimeException(sprintf('Flex saveRow(%s): %s', $key, $e->getMessage()));
|
||||
}
|
||||
|
||||
$row['__META'] = $this->getObjectMeta($key, true);
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param bool $variations
|
||||
* @return array
|
||||
*/
|
||||
public function parseKey(string $key, bool $variations = true): array
|
||||
{
|
||||
return [
|
||||
'key' => $key,
|
||||
];
|
||||
}
|
||||
|
||||
protected function save(): void
|
||||
{
|
||||
if (null === $this->data) {
|
||||
$this->buildIndex();
|
||||
}
|
||||
|
||||
try {
|
||||
$path = $this->getStoragePath();
|
||||
if (!$path) {
|
||||
throw new RuntimeException('Storage path is not defined');
|
||||
}
|
||||
$file = $this->getFile($path);
|
||||
if ($this->prefix) {
|
||||
$data = new Data((array)$file->content());
|
||||
$content = $data->set($this->prefix, $this->data)->toArray();
|
||||
} else {
|
||||
$content = $this->data;
|
||||
}
|
||||
$file->save($content);
|
||||
$this->modified = (int)$file->modified(); // cast false to 0
|
||||
} catch (RuntimeException $e) {
|
||||
throw new RuntimeException(sprintf('Flex save(): %s', $e->getMessage()));
|
||||
} finally {
|
||||
if (isset($file)) {
|
||||
$file->free();
|
||||
unset($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get key from the filesystem path.
|
||||
*
|
||||
* @param string $path
|
||||
* @return string
|
||||
*/
|
||||
protected function getKeyFromPath(string $path): string
|
||||
{
|
||||
return basename($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of all stored keys in [key => timestamp] pairs.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function buildIndex(): array
|
||||
{
|
||||
$path = $this->getStoragePath();
|
||||
if (!$path) {
|
||||
$this->data = [];
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
$file = $this->getFile($path);
|
||||
$this->modified = (int)$file->modified(); // cast false to 0
|
||||
|
||||
$content = (array) $file->content();
|
||||
if ($this->prefix) {
|
||||
$data = new Data($content);
|
||||
$content = $data->get($this->prefix);
|
||||
}
|
||||
|
||||
$file->free();
|
||||
unset($file);
|
||||
|
||||
$this->data = $content;
|
||||
|
||||
$list = [];
|
||||
foreach ($this->data as $key => $info) {
|
||||
$list[$key] = $this->getObjectMeta((string)$key);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param bool $reload
|
||||
* @return array
|
||||
*/
|
||||
protected function getObjectMeta(string $key, bool $reload = false): array
|
||||
{
|
||||
$modified = isset($this->data[$key]) ? $this->modified : 0;
|
||||
|
||||
return [
|
||||
'storage_key' => $key,
|
||||
'key' => $key,
|
||||
'storage_timestamp' => $modified
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getNewKey(): string
|
||||
{
|
||||
if (null === $this->data) {
|
||||
$this->buildIndex();
|
||||
}
|
||||
|
||||
// Make sure that the key doesn't exist.
|
||||
do {
|
||||
$key = $this->generateKey();
|
||||
} while (isset($this->data[$key]));
|
||||
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
126
system/src/Grav/Framework/Flex/Traits/FlexAuthorizeTrait.php
Normal file
126
system/src/Grav/Framework/Flex/Traits/FlexAuthorizeTrait.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Traits;
|
||||
|
||||
use Grav\Common\User\Interfaces\UserInterface;
|
||||
use Grav\Framework\Flex\FlexDirectory;
|
||||
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
|
||||
|
||||
/**
|
||||
* Implements basic ACL
|
||||
*/
|
||||
trait FlexAuthorizeTrait
|
||||
{
|
||||
/**
|
||||
* Check if user is authorized for the action.
|
||||
*
|
||||
* Note: There are two deny values: denied (false), not set (null). This allows chaining multiple rules together
|
||||
* when the previous rules were not matched.
|
||||
*
|
||||
* To override the default behavior, please use isAuthorizedOverride().
|
||||
*
|
||||
* @param string $action
|
||||
* @param string|null $scope
|
||||
* @param UserInterface|null $user
|
||||
* @return bool|null
|
||||
* @final
|
||||
*/
|
||||
public function isAuthorized(string $action, string $scope = null, UserInterface $user = null): ?bool
|
||||
{
|
||||
$action = $this->getAuthorizeAction($action);
|
||||
$scope = $scope ?? $this->getAuthorizeScope();
|
||||
|
||||
$isMe = null === $user;
|
||||
if ($isMe) {
|
||||
$user = $this->getActiveUser();
|
||||
}
|
||||
|
||||
if (null === $user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Finally authorize against given action.
|
||||
return $this->isAuthorizedOverride($user, $action, $scope, $isMe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Please override this method
|
||||
*
|
||||
* @param UserInterface $user
|
||||
* @param string $action
|
||||
* @param string $scope
|
||||
* @param bool $isMe
|
||||
* @return bool|null
|
||||
*/
|
||||
protected function isAuthorizedOverride(UserInterface $user, string $action, string $scope, bool $isMe): ?bool
|
||||
{
|
||||
return $this->isAuthorizedAction($user, $action, $scope, $isMe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is authorized for the action.
|
||||
*
|
||||
* @param UserInterface $user
|
||||
* @param string $action
|
||||
* @param string $scope
|
||||
* @param bool $isMe
|
||||
* @return bool|null
|
||||
*/
|
||||
protected function isAuthorizedAction(UserInterface $user, string $action, string $scope, bool $isMe): ?bool
|
||||
{
|
||||
// Check if the action has been denied in the flex type configuration.
|
||||
$directory = $this instanceof FlexDirectory ? $this : $this->getFlexDirectory();
|
||||
$config = $directory->getConfig();
|
||||
$allowed = $config->get("{$scope}.actions.{$action}") ?? $config->get("actions.{$action}") ?? true;
|
||||
if (false === $allowed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Not needed anymore with flex users, remove in 2.0.
|
||||
$auth = $user instanceof FlexObjectInterface ? null : $user->authorize('admin.super');
|
||||
if (true === $auth) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Finally authorize the action.
|
||||
return $user->authorize($this->getAuthorizeRule($scope, $action), !$isMe ? 'test' : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UserInterface $user
|
||||
* @return bool|null
|
||||
* @deprecated 1.7 Not needed for Flex Users.
|
||||
*/
|
||||
protected function isAuthorizedSuperAdmin(UserInterface $user): ?bool
|
||||
{
|
||||
// Action authorization includes super user authorization if using Flex Users.
|
||||
if ($user instanceof FlexObjectInterface) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $user->authorize('admin.super');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $scope
|
||||
* @param string $action
|
||||
* @return string
|
||||
*/
|
||||
protected function getAuthorizeRule(string $scope, string $action): string
|
||||
{
|
||||
if ($this instanceof FlexDirectory) {
|
||||
return $this->getAuthorizeRule($scope, $action);
|
||||
}
|
||||
|
||||
return $this->getFlexDirectory()->getAuthorizeRule($scope, $action);
|
||||
}
|
||||
}
|
||||
520
system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php
Normal file
520
system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php
Normal file
@@ -0,0 +1,520 @@
|
||||
<?php
|
||||
|
||||
namespace Grav\Framework\Flex\Traits;
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Media\Interfaces\MediaCollectionInterface;
|
||||
use Grav\Common\Media\Interfaces\MediaUploadInterface;
|
||||
use Grav\Common\Media\Traits\MediaTrait;
|
||||
use Grav\Common\Page\Media;
|
||||
use Grav\Common\Page\Medium\Medium;
|
||||
use Grav\Common\Page\Medium\MediumFactory;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Framework\Cache\CacheInterface;
|
||||
use Grav\Framework\Filesystem\Filesystem;
|
||||
use Grav\Framework\Flex\FlexDirectory;
|
||||
use Grav\Framework\Form\FormFlashFile;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
use RuntimeException;
|
||||
use function array_key_exists;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function is_callable;
|
||||
use function is_int;
|
||||
use function is_object;
|
||||
use function is_string;
|
||||
use function strpos;
|
||||
|
||||
/**
|
||||
* Implements Grav Page content and header manipulation methods.
|
||||
*/
|
||||
trait FlexMediaTrait
|
||||
{
|
||||
use MediaTrait {
|
||||
MediaTrait::getMedia as protected getExistingMedia;
|
||||
}
|
||||
|
||||
/** @var array */
|
||||
protected $_uploads;
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getStorageFolder()
|
||||
{
|
||||
return $this->exists() ? $this->getFlexDirectory()->getStorageFolder($this->getStorageKey()) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getMediaFolder()
|
||||
{
|
||||
return $this->exists() ? $this->getFlexDirectory()->getMediaFolder($this->getStorageKey()) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MediaCollectionInterface
|
||||
*/
|
||||
public function getMedia()
|
||||
{
|
||||
$media = $this->media;
|
||||
if (null === $media) {
|
||||
$media = $this->getExistingMedia();
|
||||
|
||||
// Include uploaded media to the object media.
|
||||
$this->addUpdatedMedia($media);
|
||||
}
|
||||
|
||||
return $media;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
* @return MediaCollectionInterface|null
|
||||
*/
|
||||
public function getMediaField(string $field): ?MediaCollectionInterface
|
||||
{
|
||||
// Field specific media.
|
||||
$settings = $this->getFieldSettings($field);
|
||||
if (!empty($settings['media_field'])) {
|
||||
$var = 'destination';
|
||||
} elseif (!empty($settings['media_picker_field'])) {
|
||||
$var = 'folder';
|
||||
}
|
||||
|
||||
if (empty($var)) {
|
||||
// Not a media field.
|
||||
$media = null;
|
||||
} elseif ($settings['self']) {
|
||||
// Uses main media.
|
||||
$media = $this->getMedia();
|
||||
} else {
|
||||
// Uses custom media.
|
||||
$media = new Media($settings[$var]);
|
||||
$this->addUpdatedMedia($media);
|
||||
}
|
||||
|
||||
return $media;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
* @return array|null
|
||||
*/
|
||||
public function getFieldSettings(string $field): ?array
|
||||
{
|
||||
if ($field === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$type = $settings['type'] ?? '';
|
||||
|
||||
// Media field.
|
||||
if (!empty($settings['media_field']) || array_key_exists('destination', $settings) || in_array($type, ['avatar', 'file', 'pagemedia'], true)) {
|
||||
$settings['media_field'] = true;
|
||||
$var = 'destination';
|
||||
}
|
||||
|
||||
// Media picker field.
|
||||
if (!empty($settings['media_picker_field']) || in_array($type, ['filepicker', 'pagemediaselect'], true)) {
|
||||
$settings['media_picker_field'] = true;
|
||||
$var = 'folder';
|
||||
}
|
||||
|
||||
// Set media folder for media fields.
|
||||
if (isset($var)) {
|
||||
$folder = $settings[$var] ?? '';
|
||||
if (in_array(rtrim($folder, '/'), ['', '@self', 'self@', '@self@'], true)) {
|
||||
$settings[$var] = $this->getMediaFolder();
|
||||
$settings['self'] = true;
|
||||
} else {
|
||||
$settings[$var] = Utils::getPathFromToken($folder, $this);
|
||||
$settings['self'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
* @return array
|
||||
* @internal
|
||||
*/
|
||||
protected function getMediaFieldSettings(string $field): array
|
||||
{
|
||||
$settings = $this->getFieldSettings($field) ?? [];
|
||||
|
||||
return $settings + ['accept' => '*', 'limit' => 1000, 'self' => true];
|
||||
}
|
||||
|
||||
protected function getMediaFields(): array
|
||||
{
|
||||
// Load settings for the field.
|
||||
$schema = $this->getBlueprint()->schema();
|
||||
|
||||
$list = [];
|
||||
foreach ($schema->getState()['items'] as $field => $settings) {
|
||||
if (isset($settings['type']) && (in_array($settings['type'], ['avatar', 'file', 'pagemedia']) || !empty($settings['destination']))) {
|
||||
$list[] = $field;
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|mixed $value
|
||||
* @param array $settings
|
||||
* @return array|mixed
|
||||
*/
|
||||
protected function parseFileProperty($value, array $settings = [])
|
||||
{
|
||||
if (!is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$media = $this->getMedia();
|
||||
$originalMedia = is_callable([$this, 'getOriginalMedia']) ? $this->getOriginalMedia() : null;
|
||||
|
||||
$list = [];
|
||||
foreach ($value as $filename => $info) {
|
||||
if (!is_array($info)) {
|
||||
$list[$filename] = $info;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_int($filename)) {
|
||||
$filename = $info['path'] ?? $info['name'];
|
||||
}
|
||||
|
||||
/** @var Medium|null $imageFile */
|
||||
$imageFile = $media[$filename];
|
||||
|
||||
/** @var Medium|null $originalFile */
|
||||
$originalFile = $originalMedia ? $originalMedia[$filename] : null;
|
||||
|
||||
$url = $imageFile ? $imageFile->url() : null;
|
||||
$originalUrl = $originalFile ? $originalFile->url() : null;
|
||||
$list[$filename] = [
|
||||
'name' => $info['name'] ?? null,
|
||||
'type' => $info['type'] ?? null,
|
||||
'size' => $info['size'] ?? null,
|
||||
'path' => $filename,
|
||||
'thumb_url' => $url,
|
||||
'image_url' => $originalUrl ?? $url
|
||||
];
|
||||
if ($originalFile) {
|
||||
$list[$filename]['cropData'] = (object)($originalFile->metadata()['upload']['crop'] ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UploadedFileInterface $uploadedFile
|
||||
* @param string|null $filename
|
||||
* @param string|null $field
|
||||
* @return void
|
||||
* @internal
|
||||
*/
|
||||
public function checkUploadedMediaFile(UploadedFileInterface $uploadedFile, string $filename = null, string $field = null)
|
||||
{
|
||||
$media = $this->getMedia();
|
||||
if (!$media instanceof MediaUploadInterface) {
|
||||
throw new RuntimeException("Media for {$this->getFlexDirectory()->getFlexType()} doesn't support file uploads.");
|
||||
}
|
||||
|
||||
$media->checkUploadedFile($uploadedFile, $filename, $this->getMediaFieldSettings($field ?? ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UploadedFileInterface $uploadedFile
|
||||
* @param string|null $filename
|
||||
* @param string|null $field
|
||||
* @return void
|
||||
* @internal
|
||||
*/
|
||||
public function uploadMediaFile(UploadedFileInterface $uploadedFile, string $filename = null, string $field = null): void
|
||||
{
|
||||
$settings = $this->getMediaFieldSettings($field ?? '');
|
||||
|
||||
$media = $field ? $this->getMediaField($field) : $this->getMedia();
|
||||
if (!$media instanceof MediaUploadInterface) {
|
||||
throw new RuntimeException("Media for {$this->getFlexDirectory()->getFlexType()} doesn't support file uploads.");
|
||||
}
|
||||
|
||||
$filename = $media->checkUploadedFile($uploadedFile, $filename, $settings);
|
||||
$media->copyUploadedFile($uploadedFile, $filename, $settings);
|
||||
$this->clearMediaCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filename
|
||||
* @return void
|
||||
* @internal
|
||||
*/
|
||||
public function deleteMediaFile(string $filename): void
|
||||
{
|
||||
$media = $this->getMedia();
|
||||
if (!$media instanceof MediaUploadInterface) {
|
||||
throw new RuntimeException("Media for {$this->getFlexDirectory()->getFlexType()} doesn't support file uploads.");
|
||||
}
|
||||
|
||||
$media->deleteFile($filename);
|
||||
$this->clearMediaCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo()
|
||||
{
|
||||
return parent::__debugInfo() + [
|
||||
'uploads:private' => $this->getUpdatedMedia()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $files
|
||||
* @return void
|
||||
*/
|
||||
protected function setUpdatedMedia(array $files): void
|
||||
{
|
||||
$media = $this->getMedia();
|
||||
if (!$media instanceof MediaUploadInterface) {
|
||||
return;
|
||||
}
|
||||
|
||||
$filesystem = Filesystem::getInstance(false);
|
||||
|
||||
$list = [];
|
||||
foreach ($files as $field => $group) {
|
||||
$field = (string)$field;
|
||||
// Ignore files without a field and resized images.
|
||||
if ($field === '' || strpos($field, '/')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Load settings for the field.
|
||||
$settings = $this->getMediaFieldSettings($field);
|
||||
foreach ($group as $filename => $file) {
|
||||
if ($file) {
|
||||
// File upload.
|
||||
$filename = $file->getClientFilename();
|
||||
|
||||
/** @var FormFlashFile $file */
|
||||
$data = $file->jsonSerialize();
|
||||
unset($data['tmp_name'], $data['path']);
|
||||
} else {
|
||||
// File delete.
|
||||
$data = null;
|
||||
}
|
||||
|
||||
if ($file) {
|
||||
// Check file upload against media limits (except for max size).
|
||||
$filename = $media->checkUploadedFile($file, $filename, ['filesize' => 0] + $settings);
|
||||
}
|
||||
|
||||
$self = $settings['self'];
|
||||
if ($this->_loadMedia && $self) {
|
||||
$filepath = $filename;
|
||||
} else {
|
||||
$filepath = "{$settings['destination']}/{$filename}";
|
||||
}
|
||||
|
||||
// Calculate path without the retina scaling factor.
|
||||
$realpath = $filesystem->pathname($filepath) . str_replace(['@3x', '@2x'], '', basename($filepath));
|
||||
|
||||
$list[$filename] = [$file, $settings];
|
||||
|
||||
$path = str_replace('.', "\n", $field);
|
||||
if (null !== $data) {
|
||||
$data['name'] = $filename;
|
||||
$data['path'] = $filepath;
|
||||
|
||||
$this->setNestedProperty("{$path}\n{$realpath}", $data, "\n");
|
||||
} else {
|
||||
$this->unsetNestedProperty("{$path}\n{$realpath}", "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->clearMediaCache();
|
||||
|
||||
$this->_uploads = $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MediaCollectionInterface $media
|
||||
*/
|
||||
protected function addUpdatedMedia(MediaCollectionInterface $media): void
|
||||
{
|
||||
$updated = false;
|
||||
foreach ($this->getUpdatedMedia() as $filename => $upload) {
|
||||
if (is_array($upload)) {
|
||||
// Uses new format with [UploadedFileInterface, array].
|
||||
$settings = $upload[1];
|
||||
if (isset($settings['destination']) && $settings['destination'] === $media->getPath()) {
|
||||
$upload = $upload[0];
|
||||
} else {
|
||||
$upload = false;
|
||||
}
|
||||
}
|
||||
if (false !== $upload) {
|
||||
$medium = $upload ? MediumFactory::fromUploadedFile($upload) : null;
|
||||
$updated = true;
|
||||
if ($medium) {
|
||||
$medium->uploaded = true;
|
||||
$media->add($filename, $medium);
|
||||
} elseif (is_callable([$media, 'hide'])) {
|
||||
$media->hide($filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($updated) {
|
||||
$media->setTimestamps();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, UploadedFileInterface|array|null>
|
||||
*/
|
||||
protected function getUpdatedMedia(): array
|
||||
{
|
||||
return $this->_uploads ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function saveUpdatedMedia(): void
|
||||
{
|
||||
$media = $this->getMedia();
|
||||
if (!$media instanceof MediaUploadInterface) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload/delete altered files.
|
||||
/**
|
||||
* @var string $filename
|
||||
* @var UploadedFileInterface|array|null $file
|
||||
*/
|
||||
foreach ($this->getUpdatedMedia() as $filename => $file) {
|
||||
if (is_array($file)) {
|
||||
[$file, $settings] = $file;
|
||||
} else {
|
||||
$settings = null;
|
||||
}
|
||||
if ($file instanceof UploadedFileInterface) {
|
||||
$media->copyUploadedFile($file, $filename, $settings);
|
||||
} else {
|
||||
$media->deleteFile($filename, $settings);
|
||||
}
|
||||
}
|
||||
|
||||
$this->setUpdatedMedia([]);
|
||||
$this->clearMediaCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function freeMedia(): void
|
||||
{
|
||||
$this->unsetObjectProperty('media');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $uri
|
||||
* @return Medium|null
|
||||
*/
|
||||
protected function createMedium($uri)
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $grav['locator'];
|
||||
|
||||
$file = $uri && $locator->isStream($uri) ? $locator->findResource($uri) : $uri;
|
||||
|
||||
return is_string($file) && file_exists($file) ? MediumFactory::fromFile($file) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CacheInterface
|
||||
*/
|
||||
protected function getMediaCache()
|
||||
{
|
||||
return $this->getCache('object');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MediaCollectionInterface
|
||||
*/
|
||||
protected function offsetLoad_media()
|
||||
{
|
||||
return $this->getMedia();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null
|
||||
*/
|
||||
protected function offsetSerialize_media()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FlexDirectory
|
||||
*/
|
||||
abstract public function getFlexDirectory(): FlexDirectory;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getStorageKey(): string;
|
||||
|
||||
/**
|
||||
* @param string $filename
|
||||
* @return void
|
||||
* @deprecated 1.7 Use Media class that implements MediaUploadInterface instead.
|
||||
*/
|
||||
public function checkMediaFilename(string $filename)
|
||||
{
|
||||
user_error(__METHOD__ . '() is deprecated since Grav 1.7, use Media class that implements MediaUploadInterface instead', E_USER_DEPRECATED);
|
||||
|
||||
// Check the file extension.
|
||||
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Config $config */
|
||||
$config = $grav['config'];
|
||||
|
||||
// If not a supported type, return
|
||||
if (!$extension || !$config->get("media.types.{$extension}")) {
|
||||
$language = $grav['language'];
|
||||
throw new RuntimeException($language->translate('PLUGIN_ADMIN.UNSUPPORTED_FILE_TYPE') . ': ' . $extension, 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Common\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Traits;
|
||||
|
||||
use Grav\Framework\Flex\FlexCollection;
|
||||
use Grav\Framework\Flex\FlexDirectory;
|
||||
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
|
||||
use RuntimeException;
|
||||
use function in_array;
|
||||
|
||||
/**
|
||||
* Trait GravTrait
|
||||
* @package Grav\Common\Flex\Traits
|
||||
*/
|
||||
trait FlexRelatedDirectoryTrait
|
||||
{
|
||||
/**
|
||||
* @param string $type
|
||||
* @param string $property
|
||||
* @return FlexCollectionInterface
|
||||
*/
|
||||
protected function getCollectionByProperty($type, $property)
|
||||
{
|
||||
$directory = $this->getRelatedDirectory($type);
|
||||
$collection = $directory->getCollection();
|
||||
$list = $this->getNestedProperty($property) ?: [];
|
||||
|
||||
/** @var FlexCollection $collection */
|
||||
$collection = $collection->filter(static function ($object) use ($list) {
|
||||
return in_array($object->id, $list, true);
|
||||
});
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @return FlexDirectory
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function getRelatedDirectory($type): FlexDirectory
|
||||
{
|
||||
$directory = $this->getFlexContainer()->getDirectory($type);
|
||||
if (!$directory) {
|
||||
throw new RuntimeException(ucfirst($type). ' directory does not exist!');
|
||||
}
|
||||
|
||||
return $directory;
|
||||
}
|
||||
}
|
||||
566
system/src/Grav/Framework/Form/FormFlash.php
Normal file
566
system/src/Grav/Framework/Form/FormFlash.php
Normal file
@@ -0,0 +1,566 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Form
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Form;
|
||||
|
||||
use Exception;
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\User\Interfaces\UserInterface;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Framework\Form\Interfaces\FormFlashInterface;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use RocketTheme\Toolbox\File\YamlFile;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
use RuntimeException;
|
||||
use function func_get_args;
|
||||
use function is_array;
|
||||
|
||||
/**
|
||||
* Class FormFlash
|
||||
* @package Grav\Framework\Form
|
||||
*/
|
||||
class FormFlash implements FormFlashInterface
|
||||
{
|
||||
/** @var bool */
|
||||
protected $exists;
|
||||
/** @var string */
|
||||
protected $sessionId;
|
||||
/** @var string */
|
||||
protected $uniqueId;
|
||||
/** @var string */
|
||||
protected $formName;
|
||||
/** @var string */
|
||||
protected $url;
|
||||
/** @var array|null */
|
||||
protected $user;
|
||||
/** @var int */
|
||||
protected $createdTimestamp;
|
||||
/** @var int */
|
||||
protected $updatedTimestamp;
|
||||
/** @var array|null */
|
||||
protected $data;
|
||||
/** @var array */
|
||||
protected $files;
|
||||
/** @var array */
|
||||
protected $uploadedFiles;
|
||||
/** @var string[] */
|
||||
protected $uploadObjects;
|
||||
/** @var string */
|
||||
protected $folder;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function __construct($config)
|
||||
{
|
||||
// Backwards compatibility with Grav 1.6 plugins.
|
||||
if (!is_array($config)) {
|
||||
user_error(__CLASS__ . '::' . __FUNCTION__ . '($sessionId, $uniqueId, $formName) is deprecated since Grav 1.6.11, use $config parameter instead', E_USER_DEPRECATED);
|
||||
|
||||
$args = func_get_args();
|
||||
$config = [
|
||||
'session_id' => $args[0],
|
||||
'unique_id' => $args[1] ?? null,
|
||||
'form_name' => $args[2] ?? null,
|
||||
];
|
||||
$config = array_filter($config, static function ($val) {
|
||||
return $val !== null;
|
||||
});
|
||||
}
|
||||
|
||||
$this->sessionId = $config['session_id'] ?? 'no-session';
|
||||
$this->uniqueId = $config['unique_id'] ?? '';
|
||||
|
||||
$folder = $config['folder'] ?? ($this->sessionId ? 'tmp://forms/' . $this->sessionId : '');
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
|
||||
$this->folder = $folder && $locator->isStream($folder) ? $locator->findResource($folder, true, true) : $folder;
|
||||
|
||||
$this->init($this->loadStoredForm(), $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|null $data
|
||||
* @param array $config
|
||||
*/
|
||||
protected function init(?array $data, array $config): void
|
||||
{
|
||||
if (null === $data) {
|
||||
$this->exists = false;
|
||||
$this->formName = $config['form_name'] ?? '';
|
||||
$this->url = '';
|
||||
$this->createdTimestamp = $this->updatedTimestamp = time();
|
||||
$this->files = [];
|
||||
} else {
|
||||
$this->exists = true;
|
||||
$this->formName = $data['form'] ?? $config['form_name'] ?? '';
|
||||
$this->url = $data['url'] ?? '';
|
||||
$this->user = $data['user'] ?? null;
|
||||
$this->updatedTimestamp = $data['timestamps']['updated'] ?? time();
|
||||
$this->createdTimestamp = $data['timestamps']['created'] ?? $this->updatedTimestamp;
|
||||
$this->data = $data['data'] ?? null;
|
||||
$this->files = $data['files'] ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load raw flex flash data from the filesystem.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
protected function loadStoredForm(): ?array
|
||||
{
|
||||
$file = $this->getTmpIndex();
|
||||
$exists = $file->exists();
|
||||
|
||||
$data = null;
|
||||
if ($exists) {
|
||||
try {
|
||||
$data = (array)$file->content();
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getSessionId(): string
|
||||
{
|
||||
return $this->sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getUniqueId(): string
|
||||
{
|
||||
return $this->uniqueId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @deprecated 1.6.11 Use '->getUniqueId()' method instead.
|
||||
*/
|
||||
public function getUniqieId(): string
|
||||
{
|
||||
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6.11, use ->getUniqueId() method instead', E_USER_DEPRECATED);
|
||||
|
||||
return $this->getUniqueId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getFormName(): string
|
||||
{
|
||||
return $this->formName;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getUrl(): string
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getUsername(): string
|
||||
{
|
||||
return $this->user['username'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getUserEmail(): string
|
||||
{
|
||||
return $this->user['email'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getCreatedTimestamp(): int
|
||||
{
|
||||
return $this->createdTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getUpdatedTimestamp(): int
|
||||
{
|
||||
return $this->updatedTimestamp;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getData(): ?array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function setData(?array $data): void
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return $this->exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function save(bool $force = false)
|
||||
{
|
||||
if (!($this->folder && $this->uniqueId)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
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;
|
||||
} elseif ($this->exists) {
|
||||
// Delete empty form flash if it exists (it carries no information).
|
||||
return $this->delete();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
if ($this->folder && $this->uniqueId) {
|
||||
$this->removeTmpDir();
|
||||
$this->files = [];
|
||||
$this->exists = false;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getFilesByField(string $field): array
|
||||
{
|
||||
if (!isset($this->uploadObjects[$field])) {
|
||||
$objects = [];
|
||||
foreach ($this->files[$field] ?? [] as $name => $upload) {
|
||||
$objects[$name] = $upload ? new FormFlashFile($field, $upload, $this) : null;
|
||||
}
|
||||
$this->uploadedFiles[$field] = $objects;
|
||||
}
|
||||
|
||||
return $this->uploadedFiles[$field];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getFilesByFields($includeOriginal = false): array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($this->files as $field => $values) {
|
||||
if (!$includeOriginal && strpos($field, '/')) {
|
||||
continue;
|
||||
}
|
||||
$list[$field] = $this->getFilesByField($field);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function addUploadedFile(UploadedFileInterface $upload, string $field = null, array $crop = null): string
|
||||
{
|
||||
$tmp_dir = $this->getTmpDir();
|
||||
$tmp_name = Utils::generateRandomString(12);
|
||||
$name = $upload->getClientFilename();
|
||||
if (!$name) {
|
||||
throw new RuntimeException('Uploaded file has no filename');
|
||||
}
|
||||
|
||||
// Prepare upload data for later save
|
||||
$data = [
|
||||
'name' => $name,
|
||||
'type' => $upload->getClientMediaType(),
|
||||
'size' => $upload->getSize(),
|
||||
'tmp_name' => $tmp_name
|
||||
];
|
||||
|
||||
Folder::create($tmp_dir);
|
||||
$upload->moveTo("{$tmp_dir}/{$tmp_name}");
|
||||
|
||||
$this->addFileInternal($field, $name, $data, $crop);
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function addFile(string $filename, string $field, array $crop = null): bool
|
||||
{
|
||||
if (!file_exists($filename)) {
|
||||
throw new RuntimeException("File not found: {$filename}");
|
||||
}
|
||||
|
||||
// Prepare upload data for later save
|
||||
$data = [
|
||||
'name' => basename($filename),
|
||||
'type' => Utils::getMimeByLocalFile($filename),
|
||||
'size' => filesize($filename),
|
||||
];
|
||||
|
||||
$this->addFileInternal($field, $data['name'], $data, $crop);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function removeFile(string $name, string $field = null): bool
|
||||
{
|
||||
if (!$name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$field = $field ?: 'undefined';
|
||||
|
||||
$upload = $this->files[$field][$name] ?? null;
|
||||
if (null !== $upload) {
|
||||
$this->removeTmpFile($upload['tmp_name'] ?? '');
|
||||
}
|
||||
$upload = $this->files[$field . '/original'][$name] ?? null;
|
||||
if (null !== $upload) {
|
||||
$this->removeTmpFile($upload['tmp_name'] ?? '');
|
||||
}
|
||||
|
||||
// Mark file as deleted.
|
||||
$this->files[$field][$name] = null;
|
||||
$this->files[$field . '/original'][$name] = null;
|
||||
|
||||
unset(
|
||||
$this->uploadedFiles[$field][$name],
|
||||
$this->uploadedFiles[$field . '/original'][$name]
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function clearFiles()
|
||||
{
|
||||
foreach ($this->files as $field => $files) {
|
||||
foreach ($files as $name => $upload) {
|
||||
$this->removeTmpFile($upload['tmp_name'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
$this->files = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'form' => $this->formName,
|
||||
'unique_id' => $this->uniqueId,
|
||||
'url' => $this->url,
|
||||
'user' => $this->user,
|
||||
'timestamps' => [
|
||||
'created' => $this->createdTimestamp,
|
||||
'updated' => time(),
|
||||
],
|
||||
'data' => $this->data,
|
||||
'files' => $this->files
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return $this
|
||||
*/
|
||||
public function setUrl(string $url): self
|
||||
{
|
||||
$this->url = $url;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UserInterface|null $user
|
||||
* @return $this
|
||||
*/
|
||||
public function setUser(UserInterface $user = null)
|
||||
{
|
||||
if ($user && $user->username) {
|
||||
$this->user = [
|
||||
'username' => $user->username,
|
||||
'email' => $user->email ?? ''
|
||||
];
|
||||
} else {
|
||||
$this->user = null;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $username
|
||||
* @return $this
|
||||
*/
|
||||
public function setUserName(string $username = null): self
|
||||
{
|
||||
$this->user['username'] = $username;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $email
|
||||
* @return $this
|
||||
*/
|
||||
public function setUserEmail(string $email = null): self
|
||||
{
|
||||
$this->user['email'] = $email;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTmpDir(): string
|
||||
{
|
||||
return $this->folder && $this->uniqueId ? "{$this->folder}/{$this->uniqueId}" : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return YamlFile
|
||||
*/
|
||||
protected function getTmpIndex(): YamlFile
|
||||
{
|
||||
// Do not use CompiledYamlFile as the file can change multiple times per second.
|
||||
return YamlFile::instance($this->getTmpDir() . '/index.yaml');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*/
|
||||
protected function removeTmpFile(string $name): void
|
||||
{
|
||||
$tmpDir = $this->getTmpDir();
|
||||
$filename = $tmpDir ? $tmpDir . '/' . $name : '';
|
||||
if ($name && $filename && is_file($filename)) {
|
||||
unlink($filename);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function removeTmpDir(): void
|
||||
{
|
||||
// Make sure that index file cache gets always cleared.
|
||||
$file = $this->getTmpIndex();
|
||||
$file->free();
|
||||
|
||||
$tmpDir = $this->getTmpDir();
|
||||
if ($tmpDir && file_exists($tmpDir)) {
|
||||
Folder::delete($tmpDir);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $field
|
||||
* @param string $name
|
||||
* @param array $data
|
||||
* @param array|null $crop
|
||||
* @return void
|
||||
*/
|
||||
protected function addFileInternal(?string $field, string $name, array $data, array $crop = null): void
|
||||
{
|
||||
if (!($this->folder && $this->uniqueId)) {
|
||||
throw new RuntimeException('Cannot upload files: form flash folder not defined');
|
||||
}
|
||||
|
||||
$field = $field ?: 'undefined';
|
||||
if (!isset($this->files[$field])) {
|
||||
$this->files[$field] = [];
|
||||
}
|
||||
|
||||
$oldUpload = $this->files[$field][$name] ?? null;
|
||||
|
||||
if ($crop) {
|
||||
// Deal with crop upload
|
||||
if ($oldUpload) {
|
||||
$originalUpload = $this->files[$field . '/original'][$name] ?? null;
|
||||
if ($originalUpload) {
|
||||
// If there is original file already present, remove the modified file
|
||||
$this->files[$field . '/original'][$name]['crop'] = $crop;
|
||||
$this->removeTmpFile($oldUpload['tmp_name'] ?? '');
|
||||
} else {
|
||||
// Otherwise make the previous file as original
|
||||
$oldUpload['crop'] = $crop;
|
||||
$this->files[$field . '/original'][$name] = $oldUpload;
|
||||
}
|
||||
} else {
|
||||
$this->files[$field . '/original'][$name] = [
|
||||
'name' => $name,
|
||||
'type' => $data['type'],
|
||||
'crop' => $crop
|
||||
];
|
||||
}
|
||||
} else {
|
||||
// Deal with replacing upload
|
||||
$originalUpload = $this->files[$field . '/original'][$name] ?? null;
|
||||
$this->files[$field . '/original'][$name] = null;
|
||||
|
||||
$this->removeTmpFile($oldUpload['tmp_name'] ?? '');
|
||||
$this->removeTmpFile($originalUpload['tmp_name'] ?? '');
|
||||
}
|
||||
|
||||
// Prepare data to be saved later
|
||||
$this->files[$field][$name] = $data;
|
||||
}
|
||||
}
|
||||
237
system/src/Grav/Framework/Form/FormFlashFile.php
Normal file
237
system/src/Grav/Framework/Form/FormFlashFile.php
Normal file
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Form
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Form;
|
||||
|
||||
use Grav\Framework\Psr7\Stream;
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use RuntimeException;
|
||||
use function copy;
|
||||
use function fopen;
|
||||
use function is_string;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* Class FormFlashFile
|
||||
* @package Grav\Framework\Form
|
||||
*/
|
||||
class FormFlashFile implements UploadedFileInterface, \JsonSerializable
|
||||
{
|
||||
/** @var string */
|
||||
private $field;
|
||||
/** @var bool */
|
||||
private $moved = false;
|
||||
/** @var array */
|
||||
private $upload;
|
||||
/** @var FormFlash */
|
||||
private $flash;
|
||||
|
||||
/**
|
||||
* FormFlashFile constructor.
|
||||
* @param string $field
|
||||
* @param array $upload
|
||||
* @param FormFlash $flash
|
||||
*/
|
||||
public function __construct(string $field, array $upload, FormFlash $flash)
|
||||
{
|
||||
$this->field = $field;
|
||||
$this->upload = $upload;
|
||||
$this->flash = $flash;
|
||||
|
||||
$tmpFile = $this->getTmpFile();
|
||||
if (!$tmpFile && $this->isOk()) {
|
||||
$this->upload['error'] = \UPLOAD_ERR_NO_FILE;
|
||||
}
|
||||
|
||||
if (!isset($this->upload['size'])) {
|
||||
$this->upload['size'] = $tmpFile && $this->isOk() ? filesize($tmpFile) : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return StreamInterface
|
||||
*/
|
||||
public function getStream()
|
||||
{
|
||||
$this->validateActive();
|
||||
|
||||
$tmpFile = $this->getTmpFile();
|
||||
if (null === $tmpFile) {
|
||||
throw new RuntimeException('No temporary file');
|
||||
}
|
||||
|
||||
$resource = fopen($tmpFile, 'rb');
|
||||
if (false === $resource) {
|
||||
throw new RuntimeException('No temporary file');
|
||||
}
|
||||
|
||||
return Stream::create($resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $targetPath
|
||||
* @return void
|
||||
*/
|
||||
public function moveTo($targetPath)
|
||||
{
|
||||
$this->validateActive();
|
||||
|
||||
if (!is_string($targetPath) || empty($targetPath)) {
|
||||
throw new InvalidArgumentException('Invalid path provided for move operation; must be a non-empty string');
|
||||
}
|
||||
$tmpFile = $this->getTmpFile();
|
||||
if (null === $tmpFile) {
|
||||
throw new RuntimeException('No temporary file');
|
||||
}
|
||||
|
||||
$this->moved = copy($tmpFile, $targetPath);
|
||||
|
||||
if (false === $this->moved) {
|
||||
throw new RuntimeException(sprintf('Uploaded file could not be moved to %s', $targetPath));
|
||||
}
|
||||
|
||||
$filename = $this->getClientFilename();
|
||||
if ($filename) {
|
||||
$this->flash->removeFile($filename, $this->field);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getField(): string
|
||||
{
|
||||
return $this->field;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getSize()
|
||||
{
|
||||
return $this->upload['size'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getError()
|
||||
{
|
||||
return $this->upload['error'] ?? \UPLOAD_ERR_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getClientFilename()
|
||||
{
|
||||
return $this->upload['name'] ?? 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getClientMediaType()
|
||||
{
|
||||
return $this->upload['type'] ?? 'application/octet-stream';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isMoved(): bool
|
||||
{
|
||||
return $this->moved;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getMetaData(): array
|
||||
{
|
||||
if (isset($this->upload['crop'])) {
|
||||
return ['crop' => $this->upload['crop']];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDestination()
|
||||
{
|
||||
return $this->upload['path'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->upload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getTmpFile(): ?string
|
||||
{
|
||||
$tmpName = $this->upload['tmp_name'] ?? null;
|
||||
|
||||
if (!$tmpName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tmpFile = $this->flash->getTmpDir() . '/' . $tmpName;
|
||||
|
||||
return file_exists($tmpFile) ? $tmpFile : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo()
|
||||
{
|
||||
return [
|
||||
'field:private' => $this->field,
|
||||
'moved:private' => $this->moved,
|
||||
'upload:private' => $this->upload,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @throws RuntimeException if is moved or not ok
|
||||
*/
|
||||
private function validateActive(): void
|
||||
{
|
||||
if (!$this->isOk()) {
|
||||
throw new RuntimeException('Cannot retrieve stream due to upload error');
|
||||
}
|
||||
|
||||
if ($this->moved) {
|
||||
throw new RuntimeException('Cannot retrieve stream after it has already been moved');
|
||||
}
|
||||
|
||||
if (!$this->getTmpFile()) {
|
||||
throw new RuntimeException('Cannot retrieve stream as the file is missing');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool return true if there is no upload error
|
||||
*/
|
||||
private function isOk(): bool
|
||||
{
|
||||
return \UPLOAD_ERR_OK === $this->getError();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Form
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Form\Interfaces;
|
||||
|
||||
use Grav\Common\Page\Interfaces\PageInterface;
|
||||
use Grav\Common\Page\Page;
|
||||
|
||||
/**
|
||||
* Interface FormFactoryInterface
|
||||
* @package Grav\Framework\Form\Interfaces
|
||||
*/
|
||||
interface FormFactoryInterface
|
||||
{
|
||||
/**
|
||||
* @param Page $page
|
||||
* @param string $name
|
||||
* @param array $form
|
||||
* @return FormInterface|null
|
||||
* @deprecated 1.6 Use FormFactory::createFormByPage() instead.
|
||||
*/
|
||||
public function createPageForm(Page $page, string $name, array $form): ?FormInterface;
|
||||
|
||||
/**
|
||||
* Create form using the header of the page.
|
||||
*
|
||||
* @param PageInterface $page
|
||||
* @param string $name
|
||||
* @param array $form
|
||||
* @return FormInterface|null
|
||||
*
|
||||
public function createFormForPage(PageInterface $page, string $name, array $form): ?FormInterface;
|
||||
*/
|
||||
}
|
||||
174
system/src/Grav/Framework/Form/Interfaces/FormFlashInterface.php
Normal file
174
system/src/Grav/Framework/Form/Interfaces/FormFlashInterface.php
Normal file
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Form
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Form\Interfaces;
|
||||
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
|
||||
/**
|
||||
* Interface FormFlashInterface
|
||||
* @package Grav\Framework\Form\Interfaces
|
||||
*/
|
||||
interface FormFlashInterface extends \JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @param array $config Available configuration keys: session_id, unique_id, form_name
|
||||
*/
|
||||
public function __construct($config);
|
||||
|
||||
/**
|
||||
* Get session Id associated to this form instance.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSessionId(): string;
|
||||
|
||||
/**
|
||||
* Get unique identifier associated to this form instance.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUniqueId(): string;
|
||||
|
||||
/**
|
||||
* Get form name associated to this form instance.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFormName(): string;
|
||||
|
||||
/**
|
||||
* Get URL associated to this form instance.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl(): string;
|
||||
|
||||
/**
|
||||
* Get username from the user who was associated to this form instance.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUsername(): string;
|
||||
|
||||
/**
|
||||
* Get email from the user who was associated to this form instance.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserEmail(): string;
|
||||
|
||||
|
||||
/**
|
||||
* Get creation timestamp for this form flash.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getCreatedTimestamp(): int;
|
||||
|
||||
/**
|
||||
* Get last updated timestamp for this form flash.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getUpdatedTimestamp(): int;
|
||||
|
||||
/**
|
||||
* Get raw form data.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getData(): ?array;
|
||||
|
||||
/**
|
||||
* Set raw form data.
|
||||
*
|
||||
* @param array|null $data
|
||||
* @return void
|
||||
*/
|
||||
public function setData(?array $data): void;
|
||||
|
||||
/**
|
||||
* Check if this form flash exists.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function exists(): bool;
|
||||
|
||||
/**
|
||||
* Save this form flash.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function save();
|
||||
|
||||
/**
|
||||
* Delete this form flash.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function delete();
|
||||
|
||||
/**
|
||||
* Get all files associated to a form field.
|
||||
*
|
||||
* @param string $field
|
||||
* @return array
|
||||
*/
|
||||
public function getFilesByField(string $field): array;
|
||||
|
||||
/**
|
||||
* Get all files grouped by the associated form fields.
|
||||
*
|
||||
* @param bool $includeOriginal
|
||||
* @return array
|
||||
*/
|
||||
public function getFilesByFields($includeOriginal = false): array;
|
||||
|
||||
/**
|
||||
* Add uploaded file to the form flash.
|
||||
*
|
||||
* @param UploadedFileInterface $upload
|
||||
* @param string|null $field
|
||||
* @param array|null $crop
|
||||
* @return string Return name of the file
|
||||
*/
|
||||
public function addUploadedFile(UploadedFileInterface $upload, string $field = null, array $crop = null): string;
|
||||
|
||||
/**
|
||||
* Add existing file to the form flash.
|
||||
*
|
||||
* @param string $filename
|
||||
* @param string $field
|
||||
* @param array|null $crop
|
||||
* @return bool
|
||||
*/
|
||||
public function addFile(string $filename, string $field, array $crop = null): bool;
|
||||
|
||||
/**
|
||||
* Remove any file from form flash.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|null $field
|
||||
* @return bool
|
||||
*/
|
||||
public function removeFile(string $name, string $field = null): bool;
|
||||
|
||||
/**
|
||||
* Clear form flash from all uploaded files.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearFiles();
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize(): array;
|
||||
}
|
||||
187
system/src/Grav/Framework/Form/Interfaces/FormInterface.php
Normal file
187
system/src/Grav/Framework/Form/Interfaces/FormInterface.php
Normal file
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Form
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Form\Interfaces;
|
||||
|
||||
use Grav\Common\Data\Blueprint;
|
||||
use Grav\Common\Data\Data;
|
||||
use Grav\Framework\Interfaces\RenderInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
|
||||
/**
|
||||
* Interface FormInterface
|
||||
* @package Grav\Framework\Form
|
||||
*/
|
||||
interface FormInterface extends RenderInterface, \Serializable
|
||||
{
|
||||
/**
|
||||
* Get HTML id="..." attribute.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getId(): string;
|
||||
|
||||
/**
|
||||
* Sets HTML id="" attribute.
|
||||
*
|
||||
* @param string $id
|
||||
*/
|
||||
public function setId(string $id): void;
|
||||
|
||||
/**
|
||||
* Get unique id for the current form instance. By default regenerated on every page reload.
|
||||
*
|
||||
* This id is used to load the saved form state, if available.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUniqueId(): string;
|
||||
|
||||
/**
|
||||
* Sets unique form id.
|
||||
*
|
||||
* @param string $uniqueId
|
||||
*/
|
||||
public function setUniqueId(string $uniqueId): void;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string;
|
||||
|
||||
/**
|
||||
* Get form name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFormName(): string;
|
||||
|
||||
/**
|
||||
* Get nonce name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNonceName(): string;
|
||||
|
||||
/**
|
||||
* Get nonce action.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNonceAction(): string;
|
||||
|
||||
/**
|
||||
* Get the nonce value for a form
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNonce(): string;
|
||||
|
||||
/**
|
||||
* Get task for the form if set in blueprints.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTask(): string;
|
||||
|
||||
/**
|
||||
* Get form action (URL). If action is empty, it points to the current page.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAction(): string;
|
||||
|
||||
/**
|
||||
* Get current data passed to the form.
|
||||
*
|
||||
* @return Data|object
|
||||
*/
|
||||
public function getData();
|
||||
|
||||
/**
|
||||
* Get files which were passed to the form.
|
||||
*
|
||||
* @return array|UploadedFileInterface[]
|
||||
*/
|
||||
public function getFiles(): array;
|
||||
|
||||
/**
|
||||
* Get a value from the form.
|
||||
*
|
||||
* Note: Used in form fields.
|
||||
*
|
||||
* @param string $name
|
||||
* @return mixed
|
||||
*/
|
||||
public function getValue(string $name);
|
||||
|
||||
/**
|
||||
* Get form flash object.
|
||||
*
|
||||
* @return FormFlashInterface
|
||||
*/
|
||||
public function getFlash();
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @return $this
|
||||
*/
|
||||
public function handleRequest(ServerRequestInterface $request): FormInterface;
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param UploadedFileInterface[]|null $files
|
||||
* @return $this
|
||||
*/
|
||||
public function submit(array $data, array $files = null): FormInterface;
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid(): bool;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getError(): ?string;
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getErrors(): array;
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isSubmitted(): bool;
|
||||
|
||||
/**
|
||||
* Reset form.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function reset(): void;
|
||||
|
||||
/**
|
||||
* Get form fields as an array.
|
||||
*
|
||||
* Note: Used in form fields.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFields(): array;
|
||||
|
||||
/**
|
||||
* Get blueprint used in the form.
|
||||
*
|
||||
* @return Blueprint
|
||||
*/
|
||||
public function getBlueprint(): Blueprint;
|
||||
}
|
||||
844
system/src/Grav/Framework/Form/Traits/FormTrait.php
Normal file
844
system/src/Grav/Framework/Form/Traits/FormTrait.php
Normal file
@@ -0,0 +1,844 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Form
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Form\Traits;
|
||||
|
||||
use ArrayAccess;
|
||||
use Exception;
|
||||
use FilesystemIterator;
|
||||
use Grav\Common\Data\Blueprint;
|
||||
use Grav\Common\Data\Data;
|
||||
use Grav\Common\Data\ValidationException;
|
||||
use Grav\Common\Debugger;
|
||||
use Grav\Common\Form\FormFlash;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Twig\Twig;
|
||||
use Grav\Common\User\Interfaces\UserInterface;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Framework\Compat\Serializable;
|
||||
use Grav\Framework\ContentBlock\HtmlBlock;
|
||||
use Grav\Framework\Form\Interfaces\FormFlashInterface;
|
||||
use Grav\Framework\Form\Interfaces\FormInterface;
|
||||
use Grav\Framework\Session\SessionInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use RuntimeException;
|
||||
use SplFileInfo;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Template;
|
||||
use Twig\TemplateWrapper;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function is_object;
|
||||
|
||||
/**
|
||||
* Trait FormTrait
|
||||
* @package Grav\Framework\Form
|
||||
*/
|
||||
trait FormTrait
|
||||
{
|
||||
use Serializable;
|
||||
|
||||
/** @var string */
|
||||
public $status = 'success';
|
||||
/** @var string|null */
|
||||
public $message;
|
||||
/** @var string[] */
|
||||
public $messages = [];
|
||||
|
||||
/** @var string */
|
||||
private $name;
|
||||
/** @var string */
|
||||
private $id;
|
||||
/** @var string */
|
||||
private $uniqueid;
|
||||
/** @var string */
|
||||
private $sessionid;
|
||||
/** @var bool */
|
||||
private $submitted;
|
||||
/** @var ArrayAccess|Data|null */
|
||||
private $data;
|
||||
/** @var array|UploadedFileInterface[] */
|
||||
private $files;
|
||||
/** @var FormFlashInterface|null */
|
||||
private $flash;
|
||||
/** @var string */
|
||||
private $flashFolder;
|
||||
/** @var Blueprint */
|
||||
private $blueprint;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
*/
|
||||
public function setId(string $id): void
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getUniqueId(): string
|
||||
{
|
||||
return $this->uniqueid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $uniqueId
|
||||
* @return void
|
||||
*/
|
||||
public function setUniqueId(string $uniqueId): void
|
||||
{
|
||||
$this->uniqueid = $uniqueId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFormName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getNonceName(): string
|
||||
{
|
||||
return 'form-nonce';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getNonceAction(): string
|
||||
{
|
||||
return 'form';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getNonce(): string
|
||||
{
|
||||
return Utils::getNonce($this->getNonceAction());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getAction(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTask(): string
|
||||
{
|
||||
return $this->getBlueprint()->get('form/task') ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $name
|
||||
* @return mixed
|
||||
*/
|
||||
public function getData(string $name = null)
|
||||
{
|
||||
return null !== $name ? $this->data[$name] : $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|UploadedFileInterface[]
|
||||
*/
|
||||
public function getFiles(): array
|
||||
{
|
||||
return $this->files ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function getValue(string $name)
|
||||
{
|
||||
return $this->data[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function getDefaultValue(string $name)
|
||||
{
|
||||
$path = explode('.', $name) ?: [];
|
||||
$offset = array_shift($path) ?? '';
|
||||
|
||||
$current = $this->getDefaultValues();
|
||||
|
||||
if (!isset($current[$offset])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$current = $current[$offset];
|
||||
|
||||
while ($path) {
|
||||
$offset = array_shift($path);
|
||||
|
||||
if ((is_array($current) || $current instanceof ArrayAccess) && isset($current[$offset])) {
|
||||
$current = $current[$offset];
|
||||
} elseif (is_object($current) && isset($current->{$offset})) {
|
||||
$current = $current->{$offset};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return $current;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getDefaultValues(): array
|
||||
{
|
||||
return $this->getBlueprint()->getDefaults();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @return FormInterface|$this
|
||||
*/
|
||||
public function handleRequest(ServerRequestInterface $request): FormInterface
|
||||
{
|
||||
// Set current form to be active.
|
||||
$grav = Grav::instance();
|
||||
$forms = $grav['forms'] ?? null;
|
||||
if ($forms) {
|
||||
$forms->setActiveForm($this);
|
||||
|
||||
/** @var Twig $twig */
|
||||
$twig = $grav['twig'];
|
||||
$twig->twig_vars['form'] = $this;
|
||||
}
|
||||
|
||||
try {
|
||||
[$data, $files] = $this->parseRequest($request);
|
||||
|
||||
$this->submit($data, $files);
|
||||
} catch (Exception $e) {
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = $grav['debugger'];
|
||||
$debugger->addException($e);
|
||||
|
||||
$this->setError($e->getMessage());
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @return FormInterface|$this
|
||||
*/
|
||||
public function setRequest(ServerRequestInterface $request): FormInterface
|
||||
{
|
||||
[$data, $files] = $this->parseRequest($request);
|
||||
|
||||
$this->data = new Data($data, $this->getBlueprint());
|
||||
$this->files = $files;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid(): bool
|
||||
{
|
||||
return $this->status === 'success';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getError(): ?string
|
||||
{
|
||||
return !$this->isValid() ? $this->message : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getErrors(): array
|
||||
{
|
||||
return !$this->isValid() ? $this->messages : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isSubmitted(): bool
|
||||
{
|
||||
return $this->submitted;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function validate(): bool
|
||||
{
|
||||
if (!$this->isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->validateData($this->data);
|
||||
$this->validateUploads($this->getFiles());
|
||||
} catch (ValidationException $e) {
|
||||
$this->setErrors($e->getMessages());
|
||||
} catch (Exception $e) {
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = Grav::instance()['debugger'];
|
||||
$debugger->addException($e);
|
||||
|
||||
$this->setError($e->getMessage());
|
||||
}
|
||||
|
||||
$this->filterData($this->data);
|
||||
|
||||
return $this->isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param UploadedFileInterface[]|null $files
|
||||
* @return FormInterface|$this
|
||||
*/
|
||||
public function submit(array $data, array $files = null): FormInterface
|
||||
{
|
||||
try {
|
||||
if ($this->isSubmitted()) {
|
||||
throw new RuntimeException('Form has already been submitted');
|
||||
}
|
||||
|
||||
$this->data = new Data($data, $this->getBlueprint());
|
||||
$this->files = $files ?? [];
|
||||
|
||||
if (!$this->validate()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->doSubmit($this->data->toArray(), $this->files);
|
||||
|
||||
$this->submitted = true;
|
||||
} catch (Exception $e) {
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = Grav::instance()['debugger'];
|
||||
$debugger->addException($e);
|
||||
|
||||
$this->setError($e->getMessage());
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function reset(): void
|
||||
{
|
||||
// Make sure that the flash object gets deleted.
|
||||
$this->getFlash()->delete();
|
||||
|
||||
$this->data = null;
|
||||
$this->files = [];
|
||||
$this->status = 'success';
|
||||
$this->message = null;
|
||||
$this->messages = [];
|
||||
$this->submitted = false;
|
||||
$this->flash = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getFields(): array
|
||||
{
|
||||
return $this->getBlueprint()->fields();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getButtons(): array
|
||||
{
|
||||
return $this->getBlueprint()->get('form/buttons') ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getTasks(): array
|
||||
{
|
||||
return $this->getBlueprint()->get('form/tasks') ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Blueprint
|
||||
*/
|
||||
abstract public function getBlueprint(): Blueprint;
|
||||
|
||||
/**
|
||||
* Get form flash object.
|
||||
*
|
||||
* @return FormFlashInterface
|
||||
*/
|
||||
public function getFlash()
|
||||
{
|
||||
if (null === $this->flash) {
|
||||
$grav = Grav::instance();
|
||||
$config = [
|
||||
'session_id' => $this->getSessionId(),
|
||||
'unique_id' => $this->getUniqueId(),
|
||||
'form_name' => $this->getName(),
|
||||
'folder' => $this->getFlashFolder()
|
||||
];
|
||||
|
||||
|
||||
$this->flash = new FormFlash($config);
|
||||
$this->flash->setUrl($grav['uri']->url)->setUser($grav['user'] ?? null);
|
||||
}
|
||||
|
||||
return $this->flash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available form flash objects for this form.
|
||||
*
|
||||
* @return FormFlashInterface[]
|
||||
*/
|
||||
public function getAllFlashes(): array
|
||||
{
|
||||
$folder = $this->getFlashFolder();
|
||||
if (!$folder || !is_dir($folder)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$name = $this->getName();
|
||||
|
||||
$list = [];
|
||||
/** @var SplFileInfo $file */
|
||||
foreach (new FilesystemIterator($folder) as $file) {
|
||||
$uniqueId = $file->getFilename();
|
||||
$config = [
|
||||
'session_id' => $this->getSessionId(),
|
||||
'unique_id' => $uniqueId,
|
||||
'form_name' => $name,
|
||||
'folder' => $this->getFlashFolder()
|
||||
];
|
||||
$flash = new FormFlash($config);
|
||||
if ($flash->exists() && $flash->getFormName() === $name) {
|
||||
$list[] = $flash;
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FormInterface::render()
|
||||
*/
|
||||
public function render(string $layout = null, array $context = [])
|
||||
{
|
||||
if (null === $layout) {
|
||||
$layout = 'default';
|
||||
}
|
||||
|
||||
$grav = Grav::instance();
|
||||
|
||||
$block = HtmlBlock::create();
|
||||
$block->disableCache();
|
||||
|
||||
$output = $this->getTemplate($layout)->render(
|
||||
['grav' => $grav, 'config' => $grav['config'], 'block' => $block, 'form' => $this, 'layout' => $layout] + $context
|
||||
);
|
||||
|
||||
$block->setContent($output);
|
||||
|
||||
return $block;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->doSerialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
final public function __serialize(): array
|
||||
{
|
||||
return $this->doSerialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
final public function __unserialize(array $data): void
|
||||
{
|
||||
$this->doUnserialize($data);
|
||||
}
|
||||
|
||||
protected function getSessionId(): string
|
||||
{
|
||||
if (null === $this->sessionid) {
|
||||
/** @var Grav $grav */
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var SessionInterface|null $session */
|
||||
$session = $grav['session'] ?? null;
|
||||
|
||||
$this->sessionid = $session ? ($session->getId() ?? '') : '';
|
||||
}
|
||||
|
||||
return $this->sessionid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sessionId
|
||||
* @return void
|
||||
*/
|
||||
protected function setSessionId(string $sessionId): void
|
||||
{
|
||||
$this->sessionid = $sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function unsetFlash(): void
|
||||
{
|
||||
$this->flash = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
protected function getFlashFolder(): ?string
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var UserInterface|null $user */
|
||||
$user = $grav['user'] ?? null;
|
||||
if (null !== $user && $user->exists()) {
|
||||
$username = $user->username;
|
||||
$mediaFolder = $user->getMediaFolder();
|
||||
} else {
|
||||
$username = null;
|
||||
$mediaFolder = null;
|
||||
}
|
||||
$session = $grav['session'] ?? null;
|
||||
$sessionId = $session ? $session->getId() : null;
|
||||
|
||||
// Fill template token keys/value pairs.
|
||||
$dataMap = [
|
||||
'[FORM_NAME]' => $this->getName(),
|
||||
'[SESSIONID]' => $sessionId ?? '!!',
|
||||
'[USERNAME]' => $username ?? '!!',
|
||||
'[USERNAME_OR_SESSIONID]' => $username ?? $sessionId ?? '!!',
|
||||
'[ACCOUNT]' => $mediaFolder ?? '!!'
|
||||
];
|
||||
|
||||
$flashLookupFolder = $this->getFlashLookupFolder();
|
||||
|
||||
$path = str_replace(array_keys($dataMap), array_values($dataMap), $flashLookupFolder);
|
||||
|
||||
// Make sure we only return valid paths.
|
||||
return strpos($path, '!!') === false ? rtrim($path, '/') : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getFlashLookupFolder(): string
|
||||
{
|
||||
if (null === $this->flashFolder) {
|
||||
$this->flashFolder = $this->getBlueprint()->get('form/flash_folder') ?? 'tmp://forms/[SESSIONID]';
|
||||
}
|
||||
|
||||
return $this->flashFolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $folder
|
||||
* @return void
|
||||
*/
|
||||
protected function setFlashLookupFolder(string $folder): void
|
||||
{
|
||||
$this->flashFolder = $folder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a single error.
|
||||
*
|
||||
* @param string $error
|
||||
* @return void
|
||||
*/
|
||||
protected function setError(string $error): void
|
||||
{
|
||||
$this->status = 'error';
|
||||
$this->message = $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set all errors.
|
||||
*
|
||||
* @param array $errors
|
||||
* @return void
|
||||
*/
|
||||
protected function setErrors(array $errors): void
|
||||
{
|
||||
$this->status = 'error';
|
||||
$this->messages = $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $layout
|
||||
* @return Template|TemplateWrapper
|
||||
* @throws LoaderError
|
||||
* @throws SyntaxError
|
||||
*/
|
||||
protected function getTemplate($layout)
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Twig $twig */
|
||||
$twig = $grav['twig'];
|
||||
|
||||
return $twig->twig()->resolveTemplate(
|
||||
[
|
||||
"forms/{$layout}/form.html.twig",
|
||||
'forms/default/form.html.twig'
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse PSR-7 ServerRequest into data and files.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return array
|
||||
*/
|
||||
protected function parseRequest(ServerRequestInterface $request): array
|
||||
{
|
||||
$method = $request->getMethod();
|
||||
if (!in_array($method, ['PUT', 'POST', 'PATCH'])) {
|
||||
throw new RuntimeException(sprintf('FlexForm: Bad HTTP method %s', $method));
|
||||
}
|
||||
|
||||
$body = $request->getParsedBody();
|
||||
$data = isset($body['data']) ? $this->decodeData($body['data']) : null;
|
||||
|
||||
$flash = $this->getFlash();
|
||||
/*
|
||||
if (null !== $data) {
|
||||
$flash->setData($data);
|
||||
$flash->save();
|
||||
}
|
||||
*/
|
||||
|
||||
$blueprint = $this->getBlueprint();
|
||||
$includeOriginal = (bool)($blueprint->form()['images']['original'] ?? null);
|
||||
$files = $flash->getFilesByFields($includeOriginal);
|
||||
|
||||
$data = $blueprint->processForm($data ?? [], $body['toggleable_data'] ?? []);
|
||||
|
||||
return [
|
||||
$data,
|
||||
$files ?? []
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate data and throw validation exceptions if validation fails.
|
||||
*
|
||||
* @param ArrayAccess|Data|null $data
|
||||
* @return void
|
||||
* @throws ValidationException
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function validateData($data = null): void
|
||||
{
|
||||
if ($data instanceof Data) {
|
||||
$data->validate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter validated data.
|
||||
*
|
||||
* @param ArrayAccess|Data|null $data
|
||||
* @return void
|
||||
*/
|
||||
protected function filterData($data = null): void
|
||||
{
|
||||
if ($data instanceof Data) {
|
||||
$data->filter();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate all uploaded files.
|
||||
*
|
||||
* @param array $files
|
||||
* @return void
|
||||
*/
|
||||
protected function validateUploads(array $files): void
|
||||
{
|
||||
foreach ($files as $file) {
|
||||
if (null === $file) {
|
||||
continue;
|
||||
}
|
||||
if ($file instanceof UploadedFileInterface) {
|
||||
$this->validateUpload($file);
|
||||
} else {
|
||||
$this->validateUploads($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate uploaded file.
|
||||
*
|
||||
* @param UploadedFileInterface $file
|
||||
* @return void
|
||||
*/
|
||||
protected function validateUpload(UploadedFileInterface $file): void
|
||||
{
|
||||
// Handle bad filenames.
|
||||
$filename = $file->getClientFilename();
|
||||
|
||||
if ($filename && !Utils::checkFilename($filename)) {
|
||||
$grav = Grav::instance();
|
||||
throw new RuntimeException(
|
||||
sprintf($grav['language']->translate('PLUGIN_FORM.FILEUPLOAD_UNABLE_TO_UPLOAD', null, true), $filename, 'Bad filename')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode POST data
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
protected function decodeData($data): array
|
||||
{
|
||||
if (!is_array($data)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Decode JSON encoded fields and merge them to data.
|
||||
if (isset($data['_json'])) {
|
||||
$data = array_replace_recursive($data, $this->jsonDecode($data['_json']));
|
||||
if (null === $data) {
|
||||
throw new RuntimeException(__METHOD__ . '(): Unexpected error');
|
||||
}
|
||||
unset($data['_json']);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively JSON decode POST data.
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
protected function jsonDecode(array $data): array
|
||||
{
|
||||
foreach ($data as $key => &$value) {
|
||||
if (is_array($value)) {
|
||||
$value = $this->jsonDecode($value);
|
||||
} elseif (trim($value) === '') {
|
||||
unset($data[$key]);
|
||||
} else {
|
||||
$value = json_decode($value, true);
|
||||
if ($value === null && json_last_error() !== JSON_ERROR_NONE) {
|
||||
unset($data[$key]);
|
||||
$this->setError("Badly encoded JSON data (for {$key}) was sent to the form");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function doSerialize(): array
|
||||
{
|
||||
$data = $this->data instanceof Data ? $this->data->toArray() : null;
|
||||
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'id' => $this->id,
|
||||
'uniqueid' => $this->uniqueid,
|
||||
'submitted' => $this->submitted,
|
||||
'status' => $this->status,
|
||||
'message' => $this->message,
|
||||
'messages' => $this->messages,
|
||||
'data' => $data,
|
||||
'files' => $this->files,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
protected function doUnserialize(array $data): void
|
||||
{
|
||||
$this->name = $data['name'];
|
||||
$this->id = $data['id'];
|
||||
$this->uniqueid = $data['uniqueid'];
|
||||
$this->submitted = $data['submitted'] ?? false;
|
||||
$this->status = $data['status'] ?? 'success';
|
||||
$this->message = $data['message'] ?? null;
|
||||
$this->messages = $data['messages'] ?? [];
|
||||
$this->data = isset($data['data']) ? new Data($data['data'], $this->getBlueprint()) : null;
|
||||
$this->files = $data['files'] ?? [];
|
||||
}
|
||||
}
|
||||
38
system/src/Grav/Framework/Interfaces/RenderInterface.php
Normal file
38
system/src/Grav/Framework/Interfaces/RenderInterface.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Interfaces
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Interfaces;
|
||||
|
||||
use Grav\Framework\ContentBlock\ContentBlockInterface;
|
||||
use Grav\Framework\ContentBlock\HtmlBlock;
|
||||
|
||||
/**
|
||||
* Defines common interface to render any object.
|
||||
*
|
||||
* @used-by \Grav\Framework\Flex\FlexObject
|
||||
* @since 1.6
|
||||
*/
|
||||
interface RenderInterface
|
||||
{
|
||||
/**
|
||||
* Renders the object.
|
||||
*
|
||||
* @example $block = $object->render('custom', ['variable' => 'value']);
|
||||
* @example {% render object layout 'custom' with { variable: 'value' } %}
|
||||
*
|
||||
* @param string|null $layout Layout to be used.
|
||||
* @param array $context Extra context given to the renderer.
|
||||
*
|
||||
* @return ContentBlockInterface|HtmlBlock Returns `HtmlBlock` containing the rendered output.
|
||||
* @api
|
||||
*/
|
||||
public function render(string $layout = null, array $context = []);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Media
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Media\Interfaces;
|
||||
|
||||
/**
|
||||
* Class implements media collection interface.
|
||||
*/
|
||||
interface MediaCollectionInterface extends \ArrayAccess, \Countable, \Iterator
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Media
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Media\Interfaces;
|
||||
|
||||
/**
|
||||
* Class implements media interface.
|
||||
*/
|
||||
interface MediaInterface
|
||||
{
|
||||
/**
|
||||
* Gets the associated media collection.
|
||||
*
|
||||
* @return MediaCollectionInterface Collection of associated media.
|
||||
*/
|
||||
public function getMedia();
|
||||
|
||||
/**
|
||||
* Get filesystem path to the associated media.
|
||||
*
|
||||
* @return string|null Media path or null if the object doesn't have media folder.
|
||||
*/
|
||||
public function getMediaFolder();
|
||||
|
||||
/**
|
||||
* Get display order for the associated media.
|
||||
*
|
||||
* @return array Empty array means default ordering.
|
||||
*/
|
||||
public function getMediaOrder();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Media
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Media\Interfaces;
|
||||
|
||||
use Grav\Common\Media\Interfaces\MediaInterface;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
|
||||
/**
|
||||
* Interface MediaManipulationInterface
|
||||
* @package Grav\Framework\Media\Interfaces
|
||||
* @deprecated 1.7 Not used currently
|
||||
*/
|
||||
interface MediaManipulationInterface extends MediaInterface
|
||||
{
|
||||
/**
|
||||
* @param UploadedFileInterface $uploadedFile
|
||||
*/
|
||||
public function uploadMediaFile(UploadedFileInterface $uploadedFile): void;
|
||||
|
||||
/**
|
||||
* @param string $filename
|
||||
*/
|
||||
public function deleteMediaFile(string $filename): void;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Media
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Media\Interfaces;
|
||||
|
||||
/**
|
||||
* Class implements media object interface.
|
||||
*/
|
||||
interface MediaObjectInterface
|
||||
{
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Object
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -41,6 +42,7 @@ trait ArrayAccessTrait
|
||||
*
|
||||
* @param mixed $offset The offset to assign the value to.
|
||||
* @param mixed $value The value to set.
|
||||
* @return void
|
||||
*/
|
||||
public function offsetSet($offset, $value)
|
||||
{
|
||||
@@ -51,14 +53,10 @@ trait ArrayAccessTrait
|
||||
* Unsets an offset.
|
||||
*
|
||||
* @param mixed $offset The offset to unset.
|
||||
* @return void
|
||||
*/
|
||||
public function offsetUnset($offset)
|
||||
{
|
||||
$this->unsetProperty($offset);
|
||||
}
|
||||
|
||||
abstract public function hasProperty($property);
|
||||
abstract public function getProperty($property, $default = null);
|
||||
abstract public function setProperty($property, $value);
|
||||
abstract public function unsetProperty($property);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Object
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -41,6 +42,7 @@ trait NestedArrayAccessTrait
|
||||
*
|
||||
* @param mixed $offset The offset to assign the value to.
|
||||
* @param mixed $value The value to set.
|
||||
* @return void
|
||||
*/
|
||||
public function offsetSet($offset, $value)
|
||||
{
|
||||
@@ -51,14 +53,10 @@ trait NestedArrayAccessTrait
|
||||
* Unsets an offset.
|
||||
*
|
||||
* @param mixed $offset The offset to unset.
|
||||
* @return void
|
||||
*/
|
||||
public function offsetUnset($offset)
|
||||
{
|
||||
$this->unsetNestedProperty($offset);
|
||||
}
|
||||
|
||||
abstract public function hasNestedProperty($property, $separator = null);
|
||||
abstract public function getNestedProperty($property, $default = null, $separator = null);
|
||||
abstract public function setNestedProperty($property, $value, $separator = null);
|
||||
abstract public function unsetNestedProperty($property, $separator = null);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Object
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -18,7 +19,7 @@ trait NestedPropertyCollectionTrait
|
||||
{
|
||||
/**
|
||||
* @param string $property Object property to be matched.
|
||||
* @param string $separator Separator, defaults to '.'
|
||||
* @param string|null $separator Separator, defaults to '.'
|
||||
* @return array Key/Value pairs of the properties.
|
||||
*/
|
||||
public function hasNestedProperty($property, $separator = null)
|
||||
@@ -36,7 +37,7 @@ trait NestedPropertyCollectionTrait
|
||||
/**
|
||||
* @param string $property Object property to be fetched.
|
||||
* @param mixed $default Default value if not set.
|
||||
* @param string $separator Separator, defaults to '.'
|
||||
* @param string|null $separator Separator, defaults to '.'
|
||||
* @return array Key/Value pairs of the properties.
|
||||
*/
|
||||
public function getNestedProperty($property, $default = null, $separator = null)
|
||||
@@ -53,8 +54,8 @@ trait NestedPropertyCollectionTrait
|
||||
|
||||
/**
|
||||
* @param string $property Object property to be updated.
|
||||
* @param string $value New value.
|
||||
* @param string $separator Separator, defaults to '.'
|
||||
* @param mixed $value New value.
|
||||
* @param string|null $separator Separator, defaults to '.'
|
||||
* @return $this
|
||||
*/
|
||||
public function setNestedProperty($property, $value, $separator = null)
|
||||
@@ -69,7 +70,7 @@ trait NestedPropertyCollectionTrait
|
||||
|
||||
/**
|
||||
* @param string $property Object property to be updated.
|
||||
* @param string $separator Separator, defaults to '.'
|
||||
* @param string|null $separator Separator, defaults to '.'
|
||||
* @return $this
|
||||
*/
|
||||
public function unsetNestedProperty($property, $separator = null)
|
||||
@@ -85,7 +86,7 @@ trait NestedPropertyCollectionTrait
|
||||
/**
|
||||
* @param string $property Object property to be updated.
|
||||
* @param string $default Default value.
|
||||
* @param string $separator Separator, defaults to '.'
|
||||
* @param string|null $separator Separator, defaults to '.'
|
||||
* @return $this
|
||||
*/
|
||||
public function defNestedProperty($property, $default, $separator = null)
|
||||
@@ -102,7 +103,7 @@ trait NestedPropertyCollectionTrait
|
||||
* Group items in the collection by a field.
|
||||
*
|
||||
* @param string $property Object property to be used to make groups.
|
||||
* @param string $separator Separator, defaults to '.'
|
||||
* @param string|null $separator Separator, defaults to '.'
|
||||
* @return array
|
||||
*/
|
||||
public function group($property, $separator = null)
|
||||
@@ -116,9 +117,4 @@ trait NestedPropertyCollectionTrait
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Traversable
|
||||
*/
|
||||
abstract public function getIterator();
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Object
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Object\Access;
|
||||
|
||||
use Grav\Framework\Object\Interfaces\ObjectInterface;
|
||||
use RuntimeException;
|
||||
use stdClass;
|
||||
use function is_array;
|
||||
use function is_object;
|
||||
|
||||
/**
|
||||
* Nested Property Object Trait
|
||||
@@ -18,27 +23,27 @@ trait NestedPropertyTrait
|
||||
{
|
||||
/**
|
||||
* @param string $property Object property name.
|
||||
* @param string $separator Separator, defaults to '.'
|
||||
* @param string|null $separator Separator, defaults to '.'
|
||||
* @return bool True if property has been defined (can be null).
|
||||
*/
|
||||
public function hasNestedProperty($property, $separator = null)
|
||||
{
|
||||
$test = new \stdClass;
|
||||
$test = new stdClass;
|
||||
|
||||
return $this->getNestedProperty($property, $test, $separator) !== $test;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $property Object property to be fetched.
|
||||
* @param mixed $default Default value if property has not been set.
|
||||
* @param string $separator Separator, defaults to '.'
|
||||
* @param mixed|null $default Default value if property has not been set.
|
||||
* @param string|null $separator Separator, defaults to '.'
|
||||
* @return mixed Property value.
|
||||
*/
|
||||
public function getNestedProperty($property, $default = null, $separator = null)
|
||||
{
|
||||
$separator = $separator ?: '.';
|
||||
$path = explode($separator, $property);
|
||||
$offset = array_shift($path);
|
||||
$path = explode($separator, $property) ?: [];
|
||||
$offset = array_shift($path) ?? '';
|
||||
|
||||
if (!$this->hasProperty($offset)) {
|
||||
return $default;
|
||||
@@ -72,16 +77,16 @@ trait NestedPropertyTrait
|
||||
|
||||
/**
|
||||
* @param string $property Object property to be updated.
|
||||
* @param string $value New value.
|
||||
* @param string $separator Separator, defaults to '.'
|
||||
* @param mixed $value New value.
|
||||
* @param string|null $separator Separator, defaults to '.'
|
||||
* @return $this
|
||||
* @throws \RuntimeException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function setNestedProperty($property, $value, $separator = null)
|
||||
{
|
||||
$separator = $separator ?: '.';
|
||||
$path = explode($separator, $property);
|
||||
$offset = array_shift($path);
|
||||
$path = explode($separator, $property) ?: [];
|
||||
$offset = array_shift($path) ?? '';
|
||||
|
||||
if (!$path) {
|
||||
$this->setProperty($offset, $value);
|
||||
@@ -102,7 +107,7 @@ trait NestedPropertyTrait
|
||||
$current[$offset] = [];
|
||||
}
|
||||
} else {
|
||||
throw new \RuntimeException('Cannot set nested property on non-array value');
|
||||
throw new RuntimeException("Cannot set nested property {$property} on non-array value");
|
||||
}
|
||||
|
||||
$current = &$current[$offset];
|
||||
@@ -115,15 +120,15 @@ trait NestedPropertyTrait
|
||||
|
||||
/**
|
||||
* @param string $property Object property to be updated.
|
||||
* @param string $separator Separator, defaults to '.'
|
||||
* @param string|null $separator Separator, defaults to '.'
|
||||
* @return $this
|
||||
* @throws \RuntimeException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function unsetNestedProperty($property, $separator = null)
|
||||
{
|
||||
$separator = $separator ?: '.';
|
||||
$path = explode($separator, $property);
|
||||
$offset = array_shift($path);
|
||||
$path = explode($separator, $property) ?: [];
|
||||
$offset = array_shift($path) ?? '';
|
||||
|
||||
if (!$path) {
|
||||
$this->unsetProperty($offset);
|
||||
@@ -146,7 +151,7 @@ trait NestedPropertyTrait
|
||||
return $this;
|
||||
}
|
||||
} else {
|
||||
throw new \RuntimeException('Cannot set nested property on non-array value');
|
||||
throw new RuntimeException("Cannot unset nested property {$property} on non-array value");
|
||||
}
|
||||
|
||||
$current = &$current[$offset];
|
||||
@@ -159,10 +164,10 @@ trait NestedPropertyTrait
|
||||
|
||||
/**
|
||||
* @param string $property Object property to be updated.
|
||||
* @param string $default Default value.
|
||||
* @param string $separator Separator, defaults to '.'
|
||||
* @param mixed $default Default value.
|
||||
* @param string|null $separator Separator, defaults to '.'
|
||||
* @return $this
|
||||
* @throws \RuntimeException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function defNestedProperty($property, $default, $separator = null)
|
||||
{
|
||||
@@ -172,11 +177,4 @@ trait NestedPropertyTrait
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
abstract public function hasProperty($property);
|
||||
abstract public function getProperty($property, $default = null);
|
||||
abstract public function setProperty($property, $value);
|
||||
abstract public function unsetProperty($property);
|
||||
abstract protected function &doGetProperty($property, $default = null, $doCreate = false);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Object
|
||||
*
|
||||
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
@@ -41,6 +42,7 @@ trait OverloadedPropertyTrait
|
||||
*
|
||||
* @param mixed $offset The offset to assign the value to.
|
||||
* @param mixed $value The value to set.
|
||||
* @return void
|
||||
*/
|
||||
public function __set($offset, $value)
|
||||
{
|
||||
@@ -51,14 +53,10 @@ trait OverloadedPropertyTrait
|
||||
* Magic method to unset the attribute
|
||||
*
|
||||
* @param mixed $offset The name value to unset
|
||||
* @return void
|
||||
*/
|
||||
public function __unset($offset)
|
||||
{
|
||||
$this->unsetProperty($offset);
|
||||
}
|
||||
|
||||
abstract public function hasProperty($property);
|
||||
abstract public function getProperty($property, $default = null);
|
||||
abstract public function setProperty($property, $value);
|
||||
abstract public function unsetProperty($property);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user