vendor/pimcore/pimcore/models/Document.php line 201

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;
  15. use Doctrine\DBAL\Exception\DeadlockException;
  16. use Pimcore\Cache\RuntimeCache;
  17. use Pimcore\Event\DocumentEvents;
  18. use Pimcore\Event\FrontendEvents;
  19. use Pimcore\Event\Model\DocumentEvent;
  20. use Pimcore\Loader\ImplementationLoader\Exception\UnsupportedException;
  21. use Pimcore\Logger;
  22. use Pimcore\Model\Document\Hardlink\Wrapper\WrapperInterface;
  23. use Pimcore\Model\Document\Listing;
  24. use Pimcore\Model\Element\DuplicateFullPathException;
  25. use Pimcore\Model\Exception\NotFoundException;
  26. use Pimcore\Tool;
  27. use Pimcore\Tool\Frontend as FrontendTool;
  28. use Symfony\Cmf\Bundle\RoutingBundle\Routing\DynamicRouter;
  29. use Symfony\Component\EventDispatcher\GenericEvent;
  30. /**
  31. * @method \Pimcore\Model\Document\Dao getDao()
  32. * @method bool __isBasedOnLatestData()
  33. * @method int getChildAmount($user = null)
  34. * @method string getCurrentFullPath()
  35. */
  36. class Document extends Element\AbstractElement
  37. {
  38. /**
  39. * all possible types of documents
  40. *
  41. * @internal
  42. *
  43. * @deprecated will be removed in Pimcore 11. Use getTypes() method.
  44. *
  45. * @var array
  46. */
  47. public static $types = ['folder', 'page', 'snippet', 'link', 'hardlink', 'email', 'newsletter', 'printpage', 'printcontainer'];
  48. /**
  49. * @var bool
  50. */
  51. private static $hideUnpublished = false;
  52. /**
  53. * @internal
  54. *
  55. * @var string|null
  56. */
  57. protected $fullPathCache;
  58. /**
  59. * @internal
  60. *
  61. * @var string
  62. */
  63. protected string $type = '';
  64. /**
  65. * @internal
  66. *
  67. * @var string|null
  68. */
  69. protected $key;
  70. /**
  71. * @internal
  72. *
  73. * @var string|null
  74. */
  75. protected $path;
  76. /**
  77. * @internal
  78. *
  79. * @var int|null
  80. */
  81. protected ?int $index = null;
  82. /**
  83. * @internal
  84. *
  85. * @var bool
  86. */
  87. protected bool $published = true;
  88. /**
  89. * @internal
  90. *
  91. * @var int|null
  92. */
  93. protected ?int $userModification = null;
  94. /**
  95. * @internal
  96. *
  97. * @var array
  98. */
  99. protected $children = [];
  100. /**
  101. * @internal
  102. *
  103. * @var bool[]
  104. */
  105. protected $hasChildren = [];
  106. /**
  107. * @internal
  108. *
  109. * @var array
  110. */
  111. protected $siblings = [];
  112. /**
  113. * @internal
  114. *
  115. * @var bool[]
  116. */
  117. protected $hasSiblings = [];
  118. /**
  119. * {@inheritdoc}
  120. */
  121. protected function getBlockedVars(): array
  122. {
  123. $blockedVars = ['hasChildren', 'versions', 'scheduledTasks', 'parent', 'fullPathCache'];
  124. if (!$this->isInDumpState()) {
  125. // this is if we want to cache the object
  126. $blockedVars = array_merge($blockedVars, ['children', 'properties']);
  127. }
  128. return $blockedVars;
  129. }
  130. /**
  131. * get possible types
  132. *
  133. * @return array
  134. */
  135. public static function getTypes()
  136. {
  137. $documentsConfig = \Pimcore\Config::getSystemConfiguration('documents');
  138. return $documentsConfig['types'];
  139. }
  140. /**
  141. * @internal
  142. *
  143. * @param string $path
  144. *
  145. * @return string
  146. */
  147. protected static function getPathCacheKey(string $path): string
  148. {
  149. return 'document_path_' . md5($path);
  150. }
  151. /**
  152. * @param string $path
  153. * @param array|bool $force
  154. *
  155. * @return static|null
  156. */
  157. public static function getByPath($path, $force = false)
  158. {
  159. if (!$path) {
  160. return null;
  161. }
  162. $path = Element\Service::correctPath($path);
  163. $cacheKey = self::getPathCacheKey($path);
  164. $params = Element\Service::prepareGetByIdParams($force, __METHOD__, func_num_args() > 1);
  165. if (!$params['force'] && RuntimeCache::isRegistered($cacheKey)) {
  166. $document = RuntimeCache::get($cacheKey);
  167. if ($document && static::typeMatch($document)) {
  168. return $document;
  169. }
  170. }
  171. try {
  172. $helperDoc = new Document();
  173. $helperDoc->getDao()->getByPath($path);
  174. $doc = static::getById($helperDoc->getId(), $params);
  175. RuntimeCache::set($cacheKey, $doc);
  176. } catch (NotFoundException $e) {
  177. $doc = null;
  178. }
  179. return $doc;
  180. }
  181. /**
  182. * @internal
  183. *
  184. * @param Document $document
  185. *
  186. * @return bool
  187. */
  188. protected static function typeMatch(Document $document)
  189. {
  190. $staticType = static::class;
  191. if ($staticType !== Document::class) {
  192. if (!$document instanceof $staticType) {
  193. return false;
  194. }
  195. }
  196. return true;
  197. }
  198. /**
  199. * @param int|string $id
  200. * @param array|bool $force
  201. *
  202. * @return static|null
  203. */
  204. public static function getById($id, $force = false)
  205. {
  206. if (!is_numeric($id) || $id < 1) {
  207. return null;
  208. }
  209. $id = (int)$id;
  210. $cacheKey = self::getCacheKey($id);
  211. $params = Element\Service::prepareGetByIdParams($force, __METHOD__, func_num_args() > 1);
  212. if (!$params['force'] && RuntimeCache::isRegistered($cacheKey)) {
  213. $document = RuntimeCache::get($cacheKey);
  214. if ($document && static::typeMatch($document)) {
  215. return $document;
  216. }
  217. }
  218. if ($params['force'] || !($document = \Pimcore\Cache::load($cacheKey))) {
  219. $reflectionClass = new \ReflectionClass(static::class);
  220. if ($reflectionClass->isAbstract()) {
  221. $document = new Document();
  222. } else {
  223. $document = new static();
  224. }
  225. try {
  226. $document->getDao()->getById($id);
  227. } catch (NotFoundException $e) {
  228. return null;
  229. }
  230. try {
  231. // Getting classname from document resolver
  232. if(!$className = \Pimcore::getContainer()->get('pimcore.class.resolver.document')->resolve($document->getType())) {
  233. throw new UnsupportedException();
  234. }
  235. } catch(UnsupportedException $ex) {
  236. trigger_deprecation(
  237. 'pimcore/pimcore',
  238. '10.6.0',
  239. sprintf('%s - Loading documents via fixed namespace is deprecated and will be removed in Pimcore 11. Use pimcore:type_definitions instead', $ex->getMessage())
  240. );
  241. /**
  242. * @deprecated since Pimcore 10.6 and will be removed in Pimcore 11. Use type_definitions instead
  243. */
  244. $className = 'Pimcore\\Model\\Document\\' . ucfirst($document->getType());
  245. // this is the fallback for custom document types using prefixes
  246. // so we need to check if the class exists first
  247. if (!Tool::classExists($className)) {
  248. $oldStyleClass = 'Document_' . ucfirst($document->getType());
  249. if (Tool::classExists($oldStyleClass)) {
  250. $className = $oldStyleClass;
  251. }
  252. }
  253. }
  254. /** @var Document $newDocument */
  255. $newDocument = self::getModelFactory()->build($className);
  256. if (get_class($document) !== get_class($newDocument)) {
  257. $document = $newDocument;
  258. $document->getDao()->getById($id);
  259. }
  260. RuntimeCache::set($cacheKey, $document);
  261. $document->__setDataVersionTimestamp($document->getModificationDate());
  262. $document->resetDirtyMap();
  263. \Pimcore\Cache::save($document, $cacheKey);
  264. } else {
  265. RuntimeCache::set($cacheKey, $document);
  266. }
  267. if (!$document || !static::typeMatch($document)) {
  268. return null;
  269. }
  270. \Pimcore::getEventDispatcher()->dispatch(
  271. new DocumentEvent($document, ['params' => $params]),
  272. DocumentEvents::POST_LOAD
  273. );
  274. return $document;
  275. }
  276. /**
  277. * @param int $parentId
  278. * @param array $data
  279. * @param bool $save
  280. *
  281. * @return static
  282. */
  283. public static function create($parentId, $data = [], $save = true)
  284. {
  285. $document = new static();
  286. $document->setParentId($parentId);
  287. self::checkCreateData($data);
  288. $document->setValues($data);
  289. if ($save) {
  290. $document->save();
  291. }
  292. return $document;
  293. }
  294. /**
  295. * @param array $config
  296. *
  297. * @return Listing
  298. *
  299. * @throws \Exception
  300. */
  301. public static function getList(array $config = []): Listing
  302. {
  303. /** @var Listing $list */
  304. $list = self::getModelFactory()->build(Listing::class);
  305. $list->setValues($config);
  306. return $list;
  307. }
  308. /**
  309. * @deprecated will be removed in Pimcore 11
  310. *
  311. * @param array $config
  312. *
  313. * @return int count
  314. */
  315. public static function getTotalCount(array $config = []): int
  316. {
  317. $list = static::getList($config);
  318. return $list->getTotalCount();
  319. }
  320. /**
  321. * {@inheritdoc}
  322. */
  323. public function save()
  324. {
  325. $isUpdate = false;
  326. try {
  327. // additional parameters (e.g. "versionNote" for the version note)
  328. $params = [];
  329. if (func_num_args() && is_array(func_get_arg(0))) {
  330. $params = func_get_arg(0);
  331. }
  332. $preEvent = new DocumentEvent($this, $params);
  333. if ($this->getId()) {
  334. $isUpdate = true;
  335. $this->dispatchEvent($preEvent, DocumentEvents::PRE_UPDATE);
  336. } else {
  337. $this->dispatchEvent($preEvent, DocumentEvents::PRE_ADD);
  338. }
  339. $params = $preEvent->getArguments();
  340. $this->correctPath();
  341. $differentOldPath = null;
  342. // we wrap the save actions in a loop here, so that we can restart the database transactions in the case it fails
  343. // if a transaction fails it gets restarted $maxRetries times, then the exception is thrown out
  344. // this is especially useful to avoid problems with deadlocks in multi-threaded environments (forked workers, ...)
  345. $maxRetries = 5;
  346. for ($retries = 0; $retries < $maxRetries; $retries++) {
  347. $this->beginTransaction();
  348. try {
  349. $this->updateModificationInfos();
  350. if (!$isUpdate) {
  351. $this->getDao()->create();
  352. }
  353. // get the old path from the database before the update is done
  354. $oldPath = null;
  355. if ($isUpdate) {
  356. $oldPath = $this->getDao()->getCurrentFullPath();
  357. }
  358. $this->update($params);
  359. // if the old path is different from the new path, update all children
  360. $updatedChildren = [];
  361. if ($oldPath && $oldPath !== $newPath = $this->getRealFullPath()) {
  362. $differentOldPath = $oldPath;
  363. $this->getDao()->updateWorkspaces();
  364. $updatedChildren = array_map(
  365. static function (array $doc) use ($oldPath, $newPath): array {
  366. $doc['oldPath'] = substr_replace($doc['path'], $oldPath, 0, strlen($newPath));
  367. return $doc;
  368. },
  369. $this->getDao()->updateChildPaths($oldPath),
  370. );
  371. }
  372. $this->commit();
  373. break; // transaction was successfully completed, so we cancel the loop here -> no restart required
  374. } catch (\Exception $e) {
  375. try {
  376. $this->rollBack();
  377. } catch (\Exception $er) {
  378. // PDO adapter throws exceptions if rollback fails
  379. Logger::error((string) $er);
  380. }
  381. // we try to start the transaction $maxRetries times again (deadlocks, ...)
  382. if ($e instanceof DeadlockException && $retries < ($maxRetries - 1)) {
  383. $run = $retries + 1;
  384. $waitTime = rand(1, 5) * 100000; // microseconds
  385. Logger::warn('Unable to finish transaction (' . $run . ". run) because of the following reason '" . $e->getMessage() . "'. --> Retrying in " . $waitTime . ' microseconds ... (' . ($run + 1) . ' of ' . $maxRetries . ')');
  386. usleep($waitTime); // wait specified time until we restart the transaction
  387. } else {
  388. // if the transaction still fail after $maxRetries retries, we throw out the exception
  389. throw $e;
  390. }
  391. }
  392. }
  393. $additionalTags = [];
  394. if (isset($updatedChildren) && is_array($updatedChildren)) {
  395. foreach ($updatedChildren as $updatedDocument) {
  396. $tag = self::getCacheKey($updatedDocument['id']);
  397. $additionalTags[] = $tag;
  398. // remove the child also from registry (internal cache) to avoid path inconsistencies during long-running scripts, such as CLI
  399. RuntimeCache::set($tag, null);
  400. RuntimeCache::set(self::getPathCacheKey($updatedDocument['oldPath']), null);
  401. }
  402. }
  403. $this->clearDependentCache($additionalTags);
  404. $postEvent = new DocumentEvent($this, $params);
  405. if ($isUpdate) {
  406. if ($differentOldPath) {
  407. $postEvent->setArgument('oldPath', $differentOldPath);
  408. }
  409. $this->dispatchEvent($postEvent, DocumentEvents::POST_UPDATE);
  410. } else {
  411. $this->dispatchEvent($postEvent, DocumentEvents::POST_ADD);
  412. }
  413. return $this;
  414. } catch (\Exception $e) {
  415. $failureEvent = new DocumentEvent($this, $params);
  416. $failureEvent->setArgument('exception', $e);
  417. if ($isUpdate) {
  418. $this->dispatchEvent($failureEvent, DocumentEvents::POST_UPDATE_FAILURE);
  419. } else {
  420. $this->dispatchEvent($failureEvent, DocumentEvents::POST_ADD_FAILURE);
  421. }
  422. throw $e;
  423. }
  424. }
  425. /**
  426. * @throws \Exception|DuplicateFullPathException
  427. */
  428. private function correctPath()
  429. {
  430. // set path
  431. if ($this->getId() != 1) { // not for the root node
  432. // check for a valid key, home has no key, so omit the check
  433. if (!Element\Service::isValidKey($this->getKey(), 'document')) {
  434. throw new \Exception('invalid key for document with id [ ' . $this->getId() . ' ] key is: [' . $this->getKey() . ']');
  435. }
  436. if ($this->getParentId() == $this->getId()) {
  437. throw new \Exception("ParentID and ID is identical, an element can't be the parent of itself.");
  438. }
  439. $parent = Document::getById($this->getParentId());
  440. if ($parent) {
  441. // use the parent's path from the database here (getCurrentFullPath), to ensure the path really exists and does not rely on the path
  442. // that is currently in the parent object (in memory), because this might have changed but wasn't not saved
  443. $this->setPath(str_replace('//', '/', $parent->getCurrentFullPath() . '/'));
  444. } else {
  445. trigger_deprecation(
  446. 'pimcore/pimcore',
  447. '10.5',
  448. 'Fallback for parentId will be removed in Pimcore 11.',
  449. );
  450. // parent document doesn't exist anymore, set the parent to to root
  451. $this->setParentId(1);
  452. $this->setPath('/');
  453. }
  454. if (strlen($this->getKey()) < 1) {
  455. throw new \Exception('Document requires key, generated key automatically');
  456. }
  457. } elseif ($this->getId() == 1) {
  458. // some data in root node should always be the same
  459. $this->setParentId(0);
  460. $this->setPath('/');
  461. $this->setKey('');
  462. $this->setType('page');
  463. }
  464. if (Document\Service::pathExists($this->getRealFullPath())) {
  465. $duplicate = Document::getByPath($this->getRealFullPath());
  466. if ($duplicate instanceof Document && $duplicate->getId() != $this->getId()) {
  467. $duplicateFullPathException = new DuplicateFullPathException('Duplicate full path [ ' . $this->getRealFullPath() . ' ] - cannot save document');
  468. $duplicateFullPathException->setDuplicateElement($duplicate);
  469. throw $duplicateFullPathException;
  470. }
  471. }
  472. $this->validatePathLength();
  473. }
  474. /**
  475. * @internal
  476. *
  477. * @param array $params additional parameters (e.g. "versionNote" for the version note)
  478. *
  479. * @throws \Exception
  480. */
  481. protected function update($params = [])
  482. {
  483. $disallowedKeysInFirstLevel = ['install', 'admin', 'plugin'];
  484. if ($this->getParentId() == 1 && in_array($this->getKey(), $disallowedKeysInFirstLevel)) {
  485. throw new \Exception('Key: ' . $this->getKey() . ' is not allowed in first level (root-level)');
  486. }
  487. // set index if null
  488. if ($this->getIndex() === null) {
  489. $this->setIndex($this->getDao()->getNextIndex());
  490. }
  491. // save properties
  492. $this->getProperties();
  493. $this->getDao()->deleteAllProperties();
  494. if (is_array($this->getProperties()) && count($this->getProperties()) > 0) {
  495. foreach ($this->getProperties() as $property) {
  496. if (!$property->getInherited()) {
  497. $property->setDao(null);
  498. $property->setCid($this->getId());
  499. $property->setCtype('document');
  500. $property->setCpath($this->getRealFullPath());
  501. $property->save();
  502. }
  503. }
  504. }
  505. // save dependencies
  506. $d = new Dependency();
  507. $d->setSourceType('document');
  508. $d->setSourceId($this->getId());
  509. foreach ($this->resolveDependencies() as $requirement) {
  510. if ($requirement['id'] == $this->getId() && $requirement['type'] == 'document') {
  511. // dont't add a reference to yourself
  512. continue;
  513. } else {
  514. $d->addRequirement($requirement['id'], $requirement['type']);
  515. }
  516. }
  517. $d->save();
  518. $this->getDao()->update();
  519. //set document to registry
  520. RuntimeCache::set(self::getCacheKey($this->getId()), $this);
  521. }
  522. /**
  523. * @internal
  524. *
  525. * @param int $index
  526. */
  527. public function saveIndex($index)
  528. {
  529. $this->getDao()->saveIndex($index);
  530. $this->clearDependentCache();
  531. }
  532. /**
  533. * {@inheritdoc}
  534. */
  535. public function clearDependentCache($additionalTags = [])
  536. {
  537. try {
  538. $tags = [$this->getCacheTag(), 'document_properties', 'output'];
  539. $tags = array_merge($tags, $additionalTags);
  540. \Pimcore\Cache::clearTags($tags);
  541. } catch (\Exception $e) {
  542. Logger::crit((string) $e);
  543. }
  544. }
  545. /**
  546. * set the children of the document
  547. *
  548. * @param Document[]|null $children
  549. * @param bool $includingUnpublished
  550. *
  551. * @return $this
  552. */
  553. public function setChildren($children, $includingUnpublished = false)
  554. {
  555. if ($children === null) {
  556. // unset all cached children
  557. $this->hasChildren = [];
  558. $this->children = [];
  559. } elseif (is_array($children)) {
  560. $cacheKey = $this->getListingCacheKey([$includingUnpublished]);
  561. $this->children[$cacheKey] = $children;
  562. $this->hasChildren[$cacheKey] = (bool) count($children);
  563. }
  564. return $this;
  565. }
  566. /**
  567. * Get a list of the children (not recursivly)
  568. *
  569. * @param bool $includingUnpublished
  570. *
  571. * @return self[]
  572. */
  573. public function getChildren($includingUnpublished = false)
  574. {
  575. $cacheKey = $this->getListingCacheKey(func_get_args());
  576. if (!isset($this->children[$cacheKey])) {
  577. if ($this->getId()) {
  578. $list = new Document\Listing();
  579. $list->setUnpublished($includingUnpublished);
  580. $list->setCondition('parentId = ?', $this->getId());
  581. $list->setOrderKey('index');
  582. $list->setOrder('asc');
  583. $this->children[$cacheKey] = $list->load();
  584. } else {
  585. $this->children[$cacheKey] = [];
  586. }
  587. }
  588. return $this->children[$cacheKey];
  589. }
  590. /**
  591. * Returns true if the document has at least one child
  592. *
  593. * @param bool $includingUnpublished
  594. *
  595. * @return bool
  596. */
  597. public function hasChildren($includingUnpublished = null)
  598. {
  599. $cacheKey = $this->getListingCacheKey(func_get_args());
  600. if (isset($this->hasChildren[$cacheKey])) {
  601. return $this->hasChildren[$cacheKey];
  602. }
  603. return $this->hasChildren[$cacheKey] = $this->getDao()->hasChildren($includingUnpublished);
  604. }
  605. /**
  606. * Get a list of the sibling documents
  607. *
  608. * @param bool $includingUnpublished
  609. *
  610. * @return array
  611. */
  612. public function getSiblings($includingUnpublished = false)
  613. {
  614. $cacheKey = $this->getListingCacheKey(func_get_args());
  615. if (!isset($this->siblings[$cacheKey])) {
  616. if ($this->getParentId()) {
  617. $list = new Document\Listing();
  618. $list->setUnpublished($includingUnpublished);
  619. $list->addConditionParam('parentId = ?', $this->getParentId());
  620. if ($this->getId()) {
  621. $list->addConditionParam('id != ?', $this->getId());
  622. }
  623. $list->setOrderKey('index');
  624. $list->setOrder('asc');
  625. $this->siblings[$cacheKey] = $list->load();
  626. $this->hasSiblings[$cacheKey] = (bool) count($this->siblings[$cacheKey]);
  627. } else {
  628. $this->siblings[$cacheKey] = [];
  629. $this->hasSiblings[$cacheKey] = false;
  630. }
  631. }
  632. return $this->siblings[$cacheKey];
  633. }
  634. /**
  635. * Returns true if the document has at least one sibling
  636. *
  637. * @param bool|null $includingUnpublished
  638. *
  639. * @return bool
  640. */
  641. public function hasSiblings($includingUnpublished = null)
  642. {
  643. $cacheKey = $this->getListingCacheKey(func_get_args());
  644. if (isset($this->hasSiblings[$cacheKey])) {
  645. return $this->hasSiblings[$cacheKey];
  646. }
  647. return $this->hasSiblings[$cacheKey] = $this->getDao()->hasSiblings($includingUnpublished);
  648. }
  649. /**
  650. * @internal
  651. *
  652. * @throws \Exception
  653. */
  654. protected function doDelete()
  655. {
  656. // remove children
  657. if ($this->hasChildren()) {
  658. // delete also unpublished children
  659. $unpublishedStatus = self::doHideUnpublished();
  660. self::setHideUnpublished(false);
  661. foreach ($this->getChildren(true) as $child) {
  662. if (!$child instanceof WrapperInterface) {
  663. $child->delete();
  664. }
  665. }
  666. self::setHideUnpublished($unpublishedStatus);
  667. }
  668. // remove all properties
  669. $this->getDao()->deleteAllProperties();
  670. // remove dependencies
  671. $d = $this->getDependencies();
  672. $d->cleanAllForElement($this);
  673. // remove translations
  674. $service = new Document\Service;
  675. $service->removeTranslation($this);
  676. }
  677. /**
  678. * {@inheritdoc}
  679. */
  680. public function delete()
  681. {
  682. $this->dispatchEvent(new DocumentEvent($this), DocumentEvents::PRE_DELETE);
  683. $this->beginTransaction();
  684. try {
  685. if ($this->getId() == 1) {
  686. throw new \Exception('root-node cannot be deleted');
  687. }
  688. $this->doDelete();
  689. $this->getDao()->delete();
  690. $this->commit();
  691. //clear parent data from registry
  692. $parentCacheKey = self::getCacheKey($this->getParentId());
  693. if (RuntimeCache::isRegistered($parentCacheKey)) {
  694. /** @var Document $parent */
  695. $parent = RuntimeCache::get($parentCacheKey);
  696. if ($parent instanceof self) {
  697. $parent->setChildren(null);
  698. }
  699. }
  700. } catch (\Exception $e) {
  701. $this->rollBack();
  702. $failureEvent = new DocumentEvent($this);
  703. $failureEvent->setArgument('exception', $e);
  704. $this->dispatchEvent($failureEvent, DocumentEvents::POST_DELETE_FAILURE);
  705. Logger::error((string) $e);
  706. throw $e;
  707. }
  708. // clear cache
  709. $this->clearDependentCache();
  710. //clear document from registry
  711. RuntimeCache::set(self::getCacheKey($this->getId()), null);
  712. RuntimeCache::set(self::getPathCacheKey($this->getRealFullPath()), null);
  713. $this->dispatchEvent(new DocumentEvent($this), DocumentEvents::POST_DELETE);
  714. }
  715. /**
  716. * {@inheritdoc}
  717. */
  718. public function getFullPath(bool $force = false)
  719. {
  720. $link = $force ? null : $this->fullPathCache;
  721. // check if this document is also the site root, if so return /
  722. try {
  723. if (!$link && Tool::isFrontend() && Site::isSiteRequest()) {
  724. $site = Site::getCurrentSite();
  725. if ($site instanceof Site) {
  726. if ($site->getRootDocument()->getId() == $this->getId()) {
  727. $link = '/';
  728. }
  729. }
  730. }
  731. } catch (\Exception $e) {
  732. Logger::error((string) $e);
  733. }
  734. $requestStack = \Pimcore::getContainer()->get('request_stack');
  735. $mainRequest = $requestStack->getMainRequest();
  736. $request = $requestStack->getCurrentRequest();
  737. // @TODO please forgive me, this is the dirtiest hack I've ever made :(
  738. // if you got confused by this functionality drop me a line and I'll buy you some beers :)
  739. // this is for the case that a link points to a document outside of the current site
  740. // in this case we look for a hardlink in the current site which points to the current document
  741. // why this could happen: we have 2 sites, in one site there's a hardlink to the other site and on a page inside
  742. // the hardlink there are snippets embedded and this snippets have links pointing to a document which is also
  743. // inside the hardlink scope, but this is an ID link, so we cannot rewrite the link the usual way because in the
  744. // snippet / link we don't know anymore that whe a inside a hardlink wrapped document
  745. if (!$link && Tool::isFrontend()) {
  746. $differentDomain = false;
  747. $site = FrontendTool::getSiteForDocument($this);
  748. if (Tool::isFrontendRequestByAdmin() && $site instanceof Site) {
  749. $differentDomain = $site->getMainDomain() != $request->getHost();
  750. }
  751. if ((Site::isSiteRequest() && !FrontendTool::isDocumentInCurrentSite($this))
  752. || $differentDomain) {
  753. if ($mainRequest && ($mainDocument = $mainRequest->get(DynamicRouter::CONTENT_KEY))) {
  754. if ($mainDocument instanceof WrapperInterface) {
  755. $hardlinkPath = '';
  756. $hardlink = $mainDocument->getHardLinkSource();
  757. $hardlinkTarget = $hardlink->getSourceDocument();
  758. if ($hardlinkTarget) {
  759. $hardlinkPath = preg_replace('@^' . preg_quote(Site::getCurrentSite()->getRootPath(), '@') . '@', '', $hardlink->getRealFullPath());
  760. $link = preg_replace('@^' . preg_quote($hardlinkTarget->getRealFullPath(), '@') . '@',
  761. $hardlinkPath, $this->getRealFullPath());
  762. }
  763. if (strpos($this->getRealFullPath(), Site::getCurrentSite()->getRootDocument()->getRealFullPath()) === false && strpos($link, $hardlinkPath) === false) {
  764. $link = null;
  765. }
  766. }
  767. }
  768. if (!$link) {
  769. $config = \Pimcore\Config::getSystemConfiguration('general');
  770. $scheme = 'http://';
  771. if ($request) {
  772. $scheme = $request->getScheme() . '://';
  773. }
  774. if ($site) {
  775. if ($site->getMainDomain()) {
  776. // check if current document is the root of the different site, if so, preg_replace below doesn't work, so just return /
  777. if ($site->getRootDocument()->getId() == $this->getId()) {
  778. $link = $scheme . $site->getMainDomain() . '/';
  779. } else {
  780. $link = $scheme . $site->getMainDomain() .
  781. preg_replace('@^' . $site->getRootPath() . '/@', '/', $this->getRealFullPath());
  782. }
  783. }
  784. }
  785. if (!$link && !empty($config['domain']) && !($this instanceof WrapperInterface)) {
  786. $link = $scheme . $config['domain'] . $this->getRealFullPath();
  787. }
  788. }
  789. }
  790. }
  791. if (!$link) {
  792. $link = $this->getPath() . $this->getKey();
  793. }
  794. if ($mainRequest) {
  795. // caching should only be done when main request is available as it is done for performance reasons
  796. // of the web frontend, without a request object there's no need to cache anything
  797. // for details also see https://github.com/pimcore/pimcore/issues/5707
  798. $this->fullPathCache = $link;
  799. }
  800. $link = $this->prepareFrontendPath($link);
  801. return $link;
  802. }
  803. /**
  804. * @param string $path
  805. *
  806. * @return string
  807. */
  808. private function prepareFrontendPath($path)
  809. {
  810. if (Tool::isFrontend()) {
  811. $path = urlencode_ignore_slash($path);
  812. $event = new GenericEvent($this, [
  813. 'frontendPath' => $path,
  814. ]);
  815. $this->dispatchEvent($event, FrontendEvents::DOCUMENT_PATH);
  816. $path = $event->getArgument('frontendPath');
  817. }
  818. return $path;
  819. }
  820. /**
  821. * {@inheritdoc}
  822. */
  823. public function getKey()
  824. {
  825. return $this->key;
  826. }
  827. /**
  828. * {@inheritdoc}
  829. */
  830. public function getPath()
  831. {
  832. // check for site, if so rewrite the path for output
  833. try {
  834. if (Tool::isFrontend() && Site::isSiteRequest()) {
  835. $site = Site::getCurrentSite();
  836. if ($site instanceof Site) {
  837. if ($site->getRootDocument() instanceof Document\Page && $site->getRootDocument() !== $this) {
  838. $rootPath = $site->getRootPath();
  839. $rootPath = preg_quote($rootPath, '@');
  840. $link = preg_replace('@^' . $rootPath . '@', '', $this->path);
  841. return $link;
  842. }
  843. }
  844. }
  845. } catch (\Exception $e) {
  846. Logger::error((string) $e);
  847. }
  848. return $this->path;
  849. }
  850. /**
  851. * {@inheritdoc}
  852. */
  853. public function getRealPath()
  854. {
  855. return $this->path;
  856. }
  857. /**
  858. * {@inheritdoc}
  859. */
  860. public function getRealFullPath()
  861. {
  862. $path = $this->getRealPath() . $this->getKey();
  863. return $path;
  864. }
  865. /**
  866. * {@inheritdoc}
  867. */
  868. public function setKey($key)
  869. {
  870. $this->key = (string)$key;
  871. return $this;
  872. }
  873. /**
  874. * Set the parent id of the document.
  875. *
  876. * @param int $parentId
  877. *
  878. * @return Document
  879. */
  880. public function setParentId($parentId)
  881. {
  882. parent::setParentId($parentId);
  883. $this->siblings = [];
  884. $this->hasSiblings = [];
  885. return $this;
  886. }
  887. /**
  888. * Returns the document index.
  889. *
  890. * @return int|null
  891. */
  892. public function getIndex(): ?int
  893. {
  894. return $this->index;
  895. }
  896. /**
  897. * Set the document index.
  898. *
  899. * @param int $index
  900. *
  901. * @return Document
  902. */
  903. public function setIndex($index)
  904. {
  905. $this->index = (int) $index;
  906. return $this;
  907. }
  908. /**
  909. * {@inheritdoc}
  910. */
  911. public function getType()
  912. {
  913. return $this->type;
  914. }
  915. /**
  916. * Set the document type.
  917. *
  918. * @param string $type
  919. *
  920. * @return Document
  921. */
  922. public function setType($type)
  923. {
  924. $this->type = $type;
  925. return $this;
  926. }
  927. /**
  928. * @return bool
  929. */
  930. public function isPublished()
  931. {
  932. return $this->getPublished();
  933. }
  934. /**
  935. * @return bool
  936. */
  937. public function getPublished()
  938. {
  939. return (bool) $this->published;
  940. }
  941. /**
  942. * @param bool $published
  943. *
  944. * @return Document
  945. */
  946. public function setPublished($published)
  947. {
  948. $this->published = (bool) $published;
  949. return $this;
  950. }
  951. /**
  952. * @return Document|null
  953. */
  954. public function getParent() /** : ?Document */
  955. {
  956. $parent = parent::getParent();
  957. return $parent instanceof Document ? $parent : null;
  958. }
  959. /**
  960. * Set the parent document instance.
  961. *
  962. * @param Document|null $parent
  963. *
  964. * @return Document
  965. */
  966. public function setParent($parent)
  967. {
  968. $this->parent = $parent;
  969. if ($parent instanceof Document) {
  970. $this->parentId = $parent->getId();
  971. }
  972. return $this;
  973. }
  974. /**
  975. * Set true if want to hide documents.
  976. *
  977. * @param bool $hideUnpublished
  978. */
  979. public static function setHideUnpublished($hideUnpublished)
  980. {
  981. self::$hideUnpublished = $hideUnpublished;
  982. }
  983. /**
  984. * Checks if unpublished documents should be hidden.
  985. *
  986. * @return bool
  987. */
  988. public static function doHideUnpublished()
  989. {
  990. return self::$hideUnpublished;
  991. }
  992. /**
  993. * @internal
  994. *
  995. * @param array $args
  996. *
  997. * @return string
  998. */
  999. protected function getListingCacheKey(array $args = [])
  1000. {
  1001. $includingUnpublished = (bool)($args[0] ?? false);
  1002. return 'document_list_' . ($includingUnpublished ? '1' : '0');
  1003. }
  1004. public function __clone()
  1005. {
  1006. parent::__clone();
  1007. $this->parent = null;
  1008. $this->hasSiblings = [];
  1009. $this->siblings = [];
  1010. $this->fullPathCache = null;
  1011. }
  1012. }