updated core to 1.7.15
This commit is contained in:
203
system/src/Grav/Framework/Flex/Pages/FlexPageCollection.php
Normal file
203
system/src/Grav/Framework/Flex/Pages/FlexPageCollection.php
Normal file
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Pages;
|
||||
|
||||
use Grav\Common\Page\Interfaces\PageInterface;
|
||||
use Grav\Framework\Flex\FlexCollection;
|
||||
use function array_search;
|
||||
use function assert;
|
||||
use function is_int;
|
||||
|
||||
/**
|
||||
* Class FlexPageCollection
|
||||
* @package Grav\Plugin\FlexObjects\Types\FlexPages
|
||||
* @template T of \Grav\Framework\Flex\Interfaces\FlexObjectInterface
|
||||
* @extends FlexCollection<T>
|
||||
*/
|
||||
class FlexPageCollection extends FlexCollection
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getCachedMethods(): array
|
||||
{
|
||||
return [
|
||||
// Collection filtering
|
||||
'withPublished' => true,
|
||||
'withVisible' => true,
|
||||
'withRoutable' => true,
|
||||
|
||||
'isFirst' => true,
|
||||
'isLast' => true,
|
||||
|
||||
// Find objects
|
||||
'prevSibling' => false,
|
||||
'nextSibling' => false,
|
||||
'adjacentSibling' => false,
|
||||
'currentPosition' => true,
|
||||
|
||||
'getNextOrder' => false,
|
||||
] + parent::getCachedMethods();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $bool
|
||||
* @return static
|
||||
* @phpstan-return static<T>
|
||||
*/
|
||||
public function withPublished(bool $bool = true)
|
||||
{
|
||||
$list = array_keys(array_filter($this->call('isPublished', [$bool])));
|
||||
|
||||
return $this->select($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $bool
|
||||
* @return static
|
||||
* @phpstan-return static<T>
|
||||
*/
|
||||
public function withVisible(bool $bool = true)
|
||||
{
|
||||
$list = array_keys(array_filter($this->call('isVisible', [$bool])));
|
||||
|
||||
return $this->select($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $bool
|
||||
* @return static
|
||||
* @phpstan-return static<T>
|
||||
*/
|
||||
public function withRoutable(bool $bool = true)
|
||||
{
|
||||
$list = array_keys(array_filter($this->call('isRoutable', [$bool])));
|
||||
|
||||
return $this->select($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if this item is the first in the collection.
|
||||
*
|
||||
* @param string $path
|
||||
* @return bool True if item is first.
|
||||
*/
|
||||
public function isFirst($path): bool
|
||||
{
|
||||
$keys = $this->getKeys();
|
||||
$first = reset($keys);
|
||||
|
||||
return $path === $first;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if this item is the last in the collection.
|
||||
*
|
||||
* @param string $path
|
||||
* @return bool True if item is last.
|
||||
*/
|
||||
public function isLast($path): bool
|
||||
{
|
||||
$keys = $this->getKeys();
|
||||
$last = end($keys);
|
||||
|
||||
return $path === $last;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the previous sibling based on current position.
|
||||
*
|
||||
* @param string $path
|
||||
* @return PageInterface|false The previous item.
|
||||
* @phpstan-return T|false
|
||||
*/
|
||||
public function prevSibling($path)
|
||||
{
|
||||
return $this->adjacentSibling($path, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next sibling based on current position.
|
||||
*
|
||||
* @param string $path
|
||||
* @return PageInterface|false The next item.
|
||||
* @phpstan-return T|false
|
||||
*/
|
||||
public function nextSibling($path)
|
||||
{
|
||||
return $this->adjacentSibling($path, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the adjacent sibling based on a direction.
|
||||
*
|
||||
* @param string $path
|
||||
* @param int $direction either -1 or +1
|
||||
* @return PageInterface|false The sibling item.
|
||||
* @phpstan-return T|false
|
||||
*/
|
||||
public function adjacentSibling($path, $direction = 1)
|
||||
{
|
||||
$keys = $this->getKeys();
|
||||
$pos = array_search($path, $keys, true);
|
||||
|
||||
if ($pos !== false) {
|
||||
$pos += $direction;
|
||||
if (isset($keys[$pos])) {
|
||||
return $this[$keys[$pos]];
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the item in the current position.
|
||||
*
|
||||
* @param string $path the path the item
|
||||
* @return int|null The index of the current page, null if not found.
|
||||
*/
|
||||
public function currentPosition($path): ?int
|
||||
{
|
||||
$pos = array_search($path, $this->getKeys(), true);
|
||||
|
||||
return $pos !== false ? $pos : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getNextOrder()
|
||||
{
|
||||
$directory = $this->getFlexDirectory();
|
||||
|
||||
$collection = $directory->getIndex();
|
||||
$keys = $collection->getStorageKeys();
|
||||
|
||||
// Assign next free order.
|
||||
/** @var FlexPageObject|null $last */
|
||||
$last = null;
|
||||
$order = 0;
|
||||
foreach ($keys as $folder => $key) {
|
||||
preg_match(FlexPageIndex::ORDER_PREFIX_REGEX, $folder, $test);
|
||||
$test = $test[0] ?? null;
|
||||
if ($test && $test > $order) {
|
||||
$order = $test;
|
||||
$last = $key;
|
||||
}
|
||||
}
|
||||
|
||||
$last = $collection[$last];
|
||||
|
||||
return sprintf('%d.', $last ? $last->value('order') + 1 : 1);
|
||||
}
|
||||
}
|
||||
48
system/src/Grav/Framework/Flex/Pages/FlexPageIndex.php
Normal file
48
system/src/Grav/Framework/Flex/Pages/FlexPageIndex.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Pages;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Framework\Flex\FlexIndex;
|
||||
|
||||
/**
|
||||
* Class FlexPageObject
|
||||
* @package Grav\Plugin\FlexObjects\Types\FlexPages
|
||||
*
|
||||
* @method FlexPageIndex withRoutable(bool $bool = true)
|
||||
* @method FlexPageIndex withPublished(bool $bool = true)
|
||||
* @method FlexPageIndex withVisible(bool $bool = true)
|
||||
*
|
||||
* @template T of FlexPageObject
|
||||
* @template C of FlexPageCollection
|
||||
* @extends FlexIndex<T,C>
|
||||
*/
|
||||
class FlexPageIndex extends FlexIndex
|
||||
{
|
||||
public const ORDER_PREFIX_REGEX = '/^\d+\./u';
|
||||
|
||||
/**
|
||||
* @param string $route
|
||||
* @return string
|
||||
* @internal
|
||||
*/
|
||||
public static function normalizeRoute(string $route)
|
||||
{
|
||||
static $case_insensitive;
|
||||
|
||||
if (null === $case_insensitive) {
|
||||
$case_insensitive = Grav::instance()['config']->get('system.force_lowercase_urls', false);
|
||||
}
|
||||
|
||||
return $case_insensitive ? mb_strtolower($route) : $route;
|
||||
}
|
||||
}
|
||||
495
system/src/Grav/Framework/Flex/Pages/FlexPageObject.php
Normal file
495
system/src/Grav/Framework/Flex/Pages/FlexPageObject.php
Normal file
@@ -0,0 +1,495 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Pages;
|
||||
|
||||
use DateTime;
|
||||
use Exception;
|
||||
use Grav\Common\Debugger;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Page\Interfaces\PageInterface;
|
||||
use Grav\Common\Page\Traits\PageFormTrait;
|
||||
use Grav\Common\User\Interfaces\UserCollectionInterface;
|
||||
use Grav\Framework\Flex\FlexObject;
|
||||
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexTranslateInterface;
|
||||
use Grav\Framework\Flex\Pages\Traits\PageAuthorsTrait;
|
||||
use Grav\Framework\Flex\Pages\Traits\PageContentTrait;
|
||||
use Grav\Framework\Flex\Pages\Traits\PageLegacyTrait;
|
||||
use Grav\Framework\Flex\Pages\Traits\PageRoutableTrait;
|
||||
use Grav\Framework\Flex\Pages\Traits\PageTranslateTrait;
|
||||
use Grav\Framework\Flex\Traits\FlexMediaTrait;
|
||||
use RuntimeException;
|
||||
use stdClass;
|
||||
use function array_key_exists;
|
||||
use function is_array;
|
||||
|
||||
/**
|
||||
* Class FlexPageObject
|
||||
* @package Grav\Plugin\FlexObjects\Types\FlexPages
|
||||
*/
|
||||
class FlexPageObject extends FlexObject implements PageInterface, FlexTranslateInterface
|
||||
{
|
||||
use PageAuthorsTrait;
|
||||
use PageContentTrait;
|
||||
use PageFormTrait;
|
||||
use PageLegacyTrait;
|
||||
use PageRoutableTrait;
|
||||
use PageTranslateTrait;
|
||||
use FlexMediaTrait;
|
||||
|
||||
public const PAGE_ORDER_REGEX = '/^(\d+)\.(.*)$/u';
|
||||
public const PAGE_ORDER_PREFIX_REGEX = '/^[0-9]+\./u';
|
||||
|
||||
/** @var array|null */
|
||||
protected $_reorder;
|
||||
/** @var FlexPageObject|null */
|
||||
protected $_original;
|
||||
|
||||
/**
|
||||
* Clone page.
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
parent::__clone();
|
||||
|
||||
if (isset($this->header)) {
|
||||
$this->header = clone($this->header);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getCachedMethods(): array
|
||||
{
|
||||
return [
|
||||
// Page Content Interface
|
||||
'header' => false,
|
||||
'summary' => true,
|
||||
'content' => true,
|
||||
'value' => false,
|
||||
'media' => false,
|
||||
'title' => true,
|
||||
'menu' => true,
|
||||
'visible' => true,
|
||||
'published' => true,
|
||||
'publishDate' => true,
|
||||
'unpublishDate' => true,
|
||||
'process' => true,
|
||||
'slug' => true,
|
||||
'order' => true,
|
||||
'id' => true,
|
||||
'modified' => true,
|
||||
'lastModified' => true,
|
||||
'folder' => true,
|
||||
'date' => true,
|
||||
'dateformat' => true,
|
||||
'taxonomy' => true,
|
||||
'shouldProcess' => true,
|
||||
'isPage' => true,
|
||||
'isDir' => true,
|
||||
'folderExists' => true,
|
||||
|
||||
// Page
|
||||
'isPublished' => true,
|
||||
'isOrdered' => true,
|
||||
'isVisible' => true,
|
||||
'isRoutable' => true,
|
||||
'getCreated_Timestamp' => true,
|
||||
'getPublish_Timestamp' => true,
|
||||
'getUnpublish_Timestamp' => true,
|
||||
'getUpdated_Timestamp' => true,
|
||||
] + parent::getCachedMethods();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $test
|
||||
* @return bool
|
||||
*/
|
||||
public function isPublished(bool $test = true): bool
|
||||
{
|
||||
$time = time();
|
||||
$start = $this->getPublish_Timestamp();
|
||||
$stop = $this->getUnpublish_Timestamp();
|
||||
|
||||
return $this->published() && $start <= $time && (!$stop || $time <= $stop) === $test;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $test
|
||||
* @return bool
|
||||
*/
|
||||
public function isOrdered(bool $test = true): bool
|
||||
{
|
||||
return ($this->order() !== false) === $test;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $test
|
||||
* @return bool
|
||||
*/
|
||||
public function isVisible(bool $test = true): bool
|
||||
{
|
||||
return $this->visible() === $test;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $test
|
||||
* @return bool
|
||||
*/
|
||||
public function isRoutable(bool $test = true): bool
|
||||
{
|
||||
return $this->routable() === $test;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getCreated_Timestamp(): int
|
||||
{
|
||||
return $this->getFieldTimestamp('created_date') ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getPublish_Timestamp(): int
|
||||
{
|
||||
return $this->getFieldTimestamp('publish_date') ?? $this->getCreated_Timestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getUnpublish_Timestamp(): ?int
|
||||
{
|
||||
return $this->getFieldTimestamp('unpublish_date');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getUpdated_Timestamp(): int
|
||||
{
|
||||
return $this->getFieldTimestamp('updated_date') ?? $this->getPublish_Timestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getFormValue(string $name, $default = null, string $separator = null)
|
||||
{
|
||||
$test = new stdClass();
|
||||
|
||||
$value = $this->pageContentValue($name, $test);
|
||||
if ($value !== $test) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
switch ($name) {
|
||||
case 'name':
|
||||
return $this->getProperty('template');
|
||||
case 'route':
|
||||
return $this->hasKey() ? '/' . $this->getKey() : null;
|
||||
case 'header.permissions.groups':
|
||||
$encoded = json_encode($this->getPermissions());
|
||||
if ($encoded === false) {
|
||||
throw new RuntimeException('json_encode(): failed to encode group permissions');
|
||||
}
|
||||
|
||||
return json_decode($encoded, true);
|
||||
}
|
||||
|
||||
return parent::getFormValue($name, $default, $separator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get master storage key.
|
||||
*
|
||||
* @return string
|
||||
* @see FlexObjectInterface::getStorageKey()
|
||||
*/
|
||||
public function getMasterKey(): string
|
||||
{
|
||||
$key = (string)($this->storage_key ?? $this->getMetaData()['storage_key'] ?? null);
|
||||
if (($pos = strpos($key, '|')) !== false) {
|
||||
$key = substr($key, 0, $pos);
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @see FlexObjectInterface::getCacheKey()
|
||||
*/
|
||||
public function getCacheKey(): string
|
||||
{
|
||||
return $this->hasKey() ? $this->getTypePrefix() . $this->getFlexType() . '.' . $this->getKey() . '.' . $this->getLanguage() : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $key
|
||||
* @return FlexObjectInterface
|
||||
*/
|
||||
public function createCopy(string $key = null)
|
||||
{
|
||||
$this->copy();
|
||||
|
||||
return parent::createCopy($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|bool $reorder
|
||||
* @return FlexObject|FlexObjectInterface
|
||||
*/
|
||||
public function save($reorder = true)
|
||||
{
|
||||
return parent::save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Page Unmodified (original) version of the page.
|
||||
*
|
||||
* Assumes that object has been cloned before modifying it.
|
||||
*
|
||||
* @return FlexPageObject|null The original version of the page.
|
||||
*/
|
||||
public function getOriginal()
|
||||
{
|
||||
return $this->_original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the Page Unmodified (original) version of the page.
|
||||
*
|
||||
* Can be called multiple times, only the first call matters.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function storeOriginal(): void
|
||||
{
|
||||
if (null === $this->_original) {
|
||||
$this->_original = clone $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display order for the associated media.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getMediaOrder(): array
|
||||
{
|
||||
$order = $this->getNestedProperty('header.media_order');
|
||||
|
||||
if (is_array($order)) {
|
||||
return $order;
|
||||
}
|
||||
|
||||
if (!$order) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_map('trim', explode(',', $order));
|
||||
}
|
||||
|
||||
// Overrides for header properties.
|
||||
|
||||
/**
|
||||
* Common logic to load header properties.
|
||||
*
|
||||
* @param string $property
|
||||
* @param mixed $var
|
||||
* @param callable $filter
|
||||
* @return mixed|null
|
||||
*/
|
||||
protected function loadHeaderProperty(string $property, $var, callable $filter)
|
||||
{
|
||||
// We have to use parent methods in order to avoid loops.
|
||||
$value = null === $var ? parent::getProperty($property) : null;
|
||||
if (null === $value) {
|
||||
$value = $filter($var ?? $this->getProperty('header')->get($property));
|
||||
|
||||
parent::setProperty($property, $value);
|
||||
if ($this->doHasProperty($property)) {
|
||||
$value = parent::getProperty($property);
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common logic to load header properties.
|
||||
*
|
||||
* @param string $property
|
||||
* @param mixed $var
|
||||
* @param callable $filter
|
||||
* @return mixed|null
|
||||
*/
|
||||
protected function loadProperty(string $property, $var, callable $filter)
|
||||
{
|
||||
// We have to use parent methods in order to avoid loops.
|
||||
$value = null === $var ? parent::getProperty($property) : null;
|
||||
if (null === $value) {
|
||||
$value = $filter($var);
|
||||
|
||||
parent::setProperty($property, $value);
|
||||
if ($this->doHasProperty($property)) {
|
||||
$value = parent::getProperty($property);
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $property
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function getProperty($property, $default = null)
|
||||
{
|
||||
$method = static::$headerProperties[$property] ?? static::$calculatedProperties[$property] ?? null;
|
||||
if ($method && method_exists($this, $method)) {
|
||||
return $this->{$method}();
|
||||
}
|
||||
|
||||
return parent::getProperty($property, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $property
|
||||
* @param mixed $value
|
||||
* @return $this
|
||||
*/
|
||||
public function setProperty($property, $value)
|
||||
{
|
||||
$method = static::$headerProperties[$property] ?? static::$calculatedProperties[$property] ?? null;
|
||||
if ($method && method_exists($this, $method)) {
|
||||
$this->{$method}($value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
parent::setProperty($property, $value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $property
|
||||
* @param mixed $value
|
||||
* @param string|null $separator
|
||||
* @return $this
|
||||
*/
|
||||
public function setNestedProperty($property, $value, $separator = null)
|
||||
{
|
||||
$separator = $separator ?: '.';
|
||||
if (strpos($property, 'header' . $separator) === 0) {
|
||||
$this->getProperty('header')->set(str_replace('header' . $separator, '', $property), $value, $separator);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
parent::setNestedProperty($property, $value, $separator);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $property
|
||||
* @param string|null $separator
|
||||
* @return $this
|
||||
*/
|
||||
public function unsetNestedProperty($property, $separator = null)
|
||||
{
|
||||
$separator = $separator ?: '.';
|
||||
if (strpos($property, 'header' . $separator) === 0) {
|
||||
$this->getProperty('header')->undef(str_replace('header' . $separator, '', $property), $separator);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
parent::unsetNestedProperty($property, $separator);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $elements
|
||||
* @param bool $extended
|
||||
* @return void
|
||||
*/
|
||||
protected function filterElements(array &$elements, bool $extended = false): void
|
||||
{
|
||||
// Markdown storage conversion to page structure.
|
||||
if (array_key_exists('content', $elements)) {
|
||||
$elements['markdown'] = $elements['content'];
|
||||
unset($elements['content']);
|
||||
}
|
||||
|
||||
if (!$extended) {
|
||||
$folder = !empty($elements['folder']) ? trim($elements['folder']) : '';
|
||||
|
||||
if ($folder) {
|
||||
$order = !empty($elements['order']) ? (int)$elements['order'] : null;
|
||||
// TODO: broken
|
||||
$elements['storage_key'] = $order ? sprintf('%02d.%s', $order, $folder) : $folder;
|
||||
}
|
||||
}
|
||||
|
||||
parent::filterElements($elements);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
* @return int|null
|
||||
*/
|
||||
protected function getFieldTimestamp(string $field): ?int
|
||||
{
|
||||
$date = $this->getFieldDateTime($field);
|
||||
|
||||
return $date ? $date->getTimestamp() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $field
|
||||
* @return DateTime|null
|
||||
*/
|
||||
protected function getFieldDateTime(string $field): ?DateTime
|
||||
{
|
||||
try {
|
||||
$value = $this->getProperty($field);
|
||||
if (is_numeric($value)) {
|
||||
$value = '@' . $value;
|
||||
}
|
||||
$date = $value ? new DateTime($value) : null;
|
||||
} catch (Exception $e) {
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = Grav::instance()['debugger'];
|
||||
$debugger->addException($e);
|
||||
|
||||
$date = null;
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UserCollectionInterface|null
|
||||
* @internal
|
||||
*/
|
||||
protected function loadAccounts()
|
||||
{
|
||||
return Grav::instance()['accounts'] ?? null;
|
||||
}
|
||||
}
|
||||
249
system/src/Grav/Framework/Flex/Pages/Traits/PageAuthorsTrait.php
Normal file
249
system/src/Grav/Framework/Flex/Pages/Traits/PageAuthorsTrait.php
Normal file
@@ -0,0 +1,249 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Pages\Traits;
|
||||
|
||||
use Grav\Common\User\Interfaces\UserInterface;
|
||||
use Grav\Framework\Acl\Access;
|
||||
use InvalidArgumentException;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function is_bool;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Trait PageAuthorsTrait
|
||||
* @package Grav\Framework\Flex\Pages\Traits
|
||||
*/
|
||||
trait PageAuthorsTrait
|
||||
{
|
||||
/** @var array<int,UserInterface> */
|
||||
private $_authors;
|
||||
/** @var array|null */
|
||||
private $_permissionsCache;
|
||||
|
||||
/**
|
||||
* Returns true if object has the named author.
|
||||
*
|
||||
* @param string $username
|
||||
* @return bool
|
||||
*/
|
||||
public function hasAuthor(string $username): bool
|
||||
{
|
||||
$authors = (array)$this->getNestedProperty('header.permissions.authors');
|
||||
if (empty($authors)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($authors as $author) {
|
||||
if ($username === $author) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of all author objects.
|
||||
*
|
||||
* @return array<int,UserInterface>
|
||||
*/
|
||||
public function getAuthors(): array
|
||||
{
|
||||
if (null === $this->_authors) {
|
||||
$this->_authors = $this->loadAuthors($this->getNestedProperty('header.permissions.authors', []));
|
||||
}
|
||||
|
||||
return $this->_authors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $inherit
|
||||
* @return array
|
||||
*/
|
||||
public function getPermissions(bool $inherit = false)
|
||||
{
|
||||
if (null === $this->_permissionsCache) {
|
||||
$permissions = [];
|
||||
if ($inherit && $this->getNestedProperty('header.permissions.inherit', true)) {
|
||||
$parent = $this->parent();
|
||||
if ($parent && method_exists($parent, 'getPermissions')) {
|
||||
$permissions = $parent->getPermissions($inherit);
|
||||
}
|
||||
}
|
||||
|
||||
$this->_permissionsCache = $this->loadPermissions($permissions);
|
||||
}
|
||||
|
||||
return $this->_permissionsCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param iterable $authors
|
||||
* @return array<int,UserInterface>
|
||||
*/
|
||||
protected function loadAuthors(iterable $authors): array
|
||||
{
|
||||
$accounts = $this->loadAccounts();
|
||||
if (null === $accounts || empty($authors)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$list = [];
|
||||
foreach ($authors as $username) {
|
||||
if (!is_string($username)) {
|
||||
throw new InvalidArgumentException('Iterable should return username (string).', 500);
|
||||
}
|
||||
$list[] = $accounts->load($username);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $action
|
||||
* @param string|null $scope
|
||||
* @param UserInterface|null $user
|
||||
* @param bool $isAuthor
|
||||
* @return bool|null
|
||||
*/
|
||||
public function isParentAuthorized(string $action, string $scope = null, UserInterface $user = null, bool $isAuthor = false): ?bool
|
||||
{
|
||||
$scope = $scope ?? $this->getAuthorizeScope();
|
||||
|
||||
$isMe = null === $user;
|
||||
if ($isMe) {
|
||||
$user = $this->getActiveUser();
|
||||
}
|
||||
|
||||
if (null === $user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->isAuthorizedByGroup($user, $action, $scope, $isMe, $isAuthor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UserInterface $user
|
||||
* @param string $action
|
||||
* @param string $scope
|
||||
* @param bool $isMe
|
||||
* @return bool|null
|
||||
*/
|
||||
protected function isAuthorizedOverride(UserInterface $user, string $action, string $scope, bool $isMe): ?bool
|
||||
{
|
||||
if ($action === 'delete' && $this->root()) {
|
||||
// Do not allow deleting root.
|
||||
return false;
|
||||
}
|
||||
|
||||
$isAuthor = !$isMe || $user->authorized ? $this->hasAuthor($user->username) : false;
|
||||
|
||||
return $this->isAuthorizedByGroup($user, $action, $scope, $isMe, $isAuthor) ?? parent::isAuthorizedOverride($user, $action, $scope, $isMe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Group authorization works as follows:
|
||||
*
|
||||
* 1. if any of the groups deny access, return false
|
||||
* 2. else if any of the groups allow access, return true
|
||||
* 3. else return null
|
||||
*
|
||||
* @param UserInterface $user
|
||||
* @param string $action
|
||||
* @param string $scope
|
||||
* @param bool $isMe
|
||||
* @param bool $isAuthor
|
||||
* @return bool|null
|
||||
*/
|
||||
protected function isAuthorizedByGroup(UserInterface $user, string $action, string $scope, bool $isMe, bool $isAuthor): ?bool
|
||||
{
|
||||
$authorized = null;
|
||||
|
||||
// In admin we want to check against group permissions.
|
||||
$pageGroups = $this->getPermissions();
|
||||
$userGroups = (array)$user->groups;
|
||||
|
||||
/** @var Access $access */
|
||||
foreach ($pageGroups as $group => $access) {
|
||||
if ($group === 'defaults') {
|
||||
// Special defaults permissions group does not apply to guest.
|
||||
if ($isMe && !$user->authorized) {
|
||||
continue;
|
||||
}
|
||||
} elseif ($group === 'authors') {
|
||||
if (!$isAuthor) {
|
||||
continue;
|
||||
}
|
||||
} elseif (!in_array($group, $userGroups, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$auth = $access->authorize($action);
|
||||
if (is_bool($auth)) {
|
||||
if ($auth === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$authorized = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $authorized && $this->getNestedProperty('header.permissions.inherit', true)) {
|
||||
// Authorize against parent page.
|
||||
$parent = $this->parent();
|
||||
if ($parent && method_exists($parent, 'isParentAuthorized')) {
|
||||
$authorized = $parent->isParentAuthorized($action, $scope, !$isMe ? $user : null, $isAuthor);
|
||||
}
|
||||
}
|
||||
|
||||
return $authorized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $parent
|
||||
* @return array
|
||||
*/
|
||||
protected function loadPermissions(array $parent = []): array
|
||||
{
|
||||
static $rules = [
|
||||
'c' => 'create',
|
||||
'r' => 'read',
|
||||
'u' => 'update',
|
||||
'd' => 'delete',
|
||||
'p' => 'publish',
|
||||
'l' => 'list'
|
||||
];
|
||||
|
||||
$permissions = $this->getNestedProperty('header.permissions.groups');
|
||||
$name = $this->root() ? '<root>' : '/' . $this->getKey();
|
||||
|
||||
$list = [];
|
||||
if (is_array($permissions)) {
|
||||
foreach ($permissions as $group => $access) {
|
||||
$list[$group] = new Access($access, $rules, $name);
|
||||
}
|
||||
}
|
||||
foreach ($parent as $group => $access) {
|
||||
if (isset($list[$group])) {
|
||||
$object = $list[$group];
|
||||
} else {
|
||||
$object = new Access([], $rules, $name);
|
||||
$list[$group] = $object;
|
||||
}
|
||||
|
||||
$object->inherit($access);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
840
system/src/Grav/Framework/Flex/Pages/Traits/PageContentTrait.php
Normal file
840
system/src/Grav/Framework/Flex/Pages/Traits/PageContentTrait.php
Normal file
@@ -0,0 +1,840 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Pages\Traits;
|
||||
|
||||
use Exception;
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Markdown\Parsedown;
|
||||
use Grav\Common\Markdown\ParsedownExtra;
|
||||
use Grav\Common\Page\Header;
|
||||
use Grav\Common\Page\Interfaces\PageInterface;
|
||||
use Grav\Common\Page\Markdown\Excerpts;
|
||||
use Grav\Common\Page\Media;
|
||||
use Grav\Common\Twig\Twig;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Framework\File\Formatter\YamlFormatter;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
use stdClass;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Implements PageContentInterface.
|
||||
*/
|
||||
trait PageContentTrait
|
||||
{
|
||||
/** @var array */
|
||||
protected static $headerProperties = [
|
||||
'slug' => 'slug',
|
||||
'routes' => false,
|
||||
'title' => 'title',
|
||||
'language' => 'language',
|
||||
'template' => 'template',
|
||||
'menu' => 'menu',
|
||||
'routable' => 'routable',
|
||||
'visible' => 'visible',
|
||||
'redirect' => 'redirect',
|
||||
'external_url' => false,
|
||||
'order_dir' => 'orderDir',
|
||||
'order_by' => 'orderBy',
|
||||
'order_manual' => 'orderManual',
|
||||
'dateformat' => 'dateformat',
|
||||
'date' => 'date',
|
||||
'markdown_extra' => false,
|
||||
'taxonomy' => 'taxonomy',
|
||||
'max_count' => 'maxCount',
|
||||
'process' => 'process',
|
||||
'published' => 'published',
|
||||
'publish_date' => 'publishDate',
|
||||
'unpublish_date' => 'unpublishDate',
|
||||
'expires' => 'expires',
|
||||
'cache_control' => 'cacheControl',
|
||||
'etag' => 'eTag',
|
||||
'last_modified' => 'lastModified',
|
||||
'ssl' => 'ssl',
|
||||
'template_format' => 'templateFormat',
|
||||
'debugger' => false,
|
||||
];
|
||||
|
||||
/** @var array */
|
||||
protected static $calculatedProperties = [
|
||||
'name' => 'name',
|
||||
'parent' => 'parent',
|
||||
'parent_key' => 'parentStorageKey',
|
||||
'folder' => 'folder',
|
||||
'order' => 'order',
|
||||
'template' => 'template',
|
||||
];
|
||||
|
||||
/** @var object */
|
||||
protected $header;
|
||||
|
||||
/** @var string */
|
||||
protected $_summary;
|
||||
|
||||
/** @var string */
|
||||
protected $_content;
|
||||
|
||||
/**
|
||||
* Method to normalize the route.
|
||||
*
|
||||
* @param string $route
|
||||
* @return string
|
||||
* @internal
|
||||
*/
|
||||
public static function normalizeRoute($route): string
|
||||
{
|
||||
$case_insensitive = Grav::instance()['config']->get('system.force_lowercase_urls');
|
||||
|
||||
return $case_insensitive ? mb_strtolower($route) : $route;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function header($var = null)
|
||||
{
|
||||
if (null !== $var) {
|
||||
$this->setProperty('header', $var);
|
||||
}
|
||||
|
||||
return $this->getProperty('header');
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function summary($size = null, $textOnly = false): string
|
||||
{
|
||||
return $this->processSummary($size, $textOnly);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function setSummary($summary): void
|
||||
{
|
||||
$this->_summary = $summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @throws Exception
|
||||
*/
|
||||
public function content($var = null): string
|
||||
{
|
||||
if (null !== $var) {
|
||||
$this->_content = $var;
|
||||
}
|
||||
|
||||
return $this->_content ?? $this->processContent($this->getRawContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRawContent(): string
|
||||
{
|
||||
return $this->_content ?? $this->getArrayProperty('markdown') ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function setRawContent($content): void
|
||||
{
|
||||
$this->_content = $content ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function rawMarkdown($var = null): string
|
||||
{
|
||||
if ($var !== null) {
|
||||
$this->setProperty('markdown', $var);
|
||||
}
|
||||
|
||||
return $this->getProperty('markdown') ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* Implement by calling:
|
||||
*
|
||||
* $test = new \stdClass();
|
||||
* $value = $this->pageContentValue($name, $test);
|
||||
* if ($value !== $test) {
|
||||
* return $value;
|
||||
* }
|
||||
* return parent::value($name, $default);
|
||||
*/
|
||||
abstract public function value($name, $default = null, $separator = null);
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function media($var = null): Media
|
||||
{
|
||||
if ($var instanceof Media) {
|
||||
$this->setProperty('media', $var);
|
||||
}
|
||||
|
||||
return $this->getProperty('media');
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function title($var = null): string
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'title',
|
||||
$var,
|
||||
function ($value) {
|
||||
return trim($value ?? ($this->root() ? '<root>' : ucfirst($this->slug())));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function menu($var = null): string
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'menu',
|
||||
$var,
|
||||
function ($value) {
|
||||
return trim($value ?: $this->title());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function visible($var = null): bool
|
||||
{
|
||||
$value = $this->loadHeaderProperty(
|
||||
'visible',
|
||||
$var,
|
||||
function ($value) {
|
||||
return ($value ?? $this->order() !== false) && !$this->isModule();
|
||||
}
|
||||
);
|
||||
|
||||
return $value && $this->published();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function published($var = null): bool
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'published',
|
||||
$var,
|
||||
static function ($value) {
|
||||
return (bool)($value ?? true);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function publishDate($var = null): ?int
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'publish_date',
|
||||
$var,
|
||||
function ($value) {
|
||||
return $value ? Utils::date2timestamp($value, $this->getProperty('dateformat')) : null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function unpublishDate($var = null): ?int
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'unpublish_date',
|
||||
$var,
|
||||
function ($value) {
|
||||
return $value ? Utils::date2timestamp($value, $this->getProperty('dateformat')) : null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function process($var = null): array
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'process',
|
||||
$var,
|
||||
function ($value) {
|
||||
$value = array_replace(Grav::instance()['config']->get('system.pages.process', []), is_array($value) ? $value : []) ?? [];
|
||||
foreach ($value as $process => $status) {
|
||||
$value[$process] = (bool)$status;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function slug($var = null)
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'slug',
|
||||
$var,
|
||||
function ($value) {
|
||||
if (is_string($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$folder = $this->folder();
|
||||
if (null === $folder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$folder = preg_replace(static::PAGE_ORDER_PREFIX_REGEX, '', $folder);
|
||||
if (null === $folder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return static::normalizeRoute($folder);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function order($var = null)
|
||||
{
|
||||
$property = $this->loadProperty(
|
||||
'order',
|
||||
$var,
|
||||
function ($value) {
|
||||
if (null === $value) {
|
||||
$folder = $this->folder();
|
||||
if (null !== $folder) {
|
||||
preg_match(static::PAGE_ORDER_REGEX, $folder, $order);
|
||||
}
|
||||
|
||||
$value = $order[1] ?? false;
|
||||
}
|
||||
|
||||
if ($value === '') {
|
||||
$value = false;
|
||||
}
|
||||
if ($value !== false) {
|
||||
$value = (int)$value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
);
|
||||
|
||||
return $property !== false ? sprintf('%02d.', $property) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function id($var = null): string
|
||||
{
|
||||
$property = 'id';
|
||||
$value = null === $var ? $this->getProperty($property) : null;
|
||||
if (null === $value) {
|
||||
$value = $this->language() . ($var ?? ($this->modified() . md5('flex-' . $this->getFlexType() . '-' . $this->getKey())));
|
||||
|
||||
$this->setProperty($property, $value);
|
||||
if ($this->doHasProperty($property)) {
|
||||
$value = $this->getProperty($property);
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function modified($var = null): int
|
||||
{
|
||||
$property = 'modified';
|
||||
$value = null === $var ? $this->getProperty($property) : null;
|
||||
if (null === $value) {
|
||||
$value = (int)($var ?: $this->getTimestamp());
|
||||
|
||||
$this->setProperty($property, $value);
|
||||
if ($this->doHasProperty($property)) {
|
||||
$value = $this->getProperty($property);
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function lastModified($var = null): bool
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'last_modified',
|
||||
$var,
|
||||
static function ($value) {
|
||||
return (bool)($value ?? Grav::instance()['config']->get('system.pages.last_modified'));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function date($var = null): int
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'date',
|
||||
$var,
|
||||
function ($value) {
|
||||
$value = $value ? Utils::date2timestamp($value, $this->getProperty('dateformat')) : false;
|
||||
|
||||
return $value ?: $this->modified();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function dateformat($var = null): ?string
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'dateformat',
|
||||
$var,
|
||||
static function ($value) {
|
||||
return $value;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function taxonomy($var = null): array
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'taxonomy',
|
||||
$var,
|
||||
static function ($value) {
|
||||
if (is_array($value)) {
|
||||
// make sure first level are arrays
|
||||
array_walk($value, static function (&$val) {
|
||||
$val = (array) $val;
|
||||
});
|
||||
// make sure all values are strings
|
||||
array_walk_recursive($value, static function (&$val) {
|
||||
$val = (string) $val;
|
||||
});
|
||||
}
|
||||
|
||||
return $value ?? [];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function shouldProcess($process): bool
|
||||
{
|
||||
$test = $this->process();
|
||||
|
||||
return !empty($test[$process]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function isPage(): bool
|
||||
{
|
||||
return !in_array($this->template(), ['', 'folder'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function isDir(): bool
|
||||
{
|
||||
return !$this->isPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isModule(): bool
|
||||
{
|
||||
return $this->modularTwig();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Header|stdClass|array|null $value
|
||||
* @return Header
|
||||
*/
|
||||
protected function offsetLoad_header($value)
|
||||
{
|
||||
if ($value instanceof Header) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (null === $value) {
|
||||
$value = [];
|
||||
} elseif ($value instanceof stdClass) {
|
||||
$value = (array)$value;
|
||||
}
|
||||
|
||||
return new Header($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Header|stdClass|array|null $value
|
||||
* @return Header
|
||||
*/
|
||||
protected function offsetPrepare_header($value)
|
||||
{
|
||||
return $this->offsetLoad_header($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Header|null $value
|
||||
* @return array
|
||||
*/
|
||||
protected function offsetSerialize_header(?Header $value)
|
||||
{
|
||||
return $value ? $value->toArray() : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param mixed|null $default
|
||||
* @return mixed
|
||||
*/
|
||||
protected function pageContentValue($name, $default = null)
|
||||
{
|
||||
switch ($name) {
|
||||
case 'frontmatter':
|
||||
$frontmatter = $this->getArrayProperty('frontmatter');
|
||||
if ($frontmatter === null) {
|
||||
$header = $this->prepareStorage()['header'] ?? null;
|
||||
if ($header) {
|
||||
$formatter = new YamlFormatter();
|
||||
$frontmatter = $formatter->encode($header);
|
||||
} else {
|
||||
$frontmatter = '';
|
||||
}
|
||||
}
|
||||
return $frontmatter;
|
||||
case 'content':
|
||||
return $this->getProperty('markdown');
|
||||
case 'order':
|
||||
return (string)$this->order();
|
||||
case 'menu':
|
||||
return $this->menu();
|
||||
case 'ordering':
|
||||
return $this->order() !== false ? '1' : '0';
|
||||
case 'folder':
|
||||
$folder = $this->folder();
|
||||
|
||||
return null !== $folder ? preg_replace(static::PAGE_ORDER_PREFIX_REGEX, '', $folder) : '';
|
||||
case 'slug':
|
||||
return $this->slug();
|
||||
case 'published':
|
||||
return $this->published();
|
||||
case 'visible':
|
||||
return $this->visible();
|
||||
case 'media':
|
||||
return $this->media()->all();
|
||||
case 'media.file':
|
||||
return $this->media()->files();
|
||||
case 'media.video':
|
||||
return $this->media()->videos();
|
||||
case 'media.image':
|
||||
return $this->media()->images();
|
||||
case 'media.audio':
|
||||
return $this->media()->audios();
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $size
|
||||
* @param bool $textOnly
|
||||
* @return string
|
||||
*/
|
||||
protected function processSummary($size = null, $textOnly = false): string
|
||||
{
|
||||
$config = (array)Grav::instance()['config']->get('site.summary');
|
||||
$config_page = (array)$this->getNestedProperty('header.summary');
|
||||
if ($config_page) {
|
||||
$config = array_merge($config, $config_page);
|
||||
}
|
||||
|
||||
// Summary is not enabled, return the whole content.
|
||||
if (empty($config['enabled'])) {
|
||||
return $this->content();
|
||||
}
|
||||
|
||||
$content = $this->_summary ?? $this->content();
|
||||
if ($textOnly) {
|
||||
$content = strip_tags($content);
|
||||
}
|
||||
$content_size = mb_strwidth($content, 'utf-8');
|
||||
$summary_size = $this->_summary !== null ? $content_size : $this->getProperty('summary_size');
|
||||
|
||||
// Return calculated summary based on summary divider's position.
|
||||
$format = $config['format'] ?? '';
|
||||
|
||||
// Return entire page content on wrong/unknown format.
|
||||
if ($format !== 'long' && $format !== 'short') {
|
||||
return $content;
|
||||
}
|
||||
|
||||
if ($format === 'short' && null !== $summary_size) {
|
||||
// Slice the string on breakpoint.
|
||||
if ($content_size > $summary_size) {
|
||||
return mb_substr($content, 0, $summary_size);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
// If needed, get summary size from the config.
|
||||
$size = $size ?? $config['size'] ?? null;
|
||||
|
||||
// Return calculated summary based on defaults.
|
||||
$size = is_numeric($size) ? (int)$size : -1;
|
||||
if ($size < 0) {
|
||||
$size = 300;
|
||||
}
|
||||
|
||||
// If the size is zero or smaller than the summary limit, return the entire page content.
|
||||
if ($size === 0 || $content_size <= $size) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
// Only return string but not html, wrap whatever html tag you want when using.
|
||||
if ($textOnly) {
|
||||
return mb_strimwidth($content, 0, $size, '...', 'UTF-8');
|
||||
}
|
||||
|
||||
$summary = Utils::truncateHTML($content, $size);
|
||||
|
||||
return html_entity_decode($summary, ENT_COMPAT | ENT_HTML5, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and Sets the content based on content portion of the .md file
|
||||
*
|
||||
* @param string $content
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function processContent($content): string
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Config $config */
|
||||
$config = $grav['config'];
|
||||
|
||||
$process_markdown = $this->shouldProcess('markdown');
|
||||
$process_twig = $this->shouldProcess('twig') || $this->isModule();
|
||||
$cache_enable = $this->getNestedProperty('header.cache_enable') ?? $config->get('system.cache.enabled', true);
|
||||
|
||||
$twig_first = $this->getNestedProperty('header.twig_first') ?? $config->get('system.pages.twig_first', false);
|
||||
$never_cache_twig = $this->getNestedProperty('header.never_cache_twig') ?? $config->get('system.pages.never_cache_twig', false);
|
||||
|
||||
$cached = null;
|
||||
if ($cache_enable) {
|
||||
$cache = $this->getCache('render');
|
||||
$key = md5($this->getCacheKey() . '-content');
|
||||
$cached = $cache->get($key);
|
||||
if ($cached && $cached['checksum'] === $this->getCacheChecksum()) {
|
||||
$this->_content = $cached['content'] ?? '';
|
||||
$this->_content_meta = $cached['content_meta'] ?? null;
|
||||
|
||||
if ($process_twig && $never_cache_twig) {
|
||||
$this->_content = $this->processTwig($this->_content);
|
||||
}
|
||||
} else {
|
||||
$cached = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$cached) {
|
||||
$markdown_options = [];
|
||||
if ($process_markdown) {
|
||||
// Build markdown options.
|
||||
$markdown_options = (array)$config->get('system.pages.markdown');
|
||||
$markdown_page_options = (array)$this->getNestedProperty('header.markdown');
|
||||
if ($markdown_page_options) {
|
||||
$markdown_options = array_merge($markdown_options, $markdown_page_options);
|
||||
}
|
||||
|
||||
// pages.markdown_extra is deprecated, but still check it...
|
||||
if (!isset($markdown_options['extra'])) {
|
||||
$extra = $this->getNestedProperty('header.markdown_extra') ?? $config->get('system.pages.markdown_extra');
|
||||
if (null !== $extra) {
|
||||
user_error('Configuration option \'system.pages.markdown_extra\' is deprecated since Grav 1.5, use \'system.pages.markdown.extra\' instead', E_USER_DEPRECATED);
|
||||
|
||||
$markdown_options['extra'] = $extra;
|
||||
}
|
||||
}
|
||||
}
|
||||
$options = [
|
||||
'markdown' => $markdown_options,
|
||||
'images' => $config->get('system.images', [])
|
||||
];
|
||||
|
||||
$this->_content = $content;
|
||||
$grav->fireEvent('onPageContentRaw', new Event(['page' => $this]));
|
||||
|
||||
if ($twig_first && !$never_cache_twig) {
|
||||
if ($process_twig) {
|
||||
$this->_content = $this->processTwig($this->_content);
|
||||
}
|
||||
|
||||
if ($process_markdown) {
|
||||
$this->_content = $this->processMarkdown($this->_content, $options);
|
||||
}
|
||||
|
||||
// Content Processed but not cached yet
|
||||
$grav->fireEvent('onPageContentProcessed', new Event(['page' => $this]));
|
||||
} else {
|
||||
if ($process_markdown) {
|
||||
$options['keep_twig'] = $process_twig;
|
||||
$this->_content = $this->processMarkdown($this->_content, $options);
|
||||
}
|
||||
|
||||
// Content Processed but not cached yet
|
||||
$grav->fireEvent('onPageContentProcessed', new Event(['page' => $this]));
|
||||
|
||||
if ($cache_enable && $never_cache_twig) {
|
||||
$this->cachePageContent();
|
||||
}
|
||||
|
||||
if ($process_twig) {
|
||||
$this->_content = $this->processTwig($this->_content);
|
||||
}
|
||||
}
|
||||
|
||||
if ($cache_enable && !$never_cache_twig) {
|
||||
$this->cachePageContent();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle summary divider
|
||||
$delimiter = $config->get('site.summary.delimiter', '===');
|
||||
$divider_pos = mb_strpos($this->_content, "<p>{$delimiter}</p>");
|
||||
if ($divider_pos !== false) {
|
||||
$this->setProperty('summary_size', $divider_pos);
|
||||
$this->_content = str_replace("<p>{$delimiter}</p>", '', $this->_content);
|
||||
}
|
||||
|
||||
// Fire event when Page::content() is called
|
||||
$grav->fireEvent('onPageContent', new Event(['page' => $this]));
|
||||
|
||||
return $this->_content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the Twig page content.
|
||||
*
|
||||
* @param string $content
|
||||
* @return string
|
||||
*/
|
||||
protected function processTwig($content): string
|
||||
{
|
||||
/** @var Twig $twig */
|
||||
$twig = Grav::instance()['twig'];
|
||||
|
||||
/** @var PageInterface $this */
|
||||
return $twig->processPage($this, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the Markdown content.
|
||||
*
|
||||
* Uses Parsedown or Parsedown Extra depending on configuration.
|
||||
*
|
||||
* @param string $content
|
||||
* @param array $options
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function processMarkdown($content, array $options = []): string
|
||||
{
|
||||
/** @var PageInterface $self */
|
||||
$self = $this;
|
||||
|
||||
$excerpts = new Excerpts($self, $options);
|
||||
|
||||
// Initialize the preferred variant of markdown parser.
|
||||
if (isset($options['extra'])) {
|
||||
$parsedown = new ParsedownExtra($excerpts);
|
||||
} else {
|
||||
$parsedown = new Parsedown($excerpts);
|
||||
}
|
||||
|
||||
$keepTwig = (bool)($options['keep_twig'] ?? false);
|
||||
if ($keepTwig) {
|
||||
$token = [
|
||||
'/' . Utils::generateRandomString(3),
|
||||
Utils::generateRandomString(3) . '/'
|
||||
];
|
||||
// Base64 encode any twig.
|
||||
$content = preg_replace_callback(
|
||||
['/({#.*?#})/mu', '/({{.*?}})/mu', '/({%.*?%})/mu'],
|
||||
static function ($matches) use ($token) { return $token[0] . base64_encode($matches[1]) . $token[1]; },
|
||||
$content
|
||||
);
|
||||
}
|
||||
|
||||
$content = $parsedown->text($content);
|
||||
|
||||
if ($keepTwig) {
|
||||
// Base64 decode the encoded twig.
|
||||
$content = preg_replace_callback(
|
||||
['`' . $token[0] . '([A-Za-z0-9+/]+={0,2})' . $token[1] . '`mu'],
|
||||
static function ($matches) { return base64_decode($matches[1]); },
|
||||
$content
|
||||
);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
abstract protected function loadHeaderProperty(string $property, $var, callable $filter);
|
||||
}
|
||||
1119
system/src/Grav/Framework/Flex/Pages/Traits/PageLegacyTrait.php
Normal file
1119
system/src/Grav/Framework/Flex/Pages/Traits/PageLegacyTrait.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,550 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Pages\Traits;
|
||||
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Page\Interfaces\PageCollectionInterface;
|
||||
use Grav\Common\Page\Interfaces\PageInterface;
|
||||
use Grav\Common\Page\Pages;
|
||||
use Grav\Common\Uri;
|
||||
use Grav\Framework\Filesystem\Filesystem;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
use RuntimeException;
|
||||
use function dirname;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Implements PageRoutableInterface
|
||||
*/
|
||||
trait PageRoutableTrait
|
||||
{
|
||||
/** @var bool */
|
||||
protected $root = false;
|
||||
|
||||
/** @var string|null */
|
||||
private $_route;
|
||||
/** @var string|null */
|
||||
private $_path;
|
||||
/** @var PageInterface|null */
|
||||
private $_parentCache;
|
||||
|
||||
/**
|
||||
* Returns the page extension, got from the page `url_extension` config and falls back to the
|
||||
* system config `system.pages.append_url_extension`.
|
||||
*
|
||||
* @return string The extension of this page. For example `.html`
|
||||
*/
|
||||
public function urlExtension(): string
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'url_extension',
|
||||
null,
|
||||
function ($value) {
|
||||
if ($this->home()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $value ?? Grav::instance()['config']->get('system.pages.append_url_extension', '');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and Sets whether or not this Page is routable, ie you can reach it via a URL.
|
||||
* The page must be *routable* and *published*
|
||||
*
|
||||
* @param bool|null $var true if the page is routable
|
||||
* @return bool true if the page is routable
|
||||
*/
|
||||
public function routable($var = null): bool
|
||||
{
|
||||
$value = $this->loadHeaderProperty(
|
||||
'routable',
|
||||
$var,
|
||||
static function ($value) {
|
||||
return $value ?? true;
|
||||
}
|
||||
);
|
||||
|
||||
return $value && $this->published() && !$this->isModule() && !$this->root() && $this->getLanguages(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the URL for a page - alias of url().
|
||||
*
|
||||
* @param bool $include_host
|
||||
* @return string the permalink
|
||||
*/
|
||||
public function link($include_host = false): string
|
||||
{
|
||||
return $this->url($include_host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the URL with host information, aka Permalink.
|
||||
* @return string The permalink.
|
||||
*/
|
||||
public function permalink(): string
|
||||
{
|
||||
return $this->url(true, false, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the canonical URL for a page
|
||||
*
|
||||
* @param bool $include_lang
|
||||
* @return string
|
||||
*/
|
||||
public function canonical($include_lang = true): string
|
||||
{
|
||||
return $this->url(true, true, $include_lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the url for the Page.
|
||||
*
|
||||
* @param bool $include_host Defaults false, but true would include http://yourhost.com
|
||||
* @param bool $canonical true to return the canonical URL
|
||||
* @param bool $include_base
|
||||
* @param bool $raw_route
|
||||
* @return string The url.
|
||||
*/
|
||||
public function url($include_host = false, $canonical = false, $include_base = true, $raw_route = false): string
|
||||
{
|
||||
// Override any URL when external_url is set
|
||||
$external = $this->getNestedProperty('header.external_url');
|
||||
if ($external) {
|
||||
return $external;
|
||||
}
|
||||
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Pages $pages */
|
||||
$pages = $grav['pages'];
|
||||
|
||||
/** @var Config $config */
|
||||
$config = $grav['config'];
|
||||
|
||||
// get base route (multi-site base and language)
|
||||
$route = $include_base ? $pages->baseRoute() : '';
|
||||
|
||||
// add full route if configured to do so
|
||||
if (!$include_host && $config->get('system.absolute_urls', false)) {
|
||||
$include_host = true;
|
||||
}
|
||||
|
||||
if ($canonical) {
|
||||
$route .= $this->routeCanonical();
|
||||
} elseif ($raw_route) {
|
||||
$route .= $this->rawRoute();
|
||||
} else {
|
||||
$route .= $this->route();
|
||||
}
|
||||
|
||||
/** @var Uri $uri */
|
||||
$uri = $grav['uri'];
|
||||
$url = $uri->rootUrl($include_host) . '/' . trim($route, '/') . $this->urlExtension();
|
||||
|
||||
return Uri::filterPath($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the route for the page based on the route headers if available, else from
|
||||
* the parents route and the current Page's slug.
|
||||
*
|
||||
* @param string $var Set new default route.
|
||||
* @return string|null The route for the Page.
|
||||
*/
|
||||
public function route($var = null): ?string
|
||||
{
|
||||
if (null !== $var) {
|
||||
// TODO: not the best approach, but works...
|
||||
$this->setNestedProperty('header.routes.default', $var);
|
||||
}
|
||||
|
||||
// Return default route if given.
|
||||
$default = $this->getNestedProperty('header.routes.default');
|
||||
if (is_string($default)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return $this->routeInternal();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
protected function routeInternal(): ?string
|
||||
{
|
||||
$route = $this->_route;
|
||||
if (null !== $route) {
|
||||
return $route;
|
||||
}
|
||||
|
||||
if ($this->root()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Root and orphan nodes have no route.
|
||||
$parent = $this->parent();
|
||||
if (!$parent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($parent->home()) {
|
||||
/** @var Config $config */
|
||||
$config = Grav::instance()['config'];
|
||||
$hide = (bool)$config->get('system.home.hide_in_urls', false);
|
||||
$route = '/' . ($hide ? '' : $parent->slug());
|
||||
} else {
|
||||
$route = $parent->route();
|
||||
}
|
||||
|
||||
if ($route !== '' && $route !== '/') {
|
||||
$route .= '/';
|
||||
}
|
||||
|
||||
if (!$this->home()) {
|
||||
$route .= $this->slug();
|
||||
}
|
||||
|
||||
$this->_route = $route;
|
||||
|
||||
return $route;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to clear the route out so it regenerates next time you use it
|
||||
*/
|
||||
public function unsetRouteSlug(): void
|
||||
{
|
||||
// TODO:
|
||||
throw new RuntimeException(__METHOD__ . '(): Not Implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and Sets the page raw route
|
||||
*
|
||||
* @param string|null $var
|
||||
* @return string|null
|
||||
*/
|
||||
public function rawRoute($var = null): ?string
|
||||
{
|
||||
if (null !== $var) {
|
||||
// TODO:
|
||||
throw new RuntimeException(__METHOD__ . '(string): Not Implemented');
|
||||
}
|
||||
|
||||
if ($this->root()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return '/' . $this->getKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the route aliases for the page based on page headers.
|
||||
*
|
||||
* @param array|null $var list of route aliases
|
||||
* @return array The route aliases for the Page.
|
||||
*/
|
||||
public function routeAliases($var = null): array
|
||||
{
|
||||
if (null !== $var) {
|
||||
$this->setNestedProperty('header.routes.aliases', (array)$var);
|
||||
}
|
||||
|
||||
$aliases = (array)$this->getNestedProperty('header.routes.aliases');
|
||||
$default = $this->getNestedProperty('header.routes.default');
|
||||
if ($default) {
|
||||
$aliases[] = $default;
|
||||
}
|
||||
|
||||
return $aliases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the canonical route for this page if its set. If provided it will use
|
||||
* that value, else if it's `true` it will use the default route.
|
||||
*
|
||||
* @param string|null $var
|
||||
* @return string|null
|
||||
*/
|
||||
public function routeCanonical($var = null): ?string
|
||||
{
|
||||
if (null !== $var) {
|
||||
$this->setNestedProperty('header.routes.canonical', (array)$var);
|
||||
}
|
||||
|
||||
$canonical = $this->getNestedProperty('header.routes.canonical');
|
||||
|
||||
return is_string($canonical) ? $canonical : $this->route();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the redirect set in the header.
|
||||
*
|
||||
* @param string|null $var redirect url
|
||||
* @return string|null
|
||||
*/
|
||||
public function redirect($var = null): ?string
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'redirect',
|
||||
$var,
|
||||
static function ($value) {
|
||||
return trim($value) ?: null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the clean path to the page file
|
||||
*
|
||||
* Needed in admin for Page Media.
|
||||
*/
|
||||
public function relativePagePath(): ?string
|
||||
{
|
||||
$folder = $this->getMediaFolder();
|
||||
if (!$folder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
$path = $locator->isStream($folder) ? $locator->findResource($folder, false) : $folder;
|
||||
|
||||
return is_string($path) ? $path : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and sets the path to the folder where the .md for this Page object resides.
|
||||
* This is equivalent to the filePath but without the filename.
|
||||
*
|
||||
* @param string|null $var the path
|
||||
* @return string|null the path
|
||||
*/
|
||||
public function path($var = null): ?string
|
||||
{
|
||||
if (null !== $var) {
|
||||
// TODO:
|
||||
throw new RuntimeException(__METHOD__ . '(string): Not Implemented');
|
||||
}
|
||||
|
||||
$path = $this->_path;
|
||||
if ($path) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
if ($this->root()) {
|
||||
$folder = $this->getFlexDirectory()->getStorageFolder();
|
||||
} else {
|
||||
$folder = $this->getStorageFolder();
|
||||
}
|
||||
|
||||
if ($folder) {
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
$folder = $locator->isStream($folder) ? $locator->getResource($folder) : GRAV_ROOT . "/{$folder}";
|
||||
}
|
||||
|
||||
return $this->_path = is_string($folder) ? $folder : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get/set the folder.
|
||||
*
|
||||
* @param string|null $var Optional path, including numeric prefix.
|
||||
* @return string|null
|
||||
*/
|
||||
public function folder($var = null): ?string
|
||||
{
|
||||
return $this->loadProperty(
|
||||
'folder',
|
||||
$var,
|
||||
function ($value) {
|
||||
if (null === $value) {
|
||||
$value = $this->getMasterKey() ?: $this->getKey();
|
||||
}
|
||||
|
||||
return basename($value) ?: null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get/set the folder.
|
||||
*
|
||||
* @param string|null $var Optional path, including numeric prefix.
|
||||
* @return string|null
|
||||
*/
|
||||
public function parentStorageKey($var = null): ?string
|
||||
{
|
||||
return $this->loadProperty(
|
||||
'parent_key',
|
||||
$var,
|
||||
function ($value) {
|
||||
if (null === $value) {
|
||||
$filesystem = Filesystem::getInstance(false);
|
||||
$value = $this->getMasterKey() ?: $this->getKey();
|
||||
$value = ltrim($filesystem->dirname("/{$value}"), '/') ?: '';
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets and Sets the parent object for this page
|
||||
*
|
||||
* @param PageInterface|null $var the parent page object
|
||||
* @return PageInterface|null the parent page object if it exists.
|
||||
*/
|
||||
public function parent(PageInterface $var = null)
|
||||
{
|
||||
if (null !== $var) {
|
||||
// TODO:
|
||||
throw new RuntimeException(__METHOD__ . '(PageInterface): Not Implemented');
|
||||
}
|
||||
|
||||
if ($this->_parentCache || $this->root()) {
|
||||
return $this->_parentCache;
|
||||
}
|
||||
|
||||
// Use filesystem as \dirname() does not work in Windows because of '/foo' becomes '\'.
|
||||
$filesystem = Filesystem::getInstance(false);
|
||||
$directory = $this->getFlexDirectory();
|
||||
$parentKey = ltrim($filesystem->dirname("/{$this->getKey()}"), '/');
|
||||
if ('' !== $parentKey) {
|
||||
$parent = $directory->getObject($parentKey);
|
||||
$language = $this->getLanguage();
|
||||
if ($language && $parent && method_exists($parent, 'getTranslation')) {
|
||||
$parent = $parent->getTranslation($language) ?? $parent;
|
||||
}
|
||||
|
||||
$this->_parentCache = $parent;
|
||||
} else {
|
||||
$index = $directory->getIndex();
|
||||
|
||||
$this->_parentCache = \is_callable([$index, 'getRoot']) ? $index->getRoot() : null;
|
||||
}
|
||||
|
||||
return $this->_parentCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the top parent object for this page. Can return page itself.
|
||||
*
|
||||
* @return PageInterface The top parent page object.
|
||||
*/
|
||||
public function topParent()
|
||||
{
|
||||
$topParent = $this;
|
||||
while ($topParent) {
|
||||
$parent = $topParent->parent();
|
||||
if (!$parent || !$parent->parent()) {
|
||||
break;
|
||||
}
|
||||
$topParent = $parent;
|
||||
}
|
||||
|
||||
return $topParent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the item in the current position.
|
||||
*
|
||||
* @return int|null the index of the current page.
|
||||
*/
|
||||
public function currentPosition(): ?int
|
||||
{
|
||||
$parent = $this->parent();
|
||||
$collection = $parent ? $parent->collection('content', false) : null;
|
||||
if ($collection instanceof PageCollectionInterface && $path = $this->path()) {
|
||||
return $collection->currentPosition($path);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not this page is the currently active page requested via the URL.
|
||||
*
|
||||
* @return bool True if it is active
|
||||
*/
|
||||
public function active(): bool
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
$uri_path = rtrim(urldecode($grav['uri']->path()), '/') ?: '/';
|
||||
$routes = $grav['pages']->routes();
|
||||
|
||||
return isset($routes[$uri_path]) && $routes[$uri_path] === $this->path();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not this URI's URL contains the URL of the active page.
|
||||
* Or in other words, is this page's URL in the current URL
|
||||
*
|
||||
* @return bool True if active child exists
|
||||
*/
|
||||
public function activeChild(): bool
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
/** @var Uri $uri */
|
||||
$uri = $grav['uri'];
|
||||
/** @var Pages $pages */
|
||||
$pages = $grav['pages'];
|
||||
$uri_path = rtrim(urldecode($uri->path()), '/');
|
||||
$routes = $pages->routes();
|
||||
|
||||
if (isset($routes[$uri_path])) {
|
||||
$page = $pages->find($uri->route());
|
||||
/** @var PageInterface|null $child_page */
|
||||
$child_page = $page ? $page->parent() : null;
|
||||
while ($child_page && !$child_page->root()) {
|
||||
if ($this->path() === $child_page->path()) {
|
||||
return true;
|
||||
}
|
||||
$child_page = $child_page->parent();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not this page is the currently configured home page.
|
||||
*
|
||||
* @return bool True if it is the homepage
|
||||
*/
|
||||
public function home(): bool
|
||||
{
|
||||
$home = Grav::instance()['config']->get('system.home.alias');
|
||||
|
||||
return '/' . $this->getKey() === $home;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not this page is the root node of the pages tree.
|
||||
*
|
||||
* @param bool|null $var
|
||||
* @return bool True if it is the root
|
||||
*/
|
||||
public function root($var = null): bool
|
||||
{
|
||||
if (null !== $var) {
|
||||
$this->root = (bool)$var;
|
||||
}
|
||||
|
||||
return $this->root === true || $this->getKey() === '/';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Framework\Flex
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Framework\Flex\Pages\Traits;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Language\Language;
|
||||
use Grav\Common\Page\Interfaces\PageInterface;
|
||||
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
|
||||
use function is_bool;
|
||||
|
||||
/**
|
||||
* Implements PageTranslateInterface
|
||||
*/
|
||||
trait PageTranslateTrait
|
||||
{
|
||||
/** @var array|null */
|
||||
private $_languages;
|
||||
|
||||
/** @var PageInterface[] */
|
||||
private $_translations = [];
|
||||
|
||||
/**
|
||||
* @param string|null $languageCode
|
||||
* @param bool|null $fallback
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTranslation(string $languageCode = null, bool $fallback = null): bool
|
||||
{
|
||||
$code = $this->findTranslation($languageCode, $fallback);
|
||||
|
||||
return null !== $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $languageCode
|
||||
* @param bool|null $fallback
|
||||
* @return FlexObjectInterface|PageInterface|null
|
||||
*/
|
||||
public function getTranslation(string $languageCode = null, bool $fallback = null)
|
||||
{
|
||||
if ($this->root()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$code = $this->findTranslation($languageCode, $fallback);
|
||||
if (null === $code) {
|
||||
$object = null;
|
||||
} elseif ('' === $code) {
|
||||
$object = $this->getLanguage() ? $this->getFlexDirectory()->getObject($this->getMasterKey(), 'storage_key') : $this;
|
||||
} else {
|
||||
$meta = $this->getMetaData();
|
||||
$meta['template'] = $this->getLanguageTemplates()[$code] ?? $meta['template'];
|
||||
$key = $this->getStorageKey() . '|' . $meta['template'] . '.' . $code;
|
||||
$meta['storage_key'] = $key;
|
||||
$meta['lang'] = $code;
|
||||
$object = $this->getFlexDirectory()->loadObjects([$key => $meta])[$key] ?? null;
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $includeDefault If set to true, return separate entries for '' and 'en' (default) language.
|
||||
* @return array
|
||||
*/
|
||||
public function getAllLanguages(bool $includeDefault = false): array
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Language $language */
|
||||
$language = $grav['language'];
|
||||
$languages = $language->getLanguages();
|
||||
if (!$languages) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$translated = $this->getLanguageTemplates();
|
||||
|
||||
if ($includeDefault) {
|
||||
$languages[] = '';
|
||||
} elseif (isset($translated[''])) {
|
||||
$default = $language->getDefault();
|
||||
if (is_bool($default)) {
|
||||
$default = '';
|
||||
}
|
||||
$translated[$default] = $translated[''];
|
||||
unset($translated['']);
|
||||
}
|
||||
|
||||
$languages = array_fill_keys($languages, false);
|
||||
$translated = array_fill_keys(array_keys($translated), true);
|
||||
|
||||
return array_replace($languages, $translated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all translated languages.
|
||||
*
|
||||
* @param bool $includeDefault If set to true, return separate entries for '' and 'en' (default) language.
|
||||
* @return array
|
||||
*/
|
||||
public function getLanguages(bool $includeDefault = false): array
|
||||
{
|
||||
$languages = $this->getLanguageTemplates();
|
||||
|
||||
if (!$includeDefault && isset($languages[''])) {
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Language $language */
|
||||
$language = $grav['language'];
|
||||
$default = $language->getDefault();
|
||||
if (is_bool($default)) {
|
||||
$default = '';
|
||||
}
|
||||
$languages[$default] = $languages[''];
|
||||
unset($languages['']);
|
||||
}
|
||||
|
||||
return array_keys($languages);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getLanguage(): string
|
||||
{
|
||||
return $this->language() ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $languageCode
|
||||
* @param bool|null $fallback
|
||||
* @return string|null
|
||||
*/
|
||||
public function findTranslation(string $languageCode = null, bool $fallback = null): ?string
|
||||
{
|
||||
$translated = $this->getLanguageTemplates();
|
||||
|
||||
// If there's no translations (including default), we have an empty folder.
|
||||
if (!$translated) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// FIXME: only published is not implemented...
|
||||
$languages = $this->getFallbackLanguages($languageCode, $fallback);
|
||||
|
||||
$language = null;
|
||||
foreach ($languages as $code) {
|
||||
if (isset($translated[$code])) {
|
||||
$language = $code;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $language;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array with the routes of other translated languages
|
||||
*
|
||||
* @param bool $onlyPublished only return published translations
|
||||
* @return array the page translated languages
|
||||
*/
|
||||
public function translatedLanguages($onlyPublished = false): array
|
||||
{
|
||||
// FIXME: only published is not implemented...
|
||||
$translated = $this->getLanguageTemplates();
|
||||
if (!$translated) {
|
||||
return $translated;
|
||||
}
|
||||
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Language $language */
|
||||
$language = $grav['language'];
|
||||
$languages = $language->getLanguages();
|
||||
$languages[] = '';
|
||||
|
||||
$translated = array_intersect_key($translated, array_flip($languages));
|
||||
$list = array_fill_keys($languages, null);
|
||||
foreach ($translated as $languageCode => $languageFile) {
|
||||
$path = ($languageCode ? '/' : '') . $languageCode;
|
||||
$list[$languageCode] = "{$path}/{$this->getKey()}";
|
||||
}
|
||||
|
||||
return array_filter($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array listing untranslated languages available
|
||||
*
|
||||
* @param bool $includeUnpublished also list unpublished translations
|
||||
* @return array the page untranslated languages
|
||||
*/
|
||||
public function untranslatedLanguages($includeUnpublished = false): array
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Language $language */
|
||||
$language = $grav['language'];
|
||||
|
||||
$languages = $language->getLanguages();
|
||||
$translated = array_keys($this->translatedLanguages(!$includeUnpublished));
|
||||
|
||||
return array_values(array_diff($languages, $translated));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page language
|
||||
*
|
||||
* @param string|null $var
|
||||
* @return string|null
|
||||
*/
|
||||
public function language($var = null): ?string
|
||||
{
|
||||
return $this->loadHeaderProperty(
|
||||
'lang',
|
||||
$var,
|
||||
function ($value) {
|
||||
$value = $value ?? $this->getMetaData()['lang'] ?? '';
|
||||
|
||||
return trim($value) ?: null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function getLanguageTemplates(): array
|
||||
{
|
||||
if (null === $this->_languages) {
|
||||
$template = $this->getProperty('template');
|
||||
$meta = $this->getMetaData();
|
||||
$translations = $meta['markdown'] ?? [];
|
||||
$list = [];
|
||||
foreach ($translations as $code => $search) {
|
||||
if (isset($search[$template])) {
|
||||
// Use main template if possible.
|
||||
$list[$code] = $template;
|
||||
} elseif (!empty($search)) {
|
||||
// Fall back to first matching template.
|
||||
$list[$code] = key($search);
|
||||
}
|
||||
}
|
||||
|
||||
$this->_languages = $list;
|
||||
}
|
||||
|
||||
return $this->_languages;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $languageCode
|
||||
* @param bool|null $fallback
|
||||
* @return array
|
||||
*/
|
||||
protected function getFallbackLanguages(string $languageCode = null, bool $fallback = null): array
|
||||
{
|
||||
$fallback = $fallback ?? true;
|
||||
if (!$fallback && null !== $languageCode) {
|
||||
return [$languageCode];
|
||||
}
|
||||
|
||||
$grav = Grav::instance();
|
||||
|
||||
/** @var Language $language */
|
||||
$language = $grav['language'];
|
||||
$languageCode = $languageCode ?? ($language->getLanguage() ?: '');
|
||||
if ($languageCode === '' && $fallback) {
|
||||
return $language->getFallbackLanguages(null, true);
|
||||
}
|
||||
|
||||
return $fallback ? $language->getFallbackLanguages($languageCode, true) : [$languageCode];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user