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

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