Pages.php 39 KB

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