vendor/sulu/sulu/src/Sulu/Component/Content/Repository/ContentRepository.php line 108

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\Repository;
  11. use Jackalope\Query\QOM\PropertyValue;
  12. use Jackalope\Query\Row;
  13. use PHPCR\ItemNotFoundException;
  14. use PHPCR\Query\QOM\QueryObjectModelConstantsInterface;
  15. use PHPCR\Query\QOM\QueryObjectModelFactoryInterface;
  16. use PHPCR\SessionInterface;
  17. use PHPCR\Util\PathHelper;
  18. use PHPCR\Util\QOM\QueryBuilder;
  19. use Sulu\Bundle\SecurityBundle\System\SystemStoreInterface;
  20. use Sulu\Component\Content\Compat\LocalizationFinderInterface;
  21. use Sulu\Component\Content\Compat\Structure;
  22. use Sulu\Component\Content\Compat\StructureManagerInterface;
  23. use Sulu\Component\Content\Compat\StructureType;
  24. use Sulu\Component\Content\Document\Behavior\SecurityBehavior;
  25. use Sulu\Component\Content\Document\RedirectType;
  26. use Sulu\Component\Content\Document\Subscriber\SecuritySubscriber;
  27. use Sulu\Component\Content\Document\WorkflowStage;
  28. use Sulu\Component\Content\Repository\Mapping\MappingInterface;
  29. use Sulu\Component\DocumentManager\PropertyEncoder;
  30. use Sulu\Component\Localization\Localization;
  31. use Sulu\Component\PHPCR\SessionManager\SessionManagerInterface;
  32. use Sulu\Component\Security\Authentication\UserInterface;
  33. use Sulu\Component\Security\Authorization\AccessControl\DescendantProviderInterface;
  34. use Sulu\Component\Util\SuluNodeHelper;
  35. use Sulu\Component\Webspace\Manager\WebspaceManagerInterface;
  36. /**
  37. * Content repository which query content with sql2 statements.
  38. */
  39. class ContentRepository implements ContentRepositoryInterface, DescendantProviderInterface
  40. {
  41. private static $nonFallbackProperties = [
  42. 'uuid',
  43. 'state',
  44. 'order',
  45. 'created',
  46. 'creator',
  47. 'changed',
  48. 'changer',
  49. 'published',
  50. 'shadowOn',
  51. 'shadowBase',
  52. ];
  53. /**
  54. * @var SessionManagerInterface
  55. */
  56. private $sessionManager;
  57. /**
  58. * @var PropertyEncoder
  59. */
  60. private $propertyEncoder;
  61. /**
  62. * @var WebspaceManagerInterface
  63. */
  64. private $webspaceManager;
  65. /**
  66. * @var SessionInterface
  67. */
  68. private $session;
  69. /**
  70. * @var QueryObjectModelFactoryInterface
  71. */
  72. private $qomFactory;
  73. /**
  74. * @var LocalizationFinderInterface
  75. */
  76. private $localizationFinder;
  77. /**
  78. * @var StructureManagerInterface
  79. */
  80. private $structureManager;
  81. /**
  82. * @var SuluNodeHelper
  83. */
  84. private $nodeHelper;
  85. /**
  86. * @var array
  87. */
  88. private $permissions;
  89. /**
  90. * @var SystemStoreInterface
  91. */
  92. private $systemStore;
  93. public function __construct(
  94. SessionManagerInterface $sessionManager,
  95. PropertyEncoder $propertyEncoder,
  96. WebspaceManagerInterface $webspaceManager,
  97. LocalizationFinderInterface $localizationFinder,
  98. StructureManagerInterface $structureManager,
  99. SuluNodeHelper $nodeHelper,
  100. SystemStoreInterface $systemStore,
  101. array $permissions
  102. ) {
  103. $this->sessionManager = $sessionManager;
  104. $this->propertyEncoder = $propertyEncoder;
  105. $this->webspaceManager = $webspaceManager;
  106. $this->localizationFinder = $localizationFinder;
  107. $this->structureManager = $structureManager;
  108. $this->nodeHelper = $nodeHelper;
  109. $this->systemStore = $systemStore;
  110. $this->permissions = $permissions;
  111. $this->session = $sessionManager->getSession();
  112. $this->qomFactory = $this->session->getWorkspace()->getQueryManager()->getQOMFactory();
  113. }
  114. /**
  115. * Find content by uuid.
  116. *
  117. * @param string $uuid
  118. * @param string $locale
  119. * @param string $webspaceKey
  120. * @param MappingInterface $mapping Includes array of property names
  121. *
  122. * @return Content|null
  123. */
  124. public function find($uuid, $locale, $webspaceKey, MappingInterface $mapping, ?UserInterface $user = null)
  125. {
  126. $locales = $this->getLocalesByWebspaceKey($webspaceKey);
  127. $queryBuilder = $this->getQueryBuilder($locale, $locales, $user);
  128. $queryBuilder->where(
  129. $this->qomFactory->comparison(
  130. new PropertyValue('node', 'jcr:uuid'),
  131. '=',
  132. $this->qomFactory->literal($uuid)
  133. )
  134. );
  135. $this->appendMapping($queryBuilder, $mapping, $locale, $locales);
  136. $queryResult = $queryBuilder->execute();
  137. $rows = \iterator_to_array($queryResult->getRows());
  138. if (1 !== \count($rows)) {
  139. throw new ItemNotFoundException();
  140. }
  141. $resultPermissions = $this->resolveResultPermissions($rows, $user);
  142. $permissions = empty($resultPermissions) ? [] : \current($resultPermissions);
  143. return $this->resolveContent(\current($rows), $locale, $locales, $mapping, $user, $permissions);
  144. }
  145. public function findByParentUuid(
  146. $uuid,
  147. $locale,
  148. $webspaceKey,
  149. MappingInterface $mapping,
  150. ?UserInterface $user = null
  151. ) {
  152. $path = $this->resolvePathByUuid($uuid);
  153. if (!$webspaceKey) {
  154. // TODO find a better solution than this (e.g. reuse logic from DocumentInspector and preferably in the PageController)
  155. $webspaceKey = \explode('/', $path)[2];
  156. }
  157. $locales = $this->getLocalesByWebspaceKey($webspaceKey);
  158. $queryBuilder = $this->getQueryBuilder($locale, $locales, $user);
  159. $queryBuilder->where($this->qomFactory->childNode('node', $path));
  160. $this->appendMapping($queryBuilder, $mapping, $locale, $locales);
  161. return $this->resolveQueryBuilder($queryBuilder, $locale, $locales, $mapping, $user);
  162. }
  163. public function findByWebspaceRoot($locale, $webspaceKey, MappingInterface $mapping, ?UserInterface $user = null)
  164. {
  165. $locales = $this->getLocalesByWebspaceKey($webspaceKey);
  166. $queryBuilder = $this->getQueryBuilder($locale, $locales, $user);
  167. $queryBuilder->where(
  168. $this->qomFactory->childNode('node', $this->sessionManager->getContentPath($webspaceKey))
  169. );
  170. $this->appendMapping($queryBuilder, $mapping, $locale, $locales);
  171. return $this->resolveQueryBuilder($queryBuilder, $locale, $locales, $mapping, $user);
  172. }
  173. public function findParentsWithSiblingsByUuid(
  174. $uuid,
  175. $locale,
  176. $webspaceKey,
  177. MappingInterface $mapping,
  178. ?UserInterface $user = null
  179. ) {
  180. $path = $this->resolvePathByUuid($uuid);
  181. if (empty($webspaceKey)) {
  182. $webspaceKey = $this->nodeHelper->extractWebspaceFromPath($path);
  183. }
  184. $contentPath = $this->sessionManager->getContentPath($webspaceKey);
  185. $locales = $this->getLocalesByWebspaceKey($webspaceKey);
  186. $queryBuilder = $this->getQueryBuilder($locale, $locales, $user)
  187. ->orderBy($this->qomFactory->propertyValue('node', 'jcr:path'))
  188. ->where($this->qomFactory->childNode('node', $path));
  189. while (PathHelper::getPathDepth($path) > PathHelper::getPathDepth($contentPath)) {
  190. $path = PathHelper::getParentPath($path);
  191. $queryBuilder->orWhere($this->qomFactory->childNode('node', $path));
  192. }
  193. $mapping->addProperties(['order']);
  194. $this->appendMapping($queryBuilder, $mapping, $locale, $locales);
  195. $result = $this->resolveQueryBuilder($queryBuilder, $locale, $locales, $mapping, $user);
  196. return $this->generateTreeByPath($result, $uuid);
  197. }
  198. public function findByPaths(
  199. array $paths,
  200. $locale,
  201. MappingInterface $mapping,
  202. ?UserInterface $user = null
  203. ) {
  204. $locales = $this->getLocales();
  205. $queryBuilder = $this->getQueryBuilder($locale, $locales, $user);
  206. foreach ($paths as $path) {
  207. $queryBuilder->orWhere(
  208. $this->qomFactory->sameNode('node', $path)
  209. );
  210. }
  211. $this->appendMapping($queryBuilder, $mapping, $locale, $locales);
  212. return $this->resolveQueryBuilder($queryBuilder, $locale, $locales, $mapping, $user);
  213. }
  214. public function findByUuids(
  215. array $uuids,
  216. $locale,
  217. MappingInterface $mapping,
  218. ?UserInterface $user = null
  219. ) {
  220. if (0 === \count($uuids)) {
  221. return [];
  222. }
  223. $locales = $this->getLocales();
  224. $queryBuilder = $this->getQueryBuilder($locale, $locales, $user);
  225. foreach ($uuids as $uuid) {
  226. $queryBuilder->orWhere(
  227. $this->qomFactory->comparison(
  228. $queryBuilder->qomf()->propertyValue('node', 'jcr:uuid'),
  229. QueryObjectModelConstantsInterface::JCR_OPERATOR_EQUAL_TO,
  230. $queryBuilder->qomf()->literal($uuid)
  231. )
  232. );
  233. }
  234. $this->appendMapping($queryBuilder, $mapping, $locale, $locales);
  235. $result = $this->resolveQueryBuilder($queryBuilder, $locale, $locales, $mapping, $user);
  236. \usort($result, function($a, $b) use ($uuids) {
  237. return \array_search($a->getId(), $uuids) < \array_search($b->getId(), $uuids) ? -1 : 1;
  238. });
  239. return $result;
  240. }
  241. public function findAll($locale, $webspaceKey, MappingInterface $mapping, ?UserInterface $user = null)
  242. {
  243. $contentPath = $this->sessionManager->getContentPath($webspaceKey);
  244. $locales = $this->getLocalesByWebspaceKey($webspaceKey);
  245. $queryBuilder = $this->getQueryBuilder($locale, $locales, $user)
  246. ->where($this->qomFactory->descendantNode('node', $contentPath))
  247. ->orWhere($this->qomFactory->sameNode('node', $contentPath));
  248. $this->appendMapping($queryBuilder, $mapping, $locale, $locales);
  249. return $this->resolveQueryBuilder($queryBuilder, $locale, $locales, $mapping, $user);
  250. }
  251. public function findAllByPortal($locale, $portalKey, MappingInterface $mapping, ?UserInterface $user = null)
  252. {
  253. $webspaceKey = $this->webspaceManager->findPortalByKey($portalKey)->getWebspace()->getKey();
  254. $contentPath = $this->sessionManager->getContentPath($webspaceKey);
  255. $locales = $this->getLocalesByPortalKey($portalKey);
  256. $queryBuilder = $this->getQueryBuilder($locale, $locales, $user)
  257. ->where($this->qomFactory->descendantNode('node', $contentPath))
  258. ->orWhere($this->qomFactory->sameNode('node', $contentPath));
  259. $this->appendMapping($queryBuilder, $mapping, $locale, $locales);
  260. return $this->resolveQueryBuilder($queryBuilder, $locale, $locales, $mapping, $user);
  261. }
  262. public function findDescendantIdsById($id)
  263. {
  264. $queryBuilder = $this->getQueryBuilder();
  265. $queryBuilder->where(
  266. $this->qomFactory->comparison(
  267. new PropertyValue('node', 'jcr:uuid'),
  268. '=',
  269. $this->qomFactory->literal($id)
  270. )
  271. );
  272. $result = \iterator_to_array($queryBuilder->execute());
  273. if (0 === \count($result)) {
  274. return [];
  275. }
  276. $path = $result[0]->getPath();
  277. $descendantQueryBuilder = $this->getQueryBuilder()
  278. ->where($this->qomFactory->descendantNode('node', $path));
  279. return \array_map(
  280. function(Row $row) {
  281. return $row->getNode()->getIdentifier();
  282. },
  283. \iterator_to_array($descendantQueryBuilder->execute())
  284. );
  285. }
  286. /**
  287. * Generates a content-tree with paths of given content array.
  288. *
  289. * @param Content[] $contents
  290. *
  291. * @return Content[]
  292. */
  293. private function generateTreeByPath(array $contents, $uuid)
  294. {
  295. $childrenByPath = [];
  296. foreach ($contents as $content) {
  297. $path = PathHelper::getParentPath($content->getPath());
  298. if (!isset($childrenByPath[$path])) {
  299. $childrenByPath[$path] = [];
  300. }
  301. $order = $content['order'];
  302. while (isset($childrenByPath[$path][$order])) {
  303. ++$order;
  304. }
  305. $childrenByPath[$path][$order] = $content;
  306. }
  307. foreach ($contents as $content) {
  308. if (!isset($childrenByPath[$content->getPath()])) {
  309. if ($content->getId() === $uuid) {
  310. $content->setChildren([]);
  311. }
  312. continue;
  313. }
  314. \ksort($childrenByPath[$content->getPath()]);
  315. $content->setChildren(\array_values($childrenByPath[$content->getPath()]));
  316. }
  317. if (!\array_key_exists('/', $childrenByPath) || !\is_array($childrenByPath['/'])) {
  318. return [];
  319. }
  320. \ksort($childrenByPath['/']);
  321. return \array_values($childrenByPath['/']);
  322. }
  323. /**
  324. * Resolve path for node with given uuid.
  325. *
  326. * @param string $uuid
  327. *
  328. * @return string
  329. *
  330. * @throws ItemNotFoundException
  331. */
  332. private function resolvePathByUuid($uuid)
  333. {
  334. $queryBuilder = new QueryBuilder($this->qomFactory);
  335. $queryBuilder
  336. ->select('node', 'jcr:uuid', 'uuid')
  337. ->from($this->qomFactory->selector('node', 'nt:unstructured'))
  338. ->where(
  339. $this->qomFactory->comparison(
  340. $this->qomFactory->propertyValue('node', 'jcr:uuid'),
  341. '=',
  342. $this->qomFactory->literal($uuid)
  343. )
  344. );
  345. $rows = $queryBuilder->execute();
  346. if (1 !== \count(\iterator_to_array($rows->getRows()))) {
  347. throw new ItemNotFoundException();
  348. }
  349. return $rows->getRows()->current()->getPath();
  350. }
  351. /**
  352. * Resolves query results to content.
  353. *
  354. * @param string $locale
  355. *
  356. * @return Content[]
  357. */
  358. private function resolveQueryBuilder(
  359. QueryBuilder $queryBuilder,
  360. $locale,
  361. $locales,
  362. MappingInterface $mapping,
  363. ?UserInterface $user = null
  364. ) {
  365. $result = \iterator_to_array($queryBuilder->execute());
  366. $permissions = $this->resolveResultPermissions($result, $user);
  367. return \array_values(
  368. \array_filter(
  369. \array_map(
  370. function(Row $row, $index) use ($mapping, $locale, $locales, $user, $permissions) {
  371. return $this->resolveContent(
  372. $row,
  373. $locale,
  374. $locales,
  375. $mapping,
  376. $user,
  377. $permissions[$index] ?? []
  378. );
  379. },
  380. $result,
  381. \array_keys($result)
  382. )
  383. )
  384. );
  385. }
  386. private function resolveResultPermissions(array $result, ?UserInterface $user = null)
  387. {
  388. $permissions = [];
  389. foreach ($result as $index => $row) {
  390. $permissions[$index] = [];
  391. $jsonPermission = $row->getValue(SecuritySubscriber::SECURITY_PERMISSION_PROPERTY);
  392. if (!$jsonPermission) {
  393. continue;
  394. }
  395. $rowPermissions = \json_decode($jsonPermission, true);
  396. foreach ($rowPermissions as $roleId => $rolePermissions) {
  397. foreach ($this->permissions as $permissionKey => $permission) {
  398. $permissions[$index][$roleId][$permissionKey] = false;
  399. }
  400. foreach ($rolePermissions as $rolePermission) {
  401. $permissions[$index][$roleId][$rolePermission] = true;
  402. }
  403. }
  404. }
  405. return $permissions;
  406. }
  407. /**
  408. * Returns QueryBuilder with basic select and where statements.
  409. *
  410. * @param string $locale
  411. * @param string[] $locales
  412. *
  413. * @return QueryBuilder
  414. */
  415. private function getQueryBuilder($locale = null, $locales = [], ?UserInterface $user = null)
  416. {
  417. $queryBuilder = new QueryBuilder($this->qomFactory);
  418. $queryBuilder
  419. ->select('node', 'jcr:uuid', 'uuid')
  420. ->addSelect('node', $this->getPropertyName('nodeType', $locale), 'nodeType')
  421. ->addSelect('node', $this->getPropertyName('internal_link', $locale), 'internalLink')
  422. ->addSelect('node', $this->getPropertyName('state', $locale), 'state')
  423. ->addSelect('node', $this->getPropertyName('shadow-on', $locale), 'shadowOn')
  424. ->addSelect('node', $this->getPropertyName('shadow-base', $locale), 'shadowBase')
  425. ->addSelect('node', $this->propertyEncoder->systemName('order'), 'order')
  426. ->from($this->qomFactory->selector('node', 'nt:unstructured'))
  427. ->orderBy($this->qomFactory->propertyValue('node', 'sulu:order'));
  428. $this->appendSingleMapping($queryBuilder, 'template', $locales);
  429. $this->appendSingleMapping($queryBuilder, 'shadow-on', $locales);
  430. $this->appendSingleMapping($queryBuilder, 'state', $locales);
  431. $queryBuilder->addSelect(
  432. 'node',
  433. SecuritySubscriber::SECURITY_PERMISSION_PROPERTY,
  434. SecuritySubscriber::SECURITY_PERMISSION_PROPERTY
  435. );
  436. return $queryBuilder;
  437. }
  438. private function getPropertyName($propertyName, $locale)
  439. {
  440. if ($locale) {
  441. return $this->propertyEncoder->localizedContentName($propertyName, $locale);
  442. }
  443. return $this->propertyEncoder->contentName($propertyName);
  444. }
  445. /**
  446. * Returns array of locales for given webspace key.
  447. *
  448. * @param string $webspaceKey
  449. *
  450. * @return string[]
  451. */
  452. private function getLocalesByWebspaceKey($webspaceKey)
  453. {
  454. $webspace = $this->webspaceManager->findWebspaceByKey($webspaceKey);
  455. return \array_map(
  456. function(Localization $localization) {
  457. return $localization->getLocale();
  458. },
  459. $webspace->getAllLocalizations()
  460. );
  461. }
  462. /**
  463. * Returns array of locales for given portal key.
  464. *
  465. * @param string $portalKey
  466. *
  467. * @return string[]
  468. */
  469. private function getLocalesByPortalKey($portalKey)
  470. {
  471. $portal = $this->webspaceManager->findPortalByKey($portalKey);
  472. return \array_map(
  473. function(Localization $localization) {
  474. return $localization->getLocale();
  475. },
  476. $portal->getLocalizations()
  477. );
  478. }
  479. /**
  480. * Returns array of locales for webspaces.
  481. *
  482. * @return string[]
  483. */
  484. private function getLocales()
  485. {
  486. return $this->webspaceManager->getAllLocales();
  487. }
  488. /**
  489. * Append mapping selects to given query-builder.
  490. *
  491. * @param MappingInterface $mapping Includes array of property names
  492. * @param string $locale
  493. * @param string[] $locales
  494. */
  495. private function appendMapping(QueryBuilder $queryBuilder, MappingInterface $mapping, $locale, $locales)
  496. {
  497. if ($mapping->onlyPublished()) {
  498. $queryBuilder->andWhere(
  499. $this->qomFactory->comparison(
  500. $this->qomFactory->propertyValue(
  501. 'node',
  502. $this->propertyEncoder->localizedSystemName('state', $locale)
  503. ),
  504. '=',
  505. $this->qomFactory->literal(WorkflowStage::PUBLISHED)
  506. )
  507. );
  508. }
  509. $properties = $mapping->getProperties();
  510. foreach ($properties as $propertyName) {
  511. $this->appendSingleMapping($queryBuilder, $propertyName, $locales);
  512. }
  513. if ($mapping->resolveUrl()) {
  514. $this->appendUrlMapping($queryBuilder, $locales);
  515. }
  516. }
  517. /**
  518. * Append mapping selects for a single property to given query-builder.
  519. *
  520. * @param string $propertyName
  521. * @param string[] $locales
  522. */
  523. private function appendSingleMapping(QueryBuilder $queryBuilder, $propertyName, $locales)
  524. {
  525. foreach ($locales as $locale) {
  526. $alias = \sprintf('%s%s', $locale, \str_replace('-', '_', \ucfirst($propertyName)));
  527. $queryBuilder->addSelect(
  528. 'node',
  529. $this->propertyEncoder->localizedContentName($propertyName, $locale),
  530. $alias
  531. );
  532. }
  533. }
  534. /**
  535. * Append mapping for url to given query-builder.
  536. *
  537. * @param string[] $locales
  538. */
  539. private function appendUrlMapping(QueryBuilder $queryBuilder, $locales)
  540. {
  541. $structures = $this->structureManager->getStructures(Structure::TYPE_PAGE);
  542. $urlNames = [];
  543. foreach ($structures as $structure) {
  544. if (!$structure->hasTag('sulu.rlp')) {
  545. continue;
  546. }
  547. $propertyName = $structure->getPropertyByTagName('sulu.rlp')->getName();
  548. if (!\in_array($propertyName, $urlNames)) {
  549. $this->appendSingleMapping($queryBuilder, $propertyName, $locales);
  550. $urlNames[] = $propertyName;
  551. }
  552. }
  553. }
  554. /**
  555. * Resolve a single result row to a content object.
  556. *
  557. * @param string $locale
  558. * @param string $locales
  559. *
  560. * @return Content|null
  561. */
  562. private function resolveContent(
  563. Row $row,
  564. $locale,
  565. $locales,
  566. MappingInterface $mapping,
  567. ?UserInterface $user = null,
  568. array $permissions = []
  569. ) {
  570. $webspaceKey = $this->nodeHelper->extractWebspaceFromPath($row->getPath());
  571. $originalLocale = $locale;
  572. $availableLocales = $this->resolveAvailableLocales($row);
  573. $ghostLocale = $this->localizationFinder->findAvailableLocale(
  574. $webspaceKey,
  575. $availableLocales,
  576. $locale
  577. );
  578. if (null === $ghostLocale) {
  579. $ghostLocale = \reset($availableLocales);
  580. }
  581. $type = null;
  582. if ($row->getValue('shadowOn')) {
  583. if (!$mapping->shouldHydrateShadow()) {
  584. return null;
  585. }
  586. $type = StructureType::getShadow($row->getValue('shadowBase'));
  587. } elseif (null !== $ghostLocale && $ghostLocale !== $originalLocale) {
  588. if (!$mapping->shouldHydrateGhost()) {
  589. return null;
  590. }
  591. $locale = $ghostLocale;
  592. $type = StructureType::getGhost($locale);
  593. }
  594. if (
  595. RedirectType::INTERNAL === $row->getValue('nodeType')
  596. && $mapping->followInternalLink()
  597. && '' !== $row->getValue('internalLink')
  598. && $row->getValue('internalLink') !== $row->getValue('uuid')
  599. ) {
  600. // TODO collect all internal link contents and query once
  601. return $this->resolveInternalLinkContent($row, $locale, $webspaceKey, $mapping, $type, $user);
  602. }
  603. $shadowBase = null;
  604. if ($row->getValue('shadowOn')) {
  605. $shadowBase = $row->getValue('shadowBase');
  606. }
  607. $data = [];
  608. foreach ($mapping->getProperties() as $item) {
  609. $data[$item] = $this->resolveProperty($row, $item, $locale, $shadowBase);
  610. }
  611. $content = new Content(
  612. $originalLocale,
  613. $webspaceKey,
  614. $row->getValue('uuid'),
  615. $this->resolvePath($row, $webspaceKey),
  616. $row->getValue('state'),
  617. $row->getValue('nodeType'),
  618. $this->resolveHasChildren($row), $this->resolveProperty($row, 'template', $locale, $shadowBase),
  619. $data,
  620. $permissions,
  621. $type
  622. );
  623. $content->setRow($row);
  624. if (!$content->getTemplate() || !$this->structureManager->getStructure($content->getTemplate())) {
  625. $content->setBrokenTemplate();
  626. }
  627. if ($mapping->resolveUrl()) {
  628. $url = $this->resolveUrl($row, $locale);
  629. /** @var array<string, string|null> $urls */
  630. $urls = [];
  631. \array_walk(
  632. $locales,
  633. /** @var array<string, string|null> $urls */
  634. function($locale) use (&$urls, $row) {
  635. $urls[$locale] = $this->resolveUrl($row, $locale);
  636. }
  637. );
  638. $content->setUrl($url);
  639. $content->setUrls($urls);
  640. }
  641. if ($mapping->resolveConcreteLocales()) {
  642. $locales = $this->resolveAvailableLocales($row);
  643. $content->setContentLocales($locales);
  644. }
  645. return $content;
  646. }
  647. /**
  648. * Resolves all available localizations for given row.
  649. *
  650. * @return string[]
  651. */
  652. private function resolveAvailableLocales(Row $row)
  653. {
  654. $locales = [];
  655. foreach ($row->getValues() as $key => $value) {
  656. if (\preg_match('/^node.([a-zA-Z_]*?)Template/', $key, $matches) && '' !== $value
  657. && !$row->getValue(\sprintf('node.%sShadow_on', $matches[1]))
  658. ) {
  659. $locales[] = $matches[1];
  660. }
  661. }
  662. return $locales;
  663. }
  664. /**
  665. * Resolve a single result row which is an internal link to a content object.
  666. *
  667. * @param string $locale
  668. * @param string $webspaceKey
  669. * @param MappingInterface $mapping Includes array of property names
  670. *
  671. * @return Content|null
  672. */
  673. public function resolveInternalLinkContent(
  674. Row $row,
  675. $locale,
  676. $webspaceKey,
  677. MappingInterface $mapping,
  678. ?StructureType $type = null,
  679. ?UserInterface $user = null
  680. ) {
  681. $linkedContent = $this->find($row->getValue('internalLink'), $locale, $webspaceKey, $mapping);
  682. if (null === $linkedContent) {
  683. return null;
  684. }
  685. $data = $linkedContent->getData();
  686. // return value of source node instead of link destination for title and non-fallback-properties
  687. $sourceNodeValueProperties = self::$nonFallbackProperties;
  688. $sourceNodeValueProperties[] = 'title';
  689. $properties = \array_intersect($sourceNodeValueProperties, \array_keys($data));
  690. foreach ($properties as $property) {
  691. $data[$property] = $this->resolveProperty($row, $property, $locale);
  692. }
  693. $resultPermissions = $this->resolveResultPermissions([$row], $user);
  694. $permissions = empty($resultPermissions) ? [] : \current($resultPermissions);
  695. $content = new Content(
  696. $locale,
  697. $webspaceKey,
  698. $row->getValue('uuid'),
  699. $this->resolvePath($row, $webspaceKey),
  700. $row->getValue('state'),
  701. $row->getValue('nodeType'),
  702. $this->resolveHasChildren($row), $this->resolveProperty($row, 'template', $locale),
  703. $data,
  704. $permissions,
  705. $type
  706. );
  707. if ($mapping->resolveUrl()) {
  708. $content->setUrl($linkedContent->getUrl());
  709. $content->setUrls($linkedContent->getUrls());
  710. }
  711. if (!$content->getTemplate() || !$this->structureManager->getStructure($content->getTemplate())) {
  712. $content->setBrokenTemplate();
  713. }
  714. return $content;
  715. }
  716. /**
  717. * Resolve a property and follow shadow locale if it has one.
  718. *
  719. * @param string $name
  720. * @param string $locale
  721. * @param string $shadowLocale
  722. */
  723. private function resolveProperty(Row $row, $name, $locale, $shadowLocale = null)
  724. {
  725. if (\array_key_exists(\sprintf('node.%s', $name), $row->getValues())) {
  726. return $row->getValue($name);
  727. }
  728. if (null !== $shadowLocale && !\in_array($name, self::$nonFallbackProperties)) {
  729. $locale = $shadowLocale;
  730. }
  731. $name = \sprintf('%s%s', $locale, \str_replace('-', '_', \ucfirst($name)));
  732. try {
  733. return $row->getValue($name);
  734. } catch (ItemNotFoundException $e) {
  735. // the default value of a non existing property in jackalope is an empty string
  736. return '';
  737. }
  738. }
  739. /**
  740. * Resolve url property.
  741. *
  742. * @param string $locale
  743. *
  744. * @return string|null
  745. */
  746. private function resolveUrl(Row $row, $locale)
  747. {
  748. if (WorkflowStage::PUBLISHED !== $this->resolveProperty($row, $locale . 'State', $locale)) {
  749. return null;
  750. }
  751. $template = $this->resolveProperty($row, 'template', $locale);
  752. if (empty($template)) {
  753. return null;
  754. }
  755. $structure = $this->structureManager->getStructure($template);
  756. if (!$structure || !$structure->hasTag('sulu.rlp')) {
  757. return null;
  758. }
  759. $propertyName = $structure->getPropertyByTagName('sulu.rlp')->getName();
  760. return $this->resolveProperty($row, $propertyName, $locale);
  761. }
  762. /**
  763. * Resolves path for given row.
  764. *
  765. * @param string $webspaceKey
  766. *
  767. * @return string
  768. */
  769. private function resolvePath(Row $row, $webspaceKey)
  770. {
  771. return '/' . \ltrim(\str_replace($this->sessionManager->getContentPath($webspaceKey), '', $row->getPath()), '/');
  772. }
  773. /**
  774. * Resolve property has-children with given node.
  775. *
  776. * @return bool
  777. */
  778. private function resolveHasChildren(Row $row)
  779. {
  780. $queryBuilder = new QueryBuilder($this->qomFactory);
  781. $queryBuilder
  782. ->select('node', 'jcr:uuid', 'uuid')
  783. ->from($this->qomFactory->selector('node', 'nt:unstructured'))
  784. ->where($this->qomFactory->childNode('node', $row->getPath()))
  785. ->setMaxResults(1);
  786. $result = $queryBuilder->execute();
  787. return \count(\iterator_to_array($result->getRows())) > 0;
  788. }
  789. public function supportsDescendantType(string $type): bool
  790. {
  791. try {
  792. $class = new \ReflectionClass($type);
  793. } catch (\ReflectionException $e) {
  794. // in case the class does not exist there is no support
  795. return false;
  796. }
  797. return $class->implementsInterface(SecurityBehavior::class);
  798. }
  799. }