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

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. * @param string[] $locales
  317. *
  318. * @return Content[]
  319. */
  320. private function resolveQueryBuilder(
  321. QueryBuilder $queryBuilder,
  322. $locale,
  323. $locales,
  324. MappingInterface $mapping,
  325. ?UserInterface $user = null
  326. ) {
  327. $result = \iterator_to_array($queryBuilder->execute());
  328. $permissions = $this->resolveResultPermissions($result, $user);
  329. return \array_values(
  330. \array_filter(
  331. \array_map(
  332. function(RowInterface $row, $index) use ($mapping, $locale, $locales, $user, $permissions) {
  333. return $this->resolveContent(
  334. $row,
  335. $locale,
  336. $locales,
  337. $mapping,
  338. $user,
  339. $permissions[$index] ?? []
  340. );
  341. },
  342. $result,
  343. \array_keys($result)
  344. )
  345. )
  346. );
  347. }
  348. private function resolveResultPermissions(array $result, ?UserInterface $user = null)
  349. {
  350. $permissions = [];
  351. foreach ($result as $index => $row) {
  352. $permissions[$index] = [];
  353. $jsonPermission = $row->getValue(SecuritySubscriber::SECURITY_PERMISSION_PROPERTY);
  354. if (!$jsonPermission) {
  355. continue;
  356. }
  357. $rowPermissions = \json_decode($jsonPermission, true);
  358. foreach ($rowPermissions as $roleId => $rolePermissions) {
  359. foreach ($this->permissions as $permissionKey => $permission) {
  360. $permissions[$index][$roleId][$permissionKey] = false;
  361. }
  362. foreach ($rolePermissions as $rolePermission) {
  363. $permissions[$index][$roleId][$rolePermission] = true;
  364. }
  365. }
  366. }
  367. return $permissions;
  368. }
  369. /**
  370. * Returns QueryBuilder with basic select and where statements.
  371. *
  372. * @param string $locale
  373. * @param string[] $locales
  374. *
  375. * @return QueryBuilder
  376. */
  377. private function getQueryBuilder($locale = null, $locales = [], ?UserInterface $user = null)
  378. {
  379. $queryBuilder = new QueryBuilder($this->qomFactory);
  380. $queryBuilder
  381. ->select('node', 'jcr:uuid', 'uuid')
  382. ->addSelect('node', $this->getPropertyName('nodeType', $locale), 'nodeType')
  383. ->addSelect('node', $this->getPropertyName('internal_link', $locale), 'internalLink')
  384. ->addSelect('node', $this->getPropertyName('state', $locale), 'state')
  385. ->addSelect('node', $this->getPropertyName('shadow-on', $locale), 'shadowOn')
  386. ->addSelect('node', $this->getPropertyName('shadow-base', $locale), 'shadowBase')
  387. ->addSelect('node', $this->propertyEncoder->systemName('order'), 'order')
  388. ->from($this->qomFactory->selector('node', 'nt:unstructured'))
  389. ->orderBy($this->qomFactory->propertyValue('node', 'sulu:order'));
  390. $this->appendSingleMapping($queryBuilder, 'template', $locales);
  391. $this->appendSingleMapping($queryBuilder, 'shadow-on', $locales);
  392. $this->appendSingleMapping($queryBuilder, 'state', $locales);
  393. $queryBuilder->addSelect(
  394. 'node',
  395. SecuritySubscriber::SECURITY_PERMISSION_PROPERTY,
  396. SecuritySubscriber::SECURITY_PERMISSION_PROPERTY
  397. );
  398. return $queryBuilder;
  399. }
  400. private function getPropertyName(string $propertyName, $locale): string
  401. {
  402. if ($locale) {
  403. return $this->propertyEncoder->localizedContentName($propertyName, $locale);
  404. }
  405. return $this->propertyEncoder->contentName($propertyName);
  406. }
  407. /**
  408. * Returns array of locales for given webspace key.
  409. *
  410. * @param string $webspaceKey
  411. *
  412. * @return string[]
  413. */
  414. private function getLocalesByWebspaceKey($webspaceKey)
  415. {
  416. $webspace = $this->webspaceManager->findWebspaceByKey($webspaceKey);
  417. return \array_map(
  418. function(Localization $localization) {
  419. return $localization->getLocale();
  420. },
  421. $webspace->getAllLocalizations()
  422. );
  423. }
  424. /**
  425. * Returns array of locales for given portal key.
  426. *
  427. * @param string $portalKey
  428. *
  429. * @return string[]
  430. */
  431. private function getLocalesByPortalKey($portalKey)
  432. {
  433. $portal = $this->webspaceManager->findPortalByKey($portalKey);
  434. return \array_map(
  435. function(Localization $localization) {
  436. return $localization->getLocale();
  437. },
  438. $portal->getLocalizations()
  439. );
  440. }
  441. /**
  442. * Returns array of locales for webspaces.
  443. *
  444. * @return string[]
  445. */
  446. private function getLocales()
  447. {
  448. return $this->webspaceManager->getAllLocales();
  449. }
  450. /**
  451. * Append mapping selects to given query-builder.
  452. *
  453. * @param MappingInterface $mapping Includes array of property names
  454. * @param string $locale
  455. * @param string[] $locales
  456. */
  457. private function appendMapping(QueryBuilder $queryBuilder, MappingInterface $mapping, $locale, $locales)
  458. {
  459. if ($mapping->onlyPublished()) {
  460. $queryBuilder->andWhere(
  461. $this->qomFactory->comparison(
  462. $this->qomFactory->propertyValue(
  463. 'node',
  464. $this->propertyEncoder->localizedSystemName('state', $locale)
  465. ),
  466. '=',
  467. $this->qomFactory->literal(WorkflowStage::PUBLISHED)
  468. )
  469. );
  470. }
  471. $properties = $mapping->getProperties();
  472. foreach ($properties as $propertyName) {
  473. $this->appendSingleMapping($queryBuilder, $propertyName, $locales);
  474. }
  475. if ($mapping->resolveUrl()) {
  476. $this->appendUrlMapping($queryBuilder, $locales);
  477. }
  478. }
  479. /**
  480. * Append mapping selects for a single property to given query-builder.
  481. *
  482. * @param string $propertyName
  483. * @param string[] $locales
  484. */
  485. private function appendSingleMapping(QueryBuilder $queryBuilder, $propertyName, $locales)
  486. {
  487. foreach ($locales as $locale) {
  488. $alias = \sprintf('%s%s', $locale, \str_replace('-', '_', \ucfirst($propertyName)));
  489. $queryBuilder->addSelect(
  490. 'node',
  491. $this->propertyEncoder->localizedContentName($propertyName, $locale),
  492. $alias
  493. );
  494. }
  495. }
  496. /**
  497. * Append mapping for url to given query-builder.
  498. *
  499. * @param string[] $locales
  500. */
  501. private function appendUrlMapping(QueryBuilder $queryBuilder, $locales)
  502. {
  503. $structures = $this->structureManager->getStructures(Structure::TYPE_PAGE);
  504. $urlNames = [];
  505. foreach ($structures as $structure) {
  506. if (!$structure->hasTag('sulu.rlp')) {
  507. continue;
  508. }
  509. $propertyName = $structure->getPropertyByTagName('sulu.rlp')->getName();
  510. if (!\in_array($propertyName, $urlNames)) {
  511. $this->appendSingleMapping($queryBuilder, $propertyName, $locales);
  512. $urlNames[] = $propertyName;
  513. }
  514. }
  515. }
  516. /**
  517. * Resolve a single result row to a content object.
  518. *
  519. * @param string $locale
  520. * @param string[] $locales
  521. *
  522. * @return Content|null
  523. */
  524. private function resolveContent(
  525. RowInterface $row,
  526. $locale,
  527. $locales,
  528. MappingInterface $mapping,
  529. ?UserInterface $user = null,
  530. array $permissions = []
  531. ) {
  532. $webspaceKey = $this->nodeHelper->extractWebspaceFromPath($row->getPath());
  533. $originalLocale = $locale;
  534. $availableLocales = $this->resolveAvailableLocales($row);
  535. $ghostLocale = $this->localizationFinder->findAvailableLocale(
  536. $webspaceKey,
  537. $availableLocales,
  538. $locale
  539. );
  540. if (null === $ghostLocale) {
  541. $ghostLocale = \reset($availableLocales);
  542. }
  543. $type = null;
  544. if ($row->getValue('shadowOn')) {
  545. if (!$mapping->shouldHydrateShadow()) {
  546. return null;
  547. }
  548. $type = StructureType::getShadow($row->getValue('shadowBase'));
  549. } elseif (null !== $ghostLocale && $ghostLocale !== $originalLocale) {
  550. if (!$mapping->shouldHydrateGhost()) {
  551. return null;
  552. }
  553. $locale = $ghostLocale;
  554. $type = StructureType::getGhost($locale);
  555. }
  556. if (
  557. RedirectType::INTERNAL === $row->getValue('nodeType')
  558. && $mapping->followInternalLink()
  559. && '' !== $row->getValue('internalLink')
  560. && $row->getValue('internalLink') !== $row->getValue('uuid')
  561. ) {
  562. // TODO collect all internal link contents and query once
  563. return $this->resolveInternalLinkContent($row, $locale, $webspaceKey, $mapping, $type, $user);
  564. }
  565. $shadowBase = null;
  566. if ($row->getValue('shadowOn')) {
  567. $shadowBase = $row->getValue('shadowBase');
  568. }
  569. $data = [];
  570. foreach ($mapping->getProperties() as $item) {
  571. $data[$item] = $this->resolveProperty($row, $item, $locale, $shadowBase);
  572. }
  573. $content = new Content(
  574. $originalLocale,
  575. $webspaceKey,
  576. $row->getValue('uuid'),
  577. $this->resolvePath($row, $webspaceKey),
  578. $row->getValue('state'),
  579. $row->getValue('nodeType'),
  580. $this->resolveHasChildren($row), $this->resolveProperty($row, 'template', $locale, $shadowBase),
  581. $data,
  582. $permissions,
  583. $type
  584. );
  585. $content->setRow($row);
  586. if (!$content->getTemplate() || !$this->structureManager->getStructure($content->getTemplate())) {
  587. $content->setBrokenTemplate();
  588. }
  589. if ($mapping->resolveUrl()) {
  590. $url = $this->resolveUrl($row, $locale);
  591. /** @var array<string, string|null> $urls */
  592. $urls = [];
  593. \array_walk(
  594. $locales,
  595. /** @var array<string, string|null> $urls */
  596. function($locale) use (&$urls, $row) {
  597. $urls[$locale] = $this->resolveUrl($row, $locale);
  598. }
  599. );
  600. $content->setUrl($url);
  601. $content->setUrls($urls);
  602. }
  603. if ($mapping->resolveConcreteLocales()) {
  604. $locales = $this->resolveAvailableLocales($row);
  605. $content->setContentLocales($locales);
  606. }
  607. return $content;
  608. }
  609. /**
  610. * Resolves all available localizations for given row.
  611. *
  612. * @return string[]
  613. */
  614. private function resolveAvailableLocales(RowInterface $row)
  615. {
  616. $locales = [];
  617. foreach ($row->getValues() as $key => $value) {
  618. if (\preg_match('/^node.([a-zA-Z_]*?)Template/', $key, $matches) && '' !== $value
  619. && !$row->getValue(\sprintf('node.%sShadow_on', $matches[1]))
  620. ) {
  621. $locales[] = $matches[1];
  622. }
  623. }
  624. return $locales;
  625. }
  626. /**
  627. * Resolve a single result row which is an internal link to a content object.
  628. *
  629. * @param string $locale
  630. * @param string $webspaceKey
  631. * @param MappingInterface $mapping Includes array of property names
  632. *
  633. * @return Content|null
  634. */
  635. public function resolveInternalLinkContent(
  636. RowInterface $row,
  637. $locale,
  638. $webspaceKey,
  639. MappingInterface $mapping,
  640. ?StructureType $type = null,
  641. ?UserInterface $user = null
  642. ) {
  643. $linkedContent = $this->find($row->getValue('internalLink'), $locale, $webspaceKey, $mapping);
  644. if (null === $linkedContent) {
  645. return null;
  646. }
  647. $data = $linkedContent->getData();
  648. // return value of source node instead of link destination for title and non-fallback-properties
  649. $sourceNodeValueProperties = self::$nonFallbackProperties;
  650. $sourceNodeValueProperties[] = 'title';
  651. $properties = \array_intersect($sourceNodeValueProperties, \array_keys($data));
  652. foreach ($properties as $property) {
  653. $data[$property] = $this->resolveProperty($row, $property, $locale);
  654. }
  655. $resultPermissions = $this->resolveResultPermissions([$row], $user);
  656. $permissions = empty($resultPermissions) ? [] : \current($resultPermissions);
  657. $content = new Content(
  658. $locale,
  659. $webspaceKey,
  660. $row->getValue('uuid'),
  661. $this->resolvePath($row, $webspaceKey),
  662. $row->getValue('state'),
  663. $row->getValue('nodeType'),
  664. $this->resolveHasChildren($row), $this->resolveProperty($row, 'template', $locale),
  665. $data,
  666. $permissions,
  667. $type
  668. );
  669. if ($mapping->resolveUrl()) {
  670. $content->setUrl($linkedContent->getUrl());
  671. $content->setUrls($linkedContent->getUrls());
  672. }
  673. if (!$content->getTemplate() || !$this->structureManager->getStructure($content->getTemplate())) {
  674. $content->setBrokenTemplate();
  675. }
  676. return $content;
  677. }
  678. /**
  679. * Resolve a property and follow shadow locale if it has one.
  680. *
  681. * @param string $name
  682. * @param string $locale
  683. * @param string $shadowLocale
  684. */
  685. private function resolveProperty(RowInterface $row, $name, $locale, $shadowLocale = null)
  686. {
  687. if (\array_key_exists(\sprintf('node.%s', $name), $row->getValues())) {
  688. return $row->getValue($name);
  689. }
  690. if (null !== $shadowLocale && !\in_array($name, self::$nonFallbackProperties)) {
  691. $locale = $shadowLocale;
  692. }
  693. $name = \sprintf('%s%s', $locale, \str_replace('-', '_', \ucfirst($name)));
  694. try {
  695. return $row->getValue($name);
  696. } catch (ItemNotFoundException $e) {
  697. // the default value of a non existing property in jackalope is an empty string
  698. return '';
  699. }
  700. }
  701. /**
  702. * Resolve url property.
  703. *
  704. * @param string $locale
  705. *
  706. * @return string|null
  707. */
  708. private function resolveUrl(RowInterface $row, $locale)
  709. {
  710. if (WorkflowStage::PUBLISHED !== $this->resolveProperty($row, $locale . 'State', $locale)) {
  711. return null;
  712. }
  713. $template = $this->resolveProperty($row, 'template', $locale);
  714. if (empty($template)) {
  715. return null;
  716. }
  717. $structure = $this->structureManager->getStructure($template);
  718. if (!$structure || !$structure->hasTag('sulu.rlp')) {
  719. return null;
  720. }
  721. $propertyName = $structure->getPropertyByTagName('sulu.rlp')->getName();
  722. return $this->resolveProperty($row, $propertyName, $locale);
  723. }
  724. /**
  725. * Resolves path for given row.
  726. *
  727. * @param string $webspaceKey
  728. *
  729. * @return string
  730. */
  731. private function resolvePath(RowInterface $row, $webspaceKey)
  732. {
  733. return '/' . \ltrim(\str_replace($this->sessionManager->getContentPath($webspaceKey), '', $row->getPath()), '/');
  734. }
  735. /**
  736. * Resolve property has-children with given node.
  737. *
  738. * @return bool
  739. */
  740. private function resolveHasChildren(RowInterface $row)
  741. {
  742. $queryBuilder = new QueryBuilder($this->qomFactory);
  743. $queryBuilder
  744. ->select('node', 'jcr:uuid', 'uuid')
  745. ->from($this->qomFactory->selector('node', 'nt:unstructured'))
  746. ->where($this->qomFactory->childNode('node', $row->getPath()))
  747. ->setMaxResults(1);
  748. $result = $queryBuilder->execute();
  749. return \count(\iterator_to_array($result->getRows())) > 0;
  750. }
  751. public function supportsDescendantType(string $type): bool
  752. {
  753. try {
  754. $class = new \ReflectionClass($type);
  755. } catch (\ReflectionException $e) {
  756. // in case the class does not exist there is no support
  757. return false;
  758. }
  759. return $class->implementsInterface(SecurityBehavior::class);
  760. }
  761. }