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

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