PageIndex.php 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @package Grav\Common\Flex
  5. *
  6. * @copyright Copyright (c) 2015 - 2022 Trilby Media, LLC. All rights reserved.
  7. * @license MIT License; see LICENSE file for details.
  8. */
  9. namespace Grav\Common\Flex\Types\Pages;
  10. use Exception;
  11. use Grav\Common\Debugger;
  12. use Grav\Common\File\CompiledJsonFile;
  13. use Grav\Common\File\CompiledYamlFile;
  14. use Grav\Common\Flex\Traits\FlexGravTrait;
  15. use Grav\Common\Flex\Traits\FlexIndexTrait;
  16. use Grav\Common\Grav;
  17. use Grav\Common\Language\Language;
  18. use Grav\Common\Page\Header;
  19. use Grav\Common\Page\Interfaces\PageCollectionInterface;
  20. use Grav\Common\Page\Interfaces\PageInterface;
  21. use Grav\Common\User\Interfaces\UserInterface;
  22. use Grav\Common\Utils;
  23. use Grav\Framework\Flex\FlexDirectory;
  24. use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
  25. use Grav\Framework\Flex\Pages\FlexPageIndex;
  26. use InvalidArgumentException;
  27. use RuntimeException;
  28. use function array_slice;
  29. use function count;
  30. use function in_array;
  31. use function is_array;
  32. use function is_string;
  33. /**
  34. * Class GravPageObject
  35. * @package Grav\Plugin\FlexObjects\Types\GravPages
  36. *
  37. * @template T of PageObject
  38. * @template C of PageCollection
  39. * @extends FlexPageIndex<T,C>
  40. * @implements PageCollectionInterface<string,T>
  41. *
  42. * @method PageIndex withModules(bool $bool = true)
  43. * @method PageIndex withPages(bool $bool = true)
  44. * @method PageIndex withTranslation(bool $bool = true, string $languageCode = null, bool $fallback = null)
  45. */
  46. class PageIndex extends FlexPageIndex implements PageCollectionInterface
  47. {
  48. use FlexGravTrait;
  49. use FlexIndexTrait;
  50. public const VERSION = parent::VERSION . '.5';
  51. public const ORDER_LIST_REGEX = '/(\/\d+)\.[^\/]+/u';
  52. public const PAGE_ROUTE_REGEX = '/\/\d+\./u';
  53. /** @var PageObject|array */
  54. protected $_root;
  55. /** @var array|null */
  56. protected $_params;
  57. /**
  58. * @param array $entries
  59. * @param FlexDirectory|null $directory
  60. */
  61. public function __construct(array $entries = [], FlexDirectory $directory = null)
  62. {
  63. // Remove root if it's taken.
  64. if (isset($entries[''])) {
  65. $this->_root = $entries[''];
  66. unset($entries['']);
  67. }
  68. parent::__construct($entries, $directory);
  69. }
  70. /**
  71. * @param FlexStorageInterface $storage
  72. * @return array
  73. */
  74. public static function loadEntriesFromStorage(FlexStorageInterface $storage): array
  75. {
  76. // Load saved index.
  77. $index = static::loadIndex($storage);
  78. $version = $index['version'] ?? 0;
  79. $force = static::VERSION !== $version;
  80. // TODO: Following check flex index to be out of sync after some saves, disabled until better solution is found.
  81. //$timestamp = $index['timestamp'] ?? 0;
  82. //if (!$force && $timestamp && $timestamp > time() - 1) {
  83. // return $index['index'];
  84. //}
  85. // Load up to date index.
  86. $entries = parent::loadEntriesFromStorage($storage);
  87. return static::updateIndexFile($storage, $index['index'], $entries, ['include_missing' => true, 'force_update' => $force]);
  88. }
  89. /**
  90. * @param string $key
  91. * @return PageObject|null
  92. * @phpstan-return T|null
  93. */
  94. public function get($key)
  95. {
  96. if (mb_strpos($key, '|') !== false) {
  97. [$key, $params] = explode('|', $key, 2);
  98. }
  99. $element = parent::get($key);
  100. if (null === $element) {
  101. return null;
  102. }
  103. if (isset($params)) {
  104. $element = $element->getTranslation(ltrim($params, '.'));
  105. }
  106. \assert(null === $element || $element instanceof PageObject);
  107. return $element;
  108. }
  109. /**
  110. * @return PageInterface
  111. */
  112. public function getRoot()
  113. {
  114. $root = $this->_root;
  115. if (is_array($root)) {
  116. $directory = $this->getFlexDirectory();
  117. $storage = $directory->getStorage();
  118. $defaults = [
  119. 'header' => [
  120. 'routable' => false,
  121. 'permissions' => [
  122. 'inherit' => false
  123. ]
  124. ]
  125. ];
  126. $row = $storage->readRows(['' => null])[''] ?? null;
  127. if (null !== $row) {
  128. if (isset($row['__ERROR'])) {
  129. /** @var Debugger $debugger */
  130. $debugger = Grav::instance()['debugger'];
  131. $message = sprintf('Flex Pages: root page is broken in storage: %s', $row['__ERROR']);
  132. $debugger->addException(new RuntimeException($message));
  133. $debugger->addMessage($message, 'error');
  134. $row = ['__META' => $root];
  135. }
  136. } else {
  137. $row = ['__META' => $root];
  138. }
  139. $row = array_merge_recursive($defaults, $row);
  140. /** @var PageObject $root */
  141. $root = $this->getFlexDirectory()->createObject($row, '/', false);
  142. $root->name('root.md');
  143. $root->root(true);
  144. $this->_root = $root;
  145. }
  146. return $root;
  147. }
  148. /**
  149. * @param string|null $languageCode
  150. * @param bool|null $fallback
  151. * @return static
  152. * @phpstan-return static<T,C>
  153. */
  154. public function withTranslated(string $languageCode = null, bool $fallback = null)
  155. {
  156. if (null === $languageCode) {
  157. return $this;
  158. }
  159. $entries = $this->translateEntries($this->getEntries(), $languageCode, $fallback);
  160. $params = ['language' => $languageCode, 'language_fallback' => $fallback] + $this->getParams();
  161. return $this->createFrom($entries)->setParams($params);
  162. }
  163. /**
  164. * @return string|null
  165. */
  166. public function getLanguage(): ?string
  167. {
  168. return $this->_params['language'] ?? null;
  169. }
  170. /**
  171. * Get the collection params
  172. *
  173. * @return array
  174. */
  175. public function getParams(): array
  176. {
  177. return $this->_params ?? [];
  178. }
  179. /**
  180. * Get the collection param
  181. *
  182. * @param string $name
  183. * @return mixed
  184. */
  185. public function getParam(string $name)
  186. {
  187. return $this->_params[$name] ?? null;
  188. }
  189. /**
  190. * Set parameters to the Collection
  191. *
  192. * @param array $params
  193. * @return $this
  194. */
  195. public function setParams(array $params)
  196. {
  197. $this->_params = $this->_params ? array_merge($this->_params, $params) : $params;
  198. return $this;
  199. }
  200. /**
  201. * Set a parameter to the Collection
  202. *
  203. * @param string $name
  204. * @param mixed $value
  205. * @return $this
  206. */
  207. public function setParam(string $name, $value)
  208. {
  209. $this->_params[$name] = $value;
  210. return $this;
  211. }
  212. /**
  213. * Get the collection params
  214. *
  215. * @return array
  216. */
  217. public function params(): array
  218. {
  219. return $this->getParams();
  220. }
  221. /**
  222. * {@inheritdoc}
  223. * @see FlexCollectionInterface::getCacheKey()
  224. */
  225. public function getCacheKey(): string
  226. {
  227. return $this->getTypePrefix() . $this->getFlexType() . '.' . sha1(json_encode($this->getKeys()) . $this->getKeyField() . $this->getLanguage());
  228. }
  229. /**
  230. * Filter pages by given filters.
  231. *
  232. * - search: string
  233. * - page_type: string|string[]
  234. * - modular: bool
  235. * - visible: bool
  236. * - routable: bool
  237. * - published: bool
  238. * - page: bool
  239. * - translated: bool
  240. *
  241. * @param array $filters
  242. * @param bool $recursive
  243. * @return static
  244. * @phpstan-return static<T,C>
  245. */
  246. public function filterBy(array $filters, bool $recursive = false)
  247. {
  248. if (!$filters) {
  249. return $this;
  250. }
  251. if ($recursive) {
  252. return $this->__call('filterBy', [$filters, true]);
  253. }
  254. $list = [];
  255. $index = $this;
  256. foreach ($filters as $key => $value) {
  257. switch ($key) {
  258. case 'search':
  259. $index = $index->search((string)$value);
  260. break;
  261. case 'page_type':
  262. if (!is_array($value)) {
  263. $value = is_string($value) && $value !== '' ? explode(',', $value) : [];
  264. }
  265. $index = $index->ofOneOfTheseTypes($value);
  266. break;
  267. case 'routable':
  268. $index = $index->withRoutable((bool)$value);
  269. break;
  270. case 'published':
  271. $index = $index->withPublished((bool)$value);
  272. break;
  273. case 'visible':
  274. $index = $index->withVisible((bool)$value);
  275. break;
  276. case 'module':
  277. $index = $index->withModules((bool)$value);
  278. break;
  279. case 'page':
  280. $index = $index->withPages((bool)$value);
  281. break;
  282. case 'folder':
  283. $index = $index->withPages(!$value);
  284. break;
  285. case 'translated':
  286. $index = $index->withTranslation((bool)$value);
  287. break;
  288. default:
  289. $list[$key] = $value;
  290. }
  291. }
  292. return $list ? $index->filterByParent($list) : $index;
  293. }
  294. /**
  295. * @param array $filters
  296. * @return static
  297. * @phpstan-return static<T,C>
  298. */
  299. protected function filterByParent(array $filters)
  300. {
  301. /** @var static $index */
  302. $index = parent::filterBy($filters);
  303. return $index;
  304. }
  305. /**
  306. * @param array $options
  307. * @return array
  308. */
  309. public function getLevelListing(array $options): array
  310. {
  311. // Undocumented B/C
  312. $order = $options['order'] ?? 'asc';
  313. if ($order === SORT_ASC) {
  314. $options['order'] = 'asc';
  315. } elseif ($order === SORT_DESC) {
  316. $options['order'] = 'desc';
  317. }
  318. $options += [
  319. 'field' => null,
  320. 'route' => null,
  321. 'leaf_route' => null,
  322. 'sortby' => null,
  323. 'order' => 'asc',
  324. 'lang' => null,
  325. 'filters' => [],
  326. ];
  327. $options['filters'] += [
  328. 'type' => ['root', 'dir'],
  329. ];
  330. $key = 'page.idx.lev.' . sha1(json_encode($options, JSON_THROW_ON_ERROR) . $this->getCacheKey());
  331. $checksum = $this->getCacheChecksum();
  332. $cache = $this->getCache('object');
  333. /** @var Debugger $debugger */
  334. $debugger = Grav::instance()['debugger'];
  335. $result = null;
  336. try {
  337. $cached = $cache->get($key);
  338. $test = $cached[0] ?? null;
  339. $result = $test === $checksum ? ($cached[1] ?? null) : null;
  340. } catch (\Psr\SimpleCache\InvalidArgumentException $e) {
  341. $debugger->addException($e);
  342. }
  343. try {
  344. if (null === $result) {
  345. $result = $this->getLevelListingRecurse($options);
  346. $cache->set($key, [$checksum, $result]);
  347. }
  348. } catch (\Psr\SimpleCache\InvalidArgumentException $e) {
  349. $debugger->addException($e);
  350. }
  351. return $result;
  352. }
  353. /**
  354. * @param array $entries
  355. * @param string|null $keyField
  356. * @return static
  357. * @phpstan-return static<T,C>
  358. */
  359. protected function createFrom(array $entries, string $keyField = null)
  360. {
  361. /** @var static $index */
  362. $index = parent::createFrom($entries, $keyField);
  363. $index->_root = $this->getRoot();
  364. return $index;
  365. }
  366. /**
  367. * @param array $entries
  368. * @param string $lang
  369. * @param bool|null $fallback
  370. * @return array
  371. */
  372. protected function translateEntries(array $entries, string $lang, bool $fallback = null): array
  373. {
  374. $languages = $this->getFallbackLanguages($lang, $fallback);
  375. foreach ($entries as $key => &$entry) {
  376. // Find out which version of the page we should load.
  377. $translations = $this->getLanguageTemplates((string)$key);
  378. if (!$translations) {
  379. // No translations found, is this a folder?
  380. continue;
  381. }
  382. // Find a translation.
  383. $template = null;
  384. foreach ($languages as $code) {
  385. if (isset($translations[$code])) {
  386. $template = $translations[$code];
  387. break;
  388. }
  389. }
  390. // We couldn't find a translation, remove entry from the list.
  391. if (!isset($code, $template)) {
  392. unset($entries['key']);
  393. continue;
  394. }
  395. // Get the main key without template and language.
  396. [$main_key,] = explode('|', $entry['storage_key'] . '|', 2);
  397. // Update storage key and language.
  398. $entry['storage_key'] = $main_key . '|' . $template . '.' . $code;
  399. $entry['lang'] = $code;
  400. }
  401. unset($entry);
  402. return $entries;
  403. }
  404. /**
  405. * @return array
  406. */
  407. protected function getLanguageTemplates(string $key): array
  408. {
  409. $meta = $this->getMetaData($key);
  410. $template = $meta['template'] ?? 'folder';
  411. $translations = $meta['markdown'] ?? [];
  412. $list = [];
  413. foreach ($translations as $code => $search) {
  414. if (isset($search[$template])) {
  415. // Use main template if possible.
  416. $list[$code] = $template;
  417. } elseif (!empty($search)) {
  418. // Fall back to first matching template.
  419. $list[$code] = key($search);
  420. }
  421. }
  422. return $list;
  423. }
  424. /**
  425. * @param string|null $languageCode
  426. * @param bool|null $fallback
  427. * @return array
  428. */
  429. protected function getFallbackLanguages(string $languageCode = null, bool $fallback = null): array
  430. {
  431. $fallback = $fallback ?? true;
  432. if (!$fallback && null !== $languageCode) {
  433. return [$languageCode];
  434. }
  435. $grav = Grav::instance();
  436. /** @var Language $language */
  437. $language = $grav['language'];
  438. $languageCode = $languageCode ?? '';
  439. if ($languageCode === '' && $fallback) {
  440. return $language->getFallbackLanguages(null, true);
  441. }
  442. return $fallback ? $language->getFallbackLanguages($languageCode, true) : [$languageCode];
  443. }
  444. /**
  445. * @param array $options
  446. * @return array
  447. */
  448. protected function getLevelListingRecurse(array $options): array
  449. {
  450. $filters = $options['filters'] ?? [];
  451. $field = $options['field'];
  452. $route = $options['route'];
  453. $leaf_route = $options['leaf_route'];
  454. $sortby = $options['sortby'];
  455. $order = $options['order'];
  456. $language = $options['lang'];
  457. $status = 'error';
  458. $response = [];
  459. $extra = null;
  460. // Handle leaf_route
  461. $leaf = null;
  462. if ($leaf_route && $route !== $leaf_route) {
  463. $nodes = explode('/', $leaf_route);
  464. $sub_route = '/' . implode('/', array_slice($nodes, 1, $options['level']++));
  465. $options['route'] = $sub_route;
  466. [$status,,$leaf,$extra] = $this->getLevelListingRecurse($options);
  467. }
  468. // Handle no route, assume page tree root
  469. if (!$route) {
  470. $page = $this->getRoot();
  471. } else {
  472. $page = $this->get(trim($route, '/'));
  473. }
  474. $path = $page ? $page->path() : null;
  475. if ($field) {
  476. // Get forced filters from the field.
  477. $blueprint = $page ? $page->getBlueprint() : $this->getFlexDirectory()->getBlueprint();
  478. $settings = $blueprint->schema()->getProperty($field);
  479. $filters = array_merge([], $filters, $settings['filters'] ?? []);
  480. }
  481. // Clean up filter.
  482. $filter_type = (array)($filters['type'] ?? []);
  483. unset($filters['type']);
  484. $filters = array_filter($filters, static function($val) { return $val !== null && $val !== ''; });
  485. if ($page) {
  486. $status = 'success';
  487. $msg = 'PLUGIN_ADMIN.PAGE_ROUTE_FOUND';
  488. if ($page->root() && (!$filter_type || in_array('root', $filter_type, true))) {
  489. if ($field) {
  490. $response[] = [
  491. 'name' => '<root>',
  492. 'value' => '/',
  493. 'item-key' => '',
  494. 'filename' => '.',
  495. 'extension' => '',
  496. 'type' => 'root',
  497. 'modified' => $page->modified(),
  498. 'size' => 0,
  499. 'symlink' => false,
  500. 'has-children' => false
  501. ];
  502. } else {
  503. $response[] = [
  504. 'item-key' => '-root-',
  505. 'icon' => 'root',
  506. 'title' => 'Root', // FIXME
  507. 'route' => [
  508. 'display' => '&lt;root&gt;', // FIXME
  509. 'raw' => '_root',
  510. ],
  511. 'modified' => $page->modified(),
  512. 'extras' => [
  513. 'template' => $page->template(),
  514. //'lang' => null,
  515. //'translated' => null,
  516. 'langs' => [],
  517. 'published' => false,
  518. 'visible' => false,
  519. 'routable' => false,
  520. 'tags' => ['root', 'non-routable'],
  521. 'actions' => ['edit'], // FIXME
  522. ]
  523. ];
  524. }
  525. }
  526. /** @var PageCollection|PageIndex $children */
  527. $children = $page->children();
  528. /** @var PageIndex $children */
  529. $children = $children->getIndex();
  530. $selectedChildren = $children->filterBy($filters + ['language' => $language], true);
  531. /** @var Header $header */
  532. $header = $page->header();
  533. if (!$field && $header->get('admin.children_display_order', 'collection') === 'collection' && ($orderby = $header->get('content.order.by'))) {
  534. // Use custom sorting by page header.
  535. $sortby = $orderby;
  536. $order = $header->get('content.order.dir', $order);
  537. $custom = $header->get('content.order.custom');
  538. }
  539. if ($sortby) {
  540. // Sort children.
  541. $selectedChildren = $selectedChildren->order($sortby, $order, $custom ?? null);
  542. }
  543. /** @var UserInterface|null $user */
  544. $user = Grav::instance()['user'] ?? null;
  545. /** @var PageObject $child */
  546. foreach ($selectedChildren as $child) {
  547. $selected = $child->path() === $extra;
  548. $includeChildren = is_array($leaf) && !empty($leaf) && $selected;
  549. if ($field) {
  550. $child_count = count($child->children());
  551. $payload = [
  552. 'name' => $child->menu(),
  553. 'value' => $child->rawRoute(),
  554. 'item-key' => Utils::basename($child->rawRoute() ?? ''),
  555. 'filename' => $child->folder(),
  556. 'extension' => $child->extension(),
  557. 'type' => 'dir',
  558. 'modified' => $child->modified(),
  559. 'size' => $child_count,
  560. 'symlink' => false,
  561. 'has-children' => $child_count > 0
  562. ];
  563. } else {
  564. $lang = $child->findTranslation($language) ?? 'n/a';
  565. /** @var PageObject $child */
  566. $child = $child->getTranslation($language) ?? $child;
  567. // TODO: all these features are independent from each other, we cannot just have one icon/color to catch all.
  568. // TODO: maybe icon by home/modular/page/folder (or even from blueprints) and color by visibility etc..
  569. if ($child->home()) {
  570. $icon = 'home';
  571. } elseif ($child->isModule()) {
  572. $icon = 'modular';
  573. } elseif ($child->visible()) {
  574. $icon = 'visible';
  575. } elseif ($child->isPage()) {
  576. $icon = 'page';
  577. } else {
  578. // TODO: add support
  579. $icon = 'folder';
  580. }
  581. $tags = [
  582. $child->published() ? 'published' : 'non-published',
  583. $child->visible() ? 'visible' : 'non-visible',
  584. $child->routable() ? 'routable' : 'non-routable'
  585. ];
  586. $extras = [
  587. 'template' => $child->template(),
  588. 'lang' => $lang ?: null,
  589. 'translated' => $lang ? $child->hasTranslation($language, false) : null,
  590. 'langs' => $child->getAllLanguages(true) ?: null,
  591. 'published' => $child->published(),
  592. 'published_date' => $this->jsDate($child->publishDate()),
  593. 'unpublished_date' => $this->jsDate($child->unpublishDate()),
  594. 'visible' => $child->visible(),
  595. 'routable' => $child->routable(),
  596. 'tags' => $tags,
  597. 'actions' => $this->getListingActions($child, $user),
  598. ];
  599. $extras = array_filter($extras, static function ($v) {
  600. return $v !== null;
  601. });
  602. /** @var PageIndex $tmp */
  603. $tmp = $child->children()->getIndex();
  604. $child_count = $tmp->count();
  605. $count = $filters ? $tmp->filterBy($filters, true)->count() : null;
  606. $route = $child->getRoute();
  607. $route = $route ? ($route->toString(false) ?: '/') : '';
  608. $payload = [
  609. 'item-key' => htmlspecialchars(Utils::basename($child->rawRoute() ?? $child->getKey())),
  610. 'icon' => $icon,
  611. 'title' => htmlspecialchars($child->menu()),
  612. 'route' => [
  613. 'display' => htmlspecialchars($route) ?: null,
  614. 'raw' => htmlspecialchars($child->rawRoute()),
  615. ],
  616. 'modified' => $this->jsDate($child->modified()),
  617. 'child_count' => $child_count ?: null,
  618. 'count' => $count ?? null,
  619. 'filters_hit' => $filters ? ($child->filterBy($filters, false) ?: null) : null,
  620. 'extras' => $extras
  621. ];
  622. $payload = array_filter($payload, static function ($v) {
  623. return $v !== null;
  624. });
  625. }
  626. // Add children if any
  627. if ($includeChildren) {
  628. $payload['children'] = array_values($leaf);
  629. }
  630. $response[] = $payload;
  631. }
  632. } else {
  633. $msg = 'PLUGIN_ADMIN.PAGE_ROUTE_NOT_FOUND';
  634. }
  635. if ($field) {
  636. $temp_array = [];
  637. foreach ($response as $index => $item) {
  638. $temp_array[$item['type']][$index] = $item;
  639. }
  640. $sorted = Utils::sortArrayByArray($temp_array, $filter_type);
  641. $response = Utils::arrayFlatten($sorted);
  642. }
  643. return [$status, $msg, $response, $path];
  644. }
  645. /**
  646. * @param PageObject $object
  647. * @param UserInterface $user
  648. * @return array
  649. */
  650. protected function getListingActions(PageObject $object, UserInterface $user): array
  651. {
  652. $actions = [];
  653. if ($object->isAuthorized('read', null, $user)) {
  654. $actions[] = 'preview';
  655. $actions[] = 'edit';
  656. }
  657. if ($object->isAuthorized('update', null, $user)) {
  658. $actions[] = 'copy';
  659. $actions[] = 'move';
  660. }
  661. if ($object->isAuthorized('delete', null, $user)) {
  662. $actions[] = 'delete';
  663. }
  664. return $actions;
  665. }
  666. /**
  667. * @param FlexStorageInterface $storage
  668. * @return CompiledJsonFile|CompiledYamlFile|null
  669. */
  670. protected static function getIndexFile(FlexStorageInterface $storage)
  671. {
  672. if (!method_exists($storage, 'isIndexed') || !$storage->isIndexed()) {
  673. return null;
  674. }
  675. // Load saved index file.
  676. $grav = Grav::instance();
  677. $locator = $grav['locator'];
  678. $filename = $locator->findResource('user-data://flex/indexes/pages.json', true, true);
  679. return CompiledJsonFile::instance($filename);
  680. }
  681. /**
  682. * @param int|null $timestamp
  683. * @return string|null
  684. */
  685. private function jsDate(int $timestamp = null): ?string
  686. {
  687. if (!$timestamp) {
  688. return null;
  689. }
  690. $config = Grav::instance()['config'];
  691. $dateFormat = $config->get('system.pages.dateformat.long');
  692. return date($dateFormat, $timestamp) ?: null;
  693. }
  694. /**
  695. * Add a single page to a collection
  696. *
  697. * @param PageInterface $page
  698. * @return PageCollection
  699. * @phpstan-return C
  700. */
  701. public function addPage(PageInterface $page)
  702. {
  703. return $this->getCollection()->addPage($page);
  704. }
  705. /**
  706. *
  707. * Create a copy of this collection
  708. *
  709. * @return static
  710. * @phpstan-return static<T,C>
  711. */
  712. public function copy()
  713. {
  714. return clone $this;
  715. }
  716. /**
  717. *
  718. * Merge another collection with the current collection
  719. *
  720. * @param PageCollectionInterface $collection
  721. * @return PageCollection
  722. * @phpstan-return C
  723. */
  724. public function merge(PageCollectionInterface $collection)
  725. {
  726. return $this->getCollection()->merge($collection);
  727. }
  728. /**
  729. * Intersect another collection with the current collection
  730. *
  731. * @param PageCollectionInterface $collection
  732. * @return PageCollection
  733. * @phpstan-return C
  734. */
  735. public function intersect(PageCollectionInterface $collection)
  736. {
  737. return $this->getCollection()->intersect($collection);
  738. }
  739. /**
  740. * Split collection into array of smaller collections.
  741. *
  742. * @param int $size
  743. * @return PageCollection[]
  744. * @phpstan-return C[]
  745. */
  746. public function batch($size)
  747. {
  748. return $this->getCollection()->batch($size);
  749. }
  750. /**
  751. * Remove item from the list.
  752. *
  753. * @param string $key
  754. * @return PageObject|null
  755. * @phpstan-return T|null
  756. * @throws InvalidArgumentException
  757. */
  758. public function remove($key)
  759. {
  760. return $this->getCollection()->remove($key);
  761. }
  762. /**
  763. * Reorder collection.
  764. *
  765. * @param string $by
  766. * @param string $dir
  767. * @param array $manual
  768. * @param string $sort_flags
  769. * @return static
  770. * @phpstan-return static<T,C>
  771. */
  772. public function order($by, $dir = 'asc', $manual = null, $sort_flags = null)
  773. {
  774. /** @var PageCollectionInterface $collection */
  775. $collection = $this->__call('order', [$by, $dir, $manual, $sort_flags]);
  776. return $collection;
  777. }
  778. /**
  779. * Check to see if this item is the first in the collection.
  780. *
  781. * @param string $path
  782. * @return bool True if item is first.
  783. */
  784. public function isFirst($path): bool
  785. {
  786. /** @var bool $result */
  787. $result = $this->__call('isFirst', [$path]);
  788. return $result;
  789. }
  790. /**
  791. * Check to see if this item is the last in the collection.
  792. *
  793. * @param string $path
  794. * @return bool True if item is last.
  795. */
  796. public function isLast($path): bool
  797. {
  798. /** @var bool $result */
  799. $result = $this->__call('isLast', [$path]);
  800. return $result;
  801. }
  802. /**
  803. * Gets the previous sibling based on current position.
  804. *
  805. * @param string $path
  806. * @return PageObject|null The previous item.
  807. * @phpstan-return T|null
  808. */
  809. public function prevSibling($path)
  810. {
  811. /** @var PageObject|null $result */
  812. $result = $this->__call('prevSibling', [$path]);
  813. return $result;
  814. }
  815. /**
  816. * Gets the next sibling based on current position.
  817. *
  818. * @param string $path
  819. * @return PageObject|null The next item.
  820. * @phpstan-return T|null
  821. */
  822. public function nextSibling($path)
  823. {
  824. /** @var PageObject|null $result */
  825. $result = $this->__call('nextSibling', [$path]);
  826. return $result;
  827. }
  828. /**
  829. * Returns the adjacent sibling based on a direction.
  830. *
  831. * @param string $path
  832. * @param int $direction either -1 or +1
  833. * @return PageObject|false The sibling item.
  834. * @phpstan-return T|false
  835. */
  836. public function adjacentSibling($path, $direction = 1)
  837. {
  838. /** @var PageObject|false $result */
  839. $result = $this->__call('adjacentSibling', [$path, $direction]);
  840. return $result;
  841. }
  842. /**
  843. * Returns the item in the current position.
  844. *
  845. * @param string $path the path the item
  846. * @return int|null The index of the current page, null if not found.
  847. */
  848. public function currentPosition($path): ?int
  849. {
  850. /** @var int|null $result */
  851. $result = $this->__call('currentPosition', [$path]);
  852. return $result;
  853. }
  854. /**
  855. * Returns the items between a set of date ranges of either the page date field (default) or
  856. * an arbitrary datetime page field where start date and end date are optional
  857. * Dates must be passed in as text that strtotime() can process
  858. * http://php.net/manual/en/function.strtotime.php
  859. *
  860. * @param string|null $startDate
  861. * @param string|null $endDate
  862. * @param string|null $field
  863. * @return static
  864. * @phpstan-return static<T,C>
  865. * @throws Exception
  866. */
  867. public function dateRange($startDate = null, $endDate = null, $field = null)
  868. {
  869. $collection = $this->__call('dateRange', [$startDate, $endDate, $field]);
  870. return $collection;
  871. }
  872. /**
  873. * Mimicks Pages class.
  874. *
  875. * @return $this
  876. * @deprecated 1.7 Not needed anymore in Flex Pages (does nothing).
  877. */
  878. public function all()
  879. {
  880. return $this;
  881. }
  882. /**
  883. * Creates new collection with only visible pages
  884. *
  885. * @return static The collection with only visible pages
  886. * @phpstan-return static<T,C>
  887. */
  888. public function visible()
  889. {
  890. $collection = $this->__call('visible', []);
  891. return $collection;
  892. }
  893. /**
  894. * Creates new collection with only non-visible pages
  895. *
  896. * @return static The collection with only non-visible pages
  897. * @phpstan-return static<T,C>
  898. */
  899. public function nonVisible()
  900. {
  901. $collection = $this->__call('nonVisible', []);
  902. return $collection;
  903. }
  904. /**
  905. * Creates new collection with only non-modular pages
  906. *
  907. * @return static The collection with only non-modular pages
  908. * @phpstan-return static<T,C>
  909. */
  910. public function pages()
  911. {
  912. $collection = $this->__call('pages', []);
  913. return $collection;
  914. }
  915. /**
  916. * Creates new collection with only modular pages
  917. *
  918. * @return static The collection with only modular pages
  919. * @phpstan-return static<T,C>
  920. */
  921. public function modules()
  922. {
  923. $collection = $this->__call('modules', []);
  924. return $collection;
  925. }
  926. /**
  927. * Creates new collection with only modular pages
  928. *
  929. * @return static The collection with only modular pages
  930. * @phpstan-return static<T,C>
  931. */
  932. public function modular()
  933. {
  934. return $this->modules();
  935. }
  936. /**
  937. * Creates new collection with only non-modular pages
  938. *
  939. * @return static The collection with only non-modular pages
  940. * @phpstan-return static<T,C>
  941. */
  942. public function nonModular()
  943. {
  944. return $this->pages();
  945. }
  946. /**
  947. * Creates new collection with only published pages
  948. *
  949. * @return static The collection with only published pages
  950. * @phpstan-return static<T,C>
  951. */
  952. public function published()
  953. {
  954. $collection = $this->__call('published', []);
  955. return $collection;
  956. }
  957. /**
  958. * Creates new collection with only non-published pages
  959. *
  960. * @return static The collection with only non-published pages
  961. * @phpstan-return static<T,C>
  962. */
  963. public function nonPublished()
  964. {
  965. $collection = $this->__call('nonPublished', []);
  966. return $collection;
  967. }
  968. /**
  969. * Creates new collection with only routable pages
  970. *
  971. * @return static The collection with only routable pages
  972. * @phpstan-return static<T,C>
  973. */
  974. public function routable()
  975. {
  976. $collection = $this->__call('routable', []);
  977. return $collection;
  978. }
  979. /**
  980. * Creates new collection with only non-routable pages
  981. *
  982. * @return static The collection with only non-routable pages
  983. * @phpstan-return static<T,C>
  984. */
  985. public function nonRoutable()
  986. {
  987. $collection = $this->__call('nonRoutable', []);
  988. return $collection;
  989. }
  990. /**
  991. * Creates new collection with only pages of the specified type
  992. *
  993. * @param string $type
  994. * @return static The collection
  995. * @phpstan-return static<T,C>
  996. */
  997. public function ofType($type)
  998. {
  999. $collection = $this->__call('ofType', []);
  1000. return $collection;
  1001. }
  1002. /**
  1003. * Creates new collection with only pages of one of the specified types
  1004. *
  1005. * @param string[] $types
  1006. * @return static The collection
  1007. * @phpstan-return static<T,C>
  1008. */
  1009. public function ofOneOfTheseTypes($types)
  1010. {
  1011. $collection = $this->__call('ofOneOfTheseTypes', []);
  1012. return $collection;
  1013. }
  1014. /**
  1015. * Creates new collection with only pages of one of the specified access levels
  1016. *
  1017. * @param array $accessLevels
  1018. * @return static The collection
  1019. * @phpstan-return static<T,C>
  1020. */
  1021. public function ofOneOfTheseAccessLevels($accessLevels)
  1022. {
  1023. $collection = $this->__call('ofOneOfTheseAccessLevels', []);
  1024. return $collection;
  1025. }
  1026. /**
  1027. * Converts collection into an array.
  1028. *
  1029. * @return array
  1030. */
  1031. public function toArray()
  1032. {
  1033. return $this->getCollection()->toArray();
  1034. }
  1035. /**
  1036. * Get the extended version of this Collection with each page keyed by route
  1037. *
  1038. * @return array
  1039. * @throws Exception
  1040. */
  1041. public function toExtendedArray()
  1042. {
  1043. return $this->getCollection()->toExtendedArray();
  1044. }
  1045. }