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

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
  35.      */
  36.     protected $controller;
  37.     /**
  38.      * @internal
  39.      *
  40.      * @var string
  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();
  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
  245.      */
  246.     public function getTemplate()
  247.     {
  248.         return $this->template;
  249.     }
  250.     /**
  251.      * @param string $controller
  252.      *
  253.      * @return $this
  254.      */
  255.     public function setController($controller)
  256.     {
  257.         $this->controller $controller;
  258.         return $this;
  259.     }
  260.     /**
  261.      * @param string $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)
  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.             $contentMasterDocument Document::getByPath($contentMasterDocumentId);
  367.             if ($contentMasterDocument instanceof self) {
  368.                 $contentMasterDocumentId $contentMasterDocument->getId();
  369.             }
  370.         }
  371.         if (empty($contentMasterDocumentId)) {
  372.             $contentMasterDocument null;
  373.         }
  374.         if ($contentMasterDocumentId && $contentMasterDocumentId == $this->getId()) {
  375.             throw new \Exception('You cannot use the current document as a master document, please choose a different one.');
  376.         }
  377.         $this->contentMasterDocumentId $contentMasterDocumentId;
  378.         return $this;
  379.     }
  380.     /**
  381.      * @return int|null
  382.      */
  383.     public function getContentMasterDocumentId()
  384.     {
  385.         return $this->contentMasterDocumentId;
  386.     }
  387.     /**
  388.      * @return Document|null
  389.      */
  390.     public function getContentMasterDocument()
  391.     {
  392.         if ($masterDocumentId $this->getContentMasterDocumentId()) {
  393.             return Document::getById($masterDocumentId);
  394.         }
  395.         return null;
  396.     }
  397.     /**
  398.      * @param Document $document
  399.      *
  400.      * @return $this
  401.      */
  402.     public function setContentMasterDocument($document)
  403.     {
  404.         if ($document instanceof self) {
  405.             $this->setContentMasterDocumentId($document->getId());
  406.         } else {
  407.             $this->setContentMasterDocumentId(null);
  408.         }
  409.         return $this;
  410.     }
  411.     /**
  412.      * @param string $name
  413.      *
  414.      * @return bool
  415.      */
  416.     public function hasEditable(string $name)
  417.     {
  418.         return $this->getEditable($name) !== null;
  419.     }
  420.     /**
  421.      * @return Editable[]
  422.      */
  423.     public function getEditables(): array
  424.     {
  425.         if ($this->editables === null) {
  426.             $this->setEditables($this->getDao()->getEditables());
  427.         }
  428.         return $this->editables;
  429.     }
  430.     /**
  431.      * @param array|null $editables
  432.      *
  433.      * @return $this
  434.      *
  435.      */
  436.     public function setEditables(?array $editables)
  437.     {
  438.         $this->editables $editables;
  439.         return $this;
  440.     }
  441.     /**
  442.      * @return Model\Version[]
  443.      */
  444.     public function getVersions()
  445.     {
  446.         if ($this->versions === null) {
  447.             $this->setVersions($this->getDao()->getVersions());
  448.         }
  449.         return $this->versions;
  450.     }
  451.     /**
  452.      * @param array $versions
  453.      *
  454.      * @return $this
  455.      */
  456.     public function setVersions($versions)
  457.     {
  458.         $this->versions $versions;
  459.         return $this;
  460.     }
  461.     /**
  462.      * @see Document::getFullPath
  463.      *
  464.      * @return string
  465.      */
  466.     public function getHref()
  467.     {
  468.         return $this->getFullPath();
  469.     }
  470.     /**
  471.      * {@inheritdoc}
  472.      */
  473.     public function __sleep()
  474.     {
  475.         $finalVars = [];
  476.         $parentVars parent::__sleep();
  477.         $blockedVars = ['inheritedEditables'];
  478.         foreach ($parentVars as $key) {
  479.             if (!in_array($key$blockedVars)) {
  480.                 $finalVars[] = $key;
  481.             }
  482.         }
  483.         return $finalVars;
  484.     }
  485.     /**
  486.      * @param string|null $hostname
  487.      * @param string|null $scheme
  488.      *
  489.      * @return string
  490.      *
  491.      * @throws \Exception
  492.      */
  493.     public function getUrl($hostname null$scheme null)
  494.     {
  495.         if (!$scheme) {
  496.             $scheme 'http://';
  497.             /** @var RequestHelper $requestHelper */
  498.             $requestHelper \Pimcore::getContainer()->get(RequestHelper::class);
  499.             if ($requestHelper->hasMainRequest()) {
  500.                 $scheme $requestHelper->getMainRequest()->getScheme() . '://';
  501.             }
  502.         }
  503.         if (!$hostname) {
  504.             $hostname \Pimcore\Config::getSystemConfiguration('general')['domain'];
  505.             if (empty($hostname)) {
  506.                 if (!$hostname \Pimcore\Tool::getHostname()) {
  507.                     throw new \Exception('No hostname available');
  508.                 }
  509.             }
  510.         }
  511.         $url $scheme $hostname;
  512.         if ($this instanceof Page && $this->getPrettyUrl()) {
  513.             $url .= $this->getPrettyUrl();
  514.         } else {
  515.             $url .= $this->getFullPath();
  516.         }
  517.         $site \Pimcore\Tool\Frontend::getSiteForDocument($this);
  518.         if ($site instanceof Model\Site && $site->getMainDomain()) {
  519.             $url $scheme $site->getMainDomain() . preg_replace('@^' $site->getRootPath() . '/?@''/'$this->getRealFullPath());
  520.         }
  521.         return $url;
  522.     }
  523.     /**
  524.      * checks if the document is missing values for required editables
  525.      *
  526.      * @return bool|null
  527.      */
  528.     public function getMissingRequiredEditable()
  529.     {
  530.         return $this->missingRequiredEditable;
  531.     }
  532.     /**
  533.      * @param bool|null $missingRequiredEditable
  534.      *
  535.      * @return $this
  536.      */
  537.     public function setMissingRequiredEditable($missingRequiredEditable)
  538.     {
  539.         if ($missingRequiredEditable !== null) {
  540.             $missingRequiredEditable = (bool) $missingRequiredEditable;
  541.         }
  542.         $this->missingRequiredEditable $missingRequiredEditable;
  543.         return $this;
  544.     }
  545.     /**
  546.      * @internal
  547.      *
  548.      * @return bool
  549.      */
  550.     public function supportsContentMaster(): bool
  551.     {
  552.         return $this->supportsContentMaster;
  553.     }
  554.     /**
  555.      * Validates if there is a missing value for required editable
  556.      *
  557.      * @internal
  558.      */
  559.     protected function checkMissingRequiredEditable()
  560.     {
  561.         // load data which must be requested
  562.         $this->getProperties();
  563.         $this->getEditables();
  564.         //Allowed tags for required check
  565.         $allowedTypes = ['input''wysiwyg''textarea''numeric'];
  566.         if ($this->getMissingRequiredEditable() === null) {
  567.             /** @var EditableUsageResolver $editableUsageResolver */
  568.             $editableUsageResolver \Pimcore::getContainer()->get(EditableUsageResolver::class);
  569.             try {
  570.                 $documentCopy Service::cloneMe($this);
  571.                 if ($documentCopy instanceof self) {
  572.                     // rendering could fail if the controller/action doesn't exist, in this case we can skip the required check
  573.                     $editableNames $editableUsageResolver->getUsedEditableNames($documentCopy);
  574.                     foreach ($editableNames as $editableName) {
  575.                         $editable $documentCopy->getEditable($editableName);
  576.                         if ($editable instanceof Editable && in_array($editable->getType(), $allowedTypes)) {
  577.                             $editableConfig $editable->getConfig();
  578.                             if ($editable->isEmpty() && isset($editableConfig['required']) && $editableConfig['required'] == true) {
  579.                                 $this->setMissingRequiredEditable(true);
  580.                                 break;
  581.                             }
  582.                         }
  583.                     }
  584.                 }
  585.             } catch (\Exception $e) {
  586.                 // noting to do, as rendering the document failed for whatever reason
  587.             }
  588.         }
  589.     }
  590.     /**
  591.      * @return bool|null
  592.      */
  593.     public function getStaticGeneratorEnabled(): ?bool
  594.     {
  595.         return $this->staticGeneratorEnabled;
  596.     }
  597.     /**
  598.      * @param bool|null $staticGeneratorEnabled
  599.      */
  600.     public function setStaticGeneratorEnabled(?bool $staticGeneratorEnabled): void
  601.     {
  602.         $this->staticGeneratorEnabled $staticGeneratorEnabled;
  603.     }
  604.     /**
  605.      * @return int|null
  606.      */
  607.     public function getStaticGeneratorLifetime(): ?int
  608.     {
  609.         return $this->staticGeneratorLifetime;
  610.     }
  611.     /**
  612.      * @param int|null $staticGeneratorLifetime
  613.      */
  614.     public function setStaticGeneratorLifetime(?int $staticGeneratorLifetime): void
  615.     {
  616.         $this->staticGeneratorLifetime $staticGeneratorLifetime;
  617.     }
  618. }