PageObject.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @package Grav\Common\Flex
  5. *
  6. * @copyright Copyright (c) 2015 - 2022 Trilby Media, LLC. All rights reserved.
  7. * @license MIT License; see LICENSE file for details.
  8. */
  9. namespace Grav\Common\Flex\Types\Pages;
  10. use Grav\Common\Data\Blueprint;
  11. use Grav\Common\Flex\Traits\FlexGravTrait;
  12. use Grav\Common\Flex\Traits\FlexObjectTrait;
  13. use Grav\Common\Grav;
  14. use Grav\Common\Flex\Types\Pages\Traits\PageContentTrait;
  15. use Grav\Common\Flex\Types\Pages\Traits\PageLegacyTrait;
  16. use Grav\Common\Flex\Types\Pages\Traits\PageRoutableTrait;
  17. use Grav\Common\Flex\Types\Pages\Traits\PageTranslateTrait;
  18. use Grav\Common\Language\Language;
  19. use Grav\Common\Page\Interfaces\PageInterface;
  20. use Grav\Common\Page\Pages;
  21. use Grav\Common\User\Interfaces\UserInterface;
  22. use Grav\Common\Utils;
  23. use Grav\Framework\Filesystem\Filesystem;
  24. use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
  25. use Grav\Framework\Flex\Pages\FlexPageObject;
  26. use Grav\Framework\Object\ObjectCollection;
  27. use Grav\Framework\Route\Route;
  28. use Grav\Framework\Route\RouteFactory;
  29. use Grav\Plugin\Admin\Admin;
  30. use RocketTheme\Toolbox\Event\Event;
  31. use RuntimeException;
  32. use stdClass;
  33. use function array_key_exists;
  34. use function count;
  35. use function func_get_args;
  36. use function in_array;
  37. use function is_array;
  38. /**
  39. * Class GravPageObject
  40. * @package Grav\Plugin\FlexObjects\Types\GravPages
  41. *
  42. * @property string $name
  43. * @property string $slug
  44. * @property string $route
  45. * @property string $folder
  46. * @property int|false $order
  47. * @property string $template
  48. * @property string $language
  49. */
  50. class PageObject extends FlexPageObject
  51. {
  52. use FlexGravTrait;
  53. use FlexObjectTrait;
  54. use PageContentTrait;
  55. use PageLegacyTrait;
  56. use PageRoutableTrait;
  57. use PageTranslateTrait;
  58. /** @var string Language code, eg: 'en' */
  59. protected $language;
  60. /** @var string File format, eg. 'md' */
  61. protected $format;
  62. /** @var bool */
  63. private $_initialized = false;
  64. /**
  65. * @return array
  66. */
  67. public static function getCachedMethods(): array
  68. {
  69. return [
  70. 'path' => true,
  71. 'full_order' => true,
  72. 'filterBy' => true,
  73. 'translated' => false,
  74. ] + parent::getCachedMethods();
  75. }
  76. /**
  77. * @return void
  78. */
  79. public function initialize(): void
  80. {
  81. if (!$this->_initialized) {
  82. Grav::instance()->fireEvent('onPageProcessed', new Event(['page' => $this]));
  83. $this->_initialized = true;
  84. }
  85. }
  86. /**
  87. * @param string|array $query
  88. * @return Route|null
  89. */
  90. public function getRoute($query = []): ?Route
  91. {
  92. $path = $this->route();
  93. if (null === $path) {
  94. return null;
  95. }
  96. $route = RouteFactory::createFromString($path);
  97. if ($lang = $route->getLanguage()) {
  98. $grav = Grav::instance();
  99. if (!$grav['config']->get('system.languages.include_default_lang')) {
  100. /** @var Language $language */
  101. $language = $grav['language'];
  102. if ($lang === $language->getDefault()) {
  103. $route = $route->withLanguage('');
  104. }
  105. }
  106. }
  107. if (is_array($query)) {
  108. foreach ($query as $key => $value) {
  109. $route = $route->withQueryParam($key, $value);
  110. }
  111. } else {
  112. $route = $route->withAddedPath($query);
  113. }
  114. return $route;
  115. }
  116. /**
  117. * @inheritdoc PageInterface
  118. */
  119. public function getFormValue(string $name, $default = null, string $separator = null)
  120. {
  121. $test = new stdClass();
  122. $value = $this->pageContentValue($name, $test);
  123. if ($value !== $test) {
  124. return $value;
  125. }
  126. switch ($name) {
  127. case 'name':
  128. // TODO: this should not be template!
  129. return $this->getProperty('template');
  130. case 'route':
  131. $filesystem = Filesystem::getInstance(false);
  132. $key = $filesystem->dirname($this->hasKey() ? '/' . $this->getKey() : '/');
  133. return $key !== '/' ? $key : null;
  134. case 'full_route':
  135. return $this->hasKey() ? '/' . $this->getKey() : '';
  136. case 'full_order':
  137. return $this->full_order();
  138. case 'lang':
  139. return $this->getLanguage() ?? '';
  140. case 'translations':
  141. return $this->getLanguages();
  142. }
  143. return parent::getFormValue($name, $default, $separator);
  144. }
  145. /**
  146. * {@inheritdoc}
  147. * @see FlexObjectInterface::getCacheKey()
  148. */
  149. public function getCacheKey(): string
  150. {
  151. $cacheKey = parent::getCacheKey();
  152. if ($cacheKey) {
  153. /** @var Language $language */
  154. $language = Grav::instance()['language'];
  155. $cacheKey .= '_' . $language->getActive();
  156. }
  157. return $cacheKey;
  158. }
  159. /**
  160. * @param array $variables
  161. * @return array
  162. */
  163. protected function onBeforeSave(array $variables)
  164. {
  165. $reorder = $variables[0] ?? true;
  166. $meta = $this->getMetaData();
  167. if (($meta['copy'] ?? false) === true) {
  168. $this->folder = $this->getKey();
  169. }
  170. // Figure out storage path to the new route.
  171. $parentKey = $this->getProperty('parent_key');
  172. if ($parentKey !== '') {
  173. $parentRoute = $this->getProperty('route');
  174. // Root page cannot be moved.
  175. if ($this->root()) {
  176. throw new RuntimeException(sprintf('Root page cannot be moved to %s', $parentRoute));
  177. }
  178. // Make sure page isn't being moved under itself.
  179. $key = $this->getStorageKey();
  180. /** @var PageObject|null $parent */
  181. $parent = $parentKey !== false ? $this->getFlexDirectory()->getObject($parentKey, 'storage_key') : null;
  182. if (!$parent) {
  183. // Page cannot be moved to non-existing location.
  184. throw new RuntimeException(sprintf('Page /%s cannot be moved to non-existing path %s', $key, $parentRoute));
  185. }
  186. // TODO: make sure that the page doesn't exist yet if moved/copied.
  187. }
  188. if ($reorder === true && !$this->root()) {
  189. $reorder = $this->_reorder;
  190. }
  191. // Force automatic reorder if item is supposed to be added to the last.
  192. if (!is_array($reorder) && (int)$this->order() >= 999999) {
  193. $reorder = [];
  194. }
  195. // Reorder siblings.
  196. $siblings = is_array($reorder) ? ($this->reorderSiblings($reorder) ?? []) : [];
  197. $data = $this->prepareStorage();
  198. unset($data['header']);
  199. foreach ($siblings as $sibling) {
  200. $data = $sibling->prepareStorage();
  201. unset($data['header']);
  202. }
  203. return ['reorder' => $reorder, 'siblings' => $siblings];
  204. }
  205. /**
  206. * @param array $variables
  207. * @return array
  208. */
  209. protected function onSave(array $variables): array
  210. {
  211. /** @var PageCollection $siblings */
  212. $siblings = $variables['siblings'];
  213. foreach ($siblings as $sibling) {
  214. $sibling->save(false);
  215. }
  216. return $variables;
  217. }
  218. /**
  219. * @param array $variables
  220. */
  221. protected function onAfterSave(array $variables): void
  222. {
  223. $this->getFlexDirectory()->reloadIndex();
  224. }
  225. /**
  226. * @param UserInterface|null $user
  227. */
  228. public function check(UserInterface $user = null): void
  229. {
  230. parent::check($user);
  231. if ($user && $this->isMoved()) {
  232. $parentKey = $this->getProperty('parent_key');
  233. /** @var PageObject|null $parent */
  234. $parent = $this->getFlexDirectory()->getObject($parentKey, 'storage_key');
  235. if (!$parent || !$parent->isAuthorized('create', null, $user)) {
  236. throw new \RuntimeException('Forbidden', 403);
  237. }
  238. }
  239. }
  240. /**
  241. * @param array|bool $reorder
  242. * @return static
  243. */
  244. public function save($reorder = true)
  245. {
  246. $variables = $this->onBeforeSave(func_get_args());
  247. // Backwards compatibility with older plugins.
  248. $fireEvents = $reorder && $this->isAdminSite() && $this->getFlexDirectory()->getConfig('object.compat.events', true);
  249. $grav = $this->getContainer();
  250. if ($fireEvents) {
  251. $self = $this;
  252. $grav->fireEvent('onAdminSave', new Event(['type' => 'flex', 'directory' => $this->getFlexDirectory(), 'object' => &$self]));
  253. if ($self !== $this) {
  254. throw new RuntimeException('Switching Flex Page object during onAdminSave event is not supported! Please update plugin.');
  255. }
  256. }
  257. /** @var static $instance */
  258. $instance = parent::save();
  259. $variables = $this->onSave($variables);
  260. $this->onAfterSave($variables);
  261. // Backwards compatibility with older plugins.
  262. if ($fireEvents) {
  263. $grav->fireEvent('onAdminAfterSave', new Event(['type' => 'flex', 'directory' => $this->getFlexDirectory(), 'object' => $this]));
  264. }
  265. // Reset original after save events have all been called.
  266. $this->_originalObject = null;
  267. return $instance;
  268. }
  269. /**
  270. * @return static
  271. */
  272. public function delete()
  273. {
  274. $result = parent::delete();
  275. // Backwards compatibility with older plugins.
  276. $fireEvents = $this->isAdminSite() && $this->getFlexDirectory()->getConfig('object.compat.events', true);
  277. if ($fireEvents) {
  278. $this->getContainer()->fireEvent('onAdminAfterDelete', new Event(['object' => $this]));
  279. }
  280. return $result;
  281. }
  282. /**
  283. * Prepare move page to new location. Moves also everything that's under the current page.
  284. *
  285. * You need to call $this->save() in order to perform the move.
  286. *
  287. * @param PageInterface $parent New parent page.
  288. * @return $this
  289. */
  290. public function move(PageInterface $parent)
  291. {
  292. if (!$parent instanceof FlexObjectInterface) {
  293. throw new RuntimeException('Failed: Parent is not Flex Object');
  294. }
  295. $this->_reorder = [];
  296. $this->setProperty('parent_key', $parent->getStorageKey());
  297. $this->storeOriginal();
  298. return $this;
  299. }
  300. /**
  301. * @param UserInterface $user
  302. * @param string $action
  303. * @param string $scope
  304. * @param bool $isMe
  305. * @return bool|null
  306. */
  307. protected function isAuthorizedOverride(UserInterface $user, string $action, string $scope, bool $isMe): ?bool
  308. {
  309. // Special case: creating a new page means checking parent for its permissions.
  310. if ($action === 'create' && !$this->exists()) {
  311. $parent = $this->parent();
  312. if ($parent && method_exists($parent, 'isAuthorized')) {
  313. return $parent->isAuthorized($action, $scope, $user);
  314. }
  315. return false;
  316. }
  317. return parent::isAuthorizedOverride($user, $action, $scope, $isMe);
  318. }
  319. /**
  320. * @return bool
  321. */
  322. protected function isMoved(): bool
  323. {
  324. $storageKey = $this->getMasterKey();
  325. $filesystem = Filesystem::getInstance(false);
  326. $oldParentKey = ltrim($filesystem->dirname("/{$storageKey}"), '/');
  327. $newParentKey = $this->getProperty('parent_key');
  328. return $this->exists() && $oldParentKey !== $newParentKey;
  329. }
  330. /**
  331. * @param array $ordering
  332. * @return PageCollection|null
  333. * @phpstan-return ObjectCollection<string,PageObject>|null
  334. */
  335. protected function reorderSiblings(array $ordering)
  336. {
  337. $storageKey = $this->getMasterKey();
  338. $isMoved = $this->isMoved();
  339. $order = !$isMoved ? $this->order() : false;
  340. if ($order !== false) {
  341. $order = (int)$order;
  342. }
  343. $parent = $this->parent();
  344. if (!$parent) {
  345. throw new RuntimeException('Cannot reorder a page which has no parent');
  346. }
  347. /** @var PageCollection $siblings */
  348. $siblings = $parent->children();
  349. $siblings = $siblings->getCollection()->withOrdered();
  350. // Handle special case where ordering isn't given.
  351. if ($ordering === []) {
  352. if ($order >= 999999) {
  353. // Set ordering to point to be the last item, ignoring the object itself.
  354. $order = 0;
  355. foreach ($siblings as $sibling) {
  356. if ($sibling->getKey() !== $this->getKey()) {
  357. $order = max($order, (int)$sibling->order());
  358. }
  359. }
  360. $this->order($order + 1);
  361. }
  362. // Do not change sibling ordering.
  363. return null;
  364. }
  365. $siblings = $siblings->orderBy(['order' => 'ASC']);
  366. if ($storageKey !== null) {
  367. if ($order !== false) {
  368. // Add current page back to the list if it's ordered.
  369. $siblings->set($storageKey, $this);
  370. } else {
  371. // Remove old copy of the current page from the siblings list.
  372. $siblings->remove($storageKey);
  373. }
  374. }
  375. // Add missing siblings into the end of the list, keeping the previous ordering between them.
  376. foreach ($siblings as $sibling) {
  377. $folder = (string)$sibling->getProperty('folder');
  378. $basename = preg_replace('|^\d+\.|', '', $folder);
  379. if (!in_array($basename, $ordering, true)) {
  380. $ordering[] = $basename;
  381. }
  382. }
  383. // Reorder.
  384. $ordering = array_flip(array_values($ordering));
  385. $count = count($ordering);
  386. foreach ($siblings as $sibling) {
  387. $folder = (string)$sibling->getProperty('folder');
  388. $basename = preg_replace('|^\d+\.|', '', $folder);
  389. $newOrder = $ordering[$basename] ?? null;
  390. $newOrder = null !== $newOrder ? $newOrder + 1 : (int)$sibling->order() + $count;
  391. $sibling->order($newOrder);
  392. }
  393. $siblings = $siblings->orderBy(['order' => 'ASC']);
  394. $siblings->removeElement($this);
  395. // If menu item was moved, just make it to be the last in order.
  396. if ($isMoved && $this->order() !== false) {
  397. $parentKey = $this->getProperty('parent_key');
  398. if ($parentKey === '') {
  399. /** @var PageIndex $index */
  400. $index = $this->getFlexDirectory()->getIndex();
  401. $newParent = $index->getRoot();
  402. } else {
  403. $newParent = $this->getFlexDirectory()->getObject($parentKey, 'storage_key');
  404. if (!$newParent instanceof PageInterface) {
  405. throw new RuntimeException("New parent page '{$parentKey}' not found.");
  406. }
  407. }
  408. /** @var PageCollection $newSiblings */
  409. $newSiblings = $newParent->children();
  410. $newSiblings = $newSiblings->getCollection()->withOrdered();
  411. $order = 0;
  412. foreach ($newSiblings as $sibling) {
  413. $order = max($order, (int)$sibling->order());
  414. }
  415. $this->order($order + 1);
  416. }
  417. return $siblings;
  418. }
  419. /**
  420. * @return string
  421. */
  422. public function full_order(): string
  423. {
  424. $route = $this->path() . '/' . $this->folder();
  425. return preg_replace(PageIndex::ORDER_LIST_REGEX, '\\1', $route) ?? $route;
  426. }
  427. /**
  428. * @param string $name
  429. * @return Blueprint
  430. */
  431. protected function doGetBlueprint(string $name = ''): Blueprint
  432. {
  433. try {
  434. // Make sure that pages has been initialized.
  435. Pages::getTypes();
  436. // TODO: We need to move raw blueprint logic to Grav itself to remove admin dependency here.
  437. if ($name === 'raw') {
  438. // Admin RAW mode.
  439. if ($this->isAdminSite()) {
  440. /** @var Admin $admin */
  441. $admin = Grav::instance()['admin'];
  442. $template = $this->isModule() ? 'modular_raw' : ($this->root() ? 'root_raw' : 'raw');
  443. return $admin->blueprints("admin/pages/{$template}");
  444. }
  445. }
  446. $template = $this->getProperty('template') . ($name ? '.' . $name : '');
  447. $blueprint = $this->getFlexDirectory()->getBlueprint($template, 'blueprints://pages');
  448. } catch (RuntimeException $e) {
  449. $template = 'default' . ($name ? '.' . $name : '');
  450. $blueprint = $this->getFlexDirectory()->getBlueprint($template, 'blueprints://pages');
  451. }
  452. $isNew = $blueprint->get('initialized', false) === false;
  453. if ($isNew === true && $name === '') {
  454. // Support onBlueprintCreated event just like in Pages::blueprints($template)
  455. $blueprint->set('initialized', true);
  456. $blueprint->setFilename($template);
  457. Grav::instance()->fireEvent('onBlueprintCreated', new Event(['blueprint' => $blueprint, 'type' => $template]));
  458. }
  459. return $blueprint;
  460. }
  461. /**
  462. * @param array $options
  463. * @return array
  464. */
  465. public function getLevelListing(array $options): array
  466. {
  467. $index = $this->getFlexDirectory()->getIndex();
  468. if (!is_callable([$index, 'getLevelListing'])) {
  469. return [];
  470. }
  471. // Deal with relative paths.
  472. $initial = $options['initial'] ?? null;
  473. $var = $initial ? 'leaf_route' : 'route';
  474. $route = $options[$var] ?? '';
  475. if ($route !== '' && !str_starts_with($route, '/')) {
  476. $filesystem = Filesystem::getInstance();
  477. $route = "/{$this->getKey()}/{$route}";
  478. $route = $filesystem->normalize($route);
  479. $options[$var] = $route;
  480. }
  481. [$status, $message, $response,] = $index->getLevelListing($options);
  482. return [$status, $message, $response, $options[$var] ?? null];
  483. }
  484. /**
  485. * Filter page (true/false) by given filters.
  486. *
  487. * - search: string
  488. * - extension: string
  489. * - module: bool
  490. * - visible: bool
  491. * - routable: bool
  492. * - published: bool
  493. * - page: bool
  494. * - translated: bool
  495. *
  496. * @param array $filters
  497. * @param bool $recursive
  498. * @return bool
  499. */
  500. public function filterBy(array $filters, bool $recursive = false): bool
  501. {
  502. foreach ($filters as $key => $value) {
  503. switch ($key) {
  504. case 'search':
  505. $matches = $this->search((string)$value) > 0.0;
  506. break;
  507. case 'page_type':
  508. $types = $value ? explode(',', $value) : [];
  509. $matches = in_array($this->template(), $types, true);
  510. break;
  511. case 'extension':
  512. $matches = Utils::contains((string)$value, $this->extension());
  513. break;
  514. case 'routable':
  515. $matches = $this->isRoutable() === (bool)$value;
  516. break;
  517. case 'published':
  518. $matches = $this->isPublished() === (bool)$value;
  519. break;
  520. case 'visible':
  521. $matches = $this->isVisible() === (bool)$value;
  522. break;
  523. case 'module':
  524. $matches = $this->isModule() === (bool)$value;
  525. break;
  526. case 'page':
  527. $matches = $this->isPage() === (bool)$value;
  528. break;
  529. case 'folder':
  530. $matches = $this->isPage() === !$value;
  531. break;
  532. case 'translated':
  533. $matches = $this->hasTranslation() === (bool)$value;
  534. break;
  535. default:
  536. $matches = true;
  537. break;
  538. }
  539. // If current filter does not match, we still may have match as a parent.
  540. if ($matches === false) {
  541. if (!$recursive) {
  542. return false;
  543. }
  544. /** @var PageIndex $index */
  545. $index = $this->children()->getIndex();
  546. return $index->filterBy($filters, true)->count() > 0;
  547. }
  548. }
  549. return true;
  550. }
  551. /**
  552. * {@inheritdoc}
  553. * @see FlexObjectInterface::exists()
  554. */
  555. public function exists(): bool
  556. {
  557. return $this->root ?: parent::exists();
  558. }
  559. /**
  560. * @return array
  561. */
  562. public function __debugInfo(): array
  563. {
  564. $list = parent::__debugInfo();
  565. return $list + [
  566. '_content_meta:private' => $this->getContentMeta(),
  567. '_content:private' => $this->getRawContent()
  568. ];
  569. }
  570. /**
  571. * @param array $elements
  572. * @param bool $extended
  573. */
  574. protected function filterElements(array &$elements, bool $extended = false): void
  575. {
  576. // Change parent page if needed.
  577. if (array_key_exists('route', $elements) && isset($elements['folder'], $elements['name'])) {
  578. $elements['template'] = $elements['name'];
  579. // Figure out storage path to the new route.
  580. $parentKey = trim($elements['route'] ?? '', '/');
  581. if ($parentKey !== '') {
  582. /** @var PageObject|null $parent */
  583. $parent = $this->getFlexDirectory()->getObject($parentKey);
  584. $parentKey = $parent ? $parent->getStorageKey() : $parentKey;
  585. }
  586. $elements['parent_key'] = $parentKey;
  587. }
  588. // Deal with ordering=bool and order=page1,page2,page3.
  589. if ($this->root()) {
  590. // Root page doesn't have ordering.
  591. unset($elements['ordering'], $elements['order']);
  592. } elseif (array_key_exists('ordering', $elements) && array_key_exists('order', $elements)) {
  593. // Store ordering.
  594. $ordering = $elements['order'] ?? null;
  595. $this->_reorder = !empty($ordering) ? explode(',', $ordering) : [];
  596. $order = false;
  597. if ((bool)($elements['ordering'] ?? false)) {
  598. $order = $this->order();
  599. if ($order === false) {
  600. $order = 999999;
  601. }
  602. }
  603. $elements['order'] = $order;
  604. }
  605. parent::filterElements($elements, true);
  606. }
  607. /**
  608. * @return array
  609. */
  610. public function prepareStorage(): array
  611. {
  612. $meta = $this->getMetaData();
  613. $oldLang = $meta['lang'] ?? '';
  614. $newLang = $this->getProperty('lang') ?? '';
  615. // Always clone the page to the new language.
  616. if ($oldLang !== $newLang) {
  617. $meta['clone'] = true;
  618. }
  619. // Make sure that certain elements are always sent to the storage layer.
  620. $elements = [
  621. '__META' => $meta,
  622. 'storage_key' => $this->getStorageKey(),
  623. 'parent_key' => $this->getProperty('parent_key'),
  624. 'order' => $this->getProperty('order'),
  625. 'folder' => preg_replace('|^\d+\.|', '', $this->getProperty('folder') ?? ''),
  626. 'template' => preg_replace('|modular/|', '', $this->getProperty('template') ?? ''),
  627. 'lang' => $newLang
  628. ] + parent::prepareStorage();
  629. return $elements;
  630. }
  631. }