"Fossies" - the Fresh Open Source Software Archive

Member "grav/system/src/Grav/Common/Page/Pages.php" (1 Sep 2020, 41357 Bytes) of package /linux/www/grav-v1.6.27.zip:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "Pages.php" see the Fossies "Dox" file reference documentation.

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