vendor/sulu/sulu/src/Sulu/Component/Content/Mapper/ContentMapper.php line 562

Open in your IDE?
  1. <?php
  2. /*
  3. * This file is part of Sulu.
  4. *
  5. * (c) Sulu GmbH
  6. *
  7. * This source file is subject to the MIT license that is bundled
  8. * with this source code in the file LICENSE.
  9. */
  10. namespace Sulu\Component\Content\Mapper;
  11. use Doctrine\Common\Cache\Cache;
  12. use Doctrine\Common\Cache\Psr6\DoctrineProvider;
  13. use Jackalope\Query\Row;
  14. use PHPCR\NodeInterface;
  15. use PHPCR\Query\QueryInterface;
  16. use PHPCR\Query\QueryResultInterface;
  17. use Sulu\Bundle\DocumentManagerBundle\Bridge\DocumentInspector;
  18. use Sulu\Bundle\DocumentManagerBundle\Bridge\PropertyEncoder;
  19. use Sulu\Bundle\PageBundle\Admin\PageAdmin;
  20. use Sulu\Bundle\PageBundle\Document\BasePageDocument;
  21. use Sulu\Bundle\PageBundle\Document\HomeDocument;
  22. use Sulu\Bundle\SnippetBundle\Document\SnippetDocument;
  23. use Sulu\Component\Content\BreadcrumbItem;
  24. use Sulu\Component\Content\Compat\Property as LegacyProperty;
  25. use Sulu\Component\Content\Compat\Structure as LegacyStructure;
  26. use Sulu\Component\Content\Compat\StructureInterface;
  27. use Sulu\Component\Content\Compat\StructureManagerInterface;
  28. use Sulu\Component\Content\ContentTypeManagerInterface;
  29. use Sulu\Component\Content\Document\Behavior\ExtensionBehavior;
  30. use Sulu\Component\Content\Document\Behavior\LocalizedAuthorBehavior;
  31. use Sulu\Component\Content\Document\Behavior\LocalizedLastModifiedBehavior;
  32. use Sulu\Component\Content\Document\Behavior\OrderBehavior;
  33. use Sulu\Component\Content\Document\Behavior\RedirectTypeBehavior;
  34. use Sulu\Component\Content\Document\Behavior\ResourceSegmentBehavior;
  35. use Sulu\Component\Content\Document\Behavior\SecurityBehavior;
  36. use Sulu\Component\Content\Document\Behavior\ShadowLocaleBehavior;
  37. use Sulu\Component\Content\Document\Behavior\StructureBehavior;
  38. use Sulu\Component\Content\Document\Behavior\WorkflowStageBehavior;
  39. use Sulu\Component\Content\Document\LocalizationState;
  40. use Sulu\Component\Content\Document\RedirectType;
  41. use Sulu\Component\Content\Document\Subscriber\WorkflowStageSubscriber;
  42. use Sulu\Component\Content\Document\WorkflowStage;
  43. use Sulu\Component\Content\Exception\InvalidOrderPositionException;
  44. use Sulu\Component\Content\Exception\TranslatedNodeNotFoundException;
  45. use Sulu\Component\Content\Extension\ExtensionInterface;
  46. use Sulu\Component\Content\Extension\ExtensionManagerInterface;
  47. use Sulu\Component\Content\Mapper\Event\ContentNodeEvent;
  48. use Sulu\Component\Content\Metadata\Factory\Exception\StructureTypeNotFoundException;
  49. use Sulu\Component\Content\Types\ResourceLocator;
  50. use Sulu\Component\Content\Types\ResourceLocator\Strategy\ResourceLocatorStrategyPoolInterface;
  51. use Sulu\Component\DocumentManager\Behavior\Mapping\ParentBehavior;
  52. use Sulu\Component\DocumentManager\Document\UnknownDocument;
  53. use Sulu\Component\DocumentManager\DocumentAccessor;
  54. use Sulu\Component\DocumentManager\DocumentManagerInterface;
  55. use Sulu\Component\DocumentManager\Exception\DocumentNotFoundException;
  56. use Sulu\Component\DocumentManager\NamespaceRegistry;
  57. use Sulu\Component\PHPCR\SessionManager\SessionManagerInterface;
  58. use Sulu\Component\Security\Authorization\AccessControl\AccessControlManagerInterface;
  59. use Sulu\Component\Webspace\Manager\WebspaceManagerInterface;
  60. use Symfony\Bundle\SecurityBundle\Security;
  61. use Symfony\Component\Cache\Adapter\ArrayAdapter;
  62. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  63. use Symfony\Component\Form\FormFactoryInterface;
  64. use Symfony\Component\Security\Core\Security as SymfonyCoreSecurity;
  65. /**
  66. * Maps content nodes to phpcr nodes with content types and provides utility function to handle content nodes.
  67. *
  68. * @deprecated since 1.0-? use the DocumentManager instead
  69. */
  70. class ContentMapper implements ContentMapperInterface
  71. {
  72. /**
  73. * @var Cache
  74. */
  75. private $extensionDataCache;
  76. /**
  77. * @param mixed[] $permissions
  78. */
  79. public function __construct(
  80. private DocumentManagerInterface $documentManager,
  81. private WebspaceManagerInterface $webspaceManager,
  82. private FormFactoryInterface $formFactory,
  83. private DocumentInspector $inspector,
  84. private PropertyEncoder $encoder,
  85. private StructureManagerInterface $structureManager,
  86. private ExtensionManagerInterface $extensionManager,
  87. private ContentTypeManagerInterface $contentTypeManager,
  88. private SessionManagerInterface $sessionManager,
  89. private EventDispatcherInterface $eventDispatcher,
  90. private ResourceLocatorStrategyPoolInterface $resourceLocatorStrategyPool,
  91. private NamespaceRegistry $namespaceRegistry,
  92. private AccessControlManagerInterface $accessControlManager,
  93. private $permissions,
  94. private Security|SymfonyCoreSecurity|null $security = null,
  95. ) {
  96. }
  97. /**
  98. * TODO: Refactor this .. this should be handled in a listener or in the form, or something
  99. * {@inheritdoc}
  100. */
  101. public function saveExtension(
  102. $uuid,
  103. $data,
  104. $extensionName,
  105. $webspaceKey,
  106. $locale,
  107. $userId
  108. ) {
  109. $document = $this->loadDocument(
  110. $uuid,
  111. $locale,
  112. [
  113. 'exclude_ghost' => true,
  114. ]
  115. );
  116. if (null === $document) {
  117. throw new TranslatedNodeNotFoundException($uuid, $locale);
  118. }
  119. if (!$document instanceof ExtensionBehavior) {
  120. throw new \RuntimeException(
  121. \sprintf(
  122. 'Document of class "%s" must implement the ExtensionableBehavior if it is to be extended',
  123. \get_class($document)
  124. )
  125. );
  126. }
  127. // save data of extensions
  128. $extension = $this->extensionManager->getExtension((string) $document->getStructureType(), $extensionName);
  129. $node = $this->inspector->getNode($document);
  130. $extension->save($node, $data, $webspaceKey, $locale);
  131. $extensionData = $extension->load($node, $webspaceKey, $locale);
  132. $document->setExtension($extension->getName(), $extensionData);
  133. $this->documentManager->flush();
  134. $structure = $this->documentToStructure($document);
  135. $event = new ContentNodeEvent($node, $structure);
  136. $this->eventDispatcher->dispatch($event, ContentEvents::NODE_POST_SAVE);
  137. return $structure;
  138. }
  139. public function loadByParent(
  140. $uuid,
  141. $webspaceKey,
  142. $languageCode,
  143. $depth = 1,
  144. $flat = true,
  145. $ignoreExceptions = false,
  146. $excludeGhosts = false
  147. ) {
  148. $parent = null;
  149. $options = ['load_ghost_content' => true];
  150. if ($uuid) {
  151. $parent = $this->documentManager->find($uuid, $languageCode, $options);
  152. }
  153. if (null === $parent) {
  154. $parent = $this->getContentDocument($webspaceKey, $languageCode, $options);
  155. }
  156. $fetchDepth = -1;
  157. if (false === $flat) {
  158. $fetchDepth = $depth;
  159. }
  160. $children = $this->inspector->getChildren($parent, $options);
  161. $children = $this->documentsToStructureCollection(
  162. $children->toArray(),
  163. [
  164. 'exclude_ghost' => $excludeGhosts,
  165. ]
  166. );
  167. if ($flat) {
  168. foreach ($children as $child) {
  169. if (null === $depth || $depth > 1) {
  170. $childChildren = $this->loadByParent(
  171. $child->getUuid(),
  172. $webspaceKey,
  173. $languageCode,
  174. $depth - 1,
  175. $flat,
  176. $ignoreExceptions,
  177. $excludeGhosts
  178. );
  179. $children = \array_merge($children, $childChildren);
  180. }
  181. }
  182. }
  183. return $children;
  184. }
  185. public function load($uuid, $webspaceKey, $locale, $loadGhostContent = false)
  186. {
  187. $document = $this->documentManager->find(
  188. $uuid,
  189. $locale,
  190. [
  191. 'load_ghost_content' => $loadGhostContent,
  192. ]
  193. );
  194. if ($document instanceof UnknownDocument) {
  195. throw new DocumentNotFoundException();
  196. }
  197. return $this->documentToStructure($document);
  198. }
  199. public function loadStartPage($webspaceKey, $locale)
  200. {
  201. $startPage = $this->getContentDocument($webspaceKey, $locale);
  202. $startPage->setWorkflowStage(WorkflowStage::PUBLISHED);
  203. $startPage->setNavigationContexts([]);
  204. return $this->documentToStructure($startPage);
  205. }
  206. public function loadBySql2($sql2, $locale, $webspaceKey, $limit = null)
  207. {
  208. $query = $this->documentManager->createQuery($sql2, $locale);
  209. if (null !== $limit) {
  210. $query->setMaxResults($limit);
  211. }
  212. $documents = $query->execute();
  213. return $this->documentsToStructureCollection($documents, null);
  214. }
  215. public function loadByQuery(
  216. QueryInterface $query,
  217. $locale,
  218. $webspaceKey = null,
  219. $excludeGhost = true,
  220. $loadGhostContent = false
  221. ) {
  222. $options = [
  223. 'exclude_ghost' => $excludeGhost,
  224. 'load_ghost_content' => $loadGhostContent,
  225. ];
  226. $documents = $this->documentManager->createQuery($query, $locale, $options)->execute();
  227. return $this->documentsToStructureCollection($documents, $options);
  228. }
  229. public function loadNodeAndAncestors(
  230. $uuid,
  231. $locale,
  232. $webspaceKey = null,
  233. $excludeGhost = true,
  234. $excludeShadow = true
  235. ) {
  236. $document = $this->loadDocument(
  237. $uuid,
  238. $locale,
  239. $options = [
  240. 'load_ghost_content' => true,
  241. 'exclude_ghost' => $excludeGhost,
  242. 'exclude_shadow' => $excludeShadow,
  243. ],
  244. false
  245. );
  246. if (null === $document) {
  247. return [];
  248. }
  249. $documents = [];
  250. if (!$this->optionsShouldExcludeDocument($document, $options)) {
  251. $documents[] = $document;
  252. }
  253. if ($document instanceof HomeDocument) {
  254. return $this->documentsToStructureCollection($documents, $options);
  255. }
  256. while ($document) {
  257. $parentDocument = $this->inspector->getParent($document);
  258. $documents[] = $parentDocument;
  259. if ($parentDocument instanceof HomeDocument) {
  260. return $this->documentsToStructureCollection($documents, $options);
  261. }
  262. $document = $parentDocument;
  263. }
  264. throw new \RuntimeException(
  265. \sprintf(
  266. 'Did not traverse an instance of HomeDocument when searching for desendants of document "%s"',
  267. $uuid
  268. )
  269. );
  270. }
  271. /**
  272. * Load/hydrate a shalow structure with the given node.
  273. * Shallow structures do not have content properties / extensions
  274. * hydrated.
  275. *
  276. * @param string $localization
  277. * @param string $webspaceKey
  278. *
  279. * @return StructureInterface
  280. */
  281. public function loadShallowStructureByNode(NodeInterface $contentNode, $localization, $webspaceKey)
  282. {
  283. $document = $this->documentManager->find($contentNode->getPath(), $localization);
  284. return $this->documentToStructure($document);
  285. }
  286. public function loadByNode(
  287. NodeInterface $node,
  288. $locale,
  289. $webspaceKey = null,
  290. $excludeGhost = true,
  291. $loadGhostContent = false,
  292. $excludeShadow = true
  293. ) {
  294. $document = $this->loadDocument(
  295. $node->getIdentifier(),
  296. $locale,
  297. [
  298. 'load_ghost_content' => $loadGhostContent,
  299. 'exclude_ghost' => $excludeGhost,
  300. 'exclude_shadow' => $excludeShadow,
  301. ]
  302. );
  303. return $this->documentToStructure($document);
  304. }
  305. public function loadBreadcrumb($uuid, $locale, $webspaceKey)
  306. {
  307. $document = $this->documentManager->find($uuid, $locale);
  308. $documents = [];
  309. $contentDocument = $this->getContentDocument($webspaceKey, $locale);
  310. $contentDepth = $this->inspector->getDepth($contentDocument);
  311. $document = $this->inspector->getParent($document);
  312. $documentDepth = $this->inspector->getDepth($document);
  313. while ($document instanceof StructureBehavior && $documentDepth >= $contentDepth) {
  314. $documents[] = $document;
  315. $document = $this->inspector->getParent($document);
  316. $documentDepth = $this->inspector->getDepth($document);
  317. }
  318. $items = [];
  319. foreach ($documents as $document) {
  320. $items[] = new BreadcrumbItem(
  321. $this->inspector->getDepth($document) - $contentDepth,
  322. $this->inspector->getUuid($document),
  323. $document->getTitle()
  324. );
  325. }
  326. $items = \array_reverse($items);
  327. return $items;
  328. }
  329. public function delete($uuid, $webspaceKey/*, bool $forceRemoveChildren = false*/)
  330. {
  331. $forceRemoveChildren = \func_num_args() >= 3 ? (bool) \func_get_arg(2) : false;
  332. $document = $this->documentManager->find($uuid);
  333. $this->documentManager->remove($document, [
  334. 'force_remove_children' => $forceRemoveChildren,
  335. ]);
  336. $this->documentManager->flush();
  337. }
  338. public function copyLanguage(
  339. $uuid,
  340. $userId,
  341. $webspaceKey,
  342. $srcLocale,
  343. $destLocales,
  344. $structureType = LegacyStructure::TYPE_PAGE
  345. ) {
  346. @trigger_deprecation(
  347. 'sulu/sulu',
  348. '2.3',
  349. 'The ContentMapperInterface::copyLanguage method is deprecated and will be removed in the future.'
  350. . ' Use DocumentManagerInterface::copyLocale instead.'
  351. );
  352. if (!\is_array($destLocales)) {
  353. $destLocales = [$destLocales];
  354. }
  355. $document = $this->documentManager->find($uuid, $srcLocale);
  356. $resourceLocatorStrategy = null;
  357. if ($document instanceof ResourceSegmentBehavior) {
  358. $resourceLocatorStrategy = $this->resourceLocatorStrategyPool->getStrategyByWebspaceKey($webspaceKey);
  359. }
  360. $parentUuid = null;
  361. if ($document instanceof ParentBehavior) {
  362. $parentDocument = $this->inspector->getParent($document);
  363. $parentUuid = $this->inspector->getUuid($parentDocument);
  364. }
  365. foreach ($destLocales as $destLocale) {
  366. $destDocument = $this->documentManager->find(
  367. $uuid,
  368. $destLocale
  369. );
  370. $destDocument->setLocale($destLocale);
  371. $destDocument->setTitle($document->getTitle());
  372. $destDocument->setStructureType($document->getStructureType());
  373. $destDocument->getStructure()->bind($document->getStructure()->toArray());
  374. if ($document instanceof WorkflowStageBehavior) {
  375. $documentAccessor = new DocumentAccessor($destDocument);
  376. $documentAccessor->set(WorkflowStageSubscriber::PUBLISHED_FIELD, null);
  377. }
  378. if ($document instanceof ExtensionBehavior) {
  379. $destDocument->setExtensionsData($document->getExtensionsData());
  380. }
  381. // TODO: This can be removed if RoutingAuto replaces the ResourceLocator code.
  382. if ($destDocument instanceof ResourceSegmentBehavior) {
  383. $resourceLocator = $resourceLocatorStrategy->generate(
  384. $destDocument->getTitle(),
  385. $parentUuid,
  386. $webspaceKey,
  387. $destLocale
  388. );
  389. $destDocument->setResourceSegment($resourceLocator);
  390. }
  391. $this->documentManager->persist($destDocument, $destLocale, ['omit_modified_domain_event' => true]);
  392. }
  393. $this->documentManager->flush();
  394. return $this->documentToStructure($document);
  395. }
  396. public function orderBefore($uuid, $beforeUuid, $userId, $webspaceKey, $locale)
  397. {
  398. $document = $this->documentManager->find($uuid, $locale);
  399. $this->documentManager->reorder($document, $beforeUuid);
  400. $this->documentManager->persist(
  401. $document,
  402. $locale,
  403. [
  404. 'user' => $userId,
  405. ]
  406. );
  407. return $this->documentToStructure($document);
  408. }
  409. /**
  410. * TODO: Move this logic to the DocumentManager
  411. * {@inheritdoc}
  412. */
  413. public function orderAt($uuid, $position, $userId, $webspaceKey, $locale)
  414. {
  415. $document = $this->documentManager->find($uuid, $locale);
  416. $parentDocument = $this->inspector->getParent($document);
  417. $siblingDocuments = $this->inspector->getChildren($parentDocument);
  418. $siblings = \array_values($siblingDocuments->toArray()); // get indexed array
  419. $countSiblings = \count($siblings);
  420. $currentPosition = \array_search($document, $siblings) + 1;
  421. if ($countSiblings < $position || $position <= 0) {
  422. throw new InvalidOrderPositionException(
  423. \sprintf(
  424. 'Cannot order node "%s" at out-of-range position "%s", must be >= 0 && < %d"',
  425. $this->inspector->getPath($document),
  426. $position,
  427. $countSiblings
  428. )
  429. );
  430. }
  431. if ($position === $countSiblings) {
  432. // move to the end
  433. $this->documentManager->reorder($document, null);
  434. } else {
  435. if ($currentPosition < $position) {
  436. $targetSibling = $siblings[$position];
  437. $this->documentManager->reorder($document, $targetSibling->getUuid());
  438. } elseif ($currentPosition > $position) {
  439. $targetSibling = $siblings[$position - 1];
  440. $this->documentManager->reorder($document, $targetSibling->getUuid());
  441. }
  442. }
  443. $this->documentManager->flush();
  444. return $this->documentToStructure($document);
  445. }
  446. /**
  447. * Return the resource locator content type.
  448. *
  449. * @return ResourceLocator
  450. */
  451. public function getResourceLocator()
  452. {
  453. return $this->contentTypeManager->get('resource_locator');
  454. }
  455. /**
  456. * Return the content document (aka the home page).
  457. *
  458. * @param string $webspaceKey
  459. *
  460. * @return BasePageDocument|SnippetDocument
  461. */
  462. private function getContentDocument($webspaceKey, $locale, array $options = [])
  463. {
  464. return $this->documentManager->find(
  465. $this->sessionManager->getContentPath($webspaceKey),
  466. $locale,
  467. $options
  468. );
  469. }
  470. /**
  471. * Return the node in the content repository which contains all of the routes.
  472. *
  473. * @param string $webspaceKey
  474. * @param string $locale
  475. * @param string $segment
  476. *
  477. * @return NodeInterface
  478. */
  479. protected function getRootRouteNode($webspaceKey, $locale, $segment)
  480. {
  481. return $this->documentManager->find(
  482. $this->sessionManager->getRoutePath($webspaceKey, $locale, $segment)
  483. );
  484. }
  485. public function convertQueryResultToArray(
  486. QueryResultInterface $queryResult,
  487. $webspaceKey,
  488. $locales,
  489. $fields,
  490. $maxDepth,
  491. $onlyPublished = true,
  492. $permission = null
  493. ) {
  494. $rootDepth = \substr_count($this->sessionManager->getContentPath($webspaceKey), '/');
  495. $result = [];
  496. foreach ($locales as $locale) {
  497. foreach ($queryResult->getRows() as $row) {
  498. $pageDepth = \substr_count($row->getPath('page'), '/') - $rootDepth;
  499. if (null === $maxDepth || $maxDepth < 0 || ($maxDepth > 0 && $pageDepth <= $maxDepth)) {
  500. $item = $this->rowToArray($row, $locale, $webspaceKey, $fields, $onlyPublished, $permission);
  501. if (false === $item || \in_array($item, $result)) {
  502. continue;
  503. }
  504. $result[] = $item;
  505. }
  506. }
  507. }
  508. return $result;
  509. }
  510. /**
  511. * converts a query row to an array.
  512. */
  513. private function rowToArray(
  514. Row $row,
  515. $locale,
  516. $webspaceKey,
  517. $fields,
  518. $onlyPublished = true,
  519. $permission = null
  520. ) {
  521. // reset cache
  522. $this->initializeExtensionCache();
  523. $templateName = $this->encoder->localizedSystemName('template', $locale);
  524. // check and determine shadow-nodes
  525. $node = $row->getNode('page');
  526. try {
  527. $document = $this->documentManager->find($node->getIdentifier(), $locale);
  528. } catch (StructureTypeNotFoundException $e) {
  529. return false;
  530. }
  531. $originalDocument = $document;
  532. if (!$node->hasProperty($templateName) && !$node->hasProperty('template')) {
  533. return false;
  534. }
  535. if ($document instanceof SecurityBehavior
  536. && $this->security
  537. && $permission
  538. ) {
  539. $permissionKey = \array_search($permission, $this->permissions);
  540. $documentWebspaceKey = $document->getWebspaceName();
  541. $documentWebspace = $this->webspaceManager->findWebspaceByKey($documentWebspaceKey);
  542. if ($documentWebspace && $documentWebspace->hasWebsiteSecurity()) {
  543. $documentWebspaceSecurity = $documentWebspace->getSecurity();
  544. $permissions = $this->accessControlManager->getUserPermissionByArray(
  545. $document->getLocale(),
  546. PageAdmin::SECURITY_CONTEXT_PREFIX . $documentWebspaceKey,
  547. $document->getPermissions(),
  548. $this->security->getUser(),
  549. $documentWebspaceSecurity->getSystem()
  550. );
  551. if (isset($permissions[$permissionKey]) && !$permissions[$permissionKey]) {
  552. return false;
  553. }
  554. }
  555. }
  556. $redirectType = null;
  557. $url = null;
  558. if ($document instanceof RedirectTypeBehavior) {
  559. $redirectType = $document->getRedirectType();
  560. if (RedirectType::INTERNAL === $redirectType) {
  561. $target = $document->getRedirectTarget();
  562. if (!$target instanceof ResourceSegmentBehavior) {
  563. return false;
  564. }
  565. $url = $target->getResourceSegment();
  566. $document = $target;
  567. $node = $this->inspector->getNode($document);
  568. } elseif (RedirectType::EXTERNAL === $redirectType) {
  569. $url = $document->getRedirectExternal();
  570. }
  571. }
  572. $originLocale = $locale;
  573. if ($document instanceof ShadowLocaleBehavior) {
  574. $locale = $document->isShadowLocaleEnabled() ? $document->getShadowLocale() : $originLocale;
  575. }
  576. $nodeState = null;
  577. if ($document instanceof WorkflowStageBehavior) {
  578. $nodeState = $document->getWorkflowStage();
  579. }
  580. // if page is not piblished ignore it
  581. if ($onlyPublished && WorkflowStage::PUBLISHED !== $nodeState) {
  582. return false;
  583. }
  584. if ($document instanceof ResourceSegmentBehavior && !isset($url)) {
  585. $url = $document->getResourceSegment();
  586. }
  587. // generate field data
  588. $fieldsData = $this->getFieldsData(
  589. $row,
  590. $node,
  591. $document,
  592. $fields[$originLocale],
  593. $document->getStructureType(),
  594. $webspaceKey,
  595. $locale
  596. );
  597. $structureType = $document->getStructureType();
  598. $shortPath = $this->inspector->getContentPath($originalDocument);
  599. $documentData = [
  600. 'id' => $originalDocument->getUuid(),
  601. 'uuid' => $originalDocument->getUuid(),
  602. 'nodeType' => $redirectType,
  603. 'path' => $shortPath,
  604. 'changed' => $document->getChanged(),
  605. 'changer' => $document->getChanger(),
  606. 'created' => $document->getCreated(),
  607. 'publishedState' => WorkflowStage::PUBLISHED === $nodeState,
  608. 'published' => $document->getPublished(),
  609. 'creator' => $document->getCreator(),
  610. 'title' => $originalDocument->getTitle(),
  611. 'locale' => $locale,
  612. 'webspaceKey' => $this->inspector->getWebspace($document),
  613. 'template' => $structureType,
  614. 'parent' => $this->inspector->getParent($document)->getUuid(),
  615. ];
  616. if (isset($url)) {
  617. $documentData['url'] = $url;
  618. $documentData['urls'] = $this->inspector->getLocalizedUrlsForPage($document);
  619. }
  620. if ($document instanceof LocalizedLastModifiedBehavior) {
  621. $documentData['lastModified'] = $document->getLastModified();
  622. }
  623. if ($document instanceof LocalizedAuthorBehavior) {
  624. $documentData['author'] = $document->getAuthor();
  625. $documentData['authored'] = $document->getAuthored();
  626. }
  627. if ($document instanceof OrderBehavior) {
  628. $documentData['order'] = $document->getSuluOrder();
  629. }
  630. return \array_merge($documentData, $fieldsData);
  631. }
  632. /**
  633. * Return extracted data (configured by fields array) from node.
  634. */
  635. private function getFieldsData(
  636. Row $row,
  637. NodeInterface $node,
  638. $document,
  639. $fields,
  640. $templateKey,
  641. $webspaceKey,
  642. $locale
  643. ) {
  644. $fieldsData = [];
  645. foreach ($fields as $field) {
  646. // determine target for data in result array
  647. if (isset($field['target'])) {
  648. if (!isset($fieldsData[$field['target']])) {
  649. $fieldsData[$field['target']] = [];
  650. }
  651. $target = &$fieldsData[$field['target']];
  652. } else {
  653. $target = &$fieldsData;
  654. }
  655. // create target
  656. if (!isset($target[$field['name']])) {
  657. $target[$field['name']] = '';
  658. }
  659. if (null !== ($data = $this->getFieldData(
  660. $field,
  661. $row,
  662. $node,
  663. $document,
  664. $templateKey,
  665. $webspaceKey,
  666. $locale
  667. ))
  668. ) {
  669. $target[$field['name']] = $data;
  670. }
  671. }
  672. return $fieldsData;
  673. }
  674. /**
  675. * Return data for one field.
  676. */
  677. private function getFieldData($field, Row $row, NodeInterface $node, $document, $templateKey, $webspaceKey, $locale)
  678. {
  679. if (isset($field['column'])) {
  680. // normal data from node property
  681. return $row->getValue($field['column']);
  682. } elseif (isset($field['extension'])) {
  683. // data from extension
  684. return $this->getExtensionData(
  685. $node,
  686. $field['extension'],
  687. $field['property'],
  688. $webspaceKey,
  689. $locale
  690. );
  691. } elseif (isset($field['property'])
  692. && (!isset($field['templateKey']) || $field['templateKey'] === $templateKey)
  693. ) {
  694. // not extension data but property of node
  695. return $this->getPropertyData($document, $field['property']);
  696. }
  697. return null;
  698. }
  699. /**
  700. * Returns data for property.
  701. */
  702. private function getPropertyData($document, LegacyProperty $property)
  703. {
  704. return $document->getStructure()->getContentViewProperty($property->getName())->getValue();
  705. }
  706. /**
  707. * Returns data for extension and property name.
  708. */
  709. private function getExtensionData(
  710. NodeInterface $node,
  711. ExtensionInterface $extension,
  712. $propertyName,
  713. $webspaceKey,
  714. $locale
  715. ) {
  716. // extension data: load ones
  717. if (!$this->extensionDataCache->contains($extension->getName())) {
  718. $this->extensionDataCache->save(
  719. $extension->getName(),
  720. $this->loadExtensionData(
  721. $node,
  722. $extension,
  723. $webspaceKey,
  724. $locale
  725. )
  726. );
  727. }
  728. // get extension data from cache
  729. $data = $this->extensionDataCache->fetch($extension->getName());
  730. return isset($data[$propertyName]) ? $data[$propertyName] : null;
  731. }
  732. /**
  733. * load data from extension.
  734. */
  735. private function loadExtensionData(NodeInterface $node, ExtensionInterface $extension, $webspaceKey, $locale)
  736. {
  737. $extension->setLanguageCode($locale, $this->namespaceRegistry->getPrefix('extension_localized'), '');
  738. $data = $extension->load(
  739. $node,
  740. $webspaceKey,
  741. $locale
  742. );
  743. return $extension->getContentData($data);
  744. }
  745. private function loadDocument($pathOrUuid, $locale, $options, $shouldExclude = true)
  746. {
  747. $document = $this->documentManager->find(
  748. $pathOrUuid,
  749. $locale,
  750. [
  751. 'load_ghost_content' => isset($options['load_ghost_content']) ? $options['load_ghost_content'] : true,
  752. ]
  753. );
  754. if ($shouldExclude && $this->optionsShouldExcludeDocument($document, $options)) {
  755. return;
  756. }
  757. return $document;
  758. }
  759. private function optionsShouldExcludeDocument($document, ?array $options = null)
  760. {
  761. if (null === $options) {
  762. return false;
  763. }
  764. $options = \array_merge(
  765. [
  766. 'exclude_ghost' => true,
  767. 'exclude_shadow' => true,
  768. ],
  769. $options
  770. );
  771. $state = $this->inspector->getLocalizationState($document);
  772. if ($options['exclude_ghost'] && LocalizationState::GHOST == $state) {
  773. return true;
  774. }
  775. if ($options['exclude_ghost'] && $options['exclude_shadow'] && LocalizationState::SHADOW == $state) {
  776. return true;
  777. }
  778. return false;
  779. }
  780. /**
  781. * Initializes cache of extension data.
  782. */
  783. private function initializeExtensionCache()
  784. {
  785. $this->extensionDataCache = DoctrineProvider::wrap(new ArrayAdapter());
  786. }
  787. /**
  788. * Return a structure bridge corresponding to the given document.
  789. *
  790. * @param StructureBehavior $document
  791. *
  792. * @return StructureInterface
  793. */
  794. private function documentToStructure($document)
  795. {
  796. if (null === $document) {
  797. return;
  798. }
  799. $structure = $this->inspector->getStructureMetadata($document);
  800. $documentAlias = $this->inspector->getMetadata($document)->getAlias();
  801. $structureBridge = $this->structureManager->wrapStructure($documentAlias, $structure);
  802. $structureBridge->setDocument($document);
  803. return $structureBridge;
  804. }
  805. /**
  806. * Return a collection of structures for the given documents, optionally filtering according
  807. * to the given options (as defined in optionsShouldExcludeDocument).
  808. *
  809. * @param object[] $documents
  810. * @param array|null $filterOptions
  811. */
  812. private function documentsToStructureCollection($documents, $filterOptions = null)
  813. {
  814. $collection = [];
  815. foreach ($documents as $document) {
  816. if (!$document instanceof StructureBehavior) {
  817. continue;
  818. }
  819. if ($this->optionsShouldExcludeDocument($document, $filterOptions)) {
  820. continue;
  821. }
  822. $collection[] = $this->documentToStructure($document);
  823. }
  824. return $collection;
  825. }
  826. }