Pages.php 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423
  1. <?php
  2. /**
  3. * @package Grav\Common\Page
  4. *
  5. * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common\Page;
  9. use Grav\Common\Cache;
  10. use Grav\Common\Config\Config;
  11. use Grav\Common\Data\Blueprint;
  12. use Grav\Common\Data\Blueprints;
  13. use Grav\Common\Filesystem\Folder;
  14. use Grav\Common\Grav;
  15. use Grav\Common\Language\Language;
  16. use Grav\Common\Page\Interfaces\PageInterface;
  17. use Grav\Common\Taxonomy;
  18. use Grav\Common\Uri;
  19. use Grav\Common\Utils;
  20. use Grav\Plugin\Admin;
  21. use RocketTheme\Toolbox\Event\Event;
  22. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  23. use Whoops\Exception\ErrorException;
  24. use Collator;
  25. class Pages
  26. {
  27. /**
  28. * @var Grav
  29. */
  30. protected $grav;
  31. /**
  32. * @var array|PageInterface[]
  33. */
  34. protected $instances;
  35. /**
  36. * @var array|string[]
  37. */
  38. protected $children;
  39. /**
  40. * @var string
  41. */
  42. protected $base = '';
  43. /**
  44. * @var array|string[]
  45. */
  46. protected $baseRoute = [];
  47. /**
  48. * @var array|string[]
  49. */
  50. protected $routes = [];
  51. /**
  52. * @var array
  53. */
  54. protected $sort;
  55. /**
  56. * @var Blueprints
  57. */
  58. protected $blueprints;
  59. /**
  60. * @var int
  61. */
  62. protected $last_modified;
  63. /**
  64. * @var array|string[]
  65. */
  66. protected $ignore_files;
  67. /**
  68. * @var array|string[]
  69. */
  70. protected $ignore_folders;
  71. /**
  72. * @var bool
  73. */
  74. protected $ignore_hidden;
  75. /** @var string */
  76. protected $check_method;
  77. protected $pages_cache_id;
  78. protected $initialized = false;
  79. /**
  80. * @var Types
  81. */
  82. static protected $types;
  83. /**
  84. * @var string
  85. */
  86. static protected $home_route;
  87. /**
  88. * Constructor
  89. *
  90. * @param Grav $c
  91. */
  92. public function __construct(Grav $c)
  93. {
  94. $this->grav = $c;
  95. }
  96. /**
  97. * Get or set base path for the pages.
  98. *
  99. * @param string $path
  100. *
  101. * @return string
  102. */
  103. public function base($path = null)
  104. {
  105. if ($path !== null) {
  106. $path = trim($path, '/');
  107. $this->base = $path ? '/' . $path : null;
  108. $this->baseRoute = [];
  109. }
  110. return $this->base;
  111. }
  112. /**
  113. *
  114. * Get base route for Grav pages.
  115. *
  116. * @param string $lang Optional language code for multilingual routes.
  117. *
  118. * @return string
  119. */
  120. public function baseRoute($lang = null)
  121. {
  122. $key = $lang ?: 'default';
  123. if (!isset($this->baseRoute[$key])) {
  124. /** @var Language $language */
  125. $language = $this->grav['language'];
  126. $path_base = rtrim($this->base(), '/');
  127. $path_lang = $language->enabled() ? $language->getLanguageURLPrefix($lang) : '';
  128. $this->baseRoute[$key] = $path_base . $path_lang;
  129. }
  130. return $this->baseRoute[$key];
  131. }
  132. /**
  133. *
  134. * Get route for Grav site.
  135. *
  136. * @param string $route Optional route to the page.
  137. * @param string $lang Optional language code for multilingual links.
  138. *
  139. * @return string
  140. */
  141. public function route($route = '/', $lang = null)
  142. {
  143. if (!$route || $route === '/') {
  144. return $this->baseRoute($lang) ?: '/';
  145. }
  146. return $this->baseRoute($lang) . $route;
  147. }
  148. /**
  149. *
  150. * Get base URL for Grav pages.
  151. *
  152. * @param string $lang Optional language code for multilingual links.
  153. * @param bool|null $absolute If true, return absolute url, if false, return relative url. Otherwise return default.
  154. *
  155. * @return string
  156. */
  157. public function baseUrl($lang = null, $absolute = null)
  158. {
  159. if ($absolute === null) {
  160. $type = 'base_url';
  161. } elseif ($absolute) {
  162. $type = 'base_url_absolute';
  163. } else {
  164. $type = 'base_url_relative';
  165. }
  166. return $this->grav[$type] . $this->baseRoute($lang);
  167. }
  168. /**
  169. *
  170. * Get home URL for Grav site.
  171. *
  172. * @param string $lang Optional language code for multilingual links.
  173. * @param bool $absolute If true, return absolute url, if false, return relative url. Otherwise return default.
  174. *
  175. * @return string
  176. */
  177. public function homeUrl($lang = null, $absolute = null)
  178. {
  179. return $this->baseUrl($lang, $absolute) ?: '/';
  180. }
  181. /**
  182. *
  183. * Get URL for Grav site.
  184. *
  185. * @param string $route Optional route to the page.
  186. * @param string $lang Optional language code for multilingual links.
  187. * @param bool $absolute If true, return absolute url, if false, return relative url. Otherwise return default.
  188. *
  189. * @return string
  190. */
  191. public function url($route = '/', $lang = null, $absolute = null)
  192. {
  193. if (!$route || $route === '/') {
  194. return $this->homeUrl($lang, $absolute);
  195. }
  196. return $this->baseUrl($lang, $absolute) . Uri::filterPath($route);
  197. }
  198. public function setCheckMethod($method)
  199. {
  200. $this->check_method = strtolower($method);
  201. }
  202. /**
  203. * Class initialization. Must be called before using this class.
  204. */
  205. public function init()
  206. {
  207. if ($this->initialized) {
  208. return;
  209. }
  210. $config = $this->grav['config'];
  211. $this->ignore_files = $config->get('system.pages.ignore_files');
  212. $this->ignore_folders = $config->get('system.pages.ignore_folders');
  213. $this->ignore_hidden = $config->get('system.pages.ignore_hidden');
  214. $this->instances = [];
  215. $this->children = [];
  216. $this->routes = [];
  217. if (!$this->check_method) {
  218. $this->setCheckMethod($config->get('system.cache.check.method', 'file'));
  219. }
  220. $this->buildPages();
  221. }
  222. /**
  223. * Get or set last modification time.
  224. *
  225. * @param int $modified
  226. *
  227. * @return int|null
  228. */
  229. public function lastModified($modified = null)
  230. {
  231. if ($modified && $modified > $this->last_modified) {
  232. $this->last_modified = $modified;
  233. }
  234. return $this->last_modified;
  235. }
  236. /**
  237. * Returns a list of all pages.
  238. *
  239. * @return array|PageInterface[]
  240. */
  241. public function instances()
  242. {
  243. return $this->instances;
  244. }
  245. /**
  246. * Returns a list of all routes.
  247. *
  248. * @return array
  249. */
  250. public function routes()
  251. {
  252. return $this->routes;
  253. }
  254. /**
  255. * Adds a page and assigns a route to it.
  256. *
  257. * @param PageInterface $page Page to be added.
  258. * @param string $route Optional route (uses route from the object if not set).
  259. */
  260. public function addPage(PageInterface $page, $route = null)
  261. {
  262. if (!isset($this->instances[$page->path()])) {
  263. $this->instances[$page->path()] = $page;
  264. }
  265. $route = $page->route($route);
  266. if ($page->parent()) {
  267. $this->children[$page->parent()->path()][$page->path()] = ['slug' => $page->slug()];
  268. }
  269. $this->routes[$route] = $page->path();
  270. $this->grav->fireEvent('onPageProcessed', new Event(['page' => $page]));
  271. }
  272. /**
  273. * Sort sub-pages in a page.
  274. *
  275. * @param PageInterface $page
  276. * @param string $order_by
  277. * @param string $order_dir
  278. *
  279. * @return array
  280. */
  281. public function sort(PageInterface $page, $order_by = null, $order_dir = null, $sort_flags = null)
  282. {
  283. if ($order_by === null) {
  284. $order_by = $page->orderBy();
  285. }
  286. if ($order_dir === null) {
  287. $order_dir = $page->orderDir();
  288. }
  289. $path = $page->path();
  290. $children = $this->children[$path] ?? [];
  291. if (!$children) {
  292. return $children;
  293. }
  294. if (!isset($this->sort[$path][$order_by])) {
  295. $this->buildSort($path, $children, $order_by, $page->orderManual(), $sort_flags);
  296. }
  297. $sort = $this->sort[$path][$order_by];
  298. if ($order_dir !== 'asc') {
  299. $sort = array_reverse($sort);
  300. }
  301. return $sort;
  302. }
  303. /**
  304. * @param Collection $collection
  305. * @param string|int $orderBy
  306. * @param string $orderDir
  307. * @param array|null $orderManual
  308. * @param int|null $sort_flags
  309. *
  310. * @return array
  311. * @internal
  312. */
  313. public function sortCollection(Collection $collection, $orderBy, $orderDir = 'asc', $orderManual = null, $sort_flags = null)
  314. {
  315. $items = $collection->toArray();
  316. if (!$items) {
  317. return [];
  318. }
  319. $lookup = md5(json_encode($items) . json_encode($orderManual) . $orderBy . $orderDir);
  320. if (!isset($this->sort[$lookup][$orderBy])) {
  321. $this->buildSort($lookup, $items, $orderBy, $orderManual, $sort_flags);
  322. }
  323. $sort = $this->sort[$lookup][$orderBy];
  324. if ($orderDir !== 'asc') {
  325. $sort = array_reverse($sort);
  326. }
  327. return $sort;
  328. }
  329. /**
  330. * Get a page instance.
  331. *
  332. * @param string $path The filesystem full path of the page
  333. *
  334. * @return PageInterface
  335. * @throws \Exception
  336. */
  337. public function get($path)
  338. {
  339. return $this->instances[(string)$path] ?? null;
  340. }
  341. /**
  342. * Get children of the path.
  343. *
  344. * @param string $path
  345. *
  346. * @return Collection
  347. */
  348. public function children($path)
  349. {
  350. $children = $this->children[(string)$path] ?? [];
  351. return new Collection($children, [], $this);
  352. }
  353. /**
  354. * Get a page ancestor.
  355. *
  356. * @param string $route The relative URL of the page
  357. * @param string $path The relative path of the ancestor folder
  358. *
  359. * @return PageInterface|null
  360. */
  361. public function ancestor($route, $path = null)
  362. {
  363. if ($path !== null) {
  364. $page = $this->dispatch($route, true);
  365. if ($page && $page->path() === $path) {
  366. return $page;
  367. }
  368. $parent = $page ? $page->parent() : null;
  369. if ($parent && !$parent->root()) {
  370. return $this->ancestor($parent->route(), $path);
  371. }
  372. }
  373. return null;
  374. }
  375. /**
  376. * Get a page ancestor trait.
  377. *
  378. * @param string $route The relative route of the page
  379. * @param string $field The field name of the ancestor to query for
  380. *
  381. * @return PageInterface|null
  382. */
  383. public function inherited($route, $field = null)
  384. {
  385. if ($field !== null) {
  386. $page = $this->dispatch($route, true);
  387. $parent = $page ? $page->parent() : null;
  388. if ($parent && $parent->value('header.' . $field) !== null) {
  389. return $parent;
  390. }
  391. if ($parent && !$parent->root()) {
  392. return $this->inherited($parent->route(), $field);
  393. }
  394. }
  395. return null;
  396. }
  397. /**
  398. * alias method to return find a page.
  399. *
  400. * @param string $route The relative URL of the page
  401. * @param bool $all
  402. *
  403. * @return PageInterface|null
  404. */
  405. public function find($route, $all = false)
  406. {
  407. return $this->dispatch($route, $all, false);
  408. }
  409. /**
  410. * Dispatch URI to a page.
  411. *
  412. * @param string $route The relative URL of the page
  413. * @param bool $all
  414. *
  415. * @param bool $redirect
  416. * @return PageInterface|null
  417. * @throws \Exception
  418. */
  419. public function dispatch($route, $all = false, $redirect = true)
  420. {
  421. $route = urldecode($route);
  422. // Fetch page if there's a defined route to it.
  423. $page = isset($this->routes[$route]) ? $this->get($this->routes[$route]) : null;
  424. // Try without trailing slash
  425. if (!$page && Utils::endsWith($route, '/')) {
  426. $page = isset($this->routes[rtrim($route, '/')]) ? $this->get($this->routes[rtrim($route, '/')]) : null;
  427. }
  428. // Are we in the admin? this is important!
  429. $not_admin = !isset($this->grav['admin']);
  430. // If the page cannot be reached, look into site wide redirects, routes + wildcards
  431. if (!$all && $not_admin) {
  432. // If the page is a simple redirect, just do it.
  433. if ($redirect && $page && $page->redirect()) {
  434. $this->grav->redirectLangSafe($page->redirect());
  435. }
  436. // fall back and check site based redirects
  437. if (!$page || ($page && !$page->routable())) {
  438. /** @var Config $config */
  439. $config = $this->grav['config'];
  440. // See if route matches one in the site configuration
  441. $site_route = $config->get("site.routes.{$route}");
  442. if ($site_route) {
  443. $page = $this->dispatch($site_route, $all);
  444. } else {
  445. /** @var Uri $uri */
  446. $uri = $this->grav['uri'];
  447. /** @var \Grav\Framework\Uri\Uri $source_url */
  448. $source_url = $uri->uri(false);
  449. // Try Regex style redirects
  450. $site_redirects = $config->get('site.redirects');
  451. if (is_array($site_redirects)) {
  452. foreach ((array)$site_redirects as $pattern => $replace) {
  453. $pattern = '#^' . str_replace('/', '\/', ltrim($pattern, '^')) . '#';
  454. try {
  455. $found = preg_replace($pattern, $replace, $source_url);
  456. if ($found !== $source_url) {
  457. $this->grav->redirectLangSafe($found);
  458. }
  459. } catch (ErrorException $e) {
  460. $this->grav['log']->error('site.redirects: ' . $pattern . '-> ' . $e->getMessage());
  461. }
  462. }
  463. }
  464. // Try Regex style routes
  465. $site_routes = $config->get('site.routes');
  466. if (is_array($site_routes)) {
  467. foreach ((array)$site_routes as $pattern => $replace) {
  468. $pattern = '#^' . str_replace('/', '\/', ltrim($pattern, '^')) . '#';
  469. try {
  470. $found = preg_replace($pattern, $replace, $source_url);
  471. if ($found !== $source_url) {
  472. $page = $this->dispatch($found, $all);
  473. }
  474. } catch (ErrorException $e) {
  475. $this->grav['log']->error('site.routes: ' . $pattern . '-> ' . $e->getMessage());
  476. }
  477. }
  478. }
  479. }
  480. }
  481. }
  482. return $page;
  483. }
  484. /**
  485. * Get root page.
  486. *
  487. * @return PageInterface
  488. */
  489. public function root()
  490. {
  491. /** @var UniformResourceLocator $locator */
  492. $locator = $this->grav['locator'];
  493. return $this->instances[rtrim($locator->findResource('page://'), DS)];
  494. }
  495. /**
  496. * Get a blueprint for a page type.
  497. *
  498. * @param string $type
  499. *
  500. * @return Blueprint
  501. */
  502. public function blueprints($type)
  503. {
  504. if ($this->blueprints === null) {
  505. $this->blueprints = new Blueprints(self::getTypes());
  506. }
  507. try {
  508. $blueprint = $this->blueprints->get($type);
  509. } catch (\RuntimeException $e) {
  510. $blueprint = $this->blueprints->get('default');
  511. }
  512. if (empty($blueprint->initialized)) {
  513. $this->grav->fireEvent('onBlueprintCreated', new Event(['blueprint' => $blueprint, 'type' => $type]));
  514. $blueprint->initialized = true;
  515. }
  516. return $blueprint;
  517. }
  518. /**
  519. * Get all pages
  520. *
  521. * @param PageInterface $current
  522. *
  523. * @return \Grav\Common\Page\Collection
  524. */
  525. public function all(PageInterface $current = null)
  526. {
  527. $all = new Collection();
  528. /** @var PageInterface $current */
  529. $current = $current ?: $this->root();
  530. if (!$current->root()) {
  531. $all[$current->path()] = ['slug' => $current->slug()];
  532. }
  533. foreach ($current->children() as $next) {
  534. $all->append($this->all($next));
  535. }
  536. return $all;
  537. }
  538. /**
  539. * Get available parents raw routes.
  540. *
  541. * @return array
  542. */
  543. public static function parentsRawRoutes()
  544. {
  545. $rawRoutes = true;
  546. return self::getParents($rawRoutes);
  547. }
  548. /**
  549. * Get available parents routes
  550. *
  551. * @param bool $rawRoutes get the raw route or the normal route
  552. *
  553. * @return array
  554. */
  555. private static function getParents($rawRoutes)
  556. {
  557. $grav = Grav::instance();
  558. /** @var Pages $pages */
  559. $pages = $grav['pages'];
  560. $parents = $pages->getList(null, 0, $rawRoutes);
  561. if (isset($grav['admin'])) {
  562. // Remove current route from parents
  563. /** @var Admin $admin */
  564. $admin = $grav['admin'];
  565. $page = $admin->getPage($admin->route);
  566. $page_route = $page->route();
  567. if (isset($parents[$page_route])) {
  568. unset($parents[$page_route]);
  569. }
  570. }
  571. return $parents;
  572. }
  573. /**
  574. * Get list of route/title of all pages.
  575. *
  576. * @param PageInterface $current
  577. * @param int $level
  578. * @param bool $rawRoutes
  579. *
  580. * @param bool $showAll
  581. * @param bool $showFullpath
  582. * @param bool $showSlug
  583. * @param bool $showModular
  584. * @param bool $limitLevels
  585. * @return array
  586. */
  587. public function getList(PageInterface $current = null, $level = 0, $rawRoutes = false, $showAll = true, $showFullpath = false, $showSlug = false, $showModular = false, $limitLevels = false)
  588. {
  589. if (!$current) {
  590. if ($level) {
  591. throw new \RuntimeException('Internal error');
  592. }
  593. $current = $this->root();
  594. }
  595. $list = [];
  596. if (!$current->root()) {
  597. if ($rawRoutes) {
  598. $route = $current->rawRoute();
  599. } else {
  600. $route = $current->route();
  601. }
  602. if ($showFullpath) {
  603. $option = $current->route();
  604. } else {
  605. $extra = $showSlug ? '(' . $current->slug() . ') ' : '';
  606. $option = str_repeat('&mdash;-', $level). '&rtrif; ' . $extra . $current->title();
  607. }
  608. $list[$route] = $option;
  609. }
  610. if ($limitLevels === false || ($level+1 < $limitLevels)) {
  611. foreach ($current->children() as $next) {
  612. if ($showAll || $next->routable() || ($next->modular() && $showModular)) {
  613. $list = array_merge($list, $this->getList($next, $level + 1, $rawRoutes, $showAll, $showFullpath, $showSlug, $showModular, $limitLevels));
  614. }
  615. }
  616. }
  617. return $list;
  618. }
  619. /**
  620. * Get available page types.
  621. *
  622. * @return Types
  623. */
  624. public static function getTypes()
  625. {
  626. if (!self::$types) {
  627. $grav = Grav::instance();
  628. $scanBlueprintsAndTemplates = function () use ($grav) {
  629. // Scan blueprints
  630. $event = new Event();
  631. $event->types = self::$types;
  632. $grav->fireEvent('onGetPageBlueprints', $event);
  633. self::$types->scanBlueprints('theme://blueprints/');
  634. // Scan templates
  635. $event = new Event();
  636. $event->types = self::$types;
  637. $grav->fireEvent('onGetPageTemplates', $event);
  638. self::$types->scanTemplates('theme://templates/');
  639. };
  640. if ($grav['config']->get('system.cache.enabled')) {
  641. /** @var Cache $cache */
  642. $cache = $grav['cache'];
  643. // Use cached types if possible.
  644. $types_cache_id = md5('types');
  645. self::$types = $cache->fetch($types_cache_id);
  646. if (!self::$types) {
  647. self::$types = new Types();
  648. $scanBlueprintsAndTemplates();
  649. $cache->save($types_cache_id, self::$types);
  650. }
  651. } else {
  652. self::$types = new Types();
  653. $scanBlueprintsAndTemplates();
  654. }
  655. // Register custom paths to the locator.
  656. $locator = $grav['locator'];
  657. foreach (self::$types as $type => $paths) {
  658. foreach ($paths as $k => $path) {
  659. if (strpos($path, 'blueprints://') === 0) {
  660. unset($paths[$k]);
  661. }
  662. }
  663. if ($paths) {
  664. $locator->addPath('blueprints', "pages/$type.yaml", $paths);
  665. }
  666. }
  667. }
  668. return self::$types;
  669. }
  670. /**
  671. * Get available page types.
  672. *
  673. * @return array
  674. */
  675. public static function types()
  676. {
  677. $types = self::getTypes();
  678. return $types->pageSelect();
  679. }
  680. /**
  681. * Get available page types.
  682. *
  683. * @return array
  684. */
  685. public static function modularTypes()
  686. {
  687. $types = self::getTypes();
  688. return $types->modularSelect();
  689. }
  690. /**
  691. * Get template types based on page type (standard or modular)
  692. *
  693. * @return array
  694. */
  695. public static function pageTypes()
  696. {
  697. if (isset(Grav::instance()['admin'])) {
  698. /** @var Admin $admin */
  699. $admin = Grav::instance()['admin'];
  700. /** @var PageInterface $page */
  701. $page = $admin->getPage($admin->route);
  702. if ($page && $page->modular()) {
  703. return static::modularTypes();
  704. }
  705. return static::types();
  706. }
  707. return [];
  708. }
  709. /**
  710. * Get access levels of the site pages
  711. *
  712. * @return array
  713. */
  714. public function accessLevels()
  715. {
  716. $accessLevels = [];
  717. foreach ($this->all() as $page) {
  718. if (isset($page->header()->access)) {
  719. if (\is_array($page->header()->access)) {
  720. foreach ($page->header()->access as $index => $accessLevel) {
  721. if (\is_array($accessLevel)) {
  722. foreach ($accessLevel as $innerIndex => $innerAccessLevel) {
  723. $accessLevels[] = $innerIndex;
  724. }
  725. } else {
  726. $accessLevels[] = $index;
  727. }
  728. }
  729. } else {
  730. $accessLevels[] = $page->header()->access;
  731. }
  732. }
  733. }
  734. return array_unique($accessLevels);
  735. }
  736. /**
  737. * Get available parents routes
  738. *
  739. * @return array
  740. */
  741. public static function parents()
  742. {
  743. $rawRoutes = false;
  744. return self::getParents($rawRoutes);
  745. }
  746. /**
  747. * Gets the home route
  748. *
  749. * @return string
  750. */
  751. public static function getHomeRoute()
  752. {
  753. if (empty(self::$home_route)) {
  754. $grav = Grav::instance();
  755. /** @var Config $config */
  756. $config = $grav['config'];
  757. /** @var Language $language */
  758. $language = $grav['language'];
  759. $home = $config->get('system.home.alias');
  760. if ($language->enabled()) {
  761. $home_aliases = $config->get('system.home.aliases');
  762. if ($home_aliases) {
  763. $active = $language->getActive();
  764. $default = $language->getDefault();
  765. try {
  766. if ($active) {
  767. $home = $home_aliases[$active];
  768. } else {
  769. $home = $home_aliases[$default];
  770. }
  771. } catch (ErrorException $e) {
  772. $home = $home_aliases[$default];
  773. }
  774. }
  775. }
  776. self::$home_route = trim($home, '/');
  777. }
  778. return self::$home_route;
  779. }
  780. /**
  781. * Needed for testing where we change the home route via config
  782. */
  783. public static function resetHomeRoute()
  784. {
  785. self::$home_route = null;
  786. return self::getHomeRoute();
  787. }
  788. /**
  789. * Builds pages.
  790. *
  791. * @internal
  792. */
  793. protected function buildPages()
  794. {
  795. $this->sort = [];
  796. /** @var Config $config */
  797. $config = $this->grav['config'];
  798. /** @var Language $language */
  799. $language = $this->grav['language'];
  800. /** @var UniformResourceLocator $locator */
  801. $locator = $this->grav['locator'];
  802. $pages_dir = $locator->findResource('page://');
  803. if ($config->get('system.cache.enabled')) {
  804. /** @var Cache $cache */
  805. $cache = $this->grav['cache'];
  806. /** @var Taxonomy $taxonomy */
  807. $taxonomy = $this->grav['taxonomy'];
  808. // how should we check for last modified? Default is by file
  809. switch ($this->check_method) {
  810. case 'none':
  811. case 'off':
  812. $hash = 0;
  813. break;
  814. case 'folder':
  815. $hash = Folder::lastModifiedFolder($pages_dir);
  816. break;
  817. case 'hash':
  818. $hash = Folder::hashAllFiles($pages_dir);
  819. break;
  820. default:
  821. $hash = Folder::lastModifiedFile($pages_dir);
  822. }
  823. $this->pages_cache_id = md5($pages_dir . $hash . $language->getActive() . $config->checksum());
  824. list($this->instances, $this->routes, $this->children, $taxonomy_map, $this->sort) = $cache->fetch($this->pages_cache_id);
  825. if (!$this->instances) {
  826. $this->grav['debugger']->addMessage('Page cache missed, rebuilding pages..');
  827. // recurse pages and cache result
  828. $this->resetPages($pages_dir);
  829. } else {
  830. // If pages was found in cache, set the taxonomy
  831. $this->grav['debugger']->addMessage('Page cache hit.');
  832. $taxonomy->taxonomy($taxonomy_map);
  833. }
  834. } else {
  835. $this->recurse($pages_dir);
  836. $this->buildRoutes();
  837. }
  838. }
  839. /**
  840. * Accessible method to manually reset the pages cache
  841. *
  842. * @param string $pages_dir
  843. */
  844. public function resetPages($pages_dir)
  845. {
  846. $this->recurse($pages_dir);
  847. $this->buildRoutes();
  848. // cache if needed
  849. if ($this->grav['config']->get('system.cache.enabled')) {
  850. /** @var Cache $cache */
  851. $cache = $this->grav['cache'];
  852. /** @var Taxonomy $taxonomy */
  853. $taxonomy = $this->grav['taxonomy'];
  854. // save pages, routes, taxonomy, and sort to cache
  855. $cache->save($this->pages_cache_id, [$this->instances, $this->routes, $this->children, $taxonomy->taxonomy(), $this->sort]);
  856. }
  857. }
  858. /**
  859. * Recursive function to load & build page relationships.
  860. *
  861. * @param string $directory
  862. * @param PageInterface|null $parent
  863. *
  864. * @return PageInterface
  865. * @throws \RuntimeException
  866. * @internal
  867. */
  868. protected function recurse($directory, PageInterface $parent = null)
  869. {
  870. $directory = rtrim($directory, DS);
  871. $page = new Page;
  872. /** @var Config $config */
  873. $config = $this->grav['config'];
  874. /** @var Language $language */
  875. $language = $this->grav['language'];
  876. // Stuff to do at root page
  877. // Fire event for memory and time consuming plugins...
  878. if ($parent === null && $config->get('system.pages.events.page')) {
  879. $this->grav->fireEvent('onBuildPagesInitialized');
  880. }
  881. $page->path($directory);
  882. if ($parent) {
  883. $page->parent($parent);
  884. }
  885. $page->orderDir($config->get('system.pages.order.dir'));
  886. $page->orderBy($config->get('system.pages.order.by'));
  887. // Add into instances
  888. if (!isset($this->instances[$page->path()])) {
  889. $this->instances[$page->path()] = $page;
  890. if ($parent && $page->path()) {
  891. $this->children[$parent->path()][$page->path()] = ['slug' => $page->slug()];
  892. }
  893. } else {
  894. throw new \RuntimeException('Fatal error when creating page instances.');
  895. }
  896. // Build regular expression for all the allowed page extensions.
  897. $page_extensions = $language->getFallbackPageExtensions();
  898. $regex = '/^[^\.]*(' . implode('|', array_map(
  899. function ($str) {
  900. return preg_quote($str, '/');
  901. },
  902. $page_extensions
  903. )) . ')$/';
  904. $folders = [];
  905. $page_found = null;
  906. $page_extension = '.md';
  907. $last_modified = 0;
  908. $iterator = new \FilesystemIterator($directory);
  909. /** @var \FilesystemIterator $file */
  910. foreach ($iterator as $file) {
  911. $filename = $file->getFilename();
  912. // Ignore all hidden files if set.
  913. if ($this->ignore_hidden && $filename && strpos($filename, '.') === 0) {
  914. continue;
  915. }
  916. // Handle folders later.
  917. if ($file->isDir()) {
  918. // But ignore all folders in ignore list.
  919. if (!\in_array($filename, $this->ignore_folders, true)) {
  920. $folders[] = $file;
  921. }
  922. continue;
  923. }
  924. // Ignore all files in ignore list.
  925. if (\in_array($filename, $this->ignore_files, true)) {
  926. continue;
  927. }
  928. // Update last modified date to match the last updated file in the folder.
  929. $modified = $file->getMTime();
  930. if ($modified > $last_modified) {
  931. $last_modified = $modified;
  932. }
  933. // Page is the one that matches to $page_extensions list with the lowest index number.
  934. if (preg_match($regex, $filename, $matches, PREG_OFFSET_CAPTURE)) {
  935. $ext = $matches[1][0];
  936. if ($page_found === null || array_search($ext, $page_extensions, true) < array_search($page_extension, $page_extensions, true)) {
  937. $page_found = $file;
  938. $page_extension = $ext;
  939. }
  940. }
  941. }
  942. $content_exists = false;
  943. if ($parent && $page_found) {
  944. $page->init($page_found, $page_extension);
  945. $content_exists = true;
  946. if ($config->get('system.pages.events.page')) {
  947. $this->grav->fireEvent('onPageProcessed', new Event(['page' => $page]));
  948. }
  949. }
  950. // Now handle all the folders under the page.
  951. /** @var \FilesystemIterator $file */
  952. foreach ($folders as $file) {
  953. $filename = $file->getFilename();
  954. // if folder contains separator, continue
  955. if (Utils::contains($file->getFilename(), $config->get('system.param_sep', ':'))) {
  956. continue;
  957. }
  958. if (!$page->path()) {
  959. $page->path($file->getPath());
  960. }
  961. $path = $directory . DS . $filename;
  962. $child = $this->recurse($path, $page);
  963. if (Utils::startsWith($filename, '_')) {
  964. $child->routable(false);
  965. }
  966. $this->children[$page->path()][$child->path()] = ['slug' => $child->slug()];
  967. if ($config->get('system.pages.events.page')) {
  968. $this->grav->fireEvent('onFolderProcessed', new Event(['page' => $page]));
  969. }
  970. }
  971. if (!$content_exists) {
  972. // Set routability to false if no page found
  973. $page->routable(false);
  974. // Hide empty folders if option set
  975. if ($config->get('system.pages.hide_empty_folders')) {
  976. $page->visible(false);
  977. }
  978. }
  979. // Override the modified time if modular
  980. if ($page->template() === 'modular') {
  981. foreach ($page->collection() as $child) {
  982. $modified = $child->modified();
  983. if ($modified > $last_modified) {
  984. $last_modified = $modified;
  985. }
  986. }
  987. }
  988. // Override the modified and ID so that it takes the latest change into account
  989. $page->modified($last_modified);
  990. $page->id($last_modified . md5($page->filePath()));
  991. // Sort based on Defaults or Page Overridden sort order
  992. $this->children[$page->path()] = $this->sort($page);
  993. return $page;
  994. }
  995. /**
  996. * @internal
  997. */
  998. protected function buildRoutes()
  999. {
  1000. /** @var Taxonomy $taxonomy */
  1001. $taxonomy = $this->grav['taxonomy'];
  1002. // Get the home route
  1003. $home = self::resetHomeRoute();
  1004. // Build routes and taxonomy map.
  1005. /** @var PageInterface $page */
  1006. foreach ($this->instances as $page) {
  1007. if (!$page->root()) {
  1008. // process taxonomy
  1009. $taxonomy->addTaxonomy($page);
  1010. $route = $page->route();
  1011. $raw_route = $page->rawRoute();
  1012. $page_path = $page->path();
  1013. // add regular route
  1014. $this->routes[$route] = $page_path;
  1015. // add raw route
  1016. if ($raw_route !== $route) {
  1017. $this->routes[$raw_route] = $page_path;
  1018. }
  1019. // add canonical route
  1020. $route_canonical = $page->routeCanonical();
  1021. if ($route_canonical && ($route !== $route_canonical)) {
  1022. $this->routes[$route_canonical] = $page_path;
  1023. }
  1024. // add aliases to routes list if they are provided
  1025. $route_aliases = $page->routeAliases();
  1026. if ($route_aliases) {
  1027. foreach ($route_aliases as $alias) {
  1028. $this->routes[$alias] = $page_path;
  1029. }
  1030. }
  1031. }
  1032. }
  1033. // Alias and set default route to home page.
  1034. $homeRoute = '/' . $home;
  1035. if ($home && isset($this->routes[$homeRoute])) {
  1036. $this->routes['/'] = $this->routes[$homeRoute];
  1037. $this->get($this->routes[$homeRoute])->route('/');
  1038. }
  1039. }
  1040. /**
  1041. * @param string $path
  1042. * @param array $pages
  1043. * @param string $order_by
  1044. * @param array|null $manual
  1045. * @param int|null $sort_flags
  1046. *
  1047. * @throws \RuntimeException
  1048. * @internal
  1049. */
  1050. protected function buildSort($path, array $pages, $order_by = 'default', $manual = null, $sort_flags = null)
  1051. {
  1052. $list = [];
  1053. $header_default = null;
  1054. $header_query = null;
  1055. // do this header query work only once
  1056. if (strpos($order_by, 'header.') === 0) {
  1057. $header_query = explode('|', str_replace('header.', '', $order_by));
  1058. if (isset($header_query[1])) {
  1059. $header_default = $header_query[1];
  1060. }
  1061. }
  1062. foreach ($pages as $key => $info) {
  1063. $child = $this->instances[$key] ?? null;
  1064. if (!$child) {
  1065. throw new \RuntimeException("Page does not exist: {$key}");
  1066. }
  1067. switch ($order_by) {
  1068. case 'title':
  1069. $list[$key] = $child->title();
  1070. break;
  1071. case 'date':
  1072. $list[$key] = $child->date();
  1073. $sort_flags = SORT_REGULAR;
  1074. break;
  1075. case 'modified':
  1076. $list[$key] = $child->modified();
  1077. $sort_flags = SORT_REGULAR;
  1078. break;
  1079. case 'publish_date':
  1080. $list[$key] = $child->publishDate();
  1081. $sort_flags = SORT_REGULAR;
  1082. break;
  1083. case 'unpublish_date':
  1084. $list[$key] = $child->unpublishDate();
  1085. $sort_flags = SORT_REGULAR;
  1086. break;
  1087. case 'slug':
  1088. $list[$key] = $child->slug();
  1089. break;
  1090. case 'basename':
  1091. $list[$key] = basename($key);
  1092. break;
  1093. case 'folder':
  1094. $list[$key] = $child->folder();
  1095. break;
  1096. case (is_string($header_query[0])):
  1097. $child_header = new Header((array)$child->header());
  1098. $header_value = $child_header->get($header_query[0]);
  1099. if (is_array($header_value)) {
  1100. $list[$key] = implode(',',$header_value);
  1101. } elseif ($header_value) {
  1102. $list[$key] = $header_value;
  1103. } else {
  1104. $list[$key] = $header_default ?: $key;
  1105. }
  1106. $sort_flags = $sort_flags ?: SORT_REGULAR;
  1107. break;
  1108. case 'manual':
  1109. case 'default':
  1110. default:
  1111. $list[$key] = $key;
  1112. $sort_flags = $sort_flags ?: SORT_REGULAR;
  1113. }
  1114. }
  1115. if (!$sort_flags) {
  1116. $sort_flags = SORT_NATURAL | SORT_FLAG_CASE;
  1117. }
  1118. // handle special case when order_by is random
  1119. if ($order_by === 'random') {
  1120. $list = $this->arrayShuffle($list);
  1121. } else {
  1122. // else just sort the list according to specified key
  1123. if (extension_loaded('intl') && $this->grav['config']->get('system.intl_enabled')) {
  1124. $locale = setlocale(LC_COLLATE, 0); //`setlocale` with a 0 param returns the current locale set
  1125. $col = Collator::create($locale);
  1126. if ($col) {
  1127. if (($sort_flags & SORT_NATURAL) === SORT_NATURAL) {
  1128. $list = preg_replace_callback('~([0-9]+)\.~', function($number) {
  1129. return sprintf('%032d.', $number[0]);
  1130. }, $list);
  1131. $list_vals = array_values($list);
  1132. if (is_numeric(array_shift($list_vals))) {
  1133. $sort_flags = Collator::SORT_REGULAR;
  1134. } else {
  1135. $sort_flags = Collator::SORT_STRING;
  1136. }
  1137. }
  1138. $col->asort($list, $sort_flags);
  1139. } else {
  1140. asort($list, $sort_flags);
  1141. }
  1142. } else {
  1143. asort($list, $sort_flags);
  1144. }
  1145. }
  1146. // Move manually ordered items into the beginning of the list. Order of the unlisted items does not change.
  1147. if (is_array($manual) && !empty($manual)) {
  1148. $new_list = [];
  1149. $i = count($manual);
  1150. foreach ($list as $key => $dummy) {
  1151. $info = $pages[$key];
  1152. $order = \array_search($info['slug'], $manual, true);
  1153. if ($order === false) {
  1154. $order = $i++;
  1155. }
  1156. $new_list[$key] = (int)$order;
  1157. }
  1158. $list = $new_list;
  1159. // Apply manual ordering to the list.
  1160. asort($list);
  1161. }
  1162. foreach ($list as $key => $sort) {
  1163. $info = $pages[$key];
  1164. $this->sort[$path][$order_by][$key] = $info;
  1165. }
  1166. }
  1167. /**
  1168. * Shuffles an associative array
  1169. *
  1170. * @param array $list
  1171. *
  1172. * @return array
  1173. */
  1174. protected function arrayShuffle($list)
  1175. {
  1176. $keys = array_keys($list);
  1177. shuffle($keys);
  1178. $new = [];
  1179. foreach ($keys as $key) {
  1180. $new[$key] = $list[$key];
  1181. }
  1182. return $new;
  1183. }
  1184. /**
  1185. * Get the Pages cache ID
  1186. *
  1187. * this is particularly useful to know if pages have changed and you want
  1188. * to sync another cache with pages cache - works best in `onPagesInitialized()`
  1189. *
  1190. * @return mixed
  1191. */
  1192. public function getPagesCacheId()
  1193. {
  1194. return $this->pages_cache_id;
  1195. }
  1196. }