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

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.      * @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 $typestring $nameDocument $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($eventDocumentEvents::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 $namestring $typeBlockState $blockStatestring $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 $namestring $type, array $parentBlockNamesint $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 $nameDocument $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. }