PageRoutableTrait.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. <?php
  2. /**
  3. * @package Grav\Framework\Flex
  4. *
  5. * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Framework\Flex\Pages\Traits;
  9. use Grav\Common\Config\Config;
  10. use Grav\Common\Grav;
  11. use Grav\Common\Page\Interfaces\PageCollectionInterface;
  12. use Grav\Common\Page\Interfaces\PageInterface;
  13. use Grav\Common\Page\Pages;
  14. use Grav\Common\Uri;
  15. use Grav\Framework\Filesystem\Filesystem;
  16. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  17. use RuntimeException;
  18. use function dirname;
  19. use function is_string;
  20. /**
  21. * Implements PageRoutableInterface
  22. */
  23. trait PageRoutableTrait
  24. {
  25. /** @var bool */
  26. protected $root = false;
  27. /** @var string|null */
  28. private $_route;
  29. /** @var string|null */
  30. private $_path;
  31. /** @var PageInterface|null */
  32. private $_parentCache;
  33. /**
  34. * Returns the page extension, got from the page `url_extension` config and falls back to the
  35. * system config `system.pages.append_url_extension`.
  36. *
  37. * @return string The extension of this page. For example `.html`
  38. */
  39. public function urlExtension(): string
  40. {
  41. return $this->loadHeaderProperty(
  42. 'url_extension',
  43. null,
  44. function ($value) {
  45. if ($this->home()) {
  46. return '';
  47. }
  48. return $value ?? Grav::instance()['config']->get('system.pages.append_url_extension', '');
  49. }
  50. );
  51. }
  52. /**
  53. * Gets and Sets whether or not this Page is routable, ie you can reach it via a URL.
  54. * The page must be *routable* and *published*
  55. *
  56. * @param bool|null $var true if the page is routable
  57. * @return bool true if the page is routable
  58. */
  59. public function routable($var = null): bool
  60. {
  61. $value = $this->loadHeaderProperty(
  62. 'routable',
  63. $var,
  64. static function ($value) {
  65. return $value ?? true;
  66. }
  67. );
  68. return $value && $this->published() && !$this->isModule() && !$this->root() && $this->getLanguages(true);
  69. }
  70. /**
  71. * Gets the URL for a page - alias of url().
  72. *
  73. * @param bool $include_host
  74. * @return string the permalink
  75. */
  76. public function link($include_host = false): string
  77. {
  78. return $this->url($include_host);
  79. }
  80. /**
  81. * Gets the URL with host information, aka Permalink.
  82. * @return string The permalink.
  83. */
  84. public function permalink(): string
  85. {
  86. return $this->url(true, false, true, true);
  87. }
  88. /**
  89. * Returns the canonical URL for a page
  90. *
  91. * @param bool $include_lang
  92. * @return string
  93. */
  94. public function canonical($include_lang = true): string
  95. {
  96. return $this->url(true, true, $include_lang);
  97. }
  98. /**
  99. * Gets the url for the Page.
  100. *
  101. * @param bool $include_host Defaults false, but true would include http://yourhost.com
  102. * @param bool $canonical true to return the canonical URL
  103. * @param bool $include_base
  104. * @param bool $raw_route
  105. * @return string The url.
  106. */
  107. public function url($include_host = false, $canonical = false, $include_base = true, $raw_route = false): string
  108. {
  109. // Override any URL when external_url is set
  110. $external = $this->getNestedProperty('header.external_url');
  111. if ($external) {
  112. return $external;
  113. }
  114. $grav = Grav::instance();
  115. /** @var Pages $pages */
  116. $pages = $grav['pages'];
  117. /** @var Config $config */
  118. $config = $grav['config'];
  119. // get base route (multi-site base and language)
  120. $route = $include_base ? $pages->baseRoute() : '';
  121. // add full route if configured to do so
  122. if (!$include_host && $config->get('system.absolute_urls', false)) {
  123. $include_host = true;
  124. }
  125. if ($canonical) {
  126. $route .= $this->routeCanonical();
  127. } elseif ($raw_route) {
  128. $route .= $this->rawRoute();
  129. } else {
  130. $route .= $this->route();
  131. }
  132. /** @var Uri $uri */
  133. $uri = $grav['uri'];
  134. $url = $uri->rootUrl($include_host) . '/' . trim($route, '/') . $this->urlExtension();
  135. return Uri::filterPath($url);
  136. }
  137. /**
  138. * Gets the route for the page based on the route headers if available, else from
  139. * the parents route and the current Page's slug.
  140. *
  141. * @param string $var Set new default route.
  142. * @return string|null The route for the Page.
  143. */
  144. public function route($var = null): ?string
  145. {
  146. if (null !== $var) {
  147. // TODO: not the best approach, but works...
  148. $this->setNestedProperty('header.routes.default', $var);
  149. }
  150. // Return default route if given.
  151. $default = $this->getNestedProperty('header.routes.default');
  152. if (is_string($default)) {
  153. return $default;
  154. }
  155. return $this->routeInternal();
  156. }
  157. /**
  158. * @return string|null
  159. */
  160. protected function routeInternal(): ?string
  161. {
  162. $route = $this->_route;
  163. if (null !== $route) {
  164. return $route;
  165. }
  166. if ($this->root()) {
  167. return null;
  168. }
  169. // Root and orphan nodes have no route.
  170. $parent = $this->parent();
  171. if (!$parent) {
  172. return null;
  173. }
  174. if ($parent->home()) {
  175. /** @var Config $config */
  176. $config = Grav::instance()['config'];
  177. $hide = (bool)$config->get('system.home.hide_in_urls', false);
  178. $route = '/' . ($hide ? '' : $parent->slug());
  179. } else {
  180. $route = $parent->route();
  181. }
  182. if ($route !== '' && $route !== '/') {
  183. $route .= '/';
  184. }
  185. if (!$this->home()) {
  186. $route .= $this->slug();
  187. }
  188. $this->_route = $route;
  189. return $route;
  190. }
  191. /**
  192. * Helper method to clear the route out so it regenerates next time you use it
  193. */
  194. public function unsetRouteSlug(): void
  195. {
  196. // TODO:
  197. throw new RuntimeException(__METHOD__ . '(): Not Implemented');
  198. }
  199. /**
  200. * Gets and Sets the page raw route
  201. *
  202. * @param string|null $var
  203. * @return string|null
  204. */
  205. public function rawRoute($var = null): ?string
  206. {
  207. if (null !== $var) {
  208. // TODO:
  209. throw new RuntimeException(__METHOD__ . '(string): Not Implemented');
  210. }
  211. if ($this->root()) {
  212. return null;
  213. }
  214. return '/' . $this->getKey();
  215. }
  216. /**
  217. * Gets the route aliases for the page based on page headers.
  218. *
  219. * @param array|null $var list of route aliases
  220. * @return array The route aliases for the Page.
  221. */
  222. public function routeAliases($var = null): array
  223. {
  224. if (null !== $var) {
  225. $this->setNestedProperty('header.routes.aliases', (array)$var);
  226. }
  227. $aliases = (array)$this->getNestedProperty('header.routes.aliases');
  228. $default = $this->getNestedProperty('header.routes.default');
  229. if ($default) {
  230. $aliases[] = $default;
  231. }
  232. return $aliases;
  233. }
  234. /**
  235. * Gets the canonical route for this page if its set. If provided it will use
  236. * that value, else if it's `true` it will use the default route.
  237. *
  238. * @param string|null $var
  239. * @return string|null
  240. */
  241. public function routeCanonical($var = null): ?string
  242. {
  243. if (null !== $var) {
  244. $this->setNestedProperty('header.routes.canonical', (array)$var);
  245. }
  246. $canonical = $this->getNestedProperty('header.routes.canonical');
  247. return is_string($canonical) ? $canonical : $this->route();
  248. }
  249. /**
  250. * Gets the redirect set in the header.
  251. *
  252. * @param string|null $var redirect url
  253. * @return string|null
  254. */
  255. public function redirect($var = null): ?string
  256. {
  257. return $this->loadHeaderProperty(
  258. 'redirect',
  259. $var,
  260. static function ($value) {
  261. return trim($value) ?: null;
  262. }
  263. );
  264. }
  265. /**
  266. * Returns the clean path to the page file
  267. *
  268. * Needed in admin for Page Media.
  269. */
  270. public function relativePagePath(): ?string
  271. {
  272. $folder = $this->getMediaFolder();
  273. if (!$folder) {
  274. return null;
  275. }
  276. /** @var UniformResourceLocator $locator */
  277. $locator = Grav::instance()['locator'];
  278. $path = $locator->isStream($folder) ? $locator->findResource($folder, false) : $folder;
  279. return is_string($path) ? $path : null;
  280. }
  281. /**
  282. * Gets and sets the path to the folder where the .md for this Page object resides.
  283. * This is equivalent to the filePath but without the filename.
  284. *
  285. * @param string|null $var the path
  286. * @return string|null the path
  287. */
  288. public function path($var = null): ?string
  289. {
  290. if (null !== $var) {
  291. // TODO:
  292. throw new RuntimeException(__METHOD__ . '(string): Not Implemented');
  293. }
  294. $path = $this->_path;
  295. if ($path) {
  296. return $path;
  297. }
  298. if ($this->root()) {
  299. $folder = $this->getFlexDirectory()->getStorageFolder();
  300. } else {
  301. $folder = $this->getStorageFolder();
  302. }
  303. if ($folder) {
  304. /** @var UniformResourceLocator $locator */
  305. $locator = Grav::instance()['locator'];
  306. $folder = $locator->isStream($folder) ? $locator->getResource($folder) : GRAV_ROOT . "/{$folder}";
  307. }
  308. return $this->_path = is_string($folder) ? $folder : null;
  309. }
  310. /**
  311. * Get/set the folder.
  312. *
  313. * @param string|null $var Optional path, including numeric prefix.
  314. * @return string|null
  315. */
  316. public function folder($var = null): ?string
  317. {
  318. return $this->loadProperty(
  319. 'folder',
  320. $var,
  321. function ($value) {
  322. if (null === $value) {
  323. $value = $this->getMasterKey() ?: $this->getKey();
  324. }
  325. return basename($value) ?: null;
  326. }
  327. );
  328. }
  329. /**
  330. * Get/set the folder.
  331. *
  332. * @param string|null $var Optional path, including numeric prefix.
  333. * @return string|null
  334. */
  335. public function parentStorageKey($var = null): ?string
  336. {
  337. return $this->loadProperty(
  338. 'parent_key',
  339. $var,
  340. function ($value) {
  341. if (null === $value) {
  342. $filesystem = Filesystem::getInstance(false);
  343. $value = $this->getMasterKey() ?: $this->getKey();
  344. $value = ltrim($filesystem->dirname("/{$value}"), '/') ?: '';
  345. }
  346. return $value;
  347. }
  348. );
  349. }
  350. /**
  351. * Gets and Sets the parent object for this page
  352. *
  353. * @param PageInterface|null $var the parent page object
  354. * @return PageInterface|null the parent page object if it exists.
  355. */
  356. public function parent(PageInterface $var = null)
  357. {
  358. if (null !== $var) {
  359. // TODO:
  360. throw new RuntimeException(__METHOD__ . '(PageInterface): Not Implemented');
  361. }
  362. if ($this->_parentCache || $this->root()) {
  363. return $this->_parentCache;
  364. }
  365. $directory = $this->getFlexDirectory();
  366. $parentKey = ltrim(dirname("/{$this->getKey()}"), '/');
  367. if ($parentKey) {
  368. $parent = $directory->getObject($parentKey);
  369. $language = $this->getLanguage();
  370. if ($language && $parent && method_exists($parent, 'getTranslation')) {
  371. $parent = $parent->getTranslation($language) ?? $parent;
  372. }
  373. $this->_parentCache = $parent;
  374. } else {
  375. $index = $directory->getIndex();
  376. $this->_parentCache = method_exists($index, 'getRoot') ? $index->getRoot() : null;
  377. }
  378. return $this->_parentCache;
  379. }
  380. /**
  381. * Gets the top parent object for this page. Can return page itself.
  382. *
  383. * @return PageInterface The top parent page object.
  384. */
  385. public function topParent()
  386. {
  387. $topParent = $this;
  388. while ($topParent) {
  389. $parent = $topParent->parent();
  390. if (!$parent || !$parent->parent()) {
  391. break;
  392. }
  393. $topParent = $parent;
  394. }
  395. return $topParent;
  396. }
  397. /**
  398. * Returns the item in the current position.
  399. *
  400. * @return int|null the index of the current page.
  401. */
  402. public function currentPosition(): ?int
  403. {
  404. $parent = $this->parent();
  405. $collection = $parent ? $parent->collection('content', false) : null;
  406. if ($collection instanceof PageCollectionInterface && $path = $this->path()) {
  407. return $collection->currentPosition($path);
  408. }
  409. return 1;
  410. }
  411. /**
  412. * Returns whether or not this page is the currently active page requested via the URL.
  413. *
  414. * @return bool True if it is active
  415. */
  416. public function active(): bool
  417. {
  418. $grav = Grav::instance();
  419. $uri_path = rtrim(urldecode($grav['uri']->path()), '/') ?: '/';
  420. $routes = $grav['pages']->routes();
  421. return isset($routes[$uri_path]) && $routes[$uri_path] === $this->path();
  422. }
  423. /**
  424. * Returns whether or not this URI's URL contains the URL of the active page.
  425. * Or in other words, is this page's URL in the current URL
  426. *
  427. * @return bool True if active child exists
  428. */
  429. public function activeChild(): bool
  430. {
  431. $grav = Grav::instance();
  432. $uri = $grav['uri'];
  433. $pages = $grav['pages'];
  434. $uri_path = rtrim(urldecode($uri->path()), '/');
  435. $routes = $pages->routes();
  436. if (isset($routes[$uri_path])) {
  437. /** @var PageInterface $child_page|null */
  438. $child_page = $pages->find($uri->route())->parent();
  439. if (null !== $child_page) {
  440. while (!$child_page->root()) {
  441. if ($this->path() === $child_page->path()) {
  442. return true;
  443. }
  444. /** @var PageInterface $child_page|null */
  445. $child_page = $child_page->parent();
  446. }
  447. }
  448. }
  449. return false;
  450. }
  451. /**
  452. * Returns whether or not this page is the currently configured home page.
  453. *
  454. * @return bool True if it is the homepage
  455. */
  456. public function home(): bool
  457. {
  458. $home = Grav::instance()['config']->get('system.home.alias');
  459. return '/' . $this->getKey() === $home;
  460. }
  461. /**
  462. * Returns whether or not this page is the root node of the pages tree.
  463. *
  464. * @param bool|null $var
  465. * @return bool True if it is the root
  466. */
  467. public function root($var = null): bool
  468. {
  469. if (null !== $var) {
  470. $this->root = (bool)$var;
  471. }
  472. return $this->root === true || $this->getKey() === '/';
  473. }
  474. }