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

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 $idstring $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.      * {@inheritdoc}
  322.      */
  323.     public function getConfig()
  324.     {
  325.         return is_array($this->config) ? $this->config : [];
  326.     }
  327.     /**
  328.      * {@inheritdoc}
  329.      */
  330.     public function setConfig($config)
  331.     {
  332.         $this->config $config;
  333.         return $this;
  334.     }
  335.     /**
  336.      * @param string $name
  337.      * @param mixed $value
  338.      *
  339.      * @return self
  340.      */
  341.     public function addConfig(string $name$value): self
  342.     {
  343.         if (!is_array($this->config)) {
  344.             $this->config = [];
  345.         }
  346.         $this->config[$name] = $value;
  347.         return $this;
  348.     }
  349.     /**
  350.      * @return string
  351.      */
  352.     public function getRealName()
  353.     {
  354.         return $this->realName;
  355.     }
  356.     /**
  357.      * @param string $realName
  358.      */
  359.     public function setRealName($realName)
  360.     {
  361.         $this->realName $realName;
  362.     }
  363.     final public function setParentBlockNames($parentNames)
  364.     {
  365.         if (is_array($parentNames)) {
  366.             // unfortunately we cannot make a type hint here, because of compatibility reasons
  367.             // old versions where 'parentBlockNames' was not excluded in __sleep() have still this property
  368.             // in the serialized data, and mostly with the value NULL, on restore this would lead to an error
  369.             $this->parentBlockNames $parentNames;
  370.         }
  371.     }
  372.     final public function getParentBlockNames(): array
  373.     {
  374.         return $this->parentBlockNames;
  375.     }
  376.     /**
  377.      * Returns only the properties which should be serialized
  378.      *
  379.      * @return array
  380.      */
  381.     public function __sleep()
  382.     {
  383.         $finalVars = [];
  384.         $parentVars parent::__sleep();
  385.         $blockedVars = ['editmode''parentBlockNames''document''config'];
  386.         foreach ($parentVars as $key) {
  387.             if (!in_array($key$blockedVars)) {
  388.                 $finalVars[] = $key;
  389.             }
  390.         }
  391.         return $finalVars;
  392.     }
  393.     public function __clone()
  394.     {
  395.         parent::__clone();
  396.         $this->document null;
  397.     }
  398.     /**
  399.      * {@inheritdoc}
  400.      */
  401.     final public function render()
  402.     {
  403.         if ($this->editmode) {
  404.             if ($collector $this->getEditableDefinitionCollector()) {
  405.                 $collector->add($this);
  406.             }
  407.             return $this->admin();
  408.         }
  409.         return $this->frontend();
  410.     }
  411.     /**
  412.      * direct output to the frontend
  413.      *
  414.      * @return string
  415.      */
  416.     public function __toString()
  417.     {
  418.         $result '';
  419.         try {
  420.             $result $this->render();
  421.         } catch (\Throwable $e) {
  422.             if (\Pimcore::inDebugMode()) {
  423.                 // the __toString method isn't allowed to throw exceptions
  424.                 $result '<b style="color:#f00">' $e->getMessage().' File: ' $e->getFile().' Line: '$e->getLine().'</b><br/>'.$e->getTraceAsString();
  425.                 return $result;
  426.             }
  427.             Logger::error('toString() returned an exception: {exception}', [
  428.                 'exception' => $e,
  429.             ]);
  430.             return '';
  431.         }
  432.         if (is_string($result) || is_numeric($result)) {
  433.             // we have to cast to string, because int/float is not auto-converted and throws an exception
  434.             return (string) $result;
  435.         }
  436.         return '';
  437.     }
  438.     /**
  439.      * @return bool
  440.      */
  441.     public function getEditmode()
  442.     {
  443.         return $this->editmode;
  444.     }
  445.     /**
  446.      * @param bool $editmode
  447.      *
  448.      * @return $this
  449.      */
  450.     public function setEditmode($editmode)
  451.     {
  452.         $this->editmode = (bool) $editmode;
  453.         return $this;
  454.     }
  455.     /**
  456.      * @return mixed
  457.      */
  458.     public function getDataForResource()
  459.     {
  460.         $this->checkValidity();
  461.         return $this->getData();
  462.     }
  463.     /**
  464.      * @param Model\Document\PageSnippet $ownerDocument
  465.      * @param array $tags
  466.      *
  467.      * @return array
  468.      */
  469.     public function getCacheTags(Model\Document\PageSnippet $ownerDocument, array $tags = []): array
  470.     {
  471.         return $tags;
  472.     }
  473.     /**
  474.      * This is a dummy and is mostly implemented by relation types
  475.      */
  476.     public function resolveDependencies()
  477.     {
  478.         return [];
  479.     }
  480.     /**
  481.      * @return bool
  482.      */
  483.     public function checkValidity()
  484.     {
  485.         return true;
  486.     }
  487.     /**
  488.      * @param bool $inherited
  489.      *
  490.      * @return $this
  491.      */
  492.     public function setInherited($inherited)
  493.     {
  494.         $this->inherited $inherited;
  495.         return $this;
  496.     }
  497.     /**
  498.      * @return bool
  499.      */
  500.     public function getInherited()
  501.     {
  502.         return $this->inherited;
  503.     }
  504.     /**
  505.      * @internal
  506.      *
  507.      * @return BlockState
  508.      */
  509.     protected function getBlockState(): BlockState
  510.     {
  511.         return $this->getBlockStateStack()->getCurrentState();
  512.     }
  513.     /**
  514.      * @internal
  515.      *
  516.      * @return BlockStateStack
  517.      */
  518.     protected function getBlockStateStack(): BlockStateStack
  519.     {
  520.         return \Pimcore::getContainer()->get(BlockStateStack::class);
  521.     }
  522.     /**
  523.      * Builds an editable name for an editable, taking current
  524.      * block state (block, index) and targeting into account.
  525.      *
  526.      * @internal
  527.      *
  528.      * @param string $type
  529.      * @param string $name
  530.      * @param Document|null $document
  531.      *
  532.      * @return string
  533.      *
  534.      * @throws \Exception
  535.      */
  536.     public static function buildEditableName(string $typestring $nameDocument $document null)
  537.     {
  538.         // do NOT allow dots (.) and colons (:) here as they act as delimiters
  539.         // for block hierarchy in the new naming scheme (see #1467)!
  540.         if (!preg_match("@^[a-zA-Z0-9\-_]+$@"$name)) {
  541.             throw new \InvalidArgumentException(
  542.                 'Only valid CSS class selectors are allowed as the name for an editable (which is basically [a-zA-Z0-9\-_]+). Your name was: ' $name
  543.             );
  544.         }
  545.         // @todo add document-id to registry key | for example for embeded snippets
  546.         // set suffixes if the editable is inside a block
  547.         $container \Pimcore::getContainer();
  548.         $blockState $container->get(BlockStateStack::class)->getCurrentState();
  549.         // if element not nested inside a hierarchical element (e.g. block), add the
  550.         // targeting prefix if configured on the document. hasBlocks() determines if
  551.         // there are any parent blocks for the current element
  552.         $targetGroupEditableName null;
  553.         if ($document && $document instanceof TargetingDocumentInterface) {
  554.             $targetGroupEditableName $document->getTargetGroupEditableName($name);
  555.             if (!$blockState->hasBlocks()) {
  556.                 $name $targetGroupEditableName;
  557.             }
  558.         }
  559.         $editableName self::doBuildName($name$type$blockState$targetGroupEditableName);
  560.         $event = new EditableNameEvent($type$name$blockState$editableName$document);
  561.         \Pimcore::getEventDispatcher()->dispatch($eventDocumentEvents::EDITABLE_NAME);
  562.         $editableName $event->getEditableName();
  563.         if (strlen($editableName) > 750) {
  564.             throw new \Exception(sprintf(
  565.                 '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',
  566.                 $name,
  567.                 $editableName
  568.             ));
  569.         }
  570.         return $editableName;
  571.     }
  572.     /**
  573.      * @param string $name
  574.      * @param string $type
  575.      * @param BlockState $blockState
  576.      * @param string|null $targetGroupElementName
  577.      *
  578.      * @return string
  579.      */
  580.     private static function doBuildName(string $namestring $typeBlockState $blockStatestring $targetGroupElementName null): string
  581.     {
  582.         if (!$blockState->hasBlocks()) {
  583.             return $name;
  584.         }
  585.         $blocks $blockState->getBlocks();
  586.         $indexes $blockState->getIndexes();
  587.         // check if the previous block is the name we're about to build
  588.         // TODO: can this be avoided at the block level?
  589.         if ($type === 'block' || $type == 'scheduledblock') {
  590.             $tmpBlocks $blocks;
  591.             $tmpIndexes $indexes;
  592.             array_pop($tmpBlocks);
  593.             array_pop($tmpIndexes);
  594.             $tmpName $name;
  595.             if (is_array($tmpBlocks)) {
  596.                 $tmpName self::buildHierarchicalName($name$tmpBlocks$tmpIndexes);
  597.             }
  598.             $previousBlockName $blocks[count($blocks) - 1]->getName();
  599.             if ($previousBlockName === $tmpName || ($targetGroupElementName && $previousBlockName === $targetGroupElementName)) {
  600.                 array_pop($blocks);
  601.                 array_pop($indexes);
  602.             }
  603.         }
  604.         return self::buildHierarchicalName($name$blocks$indexes);
  605.     }
  606.     /**
  607.      * @param string $name
  608.      * @param BlockName[] $blocks
  609.      * @param int[] $indexes
  610.      *
  611.      * @return string
  612.      */
  613.     private static function buildHierarchicalName(string $name, array $blocks, array $indexes): string
  614.     {
  615.         if (count($indexes) > count($blocks)) {
  616.             throw new \RuntimeException(sprintf('Index count %d is greater than blocks count %d'count($indexes), count($blocks)));
  617.         }
  618.         $parts = [];
  619.         for ($i 0$i count($blocks); $i++) {
  620.             $part $blocks[$i]->getRealName();
  621.             if (isset($indexes[$i])) {
  622.                 $part sprintf('%s:%d'$part$indexes[$i]);
  623.             }
  624.             $parts[] = $part;
  625.         }
  626.         $parts[] = $name;
  627.         return implode('.'$parts);
  628.     }
  629.     /**
  630.      * @internal
  631.      *
  632.      * @param string $name
  633.      * @param string $type
  634.      * @param array $parentBlockNames
  635.      * @param int $index
  636.      *
  637.      * @return string
  638.      *
  639.      * @throws \Exception
  640.      */
  641.     public static function buildChildEditableName(string $namestring $type, array $parentBlockNamesint $index): string
  642.     {
  643.         if (count($parentBlockNames) === 0) {
  644.             throw new \Exception(sprintf(
  645.                 'Failed to build child tag name for %s %s at index %d as no parent name was passed',
  646.                 $type,
  647.                 $name,
  648.                 $index
  649.             ));
  650.         }
  651.         $parentName array_pop($parentBlockNames);
  652.         return sprintf('%s:%d.%s'$parentName$index$name);
  653.     }
  654.     /**
  655.      * @internal
  656.      *
  657.      * @param string $name
  658.      * @param Document $document
  659.      *
  660.      * @return string
  661.      */
  662.     public static function buildEditableRealName(string $nameDocument $document): string
  663.     {
  664.         $blockState \Pimcore::getContainer()->get(BlockStateStack::class)->getCurrentState();
  665.         // if element not nested inside a hierarchical element (e.g. block), add the
  666.         // targeting prefix if configured on the document. hasBlocks() determines if
  667.         // there are any parent blocks for the current element
  668.         if ($document instanceof TargetingDocumentInterface && !$blockState->hasBlocks()) {
  669.             $name $document->getTargetGroupEditableName($name);
  670.         }
  671.         return $name;
  672.     }
  673.     /**
  674.      * @return bool
  675.      */
  676.     public function isInDialogBox(): bool
  677.     {
  678.         return (bool) $this->inDialogBox;
  679.     }
  680.     /**
  681.      * @return string|null
  682.      */
  683.     public function getInDialogBox(): ?string
  684.     {
  685.         return $this->inDialogBox;
  686.     }
  687.     /**
  688.      * @param string|null $inDialogBox
  689.      *
  690.      * @return $this
  691.      */
  692.     public function setInDialogBox(?string $inDialogBox): self
  693.     {
  694.         $this->inDialogBox $inDialogBox;
  695.         return $this;
  696.     }
  697.     /**
  698.      * @return EditmodeEditableDefinitionCollector|null
  699.      */
  700.     public function getEditableDefinitionCollector(): ?EditmodeEditableDefinitionCollector
  701.     {
  702.         return $this->editableDefinitionCollector;
  703.     }
  704.     /**
  705.      * @param EditmodeEditableDefinitionCollector|null $editableDefinitionCollector
  706.      *
  707.      * @return $this
  708.      */
  709.     public function setEditableDefinitionCollector(?EditmodeEditableDefinitionCollector $editableDefinitionCollector): self
  710.     {
  711.         $this->editableDefinitionCollector $editableDefinitionCollector;
  712.         return $this;
  713.     }
  714. }