123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744 |
- <?php
- declare(strict_types=1);
- /**
- * @package Grav\Common\Flex
- *
- * @copyright Copyright (c) 2015 - 2022 Trilby Media, LLC. All rights reserved.
- * @license MIT License; see LICENSE file for details.
- */
- namespace Grav\Common\Flex\Types\Pages;
- use Grav\Common\Data\Blueprint;
- use Grav\Common\Flex\Traits\FlexGravTrait;
- use Grav\Common\Flex\Traits\FlexObjectTrait;
- use Grav\Common\Grav;
- use Grav\Common\Flex\Types\Pages\Traits\PageContentTrait;
- use Grav\Common\Flex\Types\Pages\Traits\PageLegacyTrait;
- use Grav\Common\Flex\Types\Pages\Traits\PageRoutableTrait;
- use Grav\Common\Flex\Types\Pages\Traits\PageTranslateTrait;
- use Grav\Common\Language\Language;
- use Grav\Common\Page\Interfaces\PageInterface;
- use Grav\Common\Page\Pages;
- use Grav\Common\User\Interfaces\UserInterface;
- use Grav\Common\Utils;
- use Grav\Framework\Filesystem\Filesystem;
- use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
- use Grav\Framework\Flex\Pages\FlexPageObject;
- use Grav\Framework\Object\ObjectCollection;
- use Grav\Framework\Route\Route;
- use Grav\Framework\Route\RouteFactory;
- use Grav\Plugin\Admin\Admin;
- use RocketTheme\Toolbox\Event\Event;
- use RuntimeException;
- use stdClass;
- use function array_key_exists;
- use function count;
- use function func_get_args;
- use function in_array;
- use function is_array;
- /**
- * Class GravPageObject
- * @package Grav\Plugin\FlexObjects\Types\GravPages
- *
- * @property string $name
- * @property string $slug
- * @property string $route
- * @property string $folder
- * @property int|false $order
- * @property string $template
- * @property string $language
- */
- class PageObject extends FlexPageObject
- {
- use FlexGravTrait;
- use FlexObjectTrait;
- use PageContentTrait;
- use PageLegacyTrait;
- use PageRoutableTrait;
- use PageTranslateTrait;
- /** @var string Language code, eg: 'en' */
- protected $language;
- /** @var string File format, eg. 'md' */
- protected $format;
- /** @var bool */
- private $_initialized = false;
- /**
- * @return array
- */
- public static function getCachedMethods(): array
- {
- return [
- 'path' => true,
- 'full_order' => true,
- 'filterBy' => true,
- 'translated' => false,
- ] + parent::getCachedMethods();
- }
- /**
- * @return void
- */
- public function initialize(): void
- {
- if (!$this->_initialized) {
- Grav::instance()->fireEvent('onPageProcessed', new Event(['page' => $this]));
- $this->_initialized = true;
- }
- }
- /**
- * @param string|array $query
- * @return Route|null
- */
- public function getRoute($query = []): ?Route
- {
- $path = $this->route();
- if (null === $path) {
- return null;
- }
- $route = RouteFactory::createFromString($path);
- if ($lang = $route->getLanguage()) {
- $grav = Grav::instance();
- if (!$grav['config']->get('system.languages.include_default_lang')) {
- /** @var Language $language */
- $language = $grav['language'];
- if ($lang === $language->getDefault()) {
- $route = $route->withLanguage('');
- }
- }
- }
- if (is_array($query)) {
- foreach ($query as $key => $value) {
- $route = $route->withQueryParam($key, $value);
- }
- } else {
- $route = $route->withAddedPath($query);
- }
- return $route;
- }
- /**
- * @inheritdoc PageInterface
- */
- 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':
- // TODO: this should not be template!
- return $this->getProperty('template');
- case 'route':
- $filesystem = Filesystem::getInstance(false);
- $key = $filesystem->dirname($this->hasKey() ? '/' . $this->getKey() : '/');
- return $key !== '/' ? $key : null;
- case 'full_route':
- return $this->hasKey() ? '/' . $this->getKey() : '';
- case 'full_order':
- return $this->full_order();
- case 'lang':
- return $this->getLanguage() ?? '';
- case 'translations':
- return $this->getLanguages();
- }
- return parent::getFormValue($name, $default, $separator);
- }
- /**
- * {@inheritdoc}
- * @see FlexObjectInterface::getCacheKey()
- */
- public function getCacheKey(): string
- {
- $cacheKey = parent::getCacheKey();
- if ($cacheKey) {
- /** @var Language $language */
- $language = Grav::instance()['language'];
- $cacheKey .= '_' . $language->getActive();
- }
- return $cacheKey;
- }
- /**
- * @param array $variables
- * @return array
- */
- protected function onBeforeSave(array $variables)
- {
- $reorder = $variables[0] ?? true;
- $meta = $this->getMetaData();
- if (($meta['copy'] ?? false) === true) {
- $this->folder = $this->getKey();
- }
- // Figure out storage path to the new route.
- $parentKey = $this->getProperty('parent_key');
- if ($parentKey !== '') {
- $parentRoute = $this->getProperty('route');
- // Root page cannot be moved.
- if ($this->root()) {
- throw new RuntimeException(sprintf('Root page cannot be moved to %s', $parentRoute));
- }
- // Make sure page isn't being moved under itself.
- $key = $this->getStorageKey();
- /** @var PageObject|null $parent */
- $parent = $parentKey !== false ? $this->getFlexDirectory()->getObject($parentKey, 'storage_key') : null;
- if (!$parent) {
- // Page cannot be moved to non-existing location.
- throw new RuntimeException(sprintf('Page /%s cannot be moved to non-existing path %s', $key, $parentRoute));
- }
- // TODO: make sure that the page doesn't exist yet if moved/copied.
- }
- if ($reorder === true && !$this->root()) {
- $reorder = $this->_reorder;
- }
- // Force automatic reorder if item is supposed to be added to the last.
- if (!is_array($reorder) && (int)$this->order() >= 999999) {
- $reorder = [];
- }
- // Reorder siblings.
- $siblings = is_array($reorder) ? ($this->reorderSiblings($reorder) ?? []) : [];
- $data = $this->prepareStorage();
- unset($data['header']);
- foreach ($siblings as $sibling) {
- $data = $sibling->prepareStorage();
- unset($data['header']);
- }
- return ['reorder' => $reorder, 'siblings' => $siblings];
- }
- /**
- * @param array $variables
- * @return array
- */
- protected function onSave(array $variables): array
- {
- /** @var PageCollection $siblings */
- $siblings = $variables['siblings'];
- /** @var PageObject $sibling */
- foreach ($siblings as $sibling) {
- $sibling->save(false);
- }
- return $variables;
- }
- /**
- * @param array $variables
- */
- protected function onAfterSave(array $variables): void
- {
- $this->getFlexDirectory()->reloadIndex();
- }
- /**
- * @param UserInterface|null $user
- */
- public function check(UserInterface $user = null): void
- {
- parent::check($user);
- if ($user && $this->isMoved()) {
- $parentKey = $this->getProperty('parent_key');
- /** @var PageObject|null $parent */
- $parent = $this->getFlexDirectory()->getObject($parentKey, 'storage_key');
- if (!$parent || !$parent->isAuthorized('create', null, $user)) {
- throw new \RuntimeException('Forbidden', 403);
- }
- }
- }
- /**
- * @param array|bool $reorder
- * @return static
- */
- public function save($reorder = true)
- {
- $variables = $this->onBeforeSave(func_get_args());
- // Backwards compatibility with older plugins.
- $fireEvents = $reorder && $this->isAdminSite() && $this->getFlexDirectory()->getConfig('object.compat.events', true);
- $grav = $this->getContainer();
- if ($fireEvents) {
- $self = $this;
- $grav->fireEvent('onAdminSave', new Event(['type' => 'flex', 'directory' => $this->getFlexDirectory(), 'object' => &$self]));
- if ($self !== $this) {
- throw new RuntimeException('Switching Flex Page object during onAdminSave event is not supported! Please update plugin.');
- }
- }
- /** @var static $instance */
- $instance = parent::save();
- $variables = $this->onSave($variables);
- $this->onAfterSave($variables);
- // Backwards compatibility with older plugins.
- if ($fireEvents) {
- $grav->fireEvent('onAdminAfterSave', new Event(['type' => 'flex', 'directory' => $this->getFlexDirectory(), 'object' => $this]));
- }
- // Reset original after save events have all been called.
- $this->_originalObject = null;
- return $instance;
- }
- /**
- * @return static
- */
- public function delete()
- {
- $result = parent::delete();
- // Backwards compatibility with older plugins.
- $fireEvents = $this->isAdminSite() && $this->getFlexDirectory()->getConfig('object.compat.events', true);
- if ($fireEvents) {
- $this->getContainer()->fireEvent('onAdminAfterDelete', new Event(['object' => $this]));
- }
- return $result;
- }
- /**
- * Prepare move page to new location. Moves also everything that's under the current page.
- *
- * You need to call $this->save() in order to perform the move.
- *
- * @param PageInterface $parent New parent page.
- * @return $this
- */
- public function move(PageInterface $parent)
- {
- if (!$parent instanceof FlexObjectInterface) {
- throw new RuntimeException('Failed: Parent is not Flex Object');
- }
- $this->_reorder = [];
- $this->setProperty('parent_key', $parent->getStorageKey());
- $this->storeOriginal();
- return $this;
- }
- /**
- * @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
- {
- // Special case: creating a new page means checking parent for its permissions.
- if ($action === 'create' && !$this->exists()) {
- $parent = $this->parent();
- if ($parent && method_exists($parent, 'isAuthorized')) {
- return $parent->isAuthorized($action, $scope, $user);
- }
- return false;
- }
- return parent::isAuthorizedOverride($user, $action, $scope, $isMe);
- }
- /**
- * @return bool
- */
- protected function isMoved(): bool
- {
- $storageKey = $this->getMasterKey();
- $filesystem = Filesystem::getInstance(false);
- $oldParentKey = ltrim($filesystem->dirname("/{$storageKey}"), '/');
- $newParentKey = $this->getProperty('parent_key');
- return $this->exists() && $oldParentKey !== $newParentKey;
- }
- /**
- * @param array $ordering
- * @return PageCollection|null
- * @phpstan-return ObjectCollection<string,PageObject>|null
- */
- protected function reorderSiblings(array $ordering)
- {
- $storageKey = $this->getMasterKey();
- $isMoved = $this->isMoved();
- $order = !$isMoved ? $this->order() : false;
- if ($order !== false) {
- $order = (int)$order;
- }
- $parent = $this->parent();
- if (!$parent) {
- throw new RuntimeException('Cannot reorder a page which has no parent');
- }
- /** @var PageCollection $siblings */
- $siblings = $parent->children();
- $siblings = $siblings->getCollection()->withOrdered();
- // Handle special case where ordering isn't given.
- if ($ordering === []) {
- if ($order >= 999999) {
- // Set ordering to point to be the last item, ignoring the object itself.
- $order = 0;
- foreach ($siblings as $sibling) {
- if ($sibling->getKey() !== $this->getKey()) {
- $order = max($order, (int)$sibling->order());
- }
- }
- $this->order($order + 1);
- }
- // Do not change sibling ordering.
- return null;
- }
- $siblings = $siblings->orderBy(['order' => 'ASC']);
- if ($storageKey !== null) {
- if ($order !== false) {
- // Add current page back to the list if it's ordered.
- $siblings->set($storageKey, $this);
- } else {
- // Remove old copy of the current page from the siblings list.
- $siblings->remove($storageKey);
- }
- }
- // Add missing siblings into the end of the list, keeping the previous ordering between them.
- foreach ($siblings as $sibling) {
- $folder = (string)$sibling->getProperty('folder');
- $basename = preg_replace('|^\d+\.|', '', $folder);
- if (!in_array($basename, $ordering, true)) {
- $ordering[] = $basename;
- }
- }
- // Reorder.
- $ordering = array_flip(array_values($ordering));
- $count = count($ordering);
- foreach ($siblings as $sibling) {
- $folder = (string)$sibling->getProperty('folder');
- $basename = preg_replace('|^\d+\.|', '', $folder);
- $newOrder = $ordering[$basename] ?? null;
- $newOrder = null !== $newOrder ? $newOrder + 1 : (int)$sibling->order() + $count;
- $sibling->order($newOrder);
- }
- $siblings = $siblings->orderBy(['order' => 'ASC']);
- $siblings->removeElement($this);
- // If menu item was moved, just make it to be the last in order.
- if ($isMoved && $this->order() !== false) {
- $parentKey = $this->getProperty('parent_key');
- if ($parentKey === '') {
- /** @var PageIndex $index */
- $index = $this->getFlexDirectory()->getIndex();
- $newParent = $index->getRoot();
- } else {
- $newParent = $this->getFlexDirectory()->getObject($parentKey, 'storage_key');
- if (!$newParent instanceof PageInterface) {
- throw new RuntimeException("New parent page '{$parentKey}' not found.");
- }
- }
- /** @var PageCollection $newSiblings */
- $newSiblings = $newParent->children();
- $newSiblings = $newSiblings->getCollection()->withOrdered();
- $order = 0;
- foreach ($newSiblings as $sibling) {
- $order = max($order, (int)$sibling->order());
- }
- $this->order($order + 1);
- }
- return $siblings;
- }
- /**
- * @return string
- */
- public function full_order(): string
- {
- $route = $this->path() . '/' . $this->folder();
- return preg_replace(PageIndex::ORDER_LIST_REGEX, '\\1', $route) ?? $route;
- }
- /**
- * @param string $name
- * @return Blueprint
- */
- protected function doGetBlueprint(string $name = ''): Blueprint
- {
- try {
- // Make sure that pages has been initialized.
- Pages::getTypes();
- // TODO: We need to move raw blueprint logic to Grav itself to remove admin dependency here.
- if ($name === 'raw') {
- // Admin RAW mode.
- if ($this->isAdminSite()) {
- /** @var Admin $admin */
- $admin = Grav::instance()['admin'];
- $template = $this->isModule() ? 'modular_raw' : ($this->root() ? 'root_raw' : 'raw');
- return $admin->blueprints("admin/pages/{$template}");
- }
- }
- $template = $this->getProperty('template') . ($name ? '.' . $name : '');
- $blueprint = $this->getFlexDirectory()->getBlueprint($template, 'blueprints://pages');
- } catch (RuntimeException $e) {
- $template = 'default' . ($name ? '.' . $name : '');
- $blueprint = $this->getFlexDirectory()->getBlueprint($template, 'blueprints://pages');
- }
- $isNew = $blueprint->get('initialized', false) === false;
- if ($isNew === true && $name === '') {
- // Support onBlueprintCreated event just like in Pages::blueprints($template)
- $blueprint->set('initialized', true);
- $blueprint->setFilename($template);
- Grav::instance()->fireEvent('onBlueprintCreated', new Event(['blueprint' => $blueprint, 'type' => $template]));
- }
- return $blueprint;
- }
- /**
- * @param array $options
- * @return array
- */
- public function getLevelListing(array $options): array
- {
- $index = $this->getFlexDirectory()->getIndex();
- if (!is_callable([$index, 'getLevelListing'])) {
- return [];
- }
- // Deal with relative paths.
- $initial = $options['initial'] ?? null;
- $var = $initial ? 'leaf_route' : 'route';
- $route = $options[$var] ?? '';
- if ($route !== '' && !str_starts_with($route, '/')) {
- $filesystem = Filesystem::getInstance();
- $route = "/{$this->getKey()}/{$route}";
- $route = $filesystem->normalize($route);
- $options[$var] = $route;
- }
- [$status, $message, $response,] = $index->getLevelListing($options);
- return [$status, $message, $response, $options[$var] ?? null];
- }
- /**
- * Filter page (true/false) by given filters.
- *
- * - search: string
- * - extension: string
- * - module: bool
- * - visible: bool
- * - routable: bool
- * - published: bool
- * - page: bool
- * - translated: bool
- *
- * @param array $filters
- * @param bool $recursive
- * @return bool
- */
- public function filterBy(array $filters, bool $recursive = false): bool
- {
- $language = $filters['language'] ?? null;
- if (null !== $language) {
- /** @var PageObject $test */
- $test = $this->getTranslation($language) ?? $this;
- } else {
- $test = $this;
- }
- foreach ($filters as $key => $value) {
- switch ($key) {
- case 'search':
- $matches = $test->search((string)$value) > 0.0;
- break;
- case 'page_type':
- $types = $value ? explode(',', $value) : [];
- $matches = in_array($test->template(), $types, true);
- break;
- case 'extension':
- $matches = Utils::contains((string)$value, $test->extension());
- break;
- case 'routable':
- $matches = $test->isRoutable() === (bool)$value;
- break;
- case 'published':
- $matches = $test->isPublished() === (bool)$value;
- break;
- case 'visible':
- $matches = $test->isVisible() === (bool)$value;
- break;
- case 'module':
- $matches = $test->isModule() === (bool)$value;
- break;
- case 'page':
- $matches = $test->isPage() === (bool)$value;
- break;
- case 'folder':
- $matches = $test->isPage() === !$value;
- break;
- case 'translated':
- $matches = $test->hasTranslation() === (bool)$value;
- break;
- default:
- $matches = true;
- break;
- }
- // If current filter does not match, we still may have match as a parent.
- if ($matches === false) {
- if (!$recursive) {
- return false;
- }
- /** @var PageIndex $index */
- $index = $this->children()->getIndex();
- return $index->filterBy($filters, true)->count() > 0;
- }
- }
- return true;
- }
- /**
- * {@inheritdoc}
- * @see FlexObjectInterface::exists()
- */
- public function exists(): bool
- {
- return $this->root ?: parent::exists();
- }
- /**
- * @return array
- */
- public function __debugInfo(): array
- {
- $list = parent::__debugInfo();
- return $list + [
- '_content_meta:private' => $this->getContentMeta(),
- '_content:private' => $this->getRawContent()
- ];
- }
- /**
- * @param array $elements
- * @param bool $extended
- */
- protected function filterElements(array &$elements, bool $extended = false): void
- {
- // Change parent page if needed.
- if (array_key_exists('route', $elements) && isset($elements['folder'], $elements['name'])) {
- $elements['template'] = $elements['name'];
- // Figure out storage path to the new route.
- $parentKey = trim($elements['route'] ?? '', '/');
- if ($parentKey !== '') {
- /** @var PageObject|null $parent */
- $parent = $this->getFlexDirectory()->getObject($parentKey);
- $parentKey = $parent ? $parent->getStorageKey() : $parentKey;
- }
- $elements['parent_key'] = $parentKey;
- }
- // Deal with ordering=bool and order=page1,page2,page3.
- if ($this->root()) {
- // Root page doesn't have ordering.
- unset($elements['ordering'], $elements['order']);
- } elseif (array_key_exists('ordering', $elements) && array_key_exists('order', $elements)) {
- // Store ordering.
- $ordering = $elements['order'] ?? null;
- $this->_reorder = !empty($ordering) ? explode(',', $ordering) : [];
- $order = false;
- if ((bool)($elements['ordering'] ?? false)) {
- $order = $this->order();
- if ($order === false) {
- $order = 999999;
- }
- }
- $elements['order'] = $order;
- }
- parent::filterElements($elements, true);
- }
- /**
- * @return array
- */
- public function prepareStorage(): array
- {
- $meta = $this->getMetaData();
- $oldLang = $meta['lang'] ?? '';
- $newLang = $this->getProperty('lang') ?? '';
- // Always clone the page to the new language.
- if ($oldLang !== $newLang) {
- $meta['clone'] = true;
- }
- // Make sure that certain elements are always sent to the storage layer.
- $elements = [
- '__META' => $meta,
- 'storage_key' => $this->getStorageKey(),
- 'parent_key' => $this->getProperty('parent_key'),
- 'order' => $this->getProperty('order'),
- 'folder' => preg_replace('|^\d+\.|', '', $this->getProperty('folder') ?? ''),
- 'template' => preg_replace('|modular/|', '', $this->getProperty('template') ?? ''),
- 'lang' => $newLang
- ] + parent::prepareStorage();
- return $elements;
- }
- }
|