Pages.php 40 KB

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