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

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.         if (method_exists($this'getDataEditmode')) {
  170.             $data $this->getDataEditmode();
  171.         } else {
  172.             $data $this->getData();
  173.         }
  174.         return $data;
  175.     }
  176.     /**
  177.      * Builds attributes used on the editmode HTML element
  178.      *
  179.      * @return array
  180.      *
  181.      * @internal
  182.      */
  183.     protected function getEditmodeElementAttributes(): array
  184.     {
  185.         $config $this->getEditmodeDefinition();
  186.         if (!isset($config['id'])) {
  187.             throw new \RuntimeException(sprintf('Expected an "id" option to be set on the "%s" editable config array'$this->getName()));
  188.         }
  189.         $attributes array_merge($this->getEditmodeBlockStateAttributes(), [
  190.             'id' => $config['id'],
  191.             'class' => implode(' '$this->getEditmodeElementClasses()),
  192.         ]);
  193.         return $attributes;
  194.     }
  195.     /**
  196.      * @return array
  197.      *
  198.      * @internal
  199.      */
  200.     protected function getEditmodeBlockStateAttributes(): array
  201.     {
  202.         $blockState $this->getBlockState();
  203.         $blockNames array_map(function (BlockName $blockName) {
  204.             return $blockName->getRealName();
  205.         }, $blockState->getBlocks());
  206.         $attributes = [
  207.             'data-name' => $this->getName(),
  208.             'data-real-name' => $this->getRealName(),
  209.             'data-type' => $this->getType(),
  210.             'data-block-names' => implode(', '$blockNames),
  211.             'data-block-indexes' => implode(', '$blockState->getIndexes()),
  212.         ];
  213.         return $attributes;
  214.     }
  215.     /**
  216.      * Builds classes used on the editmode HTML element
  217.      *
  218.      * @return array
  219.      *
  220.      * @internal
  221.      */
  222.     protected function getEditmodeElementClasses(): array
  223.     {
  224.         $classes = [
  225.             'pimcore_editable',
  226.             'pimcore_editable_' $this->getType(),
  227.         ];
  228.         $editableConfig $this->getConfig();
  229.         if (isset($editableConfig['class'])) {
  230.             if (is_array($editableConfig['class'])) {
  231.                 $classes array_merge($classes$editableConfig['class']);
  232.             } else {
  233.                 $classes[] = (string)$editableConfig['class'];
  234.             }
  235.         }
  236.         return $classes;
  237.     }
  238.     /**
  239.      * Sends data to the output stream
  240.      *
  241.      * @param string $value
  242.      */
  243.     protected function outputEditmode($value)
  244.     {
  245.         if ($this->getEditmode()) {
  246.             echo $value "\n";
  247.         }
  248.     }
  249.     /**
  250.      * @return mixed
  251.      */
  252.     public function getValue()
  253.     {
  254.         return $this->getData();
  255.     }
  256.     /**
  257.      * @return string
  258.      */
  259.     public function getName()
  260.     {
  261.         return $this->name;
  262.     }
  263.     /**
  264.      * @param string $name
  265.      *
  266.      * @return $this
  267.      */
  268.     public function setName($name)
  269.     {
  270.         $this->name $name;
  271.         return $this;
  272.     }
  273.     /**
  274.      * @param int $id
  275.      *
  276.      * @return $this
  277.      */
  278.     public function setDocumentId($id)
  279.     {
  280.         $this->documentId = (int) $id;
  281.         if ($this->document instanceof PageSnippet && $this->document->getId() !== $this->documentId) {
  282.             $this->document null;
  283.         }
  284.         return $this;
  285.     }
  286.     /**
  287.      * @return int
  288.      */
  289.     public function getDocumentId()
  290.     {
  291.         return $this->documentId;
  292.     }
  293.     /**
  294.      * @param Document\PageSnippet $document
  295.      *
  296.      * @return $this
  297.      */
  298.     public function setDocument(Document\PageSnippet $document)
  299.     {
  300.         $this->document $document;
  301.         $this->documentId = (int) $document->getId();
  302.         return $this;
  303.     }
  304.     /**
  305.      * @return Document\PageSnippet
  306.      */
  307.     public function getDocument()
  308.     {
  309.         if (!$this->document) {
  310.             $this->document Document\PageSnippet::getById($this->documentId);
  311.         }
  312.         return $this->document;
  313.     }
  314.     /**
  315.      * {@inheritdoc}
  316.      */
  317.     public function getConfig()
  318.     {
  319.         return is_array($this->config) ? $this->config : [];
  320.     }
  321.     /**
  322.      * {@inheritdoc}
  323.      */
  324.     public function setConfig($config)
  325.     {
  326.         $this->config $config;
  327.         return $this;
  328.     }
  329.     /**
  330.      * @param string $name
  331.      * @param mixed $value
  332.      *
  333.      * @return self
  334.      */
  335.     public function addConfig(string $name$value): self
  336.     {
  337.         if (!is_array($this->config)) {
  338.             $this->config = [];
  339.         }
  340.         $this->config[$name] = $value;
  341.         return $this;
  342.     }
  343.     /**
  344.      * @return string
  345.      */
  346.     public function getRealName()
  347.     {
  348.         return $this->realName;
  349.     }
  350.     /**
  351.      * @param string $realName
  352.      */
  353.     public function setRealName($realName)
  354.     {
  355.         $this->realName $realName;
  356.     }
  357.     final public function setParentBlockNames($parentNames)
  358.     {
  359.         if (is_array($parentNames)) {
  360.             // unfortunately we cannot make a type hint here, because of compatibility reasons
  361.             // old versions where 'parentBlockNames' was not excluded in __sleep() have still this property
  362.             // in the serialized data, and mostly with the value NULL, on restore this would lead to an error
  363.             $this->parentBlockNames $parentNames;
  364.         }
  365.     }
  366.     final public function getParentBlockNames(): array
  367.     {
  368.         return $this->parentBlockNames;
  369.     }
  370.     /**
  371.      * Returns only the properties which should be serialized
  372.      *
  373.      * @return array
  374.      */
  375.     public function __sleep()
  376.     {
  377.         $finalVars = [];
  378.         $parentVars parent::__sleep();
  379.         $blockedVars = ['editmode''parentBlockNames''document''config'];
  380.         foreach ($parentVars as $key) {
  381.             if (!in_array($key$blockedVars)) {
  382.                 $finalVars[] = $key;
  383.             }
  384.         }
  385.         return $finalVars;
  386.     }
  387.     public function __clone()
  388.     {
  389.         parent::__clone();
  390.         $this->document null;
  391.     }
  392.     /**
  393.      * {@inheritdoc}
  394.      */
  395.     final public function render()
  396.     {
  397.         if ($this->editmode) {
  398.             if ($collector $this->getEditableDefinitionCollector()) {
  399.                 $collector->add($this);
  400.             }
  401.             return $this->admin();
  402.         }
  403.         return $this->frontend();
  404.     }
  405.     /**
  406.      * direct output to the frontend
  407.      *
  408.      * @return string
  409.      */
  410.     public function __toString()
  411.     {
  412.         $result '';
  413.         try {
  414.             $result $this->render();
  415.         } catch (\Throwable $e) {
  416.             if (\Pimcore::inDebugMode()) {
  417.                 // the __toString method isn't allowed to throw exceptions
  418.                 $result '<b style="color:#f00">' $e->getMessage().'</b><br/>'.$e->getTraceAsString();
  419.                 return $result;
  420.             }
  421.             Logger::error('toString() returned an exception: {exception}', [
  422.                 'exception' => $e,
  423.             ]);
  424.             return '';
  425.         }
  426.         if (is_string($result) || is_numeric($result)) {
  427.             // we have to cast to string, because int/float is not auto-converted and throws an exception
  428.             return (string) $result;
  429.         }
  430.         return '';
  431.     }
  432.     /**
  433.      * @return bool
  434.      */
  435.     public function getEditmode()
  436.     {
  437.         return $this->editmode;
  438.     }
  439.     /**
  440.      * @param bool $editmode
  441.      *
  442.      * @return $this
  443.      */
  444.     public function setEditmode($editmode)
  445.     {
  446.         $this->editmode = (bool) $editmode;
  447.         return $this;
  448.     }
  449.     /**
  450.      * @return mixed
  451.      */
  452.     public function getDataForResource()
  453.     {
  454.         $this->checkValidity();
  455.         return $this->getData();
  456.     }
  457.     /**
  458.      * @param Model\Document\PageSnippet $ownerDocument
  459.      * @param array $tags
  460.      *
  461.      * @return array
  462.      */
  463.     public function getCacheTags(Model\Document\PageSnippet $ownerDocument, array $tags = []): array
  464.     {
  465.         return $tags;
  466.     }
  467.     /**
  468.      * This is a dummy and is mostly implemented by relation types
  469.      */
  470.     public function resolveDependencies()
  471.     {
  472.         return [];
  473.     }
  474.     /**
  475.      * @return bool
  476.      */
  477.     public function checkValidity()
  478.     {
  479.         return true;
  480.     }
  481.     /**
  482.      * @param bool $inherited
  483.      *
  484.      * @return $this
  485.      */
  486.     public function setInherited($inherited)
  487.     {
  488.         $this->inherited $inherited;
  489.         return $this;
  490.     }
  491.     /**
  492.      * @return bool
  493.      */
  494.     public function getInherited()
  495.     {
  496.         return $this->inherited;
  497.     }
  498.     /**
  499.      * @internal
  500.      *
  501.      * @return BlockState
  502.      */
  503.     protected function getBlockState(): BlockState
  504.     {
  505.         return $this->getBlockStateStack()->getCurrentState();
  506.     }
  507.     /**
  508.      * @internal
  509.      *
  510.      * @return BlockStateStack
  511.      */
  512.     protected function getBlockStateStack(): BlockStateStack
  513.     {
  514.         return \Pimcore::getContainer()->get(BlockStateStack::class);
  515.     }
  516.     /**
  517.      * Builds an editable name for an editable, taking current
  518.      * block state (block, index) and targeting into account.
  519.      *
  520.      * @internal
  521.      *
  522.      * @param string $type
  523.      * @param string $name
  524.      * @param Document|null $document
  525.      *
  526.      * @return string
  527.      *
  528.      * @throws \Exception
  529.      */
  530.     public static function buildEditableName(string $typestring $nameDocument $document null)
  531.     {
  532.         // do NOT allow dots (.) and colons (:) here as they act as delimiters
  533.         // for block hierarchy in the new naming scheme (see #1467)!
  534.         if (!preg_match("@^[a-zA-Z0-9\-_]+$@"$name)) {
  535.             throw new \InvalidArgumentException(
  536.                 'Only valid CSS class selectors are allowed as the name for an editable (which is basically [a-zA-Z0-9\-_]+). Your name was: ' $name
  537.             );
  538.         }
  539.         // @todo add document-id to registry key | for example for embeded snippets
  540.         // set suffixes if the editable is inside a block
  541.         $container \Pimcore::getContainer();
  542.         $blockState $container->get(BlockStateStack::class)->getCurrentState();
  543.         // if element not nested inside a hierarchical element (e.g. block), add the
  544.         // targeting prefix if configured on the document. hasBlocks() determines if
  545.         // there are any parent blocks for the current element
  546.         $targetGroupEditableName null;
  547.         if ($document && $document instanceof TargetingDocumentInterface) {
  548.             $targetGroupEditableName $document->getTargetGroupEditableName($name);
  549.             if (!$blockState->hasBlocks()) {
  550.                 $name $targetGroupEditableName;
  551.             }
  552.         }
  553.         $editableName self::doBuildName($name$type$blockState$targetGroupEditableName);
  554.         $event = new EditableNameEvent($type$name$blockState$editableName$document);
  555.         \Pimcore::getEventDispatcher()->dispatch($eventDocumentEvents::EDITABLE_NAME);
  556.         $editableName $event->getEditableName();
  557.         if (strlen($editableName) > 750) {
  558.             throw new \Exception(sprintf(
  559.                 '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',
  560.                 $name,
  561.                 $editableName
  562.             ));
  563.         }
  564.         return $editableName;
  565.     }
  566.     /**
  567.      * @param string $name
  568.      * @param string $type
  569.      * @param BlockState $blockState
  570.      * @param string|null $targetGroupElementName
  571.      *
  572.      * @return string
  573.      */
  574.     private static function doBuildName(string $namestring $typeBlockState $blockStatestring $targetGroupElementName null): string
  575.     {
  576.         if (!$blockState->hasBlocks()) {
  577.             return $name;
  578.         }
  579.         $blocks $blockState->getBlocks();
  580.         $indexes $blockState->getIndexes();
  581.         // check if the previous block is the name we're about to build
  582.         // TODO: can this be avoided at the block level?
  583.         if ($type === 'block' || $type == 'scheduledblock') {
  584.             $tmpBlocks $blocks;
  585.             $tmpIndexes $indexes;
  586.             array_pop($tmpBlocks);
  587.             array_pop($tmpIndexes);
  588.             $tmpName $name;
  589.             if (is_array($tmpBlocks)) {
  590.                 $tmpName self::buildHierarchicalName($name$tmpBlocks$tmpIndexes);
  591.             }
  592.             $previousBlockName $blocks[count($blocks) - 1]->getName();
  593.             if ($previousBlockName === $tmpName || ($targetGroupElementName && $previousBlockName === $targetGroupElementName)) {
  594.                 array_pop($blocks);
  595.                 array_pop($indexes);
  596.             }
  597.         }
  598.         return self::buildHierarchicalName($name$blocks$indexes);
  599.     }
  600.     /**
  601.      * @param string $name
  602.      * @param BlockName[] $blocks
  603.      * @param int[] $indexes
  604.      *
  605.      * @return string
  606.      */
  607.     private static function buildHierarchicalName(string $name, array $blocks, array $indexes): string
  608.     {
  609.         if (count($indexes) > count($blocks)) {
  610.             throw new \RuntimeException(sprintf('Index count %d is greater than blocks count %d'count($indexes), count($blocks)));
  611.         }
  612.         $parts = [];
  613.         for ($i 0$i count($blocks); $i++) {
  614.             $part $blocks[$i]->getRealName();
  615.             if (isset($indexes[$i])) {
  616.                 $part sprintf('%s:%d'$part$indexes[$i]);
  617.             }
  618.             $parts[] = $part;
  619.         }
  620.         $parts[] = $name;
  621.         return implode('.'$parts);
  622.     }
  623.     /**
  624.      * @internal
  625.      *
  626.      * @param string $name
  627.      * @param string $type
  628.      * @param array $parentBlockNames
  629.      * @param int $index
  630.      *
  631.      * @return string
  632.      *
  633.      * @throws \Exception
  634.      */
  635.     public static function buildChildEditableName(string $namestring $type, array $parentBlockNamesint $index): string
  636.     {
  637.         if (count($parentBlockNames) === 0) {
  638.             throw new \Exception(sprintf(
  639.                 'Failed to build child tag name for %s %s at index %d as no parent name was passed',
  640.                 $type,
  641.                 $name,
  642.                 $index
  643.             ));
  644.         }
  645.         $parentName array_pop($parentBlockNames);
  646.         return sprintf('%s:%d.%s'$parentName$index$name);
  647.     }
  648.     /**
  649.      * @internal
  650.      *
  651.      * @param string $name
  652.      * @param Document $document
  653.      *
  654.      * @return string
  655.      */
  656.     public static function buildEditableRealName(string $nameDocument $document): string
  657.     {
  658.         $blockState \Pimcore::getContainer()->get(BlockStateStack::class)->getCurrentState();
  659.         // if element not nested inside a hierarchical element (e.g. block), add the
  660.         // targeting prefix if configured on the document. hasBlocks() determines if
  661.         // there are any parent blocks for the current element
  662.         if ($document instanceof TargetingDocumentInterface && !$blockState->hasBlocks()) {
  663.             $name $document->getTargetGroupEditableName($name);
  664.         }
  665.         return $name;
  666.     }
  667.     /**
  668.      * @return bool
  669.      */
  670.     public function isInDialogBox(): bool
  671.     {
  672.         return (bool) $this->inDialogBox;
  673.     }
  674.     /**
  675.      * @return string|null
  676.      */
  677.     public function getInDialogBox(): ?string
  678.     {
  679.         return $this->inDialogBox;
  680.     }
  681.     /**
  682.      * @param string|null $inDialogBox
  683.      *
  684.      * @return $this
  685.      */
  686.     public function setInDialogBox(?string $inDialogBox): self
  687.     {
  688.         $this->inDialogBox $inDialogBox;
  689.         return $this;
  690.     }
  691.     /**
  692.      * @return EditmodeEditableDefinitionCollector|null
  693.      */
  694.     public function getEditableDefinitionCollector(): ?EditmodeEditableDefinitionCollector
  695.     {
  696.         return $this->editableDefinitionCollector;
  697.     }
  698.     /**
  699.      * @param EditmodeEditableDefinitionCollector|null $editableDefinitionCollector
  700.      *
  701.      * @return $this
  702.      */
  703.     public function setEditableDefinitionCollector(?EditmodeEditableDefinitionCollector $editableDefinitionCollector): self
  704.     {
  705.         $this->editableDefinitionCollector $editableDefinitionCollector;
  706.         return $this;
  707.     }
  708. }