vendor/pimcore/pimcore/models/Document/Editable.php line 475

Open in your IDE?
  1. <?php
  2. /**
  3. * Pimcore
  4. *
  5. * This source file is available under two different licenses:
  6. * - GNU General Public License version 3 (GPLv3)
  7. * - Pimcore Commercial License (PCL)
  8. * Full copyright and license information is available in
  9. * LICENSE.md which is distributed with this source code.
  10. *
  11. * @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
  12. * @license http://www.pimcore.org/license GPLv3 and PCL
  13. */
  14. namespace Pimcore\Model\Document;
  15. use Pimcore\Document\Editable\Block\BlockName;
  16. use Pimcore\Document\Editable\Block\BlockState;
  17. use Pimcore\Document\Editable\Block\BlockStateStack;
  18. use Pimcore\Document\Editable\EditmodeEditableDefinitionCollector;
  19. use Pimcore\Event\DocumentEvents;
  20. use Pimcore\Event\Model\Document\EditableNameEvent;
  21. use Pimcore\Logger;
  22. use Pimcore\Model;
  23. use Pimcore\Model\Document;
  24. use Pimcore\Model\Document\Targeting\TargetingDocumentInterface;
  25. use Pimcore\Tool\HtmlUtils;
  26. /**
  27. * @method \Pimcore\Model\Document\Editable\Dao getDao()
  28. * @method void save()
  29. * @method void delete()
  30. */
  31. abstract class Editable extends Model\AbstractModel implements Model\Document\Editable\EditableInterface
  32. {
  33. /**
  34. * Contains some configurations for the editmode, or the thumbnail name, ...
  35. *
  36. * @internal
  37. *
  38. * @var array|null
  39. */
  40. protected $config;
  41. /**
  42. * @internal
  43. *
  44. * @var string
  45. */
  46. protected $name;
  47. /**
  48. * Contains the real name of the editable without the prefixes and suffixes
  49. * which are generated automatically by blocks and areablocks
  50. *
  51. * @internal
  52. *
  53. * @var string
  54. */
  55. protected $realName;
  56. /**
  57. * Contains parent hierarchy names (used when building elements inside a block/areablock hierarchy)
  58. *
  59. * @var array
  60. */
  61. private $parentBlockNames = [];
  62. /**
  63. * Element belongs to the ID of the document
  64. *
  65. * @internal
  66. *
  67. * @var int
  68. */
  69. protected $documentId;
  70. /**
  71. * Element belongs to the document
  72. *
  73. * @internal
  74. *
  75. * @var Document\PageSnippet|null
  76. */
  77. protected $document;
  78. /**
  79. * In Editmode or not
  80. *
  81. * @internal
  82. *
  83. * @var bool
  84. */
  85. protected $editmode;
  86. /**
  87. * @internal
  88. *
  89. * @var bool
  90. */
  91. protected $inherited = false;
  92. /**
  93. * @internal
  94. *
  95. * @var string
  96. */
  97. protected $inDialogBox = null;
  98. /**
  99. * @var EditmodeEditableDefinitionCollector|null
  100. */
  101. private $editableDefinitionCollector;
  102. /**
  103. * @return string|void
  104. *
  105. * @throws \Exception
  106. *
  107. * @internal
  108. */
  109. public function admin()
  110. {
  111. $attributes = $this->getEditmodeElementAttributes();
  112. $attributeString = HtmlUtils::assembleAttributeString($attributes);
  113. $htmlContainerCode = ('<div ' . $attributeString . '></div>');
  114. if ($this->isInDialogBox()) {
  115. $htmlContainerCode = $this->wrapEditmodeContainerCodeForDialogBox($attributes['id'], $htmlContainerCode);
  116. }
  117. return $htmlContainerCode;
  118. }
  119. /**
  120. * Return the data for direct output to the frontend, can also contain HTML code!
  121. *
  122. * @return string|void
  123. */
  124. abstract public function frontend();
  125. /**
  126. * @param string $id
  127. * @param string $code
  128. *
  129. * @return string
  130. */
  131. private function wrapEditmodeContainerCodeForDialogBox(string $id, string $code): string
  132. {
  133. $code = '<template id="template__' . $id . '">' . $code . '</template>';
  134. return $code;
  135. }
  136. /**
  137. * Builds config passed to editmode frontend as JSON config
  138. *
  139. * @return array
  140. *
  141. * @internal
  142. */
  143. public function getEditmodeDefinition(): array
  144. {
  145. $config = [
  146. // we don't use : and . in IDs (although it's allowed in HTML spec)
  147. // because they are used in CSS syntax and therefore can't be used in querySelector()
  148. 'id' => 'pimcore_editable_' . str_replace([':', '.'], '_', $this->getName()),
  149. 'name' => $this->getName(),
  150. 'realName' => $this->getRealName(),
  151. 'config' => $this->getConfig(),
  152. 'data' => $this->getEditmodeData(),
  153. 'type' => $this->getType(),
  154. 'inherited' => $this->getInherited(),
  155. 'inDialogBox' => $this->getInDialogBox(),
  156. ];
  157. return $config;
  158. }
  159. /**
  160. * Builds data used for editmode
  161. *
  162. * @return mixed
  163. *
  164. * @internal
  165. */
  166. protected function getEditmodeData()
  167. {
  168. // get configuration data for admin
  169. //TODO Pimcore 11: remove method_exists BC layer
  170. if ($this instanceof Document\Editable\EditmodeDataInterface || method_exists($this, 'getDataEditmode')) {
  171. if (!$this instanceof Document\Editable\EditmodeDataInterface) {
  172. trigger_deprecation('pimcore/pimcore', '10.3',
  173. sprintf('Usage of method_exists is deprecated since version 10.3 and will be removed in Pimcore 11.' .
  174. 'Implement the %s interface instead.', Document\Editable\EditmodeDataInterface::class));
  175. }
  176. $data = $this->getDataEditmode();
  177. } else {
  178. $data = $this->getData();
  179. }
  180. return $data;
  181. }
  182. /**
  183. * Builds attributes used on the editmode HTML element
  184. *
  185. * @return array
  186. *
  187. * @internal
  188. */
  189. protected function getEditmodeElementAttributes(): array
  190. {
  191. $config = $this->getEditmodeDefinition();
  192. if (!isset($config['id'])) {
  193. throw new \RuntimeException(sprintf('Expected an "id" option to be set on the "%s" editable config array', $this->getName()));
  194. }
  195. $attributes = array_merge($this->getEditmodeBlockStateAttributes(), [
  196. 'id' => $config['id'],
  197. 'class' => implode(' ', $this->getEditmodeElementClasses()),
  198. ]);
  199. return $attributes;
  200. }
  201. /**
  202. * @return array
  203. *
  204. * @internal
  205. */
  206. protected function getEditmodeBlockStateAttributes(): array
  207. {
  208. $blockState = $this->getBlockState();
  209. $blockNames = array_map(function (BlockName $blockName) {
  210. return $blockName->getRealName();
  211. }, $blockState->getBlocks());
  212. $attributes = [
  213. 'data-name' => $this->getName(),
  214. 'data-real-name' => $this->getRealName(),
  215. 'data-type' => $this->getType(),
  216. 'data-block-names' => implode(', ', $blockNames),
  217. 'data-block-indexes' => implode(', ', $blockState->getIndexes()),
  218. ];
  219. return $attributes;
  220. }
  221. /**
  222. * Builds classes used on the editmode HTML element
  223. *
  224. * @return array
  225. *
  226. * @internal
  227. */
  228. protected function getEditmodeElementClasses(): array
  229. {
  230. $classes = [
  231. 'pimcore_editable',
  232. 'pimcore_editable_' . $this->getType(),
  233. ];
  234. $editableConfig = $this->getConfig();
  235. if (isset($editableConfig['class'])) {
  236. if (is_array($editableConfig['class'])) {
  237. $classes = array_merge($classes, $editableConfig['class']);
  238. } else {
  239. $classes[] = (string)$editableConfig['class'];
  240. }
  241. }
  242. return $classes;
  243. }
  244. /**
  245. * Sends data to the output stream
  246. *
  247. * @param string $value
  248. */
  249. protected function outputEditmode($value)
  250. {
  251. if ($this->getEditmode()) {
  252. echo $value . "\n";
  253. }
  254. }
  255. /**
  256. * @return mixed
  257. */
  258. public function getValue()
  259. {
  260. return $this->getData();
  261. }
  262. /**
  263. * @return string
  264. */
  265. public function getName()
  266. {
  267. return $this->name;
  268. }
  269. /**
  270. * @param string $name
  271. *
  272. * @return $this
  273. */
  274. public function setName($name)
  275. {
  276. $this->name = $name;
  277. return $this;
  278. }
  279. /**
  280. * @param int $id
  281. *
  282. * @return $this
  283. */
  284. public function setDocumentId($id)
  285. {
  286. $this->documentId = (int) $id;
  287. if ($this->document instanceof PageSnippet && $this->document->getId() !== $this->documentId) {
  288. $this->document = null;
  289. }
  290. return $this;
  291. }
  292. /**
  293. * @return int
  294. */
  295. public function getDocumentId()
  296. {
  297. return $this->documentId;
  298. }
  299. /**
  300. * @param Document\PageSnippet $document
  301. *
  302. * @return $this
  303. */
  304. public function setDocument(Document\PageSnippet $document)
  305. {
  306. $this->document = $document;
  307. $this->documentId = (int) $document->getId();
  308. return $this;
  309. }
  310. /**
  311. * @return Document\PageSnippet
  312. */
  313. public function getDocument()
  314. {
  315. if (!$this->document) {
  316. $this->document = Document\PageSnippet::getById($this->documentId);
  317. }
  318. return $this->document;
  319. }
  320. /**
  321. * @return array
  322. */
  323. public function getConfig()
  324. {
  325. return is_array($this->config) ? $this->config : [];
  326. }
  327. /**
  328. * @param array $config
  329. *
  330. * @return $this
  331. */
  332. public function setConfig($config)
  333. {
  334. $this->config = $config;
  335. return $this;
  336. }
  337. /**
  338. * @param string $name
  339. * @param mixed $value
  340. *
  341. * @return $this
  342. */
  343. public function addConfig(string $name, $value): self
  344. {
  345. if (!is_array($this->config)) {
  346. $this->config = [];
  347. }
  348. $this->config[$name] = $value;
  349. return $this;
  350. }
  351. /**
  352. * @return string
  353. */
  354. public function getRealName()
  355. {
  356. return $this->realName;
  357. }
  358. /**
  359. * @param string $realName
  360. */
  361. public function setRealName($realName)
  362. {
  363. $this->realName = $realName;
  364. }
  365. final public function setParentBlockNames($parentNames)
  366. {
  367. if (is_array($parentNames)) {
  368. // unfortunately we cannot make a type hint here, because of compatibility reasons
  369. // old versions where 'parentBlockNames' was not excluded in __sleep() have still this property
  370. // in the serialized data, and mostly with the value NULL, on restore this would lead to an error
  371. $this->parentBlockNames = $parentNames;
  372. }
  373. }
  374. final public function getParentBlockNames(): array
  375. {
  376. return $this->parentBlockNames;
  377. }
  378. /**
  379. * Returns only the properties which should be serialized
  380. *
  381. * @return array
  382. */
  383. public function __sleep()
  384. {
  385. $finalVars = [];
  386. $parentVars = parent::__sleep();
  387. $blockedVars = ['editmode', 'parentBlockNames', 'document', 'config'];
  388. foreach ($parentVars as $key) {
  389. if (!in_array($key, $blockedVars)) {
  390. $finalVars[] = $key;
  391. }
  392. }
  393. return $finalVars;
  394. }
  395. public function __clone()
  396. {
  397. parent::__clone();
  398. $this->document = null;
  399. }
  400. /**
  401. * {@inheritdoc}
  402. */
  403. final public function render()
  404. {
  405. if ($this->editmode) {
  406. if ($collector = $this->getEditableDefinitionCollector()) {
  407. $collector->add($this);
  408. }
  409. return $this->admin();
  410. }
  411. return $this->frontend();
  412. }
  413. /**
  414. * direct output to the frontend
  415. *
  416. * @return string
  417. */
  418. public function __toString()
  419. {
  420. $result = '';
  421. try {
  422. $result = $this->render();
  423. } catch (\Throwable $e) {
  424. if (\Pimcore::inDebugMode()) {
  425. // the __toString method isn't allowed to throw exceptions
  426. $result = '<b style="color:#f00">' . $e->getMessage().' File: ' . $e->getFile().' Line: '. $e->getLine().'</b><br/>'.$e->getTraceAsString();
  427. return $result;
  428. }
  429. Logger::error('toString() returned an exception: {exception}', [
  430. 'exception' => $e,
  431. ]);
  432. return '';
  433. }
  434. if (is_string($result) || is_numeric($result)) {
  435. // we have to cast to string, because int/float is not auto-converted and throws an exception
  436. return (string) $result;
  437. }
  438. return '';
  439. }
  440. /**
  441. * @return bool
  442. */
  443. public function getEditmode()
  444. {
  445. return $this->editmode;
  446. }
  447. /**
  448. * @param bool $editmode
  449. *
  450. * @return $this
  451. */
  452. public function setEditmode($editmode)
  453. {
  454. $this->editmode = (bool) $editmode;
  455. return $this;
  456. }
  457. /**
  458. * @return mixed
  459. */
  460. public function getDataForResource()
  461. {
  462. $this->checkValidity();
  463. return $this->getData();
  464. }
  465. /**
  466. * @param Model\Document\PageSnippet $ownerDocument
  467. * @param array $tags
  468. *
  469. * @return array
  470. */
  471. public function getCacheTags(Model\Document\PageSnippet $ownerDocument, array $tags = []): array
  472. {
  473. return $tags;
  474. }
  475. /**
  476. * This is a dummy and is mostly implemented by relation types
  477. */
  478. public function resolveDependencies()
  479. {
  480. return [];
  481. }
  482. /**
  483. * @return bool
  484. */
  485. public function checkValidity()
  486. {
  487. return true;
  488. }
  489. /**
  490. * @param bool $inherited
  491. *
  492. * @return $this
  493. */
  494. public function setInherited($inherited)
  495. {
  496. $this->inherited = $inherited;
  497. return $this;
  498. }
  499. /**
  500. * @return bool
  501. */
  502. public function getInherited()
  503. {
  504. return $this->inherited;
  505. }
  506. /**
  507. * @internal
  508. *
  509. * @return BlockState
  510. */
  511. protected function getBlockState(): BlockState
  512. {
  513. return $this->getBlockStateStack()->getCurrentState();
  514. }
  515. /**
  516. * @internal
  517. *
  518. * @return BlockStateStack
  519. */
  520. protected function getBlockStateStack(): BlockStateStack
  521. {
  522. return \Pimcore::getContainer()->get(BlockStateStack::class);
  523. }
  524. /**
  525. * Builds an editable name for an editable, taking current
  526. * block state (block, index) and targeting into account.
  527. *
  528. * @internal
  529. *
  530. * @param string $type
  531. * @param string $name
  532. * @param Document|null $document
  533. *
  534. * @return string
  535. *
  536. * @throws \Exception
  537. */
  538. public static function buildEditableName(string $type, string $name, Document $document = null)
  539. {
  540. // do NOT allow dots (.) and colons (:) here as they act as delimiters
  541. // for block hierarchy in the new naming scheme (see #1467)!
  542. if (!preg_match("@^[a-zA-Z0-9\-_]+$@", $name)) {
  543. throw new \InvalidArgumentException(
  544. 'Only valid CSS class selectors are allowed as the name for an editable (which is basically [a-zA-Z0-9\-_]+). Your name was: ' . $name
  545. );
  546. }
  547. // @todo add document-id to registry key | for example for embeded snippets
  548. // set suffixes if the editable is inside a block
  549. $container = \Pimcore::getContainer();
  550. $blockState = $container->get(BlockStateStack::class)->getCurrentState();
  551. // if element not nested inside a hierarchical element (e.g. block), add the
  552. // targeting prefix if configured on the document. hasBlocks() determines if
  553. // there are any parent blocks for the current element
  554. $targetGroupEditableName = null;
  555. if ($document && $document instanceof TargetingDocumentInterface) {
  556. $targetGroupEditableName = $document->getTargetGroupEditableName($name);
  557. if (!$blockState->hasBlocks()) {
  558. $name = $targetGroupEditableName;
  559. }
  560. }
  561. $editableName = self::doBuildName($name, $type, $blockState, $targetGroupEditableName);
  562. $event = new EditableNameEvent($type, $name, $blockState, $editableName, $document);
  563. \Pimcore::getEventDispatcher()->dispatch($event, DocumentEvents::EDITABLE_NAME);
  564. $editableName = $event->getEditableName();
  565. if (strlen($editableName) > 750) {
  566. throw new \Exception(sprintf(
  567. 'Composite name for editable "%s" is longer than 750 characters. Use shorter names for your editables or reduce amount of nesting levels. Name is: %s',
  568. $name,
  569. $editableName
  570. ));
  571. }
  572. return $editableName;
  573. }
  574. /**
  575. * @param string $name
  576. * @param string $type
  577. * @param BlockState $blockState
  578. * @param string|null $targetGroupElementName
  579. *
  580. * @return string
  581. */
  582. private static function doBuildName(string $name, string $type, BlockState $blockState, string $targetGroupElementName = null): string
  583. {
  584. if (!$blockState->hasBlocks()) {
  585. return $name;
  586. }
  587. $blocks = $blockState->getBlocks();
  588. $indexes = $blockState->getIndexes();
  589. // check if the previous block is the name we're about to build
  590. // TODO: can this be avoided at the block level?
  591. if ($type === 'block' || $type == 'scheduledblock') {
  592. $tmpBlocks = $blocks;
  593. $tmpIndexes = $indexes;
  594. array_pop($tmpBlocks);
  595. array_pop($tmpIndexes);
  596. $tmpName = $name;
  597. if (is_array($tmpBlocks)) {
  598. $tmpName = self::buildHierarchicalName($name, $tmpBlocks, $tmpIndexes);
  599. }
  600. $previousBlockName = $blocks[count($blocks) - 1]->getName();
  601. if ($previousBlockName === $tmpName || ($targetGroupElementName && $previousBlockName === $targetGroupElementName)) {
  602. array_pop($blocks);
  603. array_pop($indexes);
  604. }
  605. }
  606. return self::buildHierarchicalName($name, $blocks, $indexes);
  607. }
  608. /**
  609. * @param string $name
  610. * @param BlockName[] $blocks
  611. * @param int[] $indexes
  612. *
  613. * @return string
  614. */
  615. private static function buildHierarchicalName(string $name, array $blocks, array $indexes): string
  616. {
  617. if (count($indexes) > count($blocks)) {
  618. throw new \RuntimeException(sprintf('Index count %d is greater than blocks count %d', count($indexes), count($blocks)));
  619. }
  620. $parts = [];
  621. for ($i = 0; $i < count($blocks); $i++) {
  622. $part = $blocks[$i]->getRealName();
  623. if (isset($indexes[$i])) {
  624. $part = sprintf('%s:%d', $part, $indexes[$i]);
  625. }
  626. $parts[] = $part;
  627. }
  628. $parts[] = $name;
  629. return implode('.', $parts);
  630. }
  631. /**
  632. * @internal
  633. *
  634. * @param string $name
  635. * @param string $type
  636. * @param array $parentBlockNames
  637. * @param int $index
  638. *
  639. * @return string
  640. *
  641. * @throws \Exception
  642. */
  643. public static function buildChildEditableName(string $name, string $type, array $parentBlockNames, int $index): string
  644. {
  645. if (count($parentBlockNames) === 0) {
  646. throw new \Exception(sprintf(
  647. 'Failed to build child tag name for %s %s at index %d as no parent name was passed',
  648. $type,
  649. $name,
  650. $index
  651. ));
  652. }
  653. $parentName = array_pop($parentBlockNames);
  654. return sprintf('%s:%d.%s', $parentName, $index, $name);
  655. }
  656. /**
  657. * @internal
  658. *
  659. * @param string $name
  660. * @param Document $document
  661. *
  662. * @return string
  663. */
  664. public static function buildEditableRealName(string $name, Document $document): string
  665. {
  666. $blockState = \Pimcore::getContainer()->get(BlockStateStack::class)->getCurrentState();
  667. // if element not nested inside a hierarchical element (e.g. block), add the
  668. // targeting prefix if configured on the document. hasBlocks() determines if
  669. // there are any parent blocks for the current element
  670. if ($document instanceof TargetingDocumentInterface && !$blockState->hasBlocks()) {
  671. $name = $document->getTargetGroupEditableName($name);
  672. }
  673. return $name;
  674. }
  675. /**
  676. * @return bool
  677. */
  678. public function isInDialogBox(): bool
  679. {
  680. return (bool) $this->inDialogBox;
  681. }
  682. /**
  683. * @return string|null
  684. */
  685. public function getInDialogBox(): ?string
  686. {
  687. return $this->inDialogBox;
  688. }
  689. /**
  690. * @param string|null $inDialogBox
  691. *
  692. * @return $this
  693. */
  694. public function setInDialogBox(?string $inDialogBox): self
  695. {
  696. $this->inDialogBox = $inDialogBox;
  697. return $this;
  698. }
  699. /**
  700. * @return EditmodeEditableDefinitionCollector|null
  701. */
  702. public function getEditableDefinitionCollector(): ?EditmodeEditableDefinitionCollector
  703. {
  704. return $this->editableDefinitionCollector;
  705. }
  706. /**
  707. * @param EditmodeEditableDefinitionCollector|null $editableDefinitionCollector
  708. *
  709. * @return $this
  710. */
  711. public function setEditableDefinitionCollector(?EditmodeEditableDefinitionCollector $editableDefinitionCollector): self
  712. {
  713. $this->editableDefinitionCollector = $editableDefinitionCollector;
  714. return $this;
  715. }
  716. }