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

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