vendor/pimcore/pimcore/models/Document/PageSnippet.php line 518

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\EditableUsageResolver;
  16. use Pimcore\Event\DocumentEvents;
  17. use Pimcore\Event\Model\DocumentEvent;
  18. use Pimcore\Http\RequestHelper;
  19. use Pimcore\Logger;
  20. use Pimcore\Messenger\VersionDeleteMessage;
  21. use Pimcore\Model;
  22. use Pimcore\Model\Document;
  23. use Pimcore\Model\Document\Editable\Loader\EditableLoaderInterface;
  24. /**
  25.  * @method \Pimcore\Model\Document\PageSnippet\Dao getDao()
  26.  * @method \Pimcore\Model\Version|null getLatestVersion(?int $userId = null)
  27.  */
  28. abstract class PageSnippet extends Model\Document
  29. {
  30.     use Model\Element\Traits\ScheduledTasksTrait;
  31.     /**
  32.      * @internal
  33.      *
  34.      * @var string|null
  35.      */
  36.     protected $controller;
  37.     /**
  38.      * @internal
  39.      *
  40.      * @var string|null
  41.      */
  42.     protected $template;
  43.     /**
  44.      * Contains all content-editables of the document
  45.      *
  46.      * @internal
  47.      *
  48.      * @var array|null
  49.      *
  50.      */
  51.     protected $editables null;
  52.     /**
  53.      * Contains all versions of the document
  54.      *
  55.      * @internal
  56.      *
  57.      * @var array
  58.      */
  59.     protected $versions null;
  60.     /**
  61.      * @internal
  62.      *
  63.      * @var null|int
  64.      */
  65.     protected $contentMasterDocumentId;
  66.     /**
  67.      * @internal
  68.      *
  69.      * @var bool
  70.      */
  71.     protected bool $supportsContentMaster true;
  72.     /**
  73.      * @internal
  74.      *
  75.      * @var null|bool
  76.      */
  77.     protected $missingRequiredEditable null;
  78.     /**
  79.      * @internal
  80.      *
  81.      * @var null|bool
  82.      */
  83.     protected $staticGeneratorEnabled null;
  84.     /**
  85.      * @internal
  86.      *
  87.      * @var null|int
  88.      */
  89.     protected $staticGeneratorLifetime null;
  90.     /**
  91.      * @internal
  92.      *
  93.      * @var array
  94.      */
  95.     protected $inheritedEditables = [];
  96.     private static bool $getInheritedValues false;
  97.     public static function setGetInheritedValues(bool $getInheritedValues): void
  98.     {
  99.         self::$getInheritedValues $getInheritedValues;
  100.     }
  101.     public static function getGetInheritedValues(): bool
  102.     {
  103.         return self::$getInheritedValues;
  104.     }
  105.     /**
  106.      * {@inheritdoc}
  107.      */
  108.     public function save()
  109.     {
  110.         // checking the required editables renders the document, so this needs to be
  111.         // before the database transaction, see also https://github.com/pimcore/pimcore/issues/8992
  112.         $this->checkMissingRequiredEditable();
  113.         if ($this->getMissingRequiredEditable() && $this->getPublished()) {
  114.             throw new Model\Element\ValidationException('Prevented publishing document - missing values for required editables');
  115.         }
  116.         return parent::save(...func_get_args());
  117.     }
  118.     /**
  119.      * {@inheritdoc}
  120.      */
  121.     protected function update($params = [])
  122.     {
  123.         // update elements
  124.         $editables $this->getEditables();
  125.         $this->getDao()->deleteAllEditables();
  126.         parent::update($params);
  127.         if (is_array($editables) && count($editables)) {
  128.             foreach ($editables as $editable) {
  129.                 if (!$editable->getInherited()) {
  130.                     $editable->setDao(null);
  131.                     $editable->setDocumentId($this->getId());
  132.                     $editable->save();
  133.                 }
  134.             }
  135.         }
  136.         // scheduled tasks are saved in $this->saveVersion();
  137.         // save version if needed
  138.         $this->saveVersion(falsefalse$params['versionNote'] ?? null);
  139.     }
  140.     /**
  141.      * @param bool $setModificationDate
  142.      * @param bool $saveOnlyVersion
  143.      * @param string $versionNote
  144.      * @param bool $isAutoSave
  145.      *
  146.      * @return null|Model\Version
  147.      *
  148.      * @throws \Exception
  149.      */
  150.     public function saveVersion($setModificationDate true$saveOnlyVersion true$versionNote null$isAutoSave false)
  151.     {
  152.         try {
  153.             // hook should be also called if "save only new version" is selected
  154.             if ($saveOnlyVersion) {
  155.                 $preUpdateEvent = new DocumentEvent($this, [
  156.                     'saveVersionOnly' => true,
  157.                     'isAutoSave' => $isAutoSave,
  158.                 ]);
  159.                 \Pimcore::getEventDispatcher()->dispatch($preUpdateEventDocumentEvents::PRE_UPDATE);
  160.             }
  161.             // set date
  162.             if ($setModificationDate) {
  163.                 $this->setModificationDate(time());
  164.             }
  165.             // scheduled tasks are saved always, they are not versioned!
  166.             $this->saveScheduledTasks();
  167.             // create version
  168.             $version null;
  169.             // only create a new version if there is at least 1 allowed
  170.             // or if saveVersion() was called directly (it's a newer version of the object)
  171.             $documentsConfig \Pimcore\Config::getSystemConfiguration('documents');
  172.             if ((is_null($documentsConfig['versions']['days'] ?? null) && is_null($documentsConfig['versions']['steps'] ?? null))
  173.                 || (!empty($documentsConfig['versions']['steps']))
  174.                 || !empty($documentsConfig['versions']['days'])
  175.                 || $setModificationDate) {
  176.                 $saveStackTrace = !($documentsConfig['versions']['disable_stack_trace'] ?? false);
  177.                 $version $this->doSaveVersion($versionNote$saveOnlyVersion$saveStackTrace$isAutoSave);
  178.             }
  179.             // hook should be also called if "save only new version" is selected
  180.             if ($saveOnlyVersion) {
  181.                 $postUpdateEvent = new DocumentEvent($this, [
  182.                     'saveVersionOnly' => true,
  183.                     'isAutoSave' => $isAutoSave,
  184.                 ]);
  185.                 \Pimcore::getEventDispatcher()->dispatch($postUpdateEventDocumentEvents::POST_UPDATE);
  186.             }
  187.             return $version;
  188.         } catch (\Exception $e) {
  189.             $postUpdateFailureEvent = new DocumentEvent($this, [
  190.                 'saveVersionOnly' => true,
  191.                 'exception' => $e,
  192.                 'isAutoSave' => $isAutoSave,
  193.             ]);
  194.             \Pimcore::getEventDispatcher()->dispatch($postUpdateFailureEventDocumentEvents::POST_UPDATE_FAILURE);
  195.             throw $e;
  196.         }
  197.     }
  198.     /**
  199.      * {@inheritdoc}
  200.      */
  201.     protected function doDelete()
  202.     {
  203.         // Dispatch Symfony Message Bus to delete versions
  204.         \Pimcore::getContainer()->get('messenger.bus.pimcore-core')->dispatch(
  205.             new VersionDeleteMessage(Service::getElementType($this), $this->getId())
  206.         );
  207.         // remove all tasks
  208.         $this->getDao()->deleteAllTasks();
  209.         parent::doDelete();
  210.     }
  211.     /**
  212.      * {@inheritdoc}
  213.      */
  214.     public function getCacheTags(array $tags = []): array
  215.     {
  216.         $tags parent::getCacheTags($tags);
  217.         foreach ($this->getEditables() as $editable) {
  218.             $tags $editable->getCacheTags($this$tags);
  219.         }
  220.         return $tags;
  221.     }
  222.     /**
  223.      * {@inheritdoc}
  224.      */
  225.     protected function resolveDependencies(): array
  226.     {
  227.         $dependencies = [parent::resolveDependencies()];
  228.         foreach ($this->getEditables() as $editable) {
  229.             $dependencies[] = $editable->resolveDependencies();
  230.         }
  231.         if ($this->getContentMasterDocument() instanceof Document) {
  232.             $masterDocumentId $this->getContentMasterDocument()->getId();
  233.             $dependencies[] = [
  234.                 'document_' $masterDocumentId => [
  235.                     'id' => $masterDocumentId,
  236.                     'type' => 'document',
  237.                 ],
  238.             ];
  239.         }
  240.         return array_merge(...$dependencies);
  241.     }
  242.     /**
  243.      * @return string
  244.      */
  245.     public function getController()
  246.     {
  247.         if (empty($this->controller)) {
  248.             $this->controller \Pimcore::getContainer()->getParameter('pimcore.documents.default_controller');
  249.         }
  250.         return $this->controller;
  251.     }
  252.     /**
  253.      * @return string|null
  254.      */
  255.     public function getTemplate()
  256.     {
  257.         return $this->template;
  258.     }
  259.     /**
  260.      * @param string|null $controller
  261.      *
  262.      * @return $this
  263.      */
  264.     public function setController($controller)
  265.     {
  266.         $this->controller $controller;
  267.         return $this;
  268.     }
  269.     /**
  270.      * @param string|null $template
  271.      *
  272.      * @return $this
  273.      */
  274.     public function setTemplate($template)
  275.     {
  276.         $this->template $template;
  277.         return $this;
  278.     }
  279.     /**
  280.      * Set raw data of an editable (eg. for editmode)
  281.      *
  282.      * @internal
  283.      *
  284.      * @param string $name
  285.      * @param string $type
  286.      * @param mixed $data
  287.      *
  288.      * @return $this
  289.      */
  290.     public function setRawEditable(string $namestring $type$data)
  291.     {
  292.         try {
  293.             if ($type) {
  294.                 /** @var EditableLoaderInterface $loader */
  295.                 $loader \Pimcore::getContainer()->get(Document\Editable\Loader\EditableLoader::class);
  296.                 $editable $loader->build($type);
  297.                 $this->editables $this->editables ?? [];
  298.                 $this->editables[$name] = $editable;
  299.                 $this->editables[$name]->setDataFromEditmode($data);
  300.                 $this->editables[$name]->setName($name);
  301.                 $this->editables[$name]->setDocument($this);
  302.             }
  303.         } catch (\Exception $e) {
  304.             Logger::warning("can't set element " $name ' with the type ' $type ' to the document: ' $this->getRealFullPath());
  305.         }
  306.         return $this;
  307.     }
  308.     /**
  309.      * Set an element with the given key/name
  310.      *
  311.      * @param Editable $editable
  312.      *
  313.      * @return $this
  314.      */
  315.     public function setEditable(Editable $editable)
  316.     {
  317.         $this->getEditables();
  318.         $this->editables[$editable->getName()] = $editable;
  319.         return $this;
  320.     }
  321.     /**
  322.      * @param string $name
  323.      *
  324.      * @return $this
  325.      */
  326.     public function removeEditable(string $name)
  327.     {
  328.         $this->getEditables();
  329.         if (isset($this->editables[$name])) {
  330.             unset($this->editables[$name]);
  331.         }
  332.         return $this;
  333.     }
  334.     /**
  335.      * Get an editable with the given key/name
  336.      *
  337.      * @param string $name
  338.      *
  339.      * @return Editable|null
  340.      */
  341.     public function getEditable(string $name)
  342.     {
  343.         $editables $this->getEditables();
  344.         if (isset($this->editables[$name])) {
  345.             return $editables[$name];
  346.         }
  347.         if (array_key_exists($name$this->inheritedEditables)) {
  348.             return $this->inheritedEditables[$name];
  349.         }
  350.         // check for content master document (inherit data)
  351.         if ($contentMasterDocument $this->getContentMasterDocument()) {
  352.             if ($contentMasterDocument instanceof self) {
  353.                 $inheritedEditable $contentMasterDocument->getEditable($name);
  354.                 if ($inheritedEditable) {
  355.                     $inheritedEditable = clone $inheritedEditable;
  356.                     $inheritedEditable->setInherited(true);
  357.                     $this->inheritedEditables[$name] = $inheritedEditable;
  358.                     return $inheritedEditable;
  359.                 }
  360.             }
  361.         }
  362.         return null;
  363.     }
  364.     /**
  365.      * @param int|string|null $contentMasterDocumentId
  366.      *
  367.      * @return $this
  368.      *
  369.      * @throws \Exception
  370.      */
  371.     public function setContentMasterDocumentId($contentMasterDocumentId/*, bool $validate*/)
  372.     {
  373.         // this is that the path is automatically converted to ID => when setting directly from admin UI
  374.         if (!is_numeric($contentMasterDocumentId) && !empty($contentMasterDocumentId)) {
  375.             if ($contentMasterDocument Document\PageSnippet::getByPath($contentMasterDocumentId)) {
  376.                 $contentMasterDocumentId $contentMasterDocument->getId();
  377.             } else {
  378.                 // Content master document was deleted or don't exist
  379.                 $contentMasterDocumentId null;
  380.             }
  381.         }
  382.         // Don't set the content master document if the document is already part of the master document chain
  383.         if ($contentMasterDocumentId) {
  384.             if ($currentContentMasterDocument Document\PageSnippet::getById($contentMasterDocumentId)) {
  385.                 $validate \func_get_args()[1] ?? false;
  386.                 $maxDepth 20;
  387.                 do {
  388.                     if ($currentContentMasterDocument->getId() === $this->getId()) {
  389.                         throw new \Exception('This document is already part of the master document chain, please choose a different one.');
  390.                     }
  391.                     $currentContentMasterDocument $currentContentMasterDocument->getContentMasterDocument();
  392.                 } while ($currentContentMasterDocument && $maxDepth-- > && $validate);
  393.             } else {
  394.                 // Content master document was deleted or don't exist
  395.                 $contentMasterDocumentId null;
  396.             }
  397.         }
  398.         $this->contentMasterDocumentId $contentMasterDocumentId;
  399.         return $this;
  400.     }
  401.     /**
  402.      * @return int|null
  403.      */
  404.     public function getContentMasterDocumentId()
  405.     {
  406.         return $this->contentMasterDocumentId;
  407.     }
  408.     /**
  409.      * @return Document\PageSnippet|null
  410.      */
  411.     public function getContentMasterDocument()
  412.     {
  413.         if ($masterDocumentId $this->getContentMasterDocumentId()) {
  414.             return Document\PageSnippet::getById($masterDocumentId);
  415.         }
  416.         return null;
  417.     }
  418.     /**
  419.      * @param Document\PageSnippet|null $document
  420.      *
  421.      * @return $this
  422.      */
  423.     public function setContentMasterDocument($document)
  424.     {
  425.         if ($document instanceof self) {
  426.             $this->setContentMasterDocumentId($document->getId(), true);
  427.         } else {
  428.             $this->setContentMasterDocumentId(null);
  429.         }
  430.         return $this;
  431.     }
  432.     /**
  433.      * @param string $name
  434.      *
  435.      * @return bool
  436.      */
  437.     public function hasEditable(string $name)
  438.     {
  439.         return $this->getEditable($name) !== null;
  440.     }
  441.     /**
  442.      * @return Editable[]
  443.      */
  444.     public function getEditables(): array
  445.     {
  446.         if ($this->editables === null) {
  447.             $documentEditables $this->getDao()->getEditables();
  448.             if (self::getGetInheritedValues() && $this->supportsContentMaster() && $this->getContentMasterDocument()) {
  449.                 $contentMasterEditables $this->getContentMasterDocument()->getEditables();
  450.                 $documentEditables array_merge($contentMasterEditables$documentEditables);
  451.                 $this->inheritedEditables $documentEditables;
  452.             }
  453.             $this->setEditables($documentEditables);
  454.         }
  455.         return $this->editables;
  456.     }
  457.     /**
  458.      * @param array|null $editables
  459.      *
  460.      * @return $this
  461.      *
  462.      */
  463.     public function setEditables(?array $editables)
  464.     {
  465.         $this->editables $editables;
  466.         return $this;
  467.     }
  468.     /**
  469.      * @return Model\Version[]
  470.      */
  471.     public function getVersions()
  472.     {
  473.         if ($this->versions === null) {
  474.             $this->setVersions($this->getDao()->getVersions());
  475.         }
  476.         return $this->versions;
  477.     }
  478.     /**
  479.      * @param array $versions
  480.      *
  481.      * @return $this
  482.      */
  483.     public function setVersions($versions)
  484.     {
  485.         $this->versions $versions;
  486.         return $this;
  487.     }
  488.     /**
  489.      * @see Document::getFullPath
  490.      *
  491.      * @return string
  492.      */
  493.     public function getHref()
  494.     {
  495.         return $this->getFullPath();
  496.     }
  497.     /**
  498.      * {@inheritdoc}
  499.      */
  500.     public function __sleep()
  501.     {
  502.         $finalVars = [];
  503.         $parentVars parent::__sleep();
  504.         $blockedVars = ['inheritedEditables'];
  505.         foreach ($parentVars as $key) {
  506.             if (!in_array($key$blockedVars)) {
  507.                 $finalVars[] = $key;
  508.             }
  509.         }
  510.         return $finalVars;
  511.     }
  512.     /**
  513.      * @param string|null $hostname
  514.      * @param string|null $scheme
  515.      *
  516.      * @return string
  517.      *
  518.      * @throws \Exception
  519.      */
  520.     public function getUrl($hostname null$scheme null)
  521.     {
  522.         if (!$scheme) {
  523.             $scheme 'http://';
  524.             /** @var RequestHelper $requestHelper */
  525.             $requestHelper \Pimcore::getContainer()->get(RequestHelper::class);
  526.             if ($requestHelper->hasMainRequest()) {
  527.                 $scheme $requestHelper->getMainRequest()->getScheme() . '://';
  528.             }
  529.         }
  530.         if (!$hostname) {
  531.             $hostname \Pimcore\Config::getSystemConfiguration('general')['domain'];
  532.             if (empty($hostname)) {
  533.                 if (!$hostname \Pimcore\Tool::getHostname()) {
  534.                     throw new \Exception('No hostname available');
  535.                 }
  536.             }
  537.         }
  538.         $url $scheme $hostname;
  539.         if ($this instanceof Page && $this->getPrettyUrl()) {
  540.             $url .= $this->getPrettyUrl();
  541.         } else {
  542.             $url .= $this->getFullPath();
  543.         }
  544.         $site \Pimcore\Tool\Frontend::getSiteForDocument($this);
  545.         if ($site instanceof Model\Site && $site->getMainDomain()) {
  546.             $url $scheme $site->getMainDomain() . preg_replace('@^' $site->getRootPath() . '/?@''/'$this->getRealFullPath());
  547.         }
  548.         return $url;
  549.     }
  550.     /**
  551.      * checks if the document is missing values for required editables
  552.      *
  553.      * @return bool|null
  554.      */
  555.     public function getMissingRequiredEditable()
  556.     {
  557.         return $this->missingRequiredEditable;
  558.     }
  559.     /**
  560.      * @param bool|null $missingRequiredEditable
  561.      *
  562.      * @return $this
  563.      */
  564.     public function setMissingRequiredEditable($missingRequiredEditable)
  565.     {
  566.         if ($missingRequiredEditable !== null) {
  567.             $missingRequiredEditable = (bool) $missingRequiredEditable;
  568.         }
  569.         $this->missingRequiredEditable $missingRequiredEditable;
  570.         return $this;
  571.     }
  572.     /**
  573.      * @internal
  574.      *
  575.      * @return bool
  576.      */
  577.     public function supportsContentMaster(): bool
  578.     {
  579.         return $this->supportsContentMaster;
  580.     }
  581.     /**
  582.      * Validates if there is a missing value for required editable
  583.      *
  584.      * @internal
  585.      */
  586.     protected function checkMissingRequiredEditable()
  587.     {
  588.         // load data which must be requested
  589.         $this->getProperties();
  590.         $this->getEditables();
  591.         //Allowed tags for required check
  592.         $allowedTypes = ['input''wysiwyg''textarea''numeric'];
  593.         if ($this->getMissingRequiredEditable() === null) {
  594.             /** @var EditableUsageResolver $editableUsageResolver */
  595.             $editableUsageResolver \Pimcore::getContainer()->get(EditableUsageResolver::class);
  596.             try {
  597.                 $documentCopy Service::cloneMe($this);
  598.                 if ($documentCopy instanceof self) {
  599.                     // rendering could fail if the controller/action doesn't exist, in this case we can skip the required check
  600.                     $editableNames $editableUsageResolver->getUsedEditableNames($documentCopy);
  601.                     foreach ($editableNames as $editableName) {
  602.                         $editable $documentCopy->getEditable($editableName);
  603.                         if ($editable instanceof Editable && in_array($editable->getType(), $allowedTypes)) {
  604.                             $editableConfig $editable->getConfig();
  605.                             if ($editable->isEmpty() && isset($editableConfig['required']) && $editableConfig['required'] == true) {
  606.                                 $this->setMissingRequiredEditable(true);
  607.                                 break;
  608.                             }
  609.                         }
  610.                     }
  611.                 }
  612.             } catch (\Exception $e) {
  613.                 // noting to do, as rendering the document failed for whatever reason
  614.             }
  615.         }
  616.     }
  617.     /**
  618.      * @return bool|null
  619.      */
  620.     public function getStaticGeneratorEnabled(): ?bool
  621.     {
  622.         return $this->staticGeneratorEnabled;
  623.     }
  624.     /**
  625.      * @param bool|null $staticGeneratorEnabled
  626.      */
  627.     public function setStaticGeneratorEnabled(?bool $staticGeneratorEnabled): void
  628.     {
  629.         $this->staticGeneratorEnabled $staticGeneratorEnabled;
  630.     }
  631.     /**
  632.      * @return int|null
  633.      */
  634.     public function getStaticGeneratorLifetime(): ?int
  635.     {
  636.         return $this->staticGeneratorLifetime;
  637.     }
  638.     /**
  639.      * @param int|null $staticGeneratorLifetime
  640.      */
  641.     public function setStaticGeneratorLifetime(?int $staticGeneratorLifetime): void
  642.     {
  643.         $this->staticGeneratorLifetime $staticGeneratorLifetime;
  644.     }
  645. }