PageLegacyTrait.php 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111
  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 Exception;
  10. use Grav\Common\Grav;
  11. use Grav\Common\Page\Collection;
  12. use Grav\Common\Page\Interfaces\PageCollectionInterface;
  13. use Grav\Common\Page\Interfaces\PageInterface;
  14. use Grav\Common\Page\Pages;
  15. use Grav\Common\Utils;
  16. use Grav\Common\Yaml;
  17. use Grav\Framework\Cache\CacheInterface;
  18. use Grav\Framework\File\Formatter\MarkdownFormatter;
  19. use Grav\Framework\File\Formatter\YamlFormatter;
  20. use Grav\Framework\Filesystem\Filesystem;
  21. use Grav\Framework\Flex\FlexDirectory;
  22. use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
  23. use Grav\Framework\Flex\Interfaces\FlexIndexInterface;
  24. use Grav\Framework\Flex\Pages\FlexPageIndex;
  25. use Grav\Framework\Flex\Pages\FlexPageObject;
  26. use InvalidArgumentException;
  27. use RocketTheme\Toolbox\File\MarkdownFile;
  28. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  29. use RuntimeException;
  30. use SplFileInfo;
  31. use function in_array;
  32. use function is_array;
  33. use function is_string;
  34. use function strlen;
  35. /**
  36. * Implements PageLegacyInterface
  37. */
  38. trait PageLegacyTrait
  39. {
  40. /** @var array|null */
  41. private $_content_meta;
  42. /** @var array|null */
  43. private $_metadata;
  44. /**
  45. * Initializes the page instance variables based on a file
  46. *
  47. * @param SplFileInfo $file The file information for the .md file that the page represents
  48. * @param string|null $extension
  49. * @return $this
  50. */
  51. public function init(SplFileInfo $file, $extension = null)
  52. {
  53. // TODO:
  54. throw new RuntimeException(__METHOD__ . '(): Not Implemented');
  55. }
  56. /**
  57. * Gets and Sets the raw data
  58. *
  59. * @param string|null $var Raw content string
  60. * @return string Raw content string
  61. */
  62. public function raw($var = null): string
  63. {
  64. if (null !== $var) {
  65. // TODO:
  66. throw new RuntimeException(__METHOD__ . '(string): Not Implemented');
  67. }
  68. $storage = $this->getFlexDirectory()->getStorage();
  69. if (method_exists($storage, 'readRaw')) {
  70. return $storage->readRaw($this->getStorageKey());
  71. }
  72. $array = $this->prepareStorage();
  73. $formatter = new MarkdownFormatter();
  74. return $formatter->encode($array);
  75. }
  76. /**
  77. * Gets and Sets the page frontmatter
  78. *
  79. * @param string|null $var
  80. * @return string
  81. */
  82. public function frontmatter($var = null): string
  83. {
  84. if (null !== $var) {
  85. $formatter = new YamlFormatter();
  86. $this->setProperty('frontmatter', $var);
  87. $this->setProperty('header', $formatter->decode($var));
  88. return $var;
  89. }
  90. $storage = $this->getFlexDirectory()->getStorage();
  91. if (method_exists($storage, 'readFrontmatter')) {
  92. return $storage->readFrontmatter($this->getStorageKey());
  93. }
  94. $array = $this->prepareStorage();
  95. $formatter = new YamlFormatter();
  96. return $formatter->encode($array['header'] ?? []);
  97. }
  98. /**
  99. * Modify a header value directly
  100. *
  101. * @param string $key
  102. * @param string|array $value
  103. * @return void
  104. */
  105. public function modifyHeader($key, $value): void
  106. {
  107. $this->setNestedProperty("header.{$key}", $value);
  108. }
  109. /**
  110. * @return int
  111. */
  112. public function httpResponseCode(): int
  113. {
  114. $code = (int)$this->getNestedProperty('header.http_response_code');
  115. return $code ?: 200;
  116. }
  117. /**
  118. * @return array
  119. */
  120. public function httpHeaders(): array
  121. {
  122. $headers = [];
  123. $format = $this->templateFormat();
  124. $cache_control = $this->cacheControl();
  125. $expires = $this->expires();
  126. // Set Content-Type header.
  127. $headers['Content-Type'] = Utils::getMimeByExtension($format, 'text/html');
  128. // Calculate Expires Headers if set to > 0.
  129. if ($expires > 0) {
  130. $expires_date = gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT';
  131. if (!$cache_control) {
  132. $headers['Cache-Control'] = 'max-age=' . $expires;
  133. }
  134. $headers['Expires'] = $expires_date;
  135. }
  136. // Set Cache-Control header.
  137. if ($cache_control) {
  138. $headers['Cache-Control'] = strtolower($cache_control);
  139. }
  140. // Set Last-Modified header.
  141. if ($this->lastModified()) {
  142. $last_modified_date = gmdate('D, d M Y H:i:s', $this->modified()) . ' GMT';
  143. $headers['Last-Modified'] = $last_modified_date;
  144. }
  145. // Calculate ETag based on the serialized page and modified time.
  146. if ($this->eTag()) {
  147. $headers['ETag'] = '1';
  148. }
  149. // Set Vary: Accept-Encoding header.
  150. $grav = Grav::instance();
  151. if ($grav['config']->get('system.pages.vary_accept_encoding', false)) {
  152. $headers['Vary'] = 'Accept-Encoding';
  153. }
  154. return $headers;
  155. }
  156. /**
  157. * Get the contentMeta array and initialize content first if it's not already
  158. *
  159. * @return array
  160. */
  161. public function contentMeta(): array
  162. {
  163. // Content meta is generated during the content is being rendered, so make sure we have done it.
  164. $this->content();
  165. return $this->_content_meta ?? [];
  166. }
  167. /**
  168. * Add an entry to the page's contentMeta array
  169. *
  170. * @param string $name
  171. * @param string $value
  172. * @return void
  173. */
  174. public function addContentMeta($name, $value): void
  175. {
  176. $this->_content_meta[$name] = $value;
  177. }
  178. /**
  179. * Return the whole contentMeta array as it currently stands
  180. *
  181. * @param string|null $name
  182. * @return string|array|null
  183. */
  184. public function getContentMeta($name = null)
  185. {
  186. if ($name) {
  187. return $this->_content_meta[$name] ?? null;
  188. }
  189. return $this->_content_meta ?? [];
  190. }
  191. /**
  192. * Sets the whole content meta array in one shot
  193. *
  194. * @param array $content_meta
  195. * @return array
  196. */
  197. public function setContentMeta($content_meta): array
  198. {
  199. return $this->_content_meta = $content_meta;
  200. }
  201. /**
  202. * Fires the onPageContentProcessed event, and caches the page content using a unique ID for the page
  203. */
  204. public function cachePageContent(): void
  205. {
  206. $value = [
  207. 'checksum' => $this->getCacheChecksum(),
  208. 'content' => $this->_content,
  209. 'content_meta' => $this->_content_meta
  210. ];
  211. $cache = $this->getCache('render');
  212. $key = md5($this->getCacheKey() . '-content');
  213. $cache->set($key, $value);
  214. }
  215. /**
  216. * Get file object to the page.
  217. *
  218. * @return MarkdownFile|null
  219. */
  220. public function file(): ?MarkdownFile
  221. {
  222. // TODO:
  223. throw new RuntimeException(__METHOD__ . '(): Not Implemented');
  224. }
  225. /**
  226. * Prepare move page to new location. Moves also everything that's under the current page.
  227. *
  228. * You need to call $this->save() in order to perform the move.
  229. *
  230. * @param PageInterface $parent New parent page.
  231. * @return $this
  232. */
  233. public function move(PageInterface $parent)
  234. {
  235. if ($this->route() === $parent->route()) {
  236. throw new RuntimeException('Failed: Cannot set page parent to self');
  237. }
  238. $rawRoute = $this->rawRoute();
  239. if ($rawRoute && Utils::startsWith($parent->rawRoute(), $rawRoute)) {
  240. throw new RuntimeException('Failed: Cannot set page parent to a child of current page');
  241. }
  242. $this->storeOriginal();
  243. // TODO:
  244. throw new RuntimeException(__METHOD__ . '(): Not Implemented');
  245. }
  246. /**
  247. * Prepare a copy from the page. Copies also everything that's under the current page.
  248. *
  249. * Returns a new Page object for the copy.
  250. * You need to call $this->save() in order to perform the move.
  251. *
  252. * @param PageInterface|null $parent New parent page.
  253. * @return $this
  254. */
  255. public function copy(PageInterface $parent = null)
  256. {
  257. $this->storeOriginal();
  258. $filesystem = Filesystem::getInstance(false);
  259. $parentStorageKey = ltrim($filesystem->dirname("/{$this->getMasterKey()}"), '/');
  260. /** @var FlexPageIndex $index */
  261. $index = $this->getFlexDirectory()->getIndex();
  262. if ($parent) {
  263. if ($parent instanceof FlexPageObject) {
  264. $k = $parent->getMasterKey();
  265. if ($k !== $parentStorageKey) {
  266. $parentStorageKey = $k;
  267. }
  268. } else {
  269. throw new RuntimeException('Cannot copy page, parent is of unknown type');
  270. }
  271. } else {
  272. $parent = $parentStorageKey
  273. ? $this->getFlexDirectory()->getObject($parentStorageKey, 'storage_key')
  274. : (method_exists($index, 'getRoot') ? $index->getRoot() : null);
  275. }
  276. // Find non-existing key.
  277. $parentKey = $parent ? $parent->getKey() : '';
  278. $key = trim($parentKey . '/' . basename($this->getKey()), '/');
  279. $key = preg_replace('/-\d+$/', '', $key);
  280. $i = 1;
  281. do {
  282. $i++;
  283. $test = "{$key}-{$i}";
  284. } while ($index->containsKey($test));
  285. $key = $test;
  286. $folder = basename($key);
  287. // Get the folder name.
  288. $order = $this->getProperty('order');
  289. if ($order) {
  290. $order++;
  291. }
  292. $parts = [];
  293. if ($parentStorageKey !== '') {
  294. $parts[] = $parentStorageKey;
  295. }
  296. $parts[] = $order ? sprintf('%02d.%s', $order, $folder) : $folder;
  297. // Finally update the object.
  298. $this->setKey($key);
  299. $this->setStorageKey(implode('/', $parts));
  300. $this->markAsCopy();
  301. return $this;
  302. }
  303. /**
  304. * Get the blueprint name for this page. Use the blueprint form field if set
  305. *
  306. * @return string
  307. */
  308. public function blueprintName(): string
  309. {
  310. $blueprint_name = filter_input(INPUT_POST, 'blueprint', FILTER_SANITIZE_STRING) ?: $this->template();
  311. return $blueprint_name;
  312. }
  313. /**
  314. * Validate page header.
  315. *
  316. * @return void
  317. * @throws Exception
  318. */
  319. public function validate(): void
  320. {
  321. $blueprint = $this->getBlueprint();
  322. $blueprint->validate($this->toArray());
  323. }
  324. /**
  325. * Filter page header from illegal contents.
  326. *
  327. * @return void
  328. */
  329. public function filter(): void
  330. {
  331. $blueprints = $this->getBlueprint();
  332. $values = $blueprints->filter($this->toArray());
  333. if ($values && isset($values['header'])) {
  334. $this->header($values['header']);
  335. }
  336. }
  337. /**
  338. * Get unknown header variables.
  339. *
  340. * @return array
  341. */
  342. public function extra(): array
  343. {
  344. $data = $this->prepareStorage();
  345. return $this->getBlueprint()->extra((array)($data['header'] ?? []), 'header.');
  346. }
  347. /**
  348. * Convert page to an array.
  349. *
  350. * @return array
  351. */
  352. public function toArray(): array
  353. {
  354. return [
  355. 'header' => (array)$this->header(),
  356. 'content' => (string)$this->getFormValue('content')
  357. ];
  358. }
  359. /**
  360. * Convert page to YAML encoded string.
  361. *
  362. * @return string
  363. */
  364. public function toYaml(): string
  365. {
  366. return Yaml::dump($this->toArray(), 20);
  367. }
  368. /**
  369. * Convert page to JSON encoded string.
  370. *
  371. * @return string
  372. */
  373. public function toJson(): string
  374. {
  375. $json = json_encode($this->toArray());
  376. if (!is_string($json)) {
  377. throw new RuntimeException('Internal error');
  378. }
  379. return $json;
  380. }
  381. /**
  382. * Gets and sets the name field. If no name field is set, it will return 'default.md'.
  383. *
  384. * @param string|null $var The name of this page.
  385. * @return string The name of this page.
  386. */
  387. public function name($var = null): string
  388. {
  389. return $this->loadProperty(
  390. 'name',
  391. $var,
  392. function ($value) {
  393. $value = $value ?? $this->getMetaData()['template'] ?? 'default';
  394. if (!preg_match('/\.md$/', $value)) {
  395. $language = $this->language();
  396. if ($language) {
  397. // TODO: better language support
  398. $value .= ".{$language}";
  399. }
  400. $value .= '.md';
  401. }
  402. $value = preg_replace('|^modular/|', '', $value);
  403. $this->unsetProperty('template');
  404. return $value;
  405. }
  406. );
  407. }
  408. /**
  409. * Returns child page type.
  410. *
  411. * @return string
  412. */
  413. public function childType(): string
  414. {
  415. return (string)$this->getNestedProperty('header.child_type');
  416. }
  417. /**
  418. * Gets and sets the template field. This is used to find the correct Twig template file to render.
  419. * If no field is set, it will return the name without the .md extension
  420. *
  421. * @param string|null $var the template name
  422. * @return string the template name
  423. */
  424. public function template($var = null): string
  425. {
  426. return $this->loadHeaderProperty(
  427. 'template',
  428. $var,
  429. function ($value) {
  430. return trim($value ?? (($this->isModule() ? 'modular/' : '') . str_replace($this->extension(), '', $this->name())));
  431. }
  432. );
  433. }
  434. /**
  435. * Allows a page to override the output render format, usually the extension provided in the URL.
  436. * (e.g. `html`, `json`, `xml`, etc).
  437. *
  438. * @param string|null $var
  439. * @return string
  440. */
  441. public function templateFormat($var = null): string
  442. {
  443. return $this->loadHeaderProperty(
  444. 'template_format',
  445. $var,
  446. function ($value) {
  447. return ltrim($value ?? $this->getNestedProperty('header.append_url_extension') ?: Utils::getPageFormat(), '.');
  448. }
  449. );
  450. }
  451. /**
  452. * Gets and sets the extension field.
  453. *
  454. * @param string|null $var
  455. * @return string
  456. */
  457. public function extension($var = null): string
  458. {
  459. if (null !== $var) {
  460. $this->setProperty('format', $var);
  461. }
  462. $language = $this->language();
  463. if ($language) {
  464. $language = '.' . $language;
  465. }
  466. $format = '.' . ($this->getProperty('format') ?? pathinfo($this->name(), PATHINFO_EXTENSION));
  467. return $language . $format;
  468. }
  469. /**
  470. * Gets and sets the expires field. If not set will return the default
  471. *
  472. * @param int|null $var The new expires value.
  473. * @return int The expires value
  474. */
  475. public function expires($var = null): int
  476. {
  477. return $this->loadHeaderProperty(
  478. 'expires',
  479. $var,
  480. static function ($value) {
  481. return (int)($value ?? Grav::instance()['config']->get('system.pages.expires'));
  482. }
  483. );
  484. }
  485. /**
  486. * Gets and sets the cache-control property. If not set it will return the default value (null)
  487. * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control for more details on valid options
  488. *
  489. * @param string|null $var
  490. * @return string|null
  491. */
  492. public function cacheControl($var = null): ?string
  493. {
  494. return $this->loadHeaderProperty(
  495. 'cache_control',
  496. $var,
  497. static function ($value) {
  498. return ((string)($value ?? Grav::instance()['config']->get('system.pages.cache_control'))) ?: null;
  499. }
  500. );
  501. }
  502. /**
  503. * @param bool|null $var
  504. * @return bool|null
  505. */
  506. public function ssl($var = null): ?bool
  507. {
  508. return $this->loadHeaderProperty(
  509. 'ssl',
  510. $var,
  511. static function ($value) {
  512. return $value ? (bool)$value : null;
  513. }
  514. );
  515. }
  516. /**
  517. * Returns the state of the debugger override setting for this page
  518. *
  519. * @return bool
  520. */
  521. public function debugger(): bool
  522. {
  523. return (bool)$this->getNestedProperty('header.debugger', true);
  524. }
  525. /**
  526. * Function to merge page metadata tags and build an array of Metadata objects
  527. * that can then be rendered in the page.
  528. *
  529. * @param array|null $var an Array of metadata values to set
  530. * @return array an Array of metadata values for the page
  531. */
  532. public function metadata($var = null): array
  533. {
  534. if ($var !== null) {
  535. $this->_metadata = (array)$var;
  536. }
  537. // if not metadata yet, process it.
  538. if (null === $this->_metadata) {
  539. $this->_metadata = [];
  540. $config = Grav::instance()['config'];
  541. // Set the Generator tag
  542. $defaultMetadata = ['generator' => 'GravCMS'];
  543. $siteMetadata = $config->get('site.metadata', []);
  544. $headerMetadata = $this->getNestedProperty('header.metadata', []);
  545. // Get initial metadata for the page
  546. $metadata = array_merge($defaultMetadata, $siteMetadata, $headerMetadata);
  547. $header_tag_http_equivs = ['content-type', 'default-style', 'refresh', 'x-ua-compatible', 'content-security-policy'];
  548. $escape = !$config->get('system.strict_mode.twig_compat', false) || $config->get('system.twig.autoescape', true);
  549. // Build an array of meta objects..
  550. foreach ($metadata as $key => $value) {
  551. // Lowercase the key
  552. $key = strtolower($key);
  553. // If this is a property type metadata: "og", "twitter", "facebook" etc
  554. // Backward compatibility for nested arrays in metas
  555. if (is_array($value)) {
  556. foreach ($value as $property => $prop_value) {
  557. $prop_key = $key . ':' . $property;
  558. $this->_metadata[$prop_key] = [
  559. 'name' => $prop_key,
  560. 'property' => $prop_key,
  561. 'content' => $escape ? htmlspecialchars($prop_value, ENT_QUOTES | ENT_HTML5, 'UTF-8') : $prop_value
  562. ];
  563. }
  564. } elseif ($value) {
  565. // If it this is a standard meta data type
  566. if (in_array($key, $header_tag_http_equivs, true)) {
  567. $this->_metadata[$key] = [
  568. 'http_equiv' => $key,
  569. 'content' => $escape ? htmlspecialchars($value, ENT_COMPAT, 'UTF-8') : $value
  570. ];
  571. } elseif ($key === 'charset') {
  572. $this->_metadata[$key] = ['charset' => $escape ? htmlspecialchars($value, ENT_QUOTES | ENT_HTML5, 'UTF-8') : $value];
  573. } else {
  574. // if it's a social metadata with separator, render as property
  575. $separator = strpos($key, ':');
  576. $hasSeparator = $separator && $separator < strlen($key) - 1;
  577. $entry = [
  578. 'content' => $escape ? htmlspecialchars($value, ENT_QUOTES | ENT_HTML5, 'UTF-8') : $value
  579. ];
  580. if ($hasSeparator && !Utils::startsWith($key, 'twitter')) {
  581. $entry['property'] = $key;
  582. } else {
  583. $entry['name'] = $key;
  584. }
  585. $this->_metadata[$key] = $entry;
  586. }
  587. }
  588. }
  589. }
  590. return $this->_metadata;
  591. }
  592. /**
  593. * Reset the metadata and pull from header again
  594. */
  595. public function resetMetadata(): void
  596. {
  597. $this->_metadata = null;
  598. }
  599. /**
  600. * Gets and sets the option to show the etag header for the page.
  601. *
  602. * @param bool|null $var show etag header
  603. * @return bool show etag header
  604. */
  605. public function eTag($var = null): bool
  606. {
  607. return $this->loadHeaderProperty(
  608. 'etag',
  609. $var,
  610. static function ($value) {
  611. return (bool)($value ?? Grav::instance()['config']->get('system.pages.etag'));
  612. }
  613. );
  614. }
  615. /**
  616. * Gets and sets the path to the .md file for this Page object.
  617. *
  618. * @param string|null $var the file path
  619. * @return string|null the file path
  620. */
  621. public function filePath($var = null): ?string
  622. {
  623. if (null !== $var) {
  624. // TODO:
  625. throw new RuntimeException(__METHOD__ . '(string): Not Implemented');
  626. }
  627. $folder = $this->getStorageFolder();
  628. if (!$folder) {
  629. return null;
  630. }
  631. /** @var UniformResourceLocator $locator */
  632. $locator = Grav::instance()['locator'];
  633. $folder = $locator->isStream($folder) ? $locator->getResource($folder) : GRAV_ROOT . "/{$folder}";
  634. return $folder . '/' . ($this->isPage() ? $this->name() : 'default.md');
  635. }
  636. /**
  637. * Gets the relative path to the .md file
  638. *
  639. * @return string|null The relative file path
  640. */
  641. public function filePathClean(): ?string
  642. {
  643. $folder = $this->getStorageFolder();
  644. if (!$folder) {
  645. return null;
  646. }
  647. /** @var UniformResourceLocator $locator */
  648. $locator = Grav::instance()['locator'];
  649. $folder = $locator->isStream($folder) ? $locator->getResource($folder, false) : $folder;
  650. return $folder . '/' . ($this->isPage() ? $this->name() : 'default.md');
  651. }
  652. /**
  653. * Gets and sets the order by which any sub-pages should be sorted.
  654. *
  655. * @param string|null $var the order, either "asc" or "desc"
  656. * @return string the order, either "asc" or "desc"
  657. */
  658. public function orderDir($var = null): string
  659. {
  660. return $this->loadHeaderProperty(
  661. 'order_dir',
  662. $var,
  663. static function ($value) {
  664. return strtolower(trim($value) ?: Grav::instance()['config']->get('system.pages.order.dir')) === 'desc' ? 'desc' : 'asc';
  665. }
  666. );
  667. }
  668. /**
  669. * Gets and sets the order by which the sub-pages should be sorted.
  670. *
  671. * default - is the order based on the file system, ie 01.Home before 02.Advark
  672. * title - is the order based on the title set in the pages
  673. * date - is the order based on the date set in the pages
  674. * folder - is the order based on the name of the folder with any numerics omitted
  675. *
  676. * @param string|null $var supported options include "default", "title", "date", and "folder"
  677. * @return string supported options include "default", "title", "date", and "folder"
  678. */
  679. public function orderBy($var = null): string
  680. {
  681. return $this->loadHeaderProperty(
  682. 'order_by',
  683. $var,
  684. static function ($value) {
  685. return trim($value) ?: Grav::instance()['config']->get('system.pages.order.by');
  686. }
  687. );
  688. }
  689. /**
  690. * Gets the manual order set in the header.
  691. *
  692. * @param string|null $var supported options include "default", "title", "date", and "folder"
  693. * @return array
  694. */
  695. public function orderManual($var = null): array
  696. {
  697. return $this->loadHeaderProperty(
  698. 'order_manual',
  699. $var,
  700. static function ($value) {
  701. return (array)$value;
  702. }
  703. );
  704. }
  705. /**
  706. * Gets and sets the maxCount field which describes how many sub-pages should be displayed if the
  707. * sub_pages header property is set for this page object.
  708. *
  709. * @param int|null $var the maximum number of sub-pages
  710. * @return int the maximum number of sub-pages
  711. */
  712. public function maxCount($var = null): int
  713. {
  714. return $this->loadHeaderProperty(
  715. 'max_count',
  716. $var,
  717. static function ($value) {
  718. return (int)($value ?? Grav::instance()['config']->get('system.pages.list.count'));
  719. }
  720. );
  721. }
  722. /**
  723. * Gets and sets the modular var that helps identify this page is a modular child
  724. *
  725. * @param bool|null $var true if modular_twig
  726. * @return bool true if modular_twig
  727. * @deprecated 1.7 Use ->isModule() or ->modularTwig() method instead.
  728. */
  729. public function modular($var = null): bool
  730. {
  731. user_error(__METHOD__ . '() is deprecated since Grav 1.7, use ->isModule() or ->modularTwig() method instead', E_USER_DEPRECATED);
  732. return $this->modularTwig($var);
  733. }
  734. /**
  735. * Gets and sets the modular_twig var that helps identify this page as a modular child page that will need
  736. * twig processing handled differently from a regular page.
  737. *
  738. * @param bool|null $var true if modular_twig
  739. * @return bool true if modular_twig
  740. */
  741. public function modularTwig($var = null): bool
  742. {
  743. if ($var !== null) {
  744. $this->setProperty('modular_twig', (bool)$var);
  745. if ($var) {
  746. $this->visible(false);
  747. }
  748. }
  749. return (bool)($this->getProperty('modular_twig') ?? strpos($this->slug(), '_') === 0);
  750. }
  751. /**
  752. * Returns children of this page.
  753. *
  754. * @return PageCollectionInterface|FlexIndexInterface
  755. */
  756. public function children()
  757. {
  758. $meta = $this->getMetaData();
  759. $keys = array_keys($meta['children'] ?? []);
  760. $prefix = $this->getMasterKey();
  761. if ($prefix) {
  762. foreach ($keys as &$key) {
  763. $key = $prefix . '/' . $key;
  764. }
  765. unset($key);
  766. }
  767. return $this->getFlexDirectory()->getIndex($keys, 'storage_key');
  768. }
  769. /**
  770. * Check to see if this item is the first in an array of sub-pages.
  771. *
  772. * @return bool True if item is first.
  773. */
  774. public function isFirst(): bool
  775. {
  776. $parent = $this->parent();
  777. $children = $parent ? $parent->children() : null;
  778. if ($children instanceof FlexCollectionInterface) {
  779. $children = $children->withKeyField();
  780. }
  781. return $children instanceof PageCollectionInterface ? $children->isFirst($this->getKey()) : true;
  782. }
  783. /**
  784. * Check to see if this item is the last in an array of sub-pages.
  785. *
  786. * @return bool True if item is last
  787. */
  788. public function isLast(): bool
  789. {
  790. $parent = $this->parent();
  791. $children = $parent ? $parent->children() : null;
  792. if ($children instanceof FlexCollectionInterface) {
  793. $children = $children->withKeyField();
  794. }
  795. return $children instanceof PageCollectionInterface ? $children->isLast($this->getKey()) : true;
  796. }
  797. /**
  798. * Gets the previous sibling based on current position.
  799. *
  800. * @return PageInterface|false the previous Page item
  801. */
  802. public function prevSibling()
  803. {
  804. return $this->adjacentSibling(-1);
  805. }
  806. /**
  807. * Gets the next sibling based on current position.
  808. *
  809. * @return PageInterface|false the next Page item
  810. */
  811. public function nextSibling()
  812. {
  813. return $this->adjacentSibling(1);
  814. }
  815. /**
  816. * Returns the adjacent sibling based on a direction.
  817. *
  818. * @param int $direction either -1 or +1
  819. * @return PageInterface|false the sibling page
  820. */
  821. public function adjacentSibling($direction = 1)
  822. {
  823. $parent = $this->parent();
  824. $children = $parent ? $parent->children() : null;
  825. if ($children instanceof FlexCollectionInterface) {
  826. $children = $children->withKeyField();
  827. }
  828. if ($children instanceof PageCollectionInterface) {
  829. $child = $children->adjacentSibling($this->getKey(), $direction);
  830. if ($child instanceof PageInterface) {
  831. return $child;
  832. }
  833. }
  834. return false;
  835. }
  836. /**
  837. * Helper method to return an ancestor page.
  838. *
  839. * @param string|null $lookup Name of the parent folder
  840. * @return PageInterface|null page you were looking for if it exists
  841. */
  842. public function ancestor($lookup = null)
  843. {
  844. /** @var Pages $pages */
  845. $pages = Grav::instance()['pages'];
  846. return $pages->ancestor($this->getProperty('parent_route'), $lookup);
  847. }
  848. /**
  849. * Helper method to return an ancestor page to inherit from. The current
  850. * page object is returned.
  851. *
  852. * @param string $field Name of the parent folder
  853. * @return PageInterface|null
  854. */
  855. public function inherited($field)
  856. {
  857. [$inherited, $currentParams] = $this->getInheritedParams($field);
  858. $this->modifyHeader($field, $currentParams);
  859. return $inherited;
  860. }
  861. /**
  862. * Helper method to return an ancestor field only to inherit from. The
  863. * first occurrence of an ancestor field will be returned if at all.
  864. *
  865. * @param string $field Name of the parent folder
  866. * @return array
  867. */
  868. public function inheritedField($field): array
  869. {
  870. [, $currentParams] = $this->getInheritedParams($field);
  871. return $currentParams;
  872. }
  873. /**
  874. * Method that contains shared logic for inherited() and inheritedField()
  875. *
  876. * @param string $field Name of the parent folder
  877. * @return array
  878. */
  879. protected function getInheritedParams($field): array
  880. {
  881. /** @var Pages $pages */
  882. $pages = Grav::instance()['pages'];
  883. $inherited = $pages->inherited($this->getProperty('parent_route'), $field);
  884. $inheritedParams = $inherited ? (array)$inherited->value('header.' . $field) : [];
  885. $currentParams = (array)$this->getFormValue('header.' . $field);
  886. if ($inheritedParams && is_array($inheritedParams)) {
  887. $currentParams = array_replace_recursive($inheritedParams, $currentParams);
  888. }
  889. return [$inherited, $currentParams];
  890. }
  891. /**
  892. * Helper method to return a page.
  893. *
  894. * @param string $url the url of the page
  895. * @param bool $all
  896. * @return PageInterface|null page you were looking for if it exists
  897. */
  898. public function find($url, $all = false)
  899. {
  900. /** @var Pages $pages */
  901. $pages = Grav::instance()['pages'];
  902. return $pages->find($url, $all);
  903. }
  904. /**
  905. * Get a collection of pages in the current context.
  906. *
  907. * @param string|array $params
  908. * @param bool $pagination
  909. * @return PageCollectionInterface|Collection
  910. * @throws InvalidArgumentException
  911. */
  912. public function collection($params = 'content', $pagination = true)
  913. {
  914. if (is_string($params)) {
  915. // Look into a page header field.
  916. $params = (array)$this->getFormValue('header.' . $params);
  917. } elseif (!is_array($params)) {
  918. throw new InvalidArgumentException('Argument should be either header variable name or array of parameters');
  919. }
  920. if (!$pagination) {
  921. $params['pagination'] = false;
  922. }
  923. $context = [
  924. 'pagination' => $pagination,
  925. 'self' => $this
  926. ];
  927. /** @var Pages $pages */
  928. $pages = Grav::instance()['pages'];
  929. return $pages->getCollection($params, $context);
  930. }
  931. /**
  932. * @param string|array $value
  933. * @param bool $only_published
  934. * @return PageCollectionInterface|Collection
  935. */
  936. public function evaluate($value, $only_published = true)
  937. {
  938. $params = [
  939. 'items' => $value,
  940. 'published' => $only_published
  941. ];
  942. $context = [
  943. 'event' => false,
  944. 'pagination' => false,
  945. 'url_taxonomy_filters' => false,
  946. 'self' => $this
  947. ];
  948. /** @var Pages $pages */
  949. $pages = Grav::instance()['pages'];
  950. return $pages->getCollection($params, $context);
  951. }
  952. /**
  953. * Returns whether or not the current folder exists
  954. *
  955. * @return bool
  956. */
  957. public function folderExists(): bool
  958. {
  959. return $this->exists() || is_dir($this->getStorageFolder() ?? '');
  960. }
  961. /**
  962. * Gets the action.
  963. *
  964. * @return string|null The Action string.
  965. */
  966. public function getAction(): ?string
  967. {
  968. $meta = $this->getMetaData();
  969. if (!empty($meta['copy'])) {
  970. return 'copy';
  971. }
  972. if (isset($meta['storage_key']) && $this->getStorageKey() !== $meta['storage_key']) {
  973. return 'move';
  974. }
  975. return null;
  976. }
  977. }