PageIndex.php 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201
  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 langauge.
  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. $msg = null;
  459. $response = [];
  460. $children = null;
  461. $sub_route = null;
  462. $extra = null;
  463. // Handle leaf_route
  464. $leaf = null;
  465. if ($leaf_route && $route !== $leaf_route) {
  466. $nodes = explode('/', $leaf_route);
  467. $sub_route = '/' . implode('/', array_slice($nodes, 1, $options['level']++));
  468. $options['route'] = $sub_route;
  469. [$status,,$leaf,$extra] = $this->getLevelListingRecurse($options);
  470. }
  471. // Handle no route, assume page tree root
  472. if (!$route) {
  473. $page = $this->getRoot();
  474. } else {
  475. $page = $this->get(trim($route, '/'));
  476. }
  477. $path = $page ? $page->path() : null;
  478. if ($field) {
  479. // Get forced filters from the field.
  480. $blueprint = $page ? $page->getBlueprint() : $this->getFlexDirectory()->getBlueprint();
  481. $settings = $blueprint->schema()->getProperty($field);
  482. $filters = array_merge([], $filters, $settings['filters'] ?? []);
  483. }
  484. // Clean up filter.
  485. $filter_type = (array)($filters['type'] ?? []);
  486. unset($filters['type']);
  487. $filters = array_filter($filters, static function($val) { return $val !== null && $val !== ''; });
  488. if ($page) {
  489. $status = 'success';
  490. $msg = 'PLUGIN_ADMIN.PAGE_ROUTE_FOUND';
  491. if ($page->root() && (!$filter_type || in_array('root', $filter_type, true))) {
  492. if ($field) {
  493. $response[] = [
  494. 'name' => '<root>',
  495. 'value' => '/',
  496. 'item-key' => '',
  497. 'filename' => '.',
  498. 'extension' => '',
  499. 'type' => 'root',
  500. 'modified' => $page->modified(),
  501. 'size' => 0,
  502. 'symlink' => false,
  503. 'has-children' => false
  504. ];
  505. } else {
  506. $response[] = [
  507. 'item-key' => '-root-',
  508. 'icon' => 'root',
  509. 'title' => 'Root', // FIXME
  510. 'route' => [
  511. 'display' => '&lt;root&gt;', // FIXME
  512. 'raw' => '_root',
  513. ],
  514. 'modified' => $page->modified(),
  515. 'extras' => [
  516. 'template' => $page->template(),
  517. //'lang' => null,
  518. //'translated' => null,
  519. 'langs' => [],
  520. 'published' => false,
  521. 'visible' => false,
  522. 'routable' => false,
  523. 'tags' => ['root', 'non-routable'],
  524. 'actions' => ['edit'], // FIXME
  525. ]
  526. ];
  527. }
  528. }
  529. /** @var PageCollection|PageIndex $children */
  530. $children = $page->children();
  531. /** @var PageIndex $children */
  532. $children = $children->getIndex();
  533. $selectedChildren = $children->filterBy($filters, true);
  534. /** @var Header $header */
  535. $header = $page->header();
  536. if (!$field && $header->get('admin.children_display_order') === 'collection' && ($orderby = $header->get('content.order.by'))) {
  537. // Use custom sorting by page header.
  538. $sortby = $orderby;
  539. $order = $header->get('content.order.dir', $order);
  540. $custom = $header->get('content.order.custom');
  541. }
  542. if ($sortby) {
  543. // Sort children.
  544. $selectedChildren = $selectedChildren->order($sortby, $order, $custom ?? null);
  545. }
  546. /** @var UserInterface|null $user */
  547. $user = Grav::instance()['user'] ?? null;
  548. /** @var PageObject $child */
  549. foreach ($selectedChildren as $child) {
  550. $selected = $child->path() === $extra;
  551. $includeChildren = is_array($leaf) && !empty($leaf) && $selected;
  552. if ($field) {
  553. $child_count = count($child->children());
  554. $payload = [
  555. 'name' => $child->menu(),
  556. 'value' => $child->rawRoute(),
  557. 'item-key' => Utils::basename($child->rawRoute() ?? ''),
  558. 'filename' => $child->folder(),
  559. 'extension' => $child->extension(),
  560. 'type' => 'dir',
  561. 'modified' => $child->modified(),
  562. 'size' => $child_count,
  563. 'symlink' => false,
  564. 'has-children' => $child_count > 0
  565. ];
  566. } else {
  567. $lang = $child->findTranslation($language) ?? 'n/a';
  568. /** @var PageObject $child */
  569. $child = $child->getTranslation($language) ?? $child;
  570. // TODO: all these features are independent from each other, we cannot just have one icon/color to catch all.
  571. // TODO: maybe icon by home/modular/page/folder (or even from blueprints) and color by visibility etc..
  572. if ($child->home()) {
  573. $icon = 'home';
  574. } elseif ($child->isModule()) {
  575. $icon = 'modular';
  576. } elseif ($child->visible()) {
  577. $icon = 'visible';
  578. } elseif ($child->isPage()) {
  579. $icon = 'page';
  580. } else {
  581. // TODO: add support
  582. $icon = 'folder';
  583. }
  584. $tags = [
  585. $child->published() ? 'published' : 'non-published',
  586. $child->visible() ? 'visible' : 'non-visible',
  587. $child->routable() ? 'routable' : 'non-routable'
  588. ];
  589. $extras = [
  590. 'template' => $child->template(),
  591. 'lang' => $lang ?: null,
  592. 'translated' => $lang ? $child->hasTranslation($language, false) : null,
  593. 'langs' => $child->getAllLanguages(true) ?: null,
  594. 'published' => $child->published(),
  595. 'published_date' => $this->jsDate($child->publishDate()),
  596. 'unpublished_date' => $this->jsDate($child->unpublishDate()),
  597. 'visible' => $child->visible(),
  598. 'routable' => $child->routable(),
  599. 'tags' => $tags,
  600. 'actions' => $this->getListingActions($child, $user),
  601. ];
  602. $extras = array_filter($extras, static function ($v) {
  603. return $v !== null;
  604. });
  605. /** @var PageIndex $tmp */
  606. $tmp = $child->children()->getIndex();
  607. $child_count = $tmp->count();
  608. $count = $filters ? $tmp->filterBy($filters, true)->count() : null;
  609. $route = $child->getRoute();
  610. $route = $route ? ($route->toString(false) ?: '/') : '';
  611. $payload = [
  612. 'item-key' => htmlspecialchars(Utils::basename($child->rawRoute() ?? $child->getKey())),
  613. 'icon' => $icon,
  614. 'title' => htmlspecialchars($child->menu()),
  615. 'route' => [
  616. 'display' => htmlspecialchars($route) ?: null,
  617. 'raw' => htmlspecialchars($child->rawRoute()),
  618. ],
  619. 'modified' => $this->jsDate($child->modified()),
  620. 'child_count' => $child_count ?: null,
  621. 'count' => $count ?? null,
  622. 'filters_hit' => $filters ? ($child->filterBy($filters, false) ?: null) : null,
  623. 'extras' => $extras
  624. ];
  625. $payload = array_filter($payload, static function ($v) {
  626. return $v !== null;
  627. });
  628. }
  629. // Add children if any
  630. if ($includeChildren) {
  631. $payload['children'] = array_values($leaf);
  632. }
  633. $response[] = $payload;
  634. }
  635. } else {
  636. $msg = 'PLUGIN_ADMIN.PAGE_ROUTE_NOT_FOUND';
  637. }
  638. if ($field) {
  639. $temp_array = [];
  640. foreach ($response as $index => $item) {
  641. $temp_array[$item['type']][$index] = $item;
  642. }
  643. $sorted = Utils::sortArrayByArray($temp_array, $filter_type);
  644. $response = Utils::arrayFlatten($sorted);
  645. }
  646. return [$status, $msg, $response, $path];
  647. }
  648. /**
  649. * @param PageObject $object
  650. * @param UserInterface $user
  651. * @return array
  652. */
  653. protected function getListingActions(PageObject $object, UserInterface $user): array
  654. {
  655. $actions = [];
  656. if ($object->isAuthorized('read', null, $user)) {
  657. $actions[] = 'preview';
  658. $actions[] = 'edit';
  659. }
  660. if ($object->isAuthorized('update', null, $user)) {
  661. $actions[] = 'copy';
  662. $actions[] = 'move';
  663. }
  664. if ($object->isAuthorized('delete', null, $user)) {
  665. $actions[] = 'delete';
  666. }
  667. return $actions;
  668. }
  669. /**
  670. * @param FlexStorageInterface $storage
  671. * @return CompiledJsonFile|CompiledYamlFile|null
  672. */
  673. protected static function getIndexFile(FlexStorageInterface $storage)
  674. {
  675. if (!method_exists($storage, 'isIndexed') || !$storage->isIndexed()) {
  676. return null;
  677. }
  678. // Load saved index file.
  679. $grav = Grav::instance();
  680. $locator = $grav['locator'];
  681. $filename = $locator->findResource('user-data://flex/indexes/pages.json', true, true);
  682. return CompiledJsonFile::instance($filename);
  683. }
  684. /**
  685. * @param int|null $timestamp
  686. * @return string|null
  687. */
  688. private function jsDate(int $timestamp = null): ?string
  689. {
  690. if (!$timestamp) {
  691. return null;
  692. }
  693. $config = Grav::instance()['config'];
  694. $dateFormat = $config->get('system.pages.dateformat.long');
  695. return date($dateFormat, $timestamp) ?: null;
  696. }
  697. /**
  698. * Add a single page to a collection
  699. *
  700. * @param PageInterface $page
  701. * @return PageCollection
  702. * @phpstan-return C
  703. */
  704. public function addPage(PageInterface $page)
  705. {
  706. return $this->getCollection()->addPage($page);
  707. }
  708. /**
  709. *
  710. * Create a copy of this collection
  711. *
  712. * @return static
  713. * @phpstan-return static<T,C>
  714. */
  715. public function copy()
  716. {
  717. return clone $this;
  718. }
  719. /**
  720. *
  721. * Merge another collection with the current collection
  722. *
  723. * @param PageCollectionInterface $collection
  724. * @return PageCollection
  725. * @phpstan-return C
  726. */
  727. public function merge(PageCollectionInterface $collection)
  728. {
  729. return $this->getCollection()->merge($collection);
  730. }
  731. /**
  732. * Intersect another collection with the current collection
  733. *
  734. * @param PageCollectionInterface $collection
  735. * @return PageCollection
  736. * @phpstan-return C
  737. */
  738. public function intersect(PageCollectionInterface $collection)
  739. {
  740. return $this->getCollection()->intersect($collection);
  741. }
  742. /**
  743. * Split collection into array of smaller collections.
  744. *
  745. * @param int $size
  746. * @return PageCollection[]
  747. * @phpstan-return C[]
  748. */
  749. public function batch($size)
  750. {
  751. return $this->getCollection()->batch($size);
  752. }
  753. /**
  754. * Remove item from the list.
  755. *
  756. * @param string $key
  757. * @return PageObject|null
  758. * @phpstan-return T|null
  759. * @throws InvalidArgumentException
  760. */
  761. public function remove($key)
  762. {
  763. return $this->getCollection()->remove($key);
  764. }
  765. /**
  766. * Reorder collection.
  767. *
  768. * @param string $by
  769. * @param string $dir
  770. * @param array $manual
  771. * @param string $sort_flags
  772. * @return static
  773. * @phpstan-return static<T,C>
  774. */
  775. public function order($by, $dir = 'asc', $manual = null, $sort_flags = null)
  776. {
  777. /** @var PageCollectionInterface $collection */
  778. $collection = $this->__call('order', [$by, $dir, $manual, $sort_flags]);
  779. return $collection;
  780. }
  781. /**
  782. * Check to see if this item is the first in the collection.
  783. *
  784. * @param string $path
  785. * @return bool True if item is first.
  786. */
  787. public function isFirst($path): bool
  788. {
  789. /** @var bool $result */
  790. $result = $this->__call('isFirst', [$path]);
  791. return $result;
  792. }
  793. /**
  794. * Check to see if this item is the last in the collection.
  795. *
  796. * @param string $path
  797. * @return bool True if item is last.
  798. */
  799. public function isLast($path): bool
  800. {
  801. /** @var bool $result */
  802. $result = $this->__call('isLast', [$path]);
  803. return $result;
  804. }
  805. /**
  806. * Gets the previous sibling based on current position.
  807. *
  808. * @param string $path
  809. * @return PageObject|null The previous item.
  810. * @phpstan-return T|null
  811. */
  812. public function prevSibling($path)
  813. {
  814. /** @var PageObject|null $result */
  815. $result = $this->__call('prevSibling', [$path]);
  816. return $result;
  817. }
  818. /**
  819. * Gets the next sibling based on current position.
  820. *
  821. * @param string $path
  822. * @return PageObject|null The next item.
  823. * @phpstan-return T|null
  824. */
  825. public function nextSibling($path)
  826. {
  827. /** @var PageObject|null $result */
  828. $result = $this->__call('nextSibling', [$path]);
  829. return $result;
  830. }
  831. /**
  832. * Returns the adjacent sibling based on a direction.
  833. *
  834. * @param string $path
  835. * @param int $direction either -1 or +1
  836. * @return PageObject|false The sibling item.
  837. * @phpstan-return T|false
  838. */
  839. public function adjacentSibling($path, $direction = 1)
  840. {
  841. /** @var PageObject|false $result */
  842. $result = $this->__call('adjacentSibling', [$path, $direction]);
  843. return $result;
  844. }
  845. /**
  846. * Returns the item in the current position.
  847. *
  848. * @param string $path the path the item
  849. * @return int|null The index of the current page, null if not found.
  850. */
  851. public function currentPosition($path): ?int
  852. {
  853. /** @var int|null $result */
  854. $result = $this->__call('currentPosition', [$path]);
  855. return $result;
  856. }
  857. /**
  858. * Returns the items between a set of date ranges of either the page date field (default) or
  859. * an arbitrary datetime page field where start date and end date are optional
  860. * Dates must be passed in as text that strtotime() can process
  861. * http://php.net/manual/en/function.strtotime.php
  862. *
  863. * @param string|null $startDate
  864. * @param string|null $endDate
  865. * @param string|null $field
  866. * @return static
  867. * @phpstan-return static<T,C>
  868. * @throws Exception
  869. */
  870. public function dateRange($startDate = null, $endDate = null, $field = null)
  871. {
  872. $collection = $this->__call('dateRange', [$startDate, $endDate, $field]);
  873. return $collection;
  874. }
  875. /**
  876. * Mimicks Pages class.
  877. *
  878. * @return $this
  879. * @deprecated 1.7 Not needed anymore in Flex Pages (does nothing).
  880. */
  881. public function all()
  882. {
  883. return $this;
  884. }
  885. /**
  886. * Creates new collection with only visible pages
  887. *
  888. * @return static The collection with only visible pages
  889. * @phpstan-return static<T,C>
  890. */
  891. public function visible()
  892. {
  893. $collection = $this->__call('visible', []);
  894. return $collection;
  895. }
  896. /**
  897. * Creates new collection with only non-visible pages
  898. *
  899. * @return static The collection with only non-visible pages
  900. * @phpstan-return static<T,C>
  901. */
  902. public function nonVisible()
  903. {
  904. $collection = $this->__call('nonVisible', []);
  905. return $collection;
  906. }
  907. /**
  908. * Creates new collection with only non-modular pages
  909. *
  910. * @return static The collection with only non-modular pages
  911. * @phpstan-return static<T,C>
  912. */
  913. public function pages()
  914. {
  915. $collection = $this->__call('pages', []);
  916. return $collection;
  917. }
  918. /**
  919. * Creates new collection with only modular pages
  920. *
  921. * @return static The collection with only modular pages
  922. * @phpstan-return static<T,C>
  923. */
  924. public function modules()
  925. {
  926. $collection = $this->__call('modules', []);
  927. return $collection;
  928. }
  929. /**
  930. * Creates new collection with only modular pages
  931. *
  932. * @return static The collection with only modular pages
  933. * @phpstan-return static<T,C>
  934. */
  935. public function modular()
  936. {
  937. return $this->modules();
  938. }
  939. /**
  940. * Creates new collection with only non-modular pages
  941. *
  942. * @return static The collection with only non-modular pages
  943. * @phpstan-return static<T,C>
  944. */
  945. public function nonModular()
  946. {
  947. return $this->pages();
  948. }
  949. /**
  950. * Creates new collection with only published pages
  951. *
  952. * @return static The collection with only published pages
  953. * @phpstan-return static<T,C>
  954. */
  955. public function published()
  956. {
  957. $collection = $this->__call('published', []);
  958. return $collection;
  959. }
  960. /**
  961. * Creates new collection with only non-published pages
  962. *
  963. * @return static The collection with only non-published pages
  964. * @phpstan-return static<T,C>
  965. */
  966. public function nonPublished()
  967. {
  968. $collection = $this->__call('nonPublished', []);
  969. return $collection;
  970. }
  971. /**
  972. * Creates new collection with only routable pages
  973. *
  974. * @return static The collection with only routable pages
  975. * @phpstan-return static<T,C>
  976. */
  977. public function routable()
  978. {
  979. $collection = $this->__call('routable', []);
  980. return $collection;
  981. }
  982. /**
  983. * Creates new collection with only non-routable pages
  984. *
  985. * @return static The collection with only non-routable pages
  986. * @phpstan-return static<T,C>
  987. */
  988. public function nonRoutable()
  989. {
  990. $collection = $this->__call('nonRoutable', []);
  991. return $collection;
  992. }
  993. /**
  994. * Creates new collection with only pages of the specified type
  995. *
  996. * @param string $type
  997. * @return static The collection
  998. * @phpstan-return static<T,C>
  999. */
  1000. public function ofType($type)
  1001. {
  1002. $collection = $this->__call('ofType', []);
  1003. return $collection;
  1004. }
  1005. /**
  1006. * Creates new collection with only pages of one of the specified types
  1007. *
  1008. * @param string[] $types
  1009. * @return static The collection
  1010. * @phpstan-return static<T,C>
  1011. */
  1012. public function ofOneOfTheseTypes($types)
  1013. {
  1014. $collection = $this->__call('ofOneOfTheseTypes', []);
  1015. return $collection;
  1016. }
  1017. /**
  1018. * Creates new collection with only pages of one of the specified access levels
  1019. *
  1020. * @param array $accessLevels
  1021. * @return static The collection
  1022. * @phpstan-return static<T,C>
  1023. */
  1024. public function ofOneOfTheseAccessLevels($accessLevels)
  1025. {
  1026. $collection = $this->__call('ofOneOfTheseAccessLevels', []);
  1027. return $collection;
  1028. }
  1029. /**
  1030. * Converts collection into an array.
  1031. *
  1032. * @return array
  1033. */
  1034. public function toArray()
  1035. {
  1036. return $this->getCollection()->toArray();
  1037. }
  1038. /**
  1039. * Get the extended version of this Collection with each page keyed by route
  1040. *
  1041. * @return array
  1042. * @throws Exception
  1043. */
  1044. public function toExtendedArray()
  1045. {
  1046. return $this->getCollection()->toExtendedArray();
  1047. }
  1048. }