vendor/pimcore/pimcore/models/Document/Service.php line 598

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\Config;
  16. use Pimcore\Document\Renderer\DocumentRenderer;
  17. use Pimcore\Document\Renderer\DocumentRendererInterface;
  18. use Pimcore\Event\DocumentEvents;
  19. use Pimcore\Event\Model\DocumentEvent;
  20. use Pimcore\File;
  21. use Pimcore\Image\Chromium;
  22. use Pimcore\Image\HtmlToImage;
  23. use Pimcore\Model;
  24. use Pimcore\Model\Document;
  25. use Pimcore\Model\Document\Editable\IdRewriterInterface;
  26. use Pimcore\Model\Document\Editable\LazyLoadingInterface;
  27. use Pimcore\Model\Element;
  28. use Pimcore\Tool;
  29. use Pimcore\Tool\Serialize;
  30. use Symfony\Component\HttpFoundation\Request;
  31. /**
  32. * @method \Pimcore\Model\Document\Service\Dao getDao()
  33. * @method int[] getTranslations(Document $document, string $task = 'open')
  34. * @method addTranslation(Document $document, Document $translation, $language = null)
  35. * @method removeTranslation(Document $document)
  36. * @method int getTranslationSourceId(Document $document)
  37. * @method removeTranslationLink(Document $document, Document $targetDocument)
  38. */
  39. class Service extends Model\Element\Service
  40. {
  41. /**
  42. * @var Model\User|null
  43. */
  44. protected $_user;
  45. /**
  46. * @var array
  47. */
  48. protected $_copyRecursiveIds;
  49. /**
  50. * @var Document[]
  51. */
  52. protected $nearestPathCache;
  53. /**
  54. * @param Model\User $user
  55. */
  56. public function __construct($user = null)
  57. {
  58. $this->_user = $user;
  59. }
  60. /**
  61. * Renders a document outside of a view
  62. *
  63. * Parameter order was kept for BC (useLayout before query and options).
  64. *
  65. * @static
  66. *
  67. * @param Document\PageSnippet $document
  68. * @param array $attributes
  69. * @param bool $useLayout
  70. * @param array $query
  71. * @param array $options
  72. *
  73. * @return string
  74. */
  75. public static function render(Document\PageSnippet $document, array $attributes = [], $useLayout = false, array $query = [], array $options = []): string
  76. {
  77. $container = \Pimcore::getContainer();
  78. /** @var DocumentRendererInterface $renderer */
  79. $renderer = $container->get(DocumentRenderer::class);
  80. // keep useLayout compatibility
  81. $attributes['_useLayout'] = $useLayout;
  82. $content = $renderer->render($document, $attributes, $query, $options);
  83. return $content;
  84. }
  85. /**
  86. * Save document and all child documents
  87. *
  88. * @param Document $document
  89. * @param int $collectGarbageAfterIteration
  90. * @param int $saved
  91. *
  92. * @throws \Exception
  93. */
  94. private static function saveRecursive($document, $collectGarbageAfterIteration = 25, &$saved = 0)
  95. {
  96. if ($document instanceof Document) {
  97. $document->save();
  98. $saved++;
  99. if ($saved % $collectGarbageAfterIteration === 0) {
  100. \Pimcore::collectGarbage();
  101. }
  102. }
  103. foreach ($document->getChildren() as $child) {
  104. if (!$child->hasChildren()) {
  105. $child->save();
  106. $saved++;
  107. if ($saved % $collectGarbageAfterIteration === 0) {
  108. \Pimcore::collectGarbage();
  109. }
  110. }
  111. if ($child->hasChildren()) {
  112. self::saveRecursive($child, $collectGarbageAfterIteration, $saved);
  113. }
  114. }
  115. }
  116. /**
  117. * @param Document $target
  118. * @param Document $source
  119. *
  120. * @return Document|null copied document
  121. *
  122. * @throws \Exception
  123. */
  124. public function copyRecursive($target, $source)
  125. {
  126. // avoid recursion
  127. if (!$this->_copyRecursiveIds) {
  128. $this->_copyRecursiveIds = [];
  129. }
  130. if (in_array($source->getId(), $this->_copyRecursiveIds)) {
  131. return null;
  132. }
  133. if ($source instanceof Document\PageSnippet) {
  134. $source->getEditables();
  135. }
  136. $source->getProperties();
  137. // triggers actions before document cloning
  138. $event = new DocumentEvent($source, [
  139. 'target_element' => $target,
  140. ]);
  141. \Pimcore::getEventDispatcher()->dispatch($event, DocumentEvents::PRE_COPY);
  142. $target = $event->getArgument('target_element');
  143. /** @var Document $new */
  144. $new = Element\Service::cloneMe($source);
  145. $new->setId(null);
  146. $new->setChildren(null);
  147. $new->setKey(Element\Service::getSafeCopyName($new->getKey(), $target));
  148. $new->setParentId($target->getId());
  149. $new->setUserOwner($this->_user ? $this->_user->getId() : 0);
  150. $new->setUserModification($this->_user ? $this->_user->getId() : 0);
  151. $new->setDao(null);
  152. $new->setLocked(null);
  153. $new->setCreationDate(time());
  154. if ($new instanceof Page) {
  155. $new->setPrettyUrl(null);
  156. }
  157. $new->save();
  158. // add to store
  159. $this->_copyRecursiveIds[] = $new->getId();
  160. foreach ($source->getChildren(true) as $child) {
  161. $this->copyRecursive($new, $child);
  162. }
  163. $this->updateChildren($target, $new);
  164. // triggers actions after the complete document cloning
  165. $event = new DocumentEvent($new, [
  166. 'base_element' => $source, // the element used to make a copy
  167. ]);
  168. \Pimcore::getEventDispatcher()->dispatch($event, DocumentEvents::POST_COPY);
  169. return $new;
  170. }
  171. /**
  172. * @param Document $target
  173. * @param Document $source
  174. * @param bool $enableInheritance
  175. * @param bool $resetIndex
  176. *
  177. * @return Document
  178. *
  179. * @throws \Exception
  180. */
  181. public function copyAsChild($target, $source, $enableInheritance = false, $resetIndex = false, $language = false)
  182. {
  183. if ($source instanceof Document\PageSnippet) {
  184. $source->getEditables();
  185. }
  186. $source->getProperties();
  187. // triggers actions before document cloning
  188. $event = new DocumentEvent($source, [
  189. 'target_element' => $target,
  190. ]);
  191. \Pimcore::getEventDispatcher()->dispatch($event, DocumentEvents::PRE_COPY);
  192. $target = $event->getArgument('target_element');
  193. /**
  194. * @var Document $new
  195. */
  196. $new = Element\Service::cloneMe($source);
  197. $new->setId(null);
  198. $new->setChildren(null);
  199. $new->setKey(Element\Service::getSafeCopyName($new->getKey(), $target));
  200. $new->setParentId($target->getId());
  201. $new->setUserOwner($this->_user ? $this->_user->getId() : 0);
  202. $new->setUserModification($this->_user ? $this->_user->getId() : 0);
  203. $new->setDao(null);
  204. $new->setLocked(null);
  205. $new->setCreationDate(time());
  206. if ($resetIndex) {
  207. // this needs to be after $new->setParentId($target->getId()); -> dependency!
  208. $new->setIndex($new->getDao()->getNextIndex());
  209. }
  210. if ($new instanceof Page) {
  211. $new->setPrettyUrl(null);
  212. }
  213. if ($enableInheritance && ($new instanceof Document\PageSnippet) && $new->supportsContentMaster()) {
  214. $new->setEditables([]);
  215. $new->setContentMasterDocumentId($source->getId(), true);
  216. }
  217. if ($language) {
  218. $new->setProperty('language', 'text', $language, false, true);
  219. }
  220. $new->save();
  221. $this->updateChildren($target, $new);
  222. //link translated document
  223. if ($language) {
  224. $this->addTranslation($source, $new, $language);
  225. }
  226. // triggers actions after the complete document cloning
  227. $event = new DocumentEvent($new, [
  228. 'base_element' => $source, // the element used to make a copy
  229. ]);
  230. \Pimcore::getEventDispatcher()->dispatch($event, DocumentEvents::POST_COPY);
  231. return $new;
  232. }
  233. /**
  234. * @param Document $target
  235. * @param Document $source
  236. *
  237. * @return Document
  238. *
  239. * @throws \Exception
  240. */
  241. public function copyContents($target, $source)
  242. {
  243. // check if the type is the same
  244. if (get_class($source) != get_class($target)) {
  245. throw new \Exception('Source and target have to be the same type');
  246. }
  247. if ($source instanceof Document\PageSnippet) {
  248. /** @var PageSnippet $target */
  249. $target->setEditables($source->getEditables());
  250. $target->setTemplate($source->getTemplate());
  251. $target->setController($source->getController());
  252. if ($source instanceof Document\Page) {
  253. /** @var Page $target */
  254. $target->setTitle($source->getTitle());
  255. $target->setDescription($source->getDescription());
  256. }
  257. } elseif ($source instanceof Document\Link) {
  258. /** @var Link $target */
  259. $target->setInternalType($source->getInternalType());
  260. $target->setInternal($source->getInternal());
  261. $target->setDirect($source->getDirect());
  262. $target->setLinktype($source->getLinktype());
  263. }
  264. $target->setUserModification($this->_user ? $this->_user->getId() : 0);
  265. $target->setProperties(self::cloneProperties($source->getProperties()));
  266. $target->save();
  267. return $target;
  268. }
  269. /**
  270. * @param Document $document
  271. *
  272. * @return array
  273. *
  274. * @internal
  275. */
  276. public static function gridDocumentData($document)
  277. {
  278. $data = Element\Service::gridElementData($document);
  279. if ($document instanceof Document\Page) {
  280. $data['title'] = $document->getTitle();
  281. $data['description'] = $document->getDescription();
  282. } else {
  283. $data['title'] = '';
  284. $data['description'] = '';
  285. $data['name'] = '';
  286. }
  287. return $data;
  288. }
  289. /**
  290. * @internal
  291. *
  292. * @param Document $doc
  293. *
  294. * @return Document
  295. */
  296. public static function loadAllDocumentFields($doc)
  297. {
  298. $doc->getProperties();
  299. if ($doc instanceof Document\PageSnippet) {
  300. foreach ($doc->getEditables() as $name => $data) {
  301. //TODO Pimcore 11: remove method_exists BC layer
  302. if ($data instanceof LazyLoadingInterface || method_exists($data, 'load')) {
  303. if (!$data instanceof LazyLoadingInterface) {
  304. trigger_deprecation('pimcore/pimcore', '10.3',
  305. sprintf('Usage of method_exists is deprecated since version 10.3 and will be removed in Pimcore 11.' .
  306. 'Implement the %s interface instead.', LazyLoadingInterface::class));
  307. }
  308. $data->load();
  309. }
  310. }
  311. }
  312. return $doc;
  313. }
  314. /**
  315. * @static
  316. *
  317. * @param string $path
  318. * @param string|null $type
  319. *
  320. * @return bool
  321. */
  322. public static function pathExists($path, $type = null)
  323. {
  324. if (!$path) {
  325. return false;
  326. }
  327. $path = Element\Service::correctPath($path);
  328. try {
  329. $document = new Document();
  330. // validate path
  331. if (self::isValidPath($path, 'document')) {
  332. $document->getDao()->getByPath($path);
  333. return true;
  334. }
  335. } catch (\Exception $e) {
  336. }
  337. return false;
  338. }
  339. /**
  340. * @param string $type
  341. *
  342. * @return bool
  343. */
  344. public static function isValidType($type)
  345. {
  346. return in_array($type, Document::getTypes());
  347. }
  348. /**
  349. * Rewrites id from source to target, $rewriteConfig contains
  350. * array(
  351. * "document" => array(
  352. * SOURCE_ID => TARGET_ID,
  353. * SOURCE_ID => TARGET_ID
  354. * ),
  355. * "object" => array(...),
  356. * "asset" => array(...)
  357. * )
  358. *
  359. * @internal
  360. *
  361. * @param Document $document
  362. * @param array $rewriteConfig
  363. * @param array $params
  364. *
  365. * @return Document
  366. */
  367. public static function rewriteIds($document, $rewriteConfig, $params = [])
  368. {
  369. // rewriting elements only for snippets and pages
  370. if ($document instanceof Document\PageSnippet) {
  371. if (array_key_exists('enableInheritance', $params) && $params['enableInheritance']) {
  372. $editables = $document->getEditables();
  373. $changedEditables = [];
  374. $contentMaster = $document->getContentMasterDocument();
  375. if ($contentMaster instanceof Document\PageSnippet) {
  376. $contentMasterEditables = $contentMaster->getEditables();
  377. foreach ($contentMasterEditables as $contentMasterEditable) {
  378. //TODO Pimcore 11: remove method_exists BC layer
  379. if ($contentMasterEditable instanceof IdRewriterInterface || method_exists($contentMasterEditable, 'rewriteIds')) {
  380. if (!$contentMasterEditable instanceof IdRewriterInterface) {
  381. trigger_deprecation('pimcore/pimcore', '10.3',
  382. sprintf('Usage of method_exists is deprecated since version 10.3 and will be removed in Pimcore 11.' .
  383. 'Implement the %s interface instead.', IdRewriterInterface::class));
  384. }
  385. $editable = clone $contentMasterEditable;
  386. $editable->rewriteIds($rewriteConfig);
  387. if (Serialize::serialize($editable) != Serialize::serialize($contentMasterEditable)) {
  388. $changedEditables[] = $editable;
  389. }
  390. }
  391. }
  392. }
  393. if (count($changedEditables) > 0) {
  394. $editables = $changedEditables;
  395. }
  396. } else {
  397. $editables = $document->getEditables();
  398. foreach ($editables as &$editable) {
  399. //TODO Pimcore 11: remove method_exists BC layer
  400. if ($editable instanceof IdRewriterInterface || method_exists($editable, 'rewriteIds')) {
  401. if (!$editable instanceof IdRewriterInterface) {
  402. trigger_deprecation('pimcore/pimcore', '10.3',
  403. sprintf('Usage of method_exists is deprecated since version 10.3 and will be removed in Pimcore 11.' .
  404. 'Implement the %s interface instead.', IdRewriterInterface::class));
  405. }
  406. $editable->rewriteIds($rewriteConfig);
  407. }
  408. }
  409. }
  410. $document->setEditables($editables);
  411. } elseif ($document instanceof Document\Hardlink) {
  412. if (array_key_exists('document', $rewriteConfig) && $document->getSourceId() && array_key_exists((int) $document->getSourceId(), $rewriteConfig['document'])) {
  413. $document->setSourceId($rewriteConfig['document'][(int) $document->getSourceId()]);
  414. }
  415. } elseif ($document instanceof Document\Link) {
  416. if (array_key_exists('document', $rewriteConfig) && $document->getLinktype() == 'internal' && $document->getInternalType() == 'document' && array_key_exists((int) $document->getInternal(), $rewriteConfig['document'])) {
  417. $document->setInternal($rewriteConfig['document'][(int) $document->getInternal()]);
  418. }
  419. }
  420. // rewriting properties
  421. $properties = $document->getProperties();
  422. foreach ($properties as &$property) {
  423. $property->rewriteIds($rewriteConfig);
  424. }
  425. $document->setProperties($properties);
  426. return $document;
  427. }
  428. /**
  429. * @internal
  430. *
  431. * @param string $url
  432. *
  433. * @return Document|null
  434. */
  435. public static function getByUrl($url)
  436. {
  437. $urlParts = parse_url($url);
  438. $document = null;
  439. if ($urlParts['path']) {
  440. $document = Document::getByPath($urlParts['path']);
  441. // search for a page in a site
  442. if (!$document) {
  443. $sitesList = new Model\Site\Listing();
  444. $sitesObjects = $sitesList->load();
  445. foreach ($sitesObjects as $site) {
  446. if ($site->getRootDocument() && (in_array($urlParts['host'], $site->getDomains()) || $site->getMainDomain() == $urlParts['host'])) {
  447. if ($document = Document::getByPath($site->getRootDocument() . $urlParts['path'])) {
  448. break;
  449. }
  450. }
  451. }
  452. }
  453. }
  454. return $document;
  455. }
  456. /**
  457. * @param Document $item
  458. * @param int $nr
  459. *
  460. * @return string
  461. *
  462. * @throws \Exception
  463. */
  464. public static function getUniqueKey($item, $nr = 0)
  465. {
  466. $list = new Listing();
  467. $list->setUnpublished(true);
  468. $key = Element\Service::getValidKey($item->getKey(), 'document');
  469. if (!$key) {
  470. throw new \Exception('No item key set.');
  471. }
  472. if ($nr) {
  473. $key = $key . '_' . $nr;
  474. }
  475. $parent = $item->getParent();
  476. if (!$parent) {
  477. throw new \Exception('You have to set a parent document to determine a unique Key');
  478. }
  479. if (!$item->getId()) {
  480. $list->setCondition('parentId = ? AND `key` = ? ', [$parent->getId(), $key]);
  481. } else {
  482. $list->setCondition('parentId = ? AND `key` = ? AND id != ? ', [$parent->getId(), $key, $item->getId()]);
  483. }
  484. $check = $list->loadIdList();
  485. if (!empty($check)) {
  486. $nr++;
  487. $key = self::getUniqueKey($item, $nr);
  488. }
  489. return $key;
  490. }
  491. /**
  492. * Get the nearest document by path. Used to match nearest document for a static route.
  493. *
  494. * @internal
  495. *
  496. * @param string|Request $path
  497. * @param bool $ignoreHardlinks
  498. * @param array $types
  499. *
  500. * @return Document|null
  501. */
  502. public function getNearestDocumentByPath($path, $ignoreHardlinks = false, $types = [])
  503. {
  504. if ($path instanceof Request) {
  505. $path = urldecode($path->getPathInfo());
  506. }
  507. $cacheKey = $ignoreHardlinks . implode('-', $types);
  508. $document = null;
  509. if (isset($this->nearestPathCache[$cacheKey])) {
  510. $document = $this->nearestPathCache[$cacheKey];
  511. } else {
  512. $paths = ['/'];
  513. $tmpPaths = [];
  514. $pathParts = explode('/', $path);
  515. foreach ($pathParts as $pathPart) {
  516. $tmpPaths[] = $pathPart;
  517. $t = implode('/', $tmpPaths);
  518. $paths[] = $t;
  519. }
  520. $paths = array_reverse($paths);
  521. foreach ($paths as $p) {
  522. if ($document = Document::getByPath($p)) {
  523. if (empty($types) || in_array($document->getType(), $types)) {
  524. $document = $this->nearestPathCache[$cacheKey] = $document;
  525. break;
  526. }
  527. } elseif (Model\Site::isSiteRequest()) {
  528. // also check for a pretty url in a site
  529. $site = Model\Site::getCurrentSite();
  530. // undo the changed made by the site detection in self::match()
  531. $originalPath = preg_replace('@^' . $site->getRootPath() . '@', '', $p);
  532. $sitePrettyDocId = $this->getDao()->getDocumentIdByPrettyUrlInSite($site, $originalPath);
  533. if ($sitePrettyDocId) {
  534. if ($sitePrettyDoc = Document::getById($sitePrettyDocId)) {
  535. $document = $this->nearestPathCache[$cacheKey] = $sitePrettyDoc;
  536. break;
  537. }
  538. }
  539. }
  540. }
  541. }
  542. if ($document) {
  543. if (!$ignoreHardlinks) {
  544. if ($document instanceof Document\Hardlink) {
  545. if ($hardLinkedDocument = Document\Hardlink\Service::getNearestChildByPath($document, $path)) {
  546. $document = $hardLinkedDocument;
  547. } else {
  548. $document = Document\Hardlink\Service::wrap($document);
  549. }
  550. }
  551. }
  552. return $document;
  553. }
  554. return null;
  555. }
  556. /**
  557. * @param int $id
  558. * @param Request $request
  559. * @param string $hostUrl
  560. *
  561. * @return bool
  562. *
  563. * @throws \Exception
  564. *
  565. * @internal
  566. */
  567. public static function generatePagePreview($id, $request = null, $hostUrl = null)
  568. {
  569. $success = false;
  570. /** @var Page $doc */
  571. $doc = Document::getById($id);
  572. if (!$hostUrl) {
  573. $hostUrl = Config::getSystemConfiguration('documents')['preview_url_prefix'];
  574. if (empty($hostUrl)) {
  575. $hostUrl = Tool::getHostUrl(null, $request);
  576. }
  577. }
  578. $url = $hostUrl . $doc->getRealFullPath();
  579. $tmpFile = PIMCORE_SYSTEM_TEMP_DIRECTORY . '/screenshot_tmp_' . $doc->getId() . '.png';
  580. $file = $doc->getPreviewImageFilesystemPath();
  581. File::mkdir(dirname($file));
  582. $tool = false;
  583. if (Chromium::isSupported()) {
  584. $tool = Chromium::class;
  585. } elseif (HtmlToImage::isSupported()) {
  586. $tool = HtmlToImage::class;
  587. }
  588. if ($tool) {
  589. /** @var Chromium|HtmlToImage $tool */
  590. if ($tool::convert($url, $tmpFile)) {
  591. $im = \Pimcore\Image::getInstance();
  592. $im->load($tmpFile);
  593. $im->scaleByWidth(800);
  594. $im->save($file, 'jpeg', 85);
  595. unlink($tmpFile);
  596. $success = true;
  597. }
  598. }
  599. return $success;
  600. }
  601. }