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

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(false, false, $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($preUpdateEvent, DocumentEvents::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($postUpdateEvent, DocumentEvents::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($postUpdateFailureEvent, DocumentEvents::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 $name, string $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-- > 0 && $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. }