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

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