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

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