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 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.      *
  122.      * @return Content|null
  123.      */
  124.     public function find($uuid$locale$webspaceKeyMappingInterface $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 (!== \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$webspaceKeyMappingInterface $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 (=== \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;
  238.         });
  239.         return $result;
  240.     }
  241.     public function findAll($locale$webspaceKeyMappingInterface $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$portalKeyMappingInterface $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 (=== \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 (!== \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($jsonPermissiontrue);
  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 $queryBuilderMappingInterface $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($nameself::$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. }