Pages.php 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428
  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. $cached = $cache->fetch($this->pages_cache_id);
  825. if ($cached) {
  826. $this->grav['debugger']->addMessage('Page cache hit.');
  827. list($this->instances, $this->routes, $this->children, $taxonomy_map, $this->sort) = $cached;
  828. // If pages was found in cache, set the taxonomy
  829. $taxonomy->taxonomy($taxonomy_map);
  830. } else {
  831. $this->grav['debugger']->addMessage('Page cache missed, rebuilding pages..');
  832. // recurse pages and cache result
  833. $this->resetPages($pages_dir);
  834. }
  835. } else {
  836. $this->recurse($pages_dir);
  837. $this->buildRoutes();
  838. }
  839. }
  840. /**
  841. * Accessible method to manually reset the pages cache
  842. *
  843. * @param string $pages_dir
  844. */
  845. public function resetPages($pages_dir)
  846. {
  847. $this->recurse($pages_dir);
  848. $this->buildRoutes();
  849. // cache if needed
  850. if ($this->grav['config']->get('system.cache.enabled')) {
  851. /** @var Cache $cache */
  852. $cache = $this->grav['cache'];
  853. /** @var Taxonomy $taxonomy */
  854. $taxonomy = $this->grav['taxonomy'];
  855. // save pages, routes, taxonomy, and sort to cache
  856. $cache->save($this->pages_cache_id, [$this->instances, $this->routes, $this->children, $taxonomy->taxonomy(), $this->sort]);
  857. }
  858. }
  859. /**
  860. * Recursive function to load & build page relationships.
  861. *
  862. * @param string $directory
  863. * @param PageInterface|null $parent
  864. *
  865. * @return PageInterface
  866. * @throws \RuntimeException
  867. * @internal
  868. */
  869. protected function recurse($directory, PageInterface $parent = null)
  870. {
  871. $directory = rtrim($directory, DS);
  872. $page = new Page;
  873. /** @var Config $config */
  874. $config = $this->grav['config'];
  875. /** @var Language $language */
  876. $language = $this->grav['language'];
  877. // Stuff to do at root page
  878. // Fire event for memory and time consuming plugins...
  879. if ($parent === null && $config->get('system.pages.events.page')) {
  880. $this->grav->fireEvent('onBuildPagesInitialized');
  881. }
  882. $page->path($directory);
  883. if ($parent) {
  884. $page->parent($parent);
  885. }
  886. $page->orderDir($config->get('system.pages.order.dir'));
  887. $page->orderBy($config->get('system.pages.order.by'));
  888. // Add into instances
  889. if (!isset($this->instances[$page->path()])) {
  890. $this->instances[$page->path()] = $page;
  891. if ($parent && $page->path()) {
  892. $this->children[$parent->path()][$page->path()] = ['slug' => $page->slug()];
  893. }
  894. } else {
  895. throw new \RuntimeException('Fatal error when creating page instances.');
  896. }
  897. // Build regular expression for all the allowed page extensions.
  898. $page_extensions = $language->getFallbackPageExtensions();
  899. $regex = '/^[^\.]*(' . implode('|', array_map(
  900. function ($str) {
  901. return preg_quote($str, '/');
  902. },
  903. $page_extensions
  904. )) . ')$/';
  905. $folders = [];
  906. $page_found = null;
  907. $page_extension = '.md';
  908. $last_modified = 0;
  909. $iterator = new \FilesystemIterator($directory);
  910. /** @var \FilesystemIterator $file */
  911. foreach ($iterator as $file) {
  912. $filename = $file->getFilename();
  913. // Ignore all hidden files if set.
  914. if ($this->ignore_hidden && $filename && strpos($filename, '.') === 0) {
  915. continue;
  916. }
  917. // Handle folders later.
  918. if ($file->isDir()) {
  919. // But ignore all folders in ignore list.
  920. if (!\in_array($filename, $this->ignore_folders, true)) {
  921. $folders[] = $file;
  922. }
  923. continue;
  924. }
  925. // Ignore all files in ignore list.
  926. if (\in_array($filename, $this->ignore_files, true)) {
  927. continue;
  928. }
  929. // Update last modified date to match the last updated file in the folder.
  930. $modified = $file->getMTime();
  931. if ($modified > $last_modified) {
  932. $last_modified = $modified;
  933. }
  934. // Page is the one that matches to $page_extensions list with the lowest index number.
  935. if (preg_match($regex, $filename, $matches, PREG_OFFSET_CAPTURE)) {
  936. $ext = $matches[1][0];
  937. if ($page_found === null || array_search($ext, $page_extensions, true) < array_search($page_extension, $page_extensions, true)) {
  938. $page_found = $file;
  939. $page_extension = $ext;
  940. }
  941. }
  942. }
  943. $content_exists = false;
  944. if ($parent && $page_found) {
  945. $page->init($page_found, $page_extension);
  946. $content_exists = true;
  947. if ($config->get('system.pages.events.page')) {
  948. $this->grav->fireEvent('onPageProcessed', new Event(['page' => $page]));
  949. }
  950. }
  951. // Now handle all the folders under the page.
  952. /** @var \FilesystemIterator $file */
  953. foreach ($folders as $file) {
  954. $filename = $file->getFilename();
  955. // if folder contains separator, continue
  956. if (Utils::contains($file->getFilename(), $config->get('system.param_sep', ':'))) {
  957. continue;
  958. }
  959. if (!$page->path()) {
  960. $page->path($file->getPath());
  961. }
  962. $path = $directory . DS . $filename;
  963. $child = $this->recurse($path, $page);
  964. if (Utils::startsWith($filename, '_')) {
  965. $child->routable(false);
  966. }
  967. $this->children[$page->path()][$child->path()] = ['slug' => $child->slug()];
  968. if ($config->get('system.pages.events.page')) {
  969. $this->grav->fireEvent('onFolderProcessed', new Event(['page' => $page]));
  970. }
  971. }
  972. if (!$content_exists) {
  973. // Set routability to false if no page found
  974. $page->routable(false);
  975. // Hide empty folders if option set
  976. if ($config->get('system.pages.hide_empty_folders')) {
  977. $page->visible(false);
  978. }
  979. }
  980. // Override the modified time if modular
  981. if ($page->template() === 'modular') {
  982. foreach ($page->collection() as $child) {
  983. $modified = $child->modified();
  984. if ($modified > $last_modified) {
  985. $last_modified = $modified;
  986. }
  987. }
  988. }
  989. // Override the modified and ID so that it takes the latest change into account
  990. $page->modified($last_modified);
  991. $page->id($last_modified . md5($page->filePath()));
  992. // Sort based on Defaults or Page Overridden sort order
  993. $this->children[$page->path()] = $this->sort($page);
  994. return $page;
  995. }
  996. /**
  997. * @internal
  998. */
  999. protected function buildRoutes()
  1000. {
  1001. /** @var Taxonomy $taxonomy */
  1002. $taxonomy = $this->grav['taxonomy'];
  1003. // Get the home route
  1004. $home = self::resetHomeRoute();
  1005. // Build routes and taxonomy map.
  1006. /** @var PageInterface $page */
  1007. foreach ($this->instances as $page) {
  1008. if (!$page->root()) {
  1009. // process taxonomy
  1010. $taxonomy->addTaxonomy($page);
  1011. $route = $page->route();
  1012. $raw_route = $page->rawRoute();
  1013. $page_path = $page->path();
  1014. // add regular route
  1015. $this->routes[$route] = $page_path;
  1016. // add raw route
  1017. if ($raw_route !== $route) {
  1018. $this->routes[$raw_route] = $page_path;
  1019. }
  1020. // add canonical route
  1021. $route_canonical = $page->routeCanonical();
  1022. if ($route_canonical && ($route !== $route_canonical)) {
  1023. $this->routes[$route_canonical] = $page_path;
  1024. }
  1025. // add aliases to routes list if they are provided
  1026. $route_aliases = $page->routeAliases();
  1027. if ($route_aliases) {
  1028. foreach ($route_aliases as $alias) {
  1029. $this->routes[$alias] = $page_path;
  1030. }
  1031. }
  1032. }
  1033. }
  1034. // Alias and set default route to home page.
  1035. $homeRoute = '/' . $home;
  1036. if ($home && isset($this->routes[$homeRoute])) {
  1037. $this->routes['/'] = $this->routes[$homeRoute];
  1038. $this->get($this->routes[$homeRoute])->route('/');
  1039. }
  1040. }
  1041. /**
  1042. * @param string $path
  1043. * @param array $pages
  1044. * @param string $order_by
  1045. * @param array|null $manual
  1046. * @param int|null $sort_flags
  1047. *
  1048. * @throws \RuntimeException
  1049. * @internal
  1050. */
  1051. protected function buildSort($path, array $pages, $order_by = 'default', $manual = null, $sort_flags = null)
  1052. {
  1053. $list = [];
  1054. $header_default = null;
  1055. $header_query = [];
  1056. // do this header query work only once
  1057. if (strpos($order_by, 'header.') === 0) {
  1058. $query = explode('|', str_replace('header.', '', $order_by), 2);
  1059. $header_query = array_shift($query) ?? '';
  1060. $header_default = array_shift($query);
  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 'manual':
  1097. case 'default':
  1098. default:
  1099. if (is_string($header_query)) {
  1100. $child_header = $child->header();
  1101. if (!$child_header instanceof Header) {
  1102. $child_header = new Header((array)$child_header);
  1103. }
  1104. $header_value = $child_header->get($header_query);
  1105. if (is_array($header_value)) {
  1106. $list[$key] = implode(',', $header_value);
  1107. } elseif ($header_value) {
  1108. $list[$key] = $header_value;
  1109. } else {
  1110. $list[$key] = $header_default ?: $key;
  1111. }
  1112. $sort_flags = $sort_flags ?: SORT_REGULAR;
  1113. break;
  1114. }
  1115. $list[$key] = $key;
  1116. $sort_flags = $sort_flags ?: SORT_REGULAR;
  1117. }
  1118. }
  1119. if (!$sort_flags) {
  1120. $sort_flags = SORT_NATURAL | SORT_FLAG_CASE;
  1121. }
  1122. // handle special case when order_by is random
  1123. if ($order_by === 'random') {
  1124. $list = $this->arrayShuffle($list);
  1125. } else {
  1126. // else just sort the list according to specified key
  1127. if (extension_loaded('intl') && $this->grav['config']->get('system.intl_enabled')) {
  1128. $locale = setlocale(LC_COLLATE, 0); //`setlocale` with a 0 param returns the current locale set
  1129. $col = Collator::create($locale);
  1130. if ($col) {
  1131. if (($sort_flags & SORT_NATURAL) === SORT_NATURAL) {
  1132. $list = preg_replace_callback('~([0-9]+)\.~', function($number) {
  1133. return sprintf('%032d.', $number[0]);
  1134. }, $list);
  1135. $list_vals = array_values($list);
  1136. if (is_numeric(array_shift($list_vals))) {
  1137. $sort_flags = Collator::SORT_REGULAR;
  1138. } else {
  1139. $sort_flags = Collator::SORT_STRING;
  1140. }
  1141. }
  1142. $col->asort($list, $sort_flags);
  1143. } else {
  1144. asort($list, $sort_flags);
  1145. }
  1146. } else {
  1147. asort($list, $sort_flags);
  1148. }
  1149. }
  1150. // Move manually ordered items into the beginning of the list. Order of the unlisted items does not change.
  1151. if (is_array($manual) && !empty($manual)) {
  1152. $new_list = [];
  1153. $i = count($manual);
  1154. foreach ($list as $key => $dummy) {
  1155. $info = $pages[$key];
  1156. $order = \array_search($info['slug'], $manual, true);
  1157. if ($order === false) {
  1158. $order = $i++;
  1159. }
  1160. $new_list[$key] = (int)$order;
  1161. }
  1162. $list = $new_list;
  1163. // Apply manual ordering to the list.
  1164. asort($list);
  1165. }
  1166. foreach ($list as $key => $sort) {
  1167. $info = $pages[$key];
  1168. $this->sort[$path][$order_by][$key] = $info;
  1169. }
  1170. }
  1171. /**
  1172. * Shuffles an associative array
  1173. *
  1174. * @param array $list
  1175. *
  1176. * @return array
  1177. */
  1178. protected function arrayShuffle($list)
  1179. {
  1180. $keys = array_keys($list);
  1181. shuffle($keys);
  1182. $new = [];
  1183. foreach ($keys as $key) {
  1184. $new[$key] = $list[$key];
  1185. }
  1186. return $new;
  1187. }
  1188. /**
  1189. * Get the Pages cache ID
  1190. *
  1191. * this is particularly useful to know if pages have changed and you want
  1192. * to sync another cache with pages cache - works best in `onPagesInitialized()`
  1193. *
  1194. * @return mixed
  1195. */
  1196. public function getPagesCacheId()
  1197. {
  1198. return $this->pages_cache_id;
  1199. }
  1200. }