updated core to 1.7.15

This commit is contained in:
2021-05-27 18:17:50 +02:00
parent dc1fdf21c9
commit 19ecb285ab
552 changed files with 80743 additions and 16675 deletions

View 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 ?? '';
}
}

View 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;
}
}

File diff suppressed because it is too large Load Diff

View 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);
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}

View 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;
}

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;
}

View 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);
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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);
}

File diff suppressed because it is too large Load Diff

View File

@@ -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() === '/';
}
}

View File

@@ -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];
}
}

View File

@@ -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);
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}

View 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);
}
}
}

View File

@@ -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;
}
}