PageLegacyTrait.php 33 KB

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