PageRenderTime 28ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/system/src/Grav/Common/Page/Pages.php

https://gitlab.com/x33n/grav
PHP | 727 lines | 414 code | 107 blank | 206 comment | 69 complexity | a19c84951710ccce7084fb950a126dfd MD5 | raw file
  1. <?php
  2. namespace Grav\Common\Page;
  3. use Grav\Common\Grav;
  4. use Grav\Common\Config\Config;
  5. use Grav\Common\Utils;
  6. use Grav\Common\Cache;
  7. use Grav\Common\Taxonomy;
  8. use Grav\Common\Data\Blueprint;
  9. use Grav\Common\Data\Blueprints;
  10. use Grav\Common\Filesystem\Folder;
  11. use RocketTheme\Toolbox\Event\Event;
  12. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  13. /**
  14. * GravPages is the class that is the entry point into the hierarchy of pages
  15. *
  16. * @author RocketTheme
  17. * @license MIT
  18. */
  19. class Pages
  20. {
  21. /**
  22. * @var Grav
  23. */
  24. protected $grav;
  25. /**
  26. * @var array|Page[]
  27. */
  28. protected $instances;
  29. /**
  30. * @var array|string[]
  31. */
  32. protected $children;
  33. /**
  34. * @var string
  35. */
  36. protected $base;
  37. /**
  38. * @var array|string[]
  39. */
  40. protected $routes = array();
  41. /**
  42. * @var array
  43. */
  44. protected $sort;
  45. /**
  46. * @var Blueprints
  47. */
  48. protected $blueprints;
  49. /**
  50. * @var int
  51. */
  52. protected $last_modified;
  53. /**
  54. * @var Types
  55. */
  56. static protected $types;
  57. /**
  58. * Constructor
  59. *
  60. * @param Grav $c
  61. */
  62. public function __construct(Grav $c)
  63. {
  64. $this->grav = $c;
  65. $this->base = '';
  66. }
  67. /**
  68. * Get or set base path for the pages.
  69. *
  70. * @param string $path
  71. * @return string
  72. */
  73. public function base($path = null)
  74. {
  75. if ($path !== null) {
  76. $path = trim($path, '/');
  77. $this->base = $path ? '/' . $path : null;
  78. }
  79. return $this->base;
  80. }
  81. /**
  82. * Class initialization. Must be called before using this class.
  83. */
  84. public function init()
  85. {
  86. $this->buildPages();
  87. }
  88. /**
  89. * Get or set last modification time.
  90. *
  91. * @param int $modified
  92. * @return int|null
  93. */
  94. public function lastModified($modified = null)
  95. {
  96. if ($modified && $modified > $this->last_modified) {
  97. $this->last_modified = $modified;
  98. }
  99. return $this->last_modified;
  100. }
  101. /**
  102. * Returns a list of all pages.
  103. *
  104. * @return Page
  105. */
  106. public function instances()
  107. {
  108. return $this->instances;
  109. }
  110. /**
  111. * Returns a list of all routes.
  112. *
  113. * @return array
  114. */
  115. public function routes()
  116. {
  117. return $this->routes;
  118. }
  119. /**
  120. * Adds a page and assigns a route to it.
  121. *
  122. * @param Page $page Page to be added.
  123. * @param string $route Optional route (uses route from the object if not set).
  124. */
  125. public function addPage(Page $page, $route = null)
  126. {
  127. if (!isset($this->instances[$page->path()])) {
  128. $this->instances[$page->path()] = $page;
  129. }
  130. $route = $page->route($route);
  131. if ($page->parent()) {
  132. $this->children[$page->parent()->path()][$page->path()] = array('slug' => $page->slug());
  133. }
  134. $this->routes[$route] = $page->path();
  135. }
  136. /**
  137. * Sort sub-pages in a page.
  138. *
  139. * @param Page $page
  140. * @param string $order_by
  141. * @param string $order_dir
  142. *
  143. * @return array
  144. */
  145. public function sort(Page $page, $order_by = null, $order_dir = null)
  146. {
  147. if ($order_by === null) {
  148. $order_by = $page->orderBy();
  149. }
  150. if ($order_dir === null) {
  151. $order_dir = $page->orderDir();
  152. }
  153. $path = $page->path();
  154. $children = isset($this->children[$path]) ? $this->children[$path] : array();
  155. if (!$children) {
  156. return $children;
  157. }
  158. if (!isset($this->sort[$path][$order_by])) {
  159. $this->buildSort($path, $children, $order_by, $page->orderManual());
  160. }
  161. $sort = $this->sort[$path][$order_by];
  162. if ($order_dir != 'asc') {
  163. $sort = array_reverse($sort);
  164. }
  165. return $sort;
  166. }
  167. /**
  168. * @param Collection $collection
  169. * @param $orderBy
  170. * @param string $orderDir
  171. * @param null $orderManual
  172. * @return array
  173. * @internal
  174. */
  175. public function sortCollection(Collection $collection, $orderBy, $orderDir = 'asc', $orderManual = null)
  176. {
  177. $items = $collection->toArray();
  178. if (!$items) {
  179. return [];
  180. }
  181. $lookup = md5(json_encode($items));
  182. if (!isset($this->sort[$lookup][$orderBy])) {
  183. $this->buildSort($lookup, $items, $orderBy, $orderManual);
  184. }
  185. $sort = $this->sort[$lookup][$orderBy];
  186. if ($orderDir != 'asc') {
  187. $sort = array_reverse($sort);
  188. }
  189. return $sort;
  190. }
  191. /**
  192. * Get a page instance.
  193. *
  194. * @param string $path
  195. * @return Page
  196. * @throws \Exception
  197. */
  198. public function get($path)
  199. {
  200. if (!is_null($path) && !is_string($path)) {
  201. throw new \Exception();
  202. }
  203. return isset($this->instances[(string) $path]) ? $this->instances[(string) $path] : null;
  204. }
  205. /**
  206. * Get children of the path.
  207. *
  208. * @param string $path
  209. * @return Collection
  210. */
  211. public function children($path)
  212. {
  213. $children = isset($this->children[(string) $path]) ? $this->children[(string) $path] : array();
  214. return new Collection($children, array(), $this);
  215. }
  216. /**
  217. * Dispatch URI to a page.
  218. *
  219. * @param $url
  220. * @param bool $all
  221. * @return Page|null
  222. */
  223. public function dispatch($url, $all = false)
  224. {
  225. // Fetch page if there's a defined route to it.
  226. $page = isset($this->routes[$url]) ? $this->get($this->routes[$url]) : null;
  227. // If the page cannot be reached, look into site wide redirects, routes + wildcards
  228. if (!$all && (!$page || !$page->routable())) {
  229. /** @var Config $config */
  230. $config = $this->grav['config'];
  231. // Try redirects
  232. $redirect = $config->get("site.redirects.{$url}");
  233. if ($redirect) {
  234. $this->grav->redirect($redirect);
  235. }
  236. // See if route matches one in the site configuration
  237. $route = $config->get("site.routes.{$url}");
  238. if ($route) {
  239. $page = $this->dispatch($route, $all);
  240. } else {
  241. // Try looking for wildcards
  242. foreach ($config->get("site.routes") as $alias => $route) {
  243. $match = rtrim($alias, '*');
  244. if (strpos($alias, '*') !== false && strpos($url, $match) !== false) {
  245. $wildcard_url = str_replace('*', str_replace($match, '', $url), $route);
  246. $page = isset($this->routes[$wildcard_url]) ? $this->get($this->routes[$wildcard_url]) : null;
  247. if ($page) {
  248. return $page;
  249. }
  250. }
  251. }
  252. }
  253. }
  254. return $page;
  255. }
  256. /**
  257. * Get root page.
  258. *
  259. * @return Page
  260. */
  261. public function root()
  262. {
  263. /** @var UniformResourceLocator $locator */
  264. $locator = $this->grav['locator'];
  265. return $this->instances[rtrim($locator->findResource('page://'), DS)];
  266. }
  267. /**
  268. * Get a blueprint for a page type.
  269. *
  270. * @param string $type
  271. * @return Blueprint
  272. */
  273. public function blueprints($type)
  274. {
  275. if (!isset($this->blueprints)) {
  276. $this->blueprints = new Blueprints(self::getTypes());
  277. }
  278. try {
  279. $blueprint = $this->blueprints->get($type);
  280. } catch (\RuntimeException $e) {
  281. $blueprint = $this->blueprints->get('default');
  282. }
  283. if (!$blueprint->initialized) {
  284. $this->grav->fireEvent('onBlueprintCreated', new Event(['blueprint' => $blueprint]));
  285. $blueprint->initialized = true;
  286. }
  287. return $blueprint;
  288. }
  289. /**
  290. * Get list of route/title of all pages.
  291. *
  292. * @param Page $current
  293. * @param int $level
  294. * @return array
  295. * @throws \RuntimeException
  296. */
  297. public function getList(Page $current = null, $level = 0)
  298. {
  299. if (!$current) {
  300. if ($level) {
  301. throw new \RuntimeException('Internal error');
  302. }
  303. $current = $this->root();
  304. }
  305. $list = array();
  306. if ($current->routable()) {
  307. $list[$current->route()] = str_repeat('&nbsp; ', ($level-1)*2) . $current->title();
  308. }
  309. foreach ($current->children() as $next) {
  310. $list = array_merge($list, $this->getList($next, $level + 1));
  311. }
  312. return $list;
  313. }
  314. /**
  315. * Get available page types.
  316. *
  317. * @return Types
  318. */
  319. public static function getTypes()
  320. {
  321. if (!self::$types) {
  322. self::$types = new Types();
  323. self::$types->scanBlueprints('theme://blueprints/');
  324. self::$types->scanTemplates('theme://templates/');
  325. $event = new Event();
  326. $event->types = self::$types;
  327. Grav::instance()->fireEvent('onGetPageTemplates', $event);
  328. }
  329. return self::$types;
  330. }
  331. /**
  332. * Get available page types.
  333. *
  334. * @return array
  335. */
  336. public static function types()
  337. {
  338. $types = self::getTypes();
  339. return $types->pageSelect();
  340. }
  341. /**
  342. * Get available page types.
  343. *
  344. * @return array
  345. */
  346. public static function modularTypes()
  347. {
  348. $types = self::getTypes();
  349. return $types->modularSelect();
  350. }
  351. /**
  352. * Get available parents.
  353. *
  354. * @return array
  355. */
  356. public static function parents()
  357. {
  358. $grav = Grav::instance();
  359. /** @var Pages $pages */
  360. $pages = $grav['pages'];
  361. return $pages->getList();
  362. }
  363. /**
  364. * Builds pages.
  365. *
  366. * @internal
  367. */
  368. protected function buildPages()
  369. {
  370. $this->sort = array();
  371. /** @var Config $config */
  372. $config = $this->grav['config'];
  373. /** @var UniformResourceLocator $locator */
  374. $locator = $this->grav['locator'];
  375. $pagesDir = $locator->findResource('page://');
  376. if ($config->get('system.cache.enabled')) {
  377. /** @var Cache $cache */
  378. $cache = $this->grav['cache'];
  379. /** @var Taxonomy $taxonomy */
  380. $taxonomy = $this->grav['taxonomy'];
  381. // how should we check for last modified? Default is by file
  382. switch (strtolower($config->get('system.cache.check.method', 'file'))) {
  383. case 'none':
  384. case 'off':
  385. $last_modified = 0;
  386. break;
  387. case 'folder':
  388. $last_modified = Folder::lastModifiedFolder($pagesDir);
  389. break;
  390. default:
  391. $last_modified = Folder::lastModifiedFile($pagesDir);
  392. }
  393. $page_cache_id = md5(USER_DIR.$last_modified.$config->checksum());
  394. list($this->instances, $this->routes, $this->children, $taxonomy_map, $this->sort) = $cache->fetch($page_cache_id);
  395. if (!$this->instances) {
  396. $this->grav['debugger']->addMessage('Page cache missed, rebuilding pages..');
  397. $this->recurse($pagesDir);
  398. $this->buildRoutes();
  399. // save pages, routes, taxonomy, and sort to cache
  400. $cache->save(
  401. $page_cache_id,
  402. array($this->instances, $this->routes, $this->children, $taxonomy->taxonomy(), $this->sort)
  403. );
  404. } else {
  405. // If pages was found in cache, set the taxonomy
  406. $this->grav['debugger']->addMessage('Page cache hit.');
  407. $taxonomy->taxonomy($taxonomy_map);
  408. }
  409. } else {
  410. $this->recurse($pagesDir);
  411. $this->buildRoutes();
  412. }
  413. }
  414. /**
  415. * Recursive function to load & build page relationships.
  416. *
  417. * @param string $directory
  418. * @param Page|null $parent
  419. * @return Page
  420. * @throws \RuntimeException
  421. * @internal
  422. */
  423. protected function recurse($directory, Page &$parent = null)
  424. {
  425. $directory = rtrim($directory, DS);
  426. $iterator = new \DirectoryIterator($directory);
  427. $page = new Page;
  428. /** @var Config $config */
  429. $config = $this->grav['config'];
  430. $page->path($directory);
  431. if ($parent) {
  432. $page->parent($parent);
  433. }
  434. $page->orderDir($config->get('system.pages.order.dir'));
  435. $page->orderBy($config->get('system.pages.order.by'));
  436. // Add into instances
  437. if (!isset($this->instances[$page->path()])) {
  438. $this->instances[$page->path()] = $page;
  439. if ($parent && $page->path()) {
  440. $this->children[$parent->path()][$page->path()] = array('slug' => $page->slug());
  441. }
  442. } else {
  443. throw new \RuntimeException('Fatal error when creating page instances.');
  444. }
  445. // set current modified of page
  446. $last_modified = $page->modified();
  447. // flat for content availability
  448. $content_exists = false;
  449. /** @var \DirectoryIterator $file */
  450. foreach ($iterator as $file) {
  451. if ($file->isDot()) {
  452. continue;
  453. }
  454. $name = $file->getFilename();
  455. if ($file->isFile()) {
  456. // Update the last modified if it's newer than already found
  457. if ($file->getBasename() !== '.DS_Store' && ($modified = $file->getMTime()) > $last_modified) {
  458. $last_modified = $modified;
  459. }
  460. if (Utils::endsWith($name, CONTENT_EXT)) {
  461. $page->init($file);
  462. $content_exists = true;
  463. if ($config->get('system.pages.events.page')) {
  464. $this->grav->fireEvent('onPageProcessed', new Event(['page' => $page]));
  465. }
  466. }
  467. } elseif ($file->isDir()) {
  468. if (!$page->path()) {
  469. $page->path($file->getPath());
  470. }
  471. $path = $directory.DS.$name;
  472. $child = $this->recurse($path, $page);
  473. if (Utils::startsWith($name, '_')) {
  474. $child->routable(false);
  475. }
  476. $this->children[$page->path()][$child->path()] = array('slug' => $child->slug());
  477. if ($config->get('system.pages.events.page')) {
  478. $this->grav->fireEvent('onFolderProcessed', new Event(['page' => $page]));
  479. }
  480. }
  481. }
  482. // Set routability to false if no page found
  483. if (!$content_exists) {
  484. $page->routable(false);
  485. }
  486. // Override the modified and ID so that it takes the latest change into account
  487. $page->modified($last_modified);
  488. $page->id($last_modified.md5($page->filePath()));
  489. // Sort based on Defaults or Page Overridden sort order
  490. $this->children[$page->path()] = $this->sort($page);
  491. return $page;
  492. }
  493. /**
  494. * @internal
  495. */
  496. protected function buildRoutes()
  497. {
  498. /** @var $taxonomy Taxonomy */
  499. $taxonomy = $this->grav['taxonomy'];
  500. // Build routes and taxonomy map.
  501. /** @var $page Page */
  502. foreach ($this->instances as $page) {
  503. $parent = $page->parent();
  504. if ($parent) {
  505. $route = rtrim($parent->route(), '/') . '/' . $page->slug();
  506. $this->routes[$route] = $page->path();
  507. $page->route($route);
  508. }
  509. if (!empty($route)) {
  510. $taxonomy->addTaxonomy($page);
  511. } else {
  512. $page->routable(false);
  513. }
  514. }
  515. /** @var Config $config */
  516. $config = $this->grav['config'];
  517. // Alias and set default route to home page.
  518. $home = trim($config->get('system.home.alias'), '/');
  519. if ($home && isset($this->routes['/' . $home])) {
  520. $this->routes['/'] = $this->routes['/' . $home];
  521. $this->get($this->routes['/' . $home])->route('/');
  522. }
  523. }
  524. /**
  525. * @param string $path
  526. * @param array $pages
  527. * @param string $order_by
  528. * @param array $manual
  529. * @throws \RuntimeException
  530. * @internal
  531. */
  532. protected function buildSort($path, array $pages, $order_by = 'default', $manual = null)
  533. {
  534. $list = array();
  535. $header_default = null;
  536. $header_query = null;
  537. // do this headery query work only once
  538. if (strpos($order_by, 'header.') === 0) {
  539. $header_query = explode('|', str_replace('header.', '', $order_by));
  540. if (isset($header_query[1])) {
  541. $header_default = $header_query[1];
  542. }
  543. }
  544. foreach ($pages as $key => $info) {
  545. $child = isset($this->instances[$key]) ? $this->instances[$key] : null;
  546. if (!$child) {
  547. throw new \RuntimeException("Page does not exist: {$key}");
  548. }
  549. switch ($order_by) {
  550. case 'title':
  551. $list[$key] = $child->title();
  552. break;
  553. case 'date':
  554. $list[$key] = $child->date();
  555. break;
  556. case 'modified':
  557. $list[$key] = $child->modified();
  558. break;
  559. case 'slug':
  560. $list[$key] = $child->slug();
  561. break;
  562. case 'basename':
  563. $list[$key] = basename($key);
  564. break;
  565. case (is_string($header_query[0])):
  566. $child_header = new Header((array)$child->header());
  567. $header_value = $child_header->get($header_query[0]);
  568. if ($header_value) {
  569. $list[$key] = $header_value;
  570. } else {
  571. $list[$key] = $header_default ?: $key;
  572. }
  573. break;
  574. case 'manual':
  575. case 'default':
  576. default:
  577. $list[$key] = $key;
  578. }
  579. }
  580. // handle special case when order_by is random
  581. if ($order_by == 'random') {
  582. $list = $this->arrayShuffle($list);
  583. } else {
  584. // else just sort the list according to specified key
  585. asort($list);
  586. }
  587. // Move manually ordered items into the beginning of the list. Order of the unlisted items does not change.
  588. if (is_array($manual) && !empty($manual)) {
  589. $new_list = array();
  590. $i = count($manual);
  591. foreach ($list as $key => $dummy) {
  592. $info = $pages[$key];
  593. $order = array_search($info['slug'], $manual);
  594. if ($order === false) {
  595. $order = $i++;
  596. }
  597. $new_list[$key] = (int) $order;
  598. }
  599. $list = $new_list;
  600. // Apply manual ordering to the list.
  601. asort($list);
  602. }
  603. foreach ($list as $key => $sort) {
  604. $info = $pages[$key];
  605. // TODO: order by manual needs a hash from the passed variables if we make this more general.
  606. $this->sort[$path][$order_by][$key] = $info;
  607. }
  608. }
  609. // Shuffles and associative array
  610. protected function arrayShuffle($list)
  611. {
  612. $keys = array_keys($list);
  613. shuffle($keys);
  614. $new = array();
  615. foreach ($keys as $key) {
  616. $new[$key] = $list[$key];
  617. }
  618. return $new;
  619. }
  620. }