vendor/sulu/sulu/src/Sulu/Component/Content/Repository/ContentRepository.php line 596
<?php/** This file is part of Sulu.** (c) Sulu GmbH** This source file is subject to the MIT license that is bundled* with this source code in the file LICENSE.*/namespace Sulu\Component\Content\Repository;use Jackalope\Query\QOM\PropertyValue;use Jackalope\Query\Row;use PHPCR\ItemNotFoundException;use PHPCR\Query\QOM\QueryObjectModelConstantsInterface;use PHPCR\Query\QOM\QueryObjectModelFactoryInterface;use PHPCR\SessionInterface;use PHPCR\Util\PathHelper;use PHPCR\Util\QOM\QueryBuilder;use Sulu\Bundle\SecurityBundle\System\SystemStoreInterface;use Sulu\Component\Content\Compat\LocalizationFinderInterface;use Sulu\Component\Content\Compat\Structure;use Sulu\Component\Content\Compat\StructureManagerInterface;use Sulu\Component\Content\Compat\StructureType;use Sulu\Component\Content\Document\Behavior\SecurityBehavior;use Sulu\Component\Content\Document\RedirectType;use Sulu\Component\Content\Document\Subscriber\SecuritySubscriber;use Sulu\Component\Content\Document\WorkflowStage;use Sulu\Component\Content\Repository\Mapping\MappingInterface;use Sulu\Component\DocumentManager\PropertyEncoder;use Sulu\Component\Localization\Localization;use Sulu\Component\PHPCR\SessionManager\SessionManagerInterface;use Sulu\Component\Security\Authentication\UserInterface;use Sulu\Component\Security\Authorization\AccessControl\DescendantProviderInterface;use Sulu\Component\Util\SuluNodeHelper;use Sulu\Component\Webspace\Manager\WebspaceManagerInterface;/*** Content repository which query content with sql2 statements.*/class ContentRepository implements ContentRepositoryInterface, DescendantProviderInterface{private static $nonFallbackProperties = ['uuid','state','order','created','creator','changed','changer','published','shadowOn','shadowBase',];/*** @var SessionManagerInterface*/private $sessionManager;/*** @var PropertyEncoder*/private $propertyEncoder;/*** @var WebspaceManagerInterface*/private $webspaceManager;/*** @var SessionInterface*/private $session;/*** @var QueryObjectModelFactoryInterface*/private $qomFactory;/*** @var LocalizationFinderInterface*/private $localizationFinder;/*** @var StructureManagerInterface*/private $structureManager;/*** @var SuluNodeHelper*/private $nodeHelper;/*** @var array*/private $permissions;/*** @var SystemStoreInterface*/private $systemStore;public function __construct(SessionManagerInterface $sessionManager,PropertyEncoder $propertyEncoder,WebspaceManagerInterface $webspaceManager,LocalizationFinderInterface $localizationFinder,StructureManagerInterface $structureManager,SuluNodeHelper $nodeHelper,SystemStoreInterface $systemStore,array $permissions) {$this->sessionManager = $sessionManager;$this->propertyEncoder = $propertyEncoder;$this->webspaceManager = $webspaceManager;$this->localizationFinder = $localizationFinder;$this->structureManager = $structureManager;$this->nodeHelper = $nodeHelper;$this->systemStore = $systemStore;$this->permissions = $permissions;$this->session = $sessionManager->getSession();$this->qomFactory = $this->session->getWorkspace()->getQueryManager()->getQOMFactory();}/*** Find content by uuid.** @param string $uuid* @param string $locale* @param string $webspaceKey* @param MappingInterface $mapping Includes array of property names* @param UserInterface $user** @return Content|null*/public function find($uuid, $locale, $webspaceKey, MappingInterface $mapping, UserInterface $user = null){$locales = $this->getLocalesByWebspaceKey($webspaceKey);$queryBuilder = $this->getQueryBuilder($locale, $locales, $user);$queryBuilder->where($this->qomFactory->comparison(new PropertyValue('node', 'jcr:uuid'),'=',$this->qomFactory->literal($uuid)));$this->appendMapping($queryBuilder, $mapping, $locale, $locales);$queryResult = $queryBuilder->execute();$rows = \iterator_to_array($queryResult->getRows());if (1 !== \count($rows)) {throw new ItemNotFoundException();}$resultPermissions = $this->resolveResultPermissions($rows, $user);$permissions = empty($resultPermissions) ? [] : \current($resultPermissions);return $this->resolveContent(\current($rows), $locale, $locales, $mapping, $user, $permissions);}public function findByParentUuid($uuid,$locale,$webspaceKey,MappingInterface $mapping,UserInterface $user = null) {$path = $this->resolvePathByUuid($uuid);if (!$webspaceKey) {// TODO find a better solution than this (e.g. reuse logic from DocumentInspector and preferably in the PageController)$webspaceKey = \explode('/', $path)[2];}$locales = $this->getLocalesByWebspaceKey($webspaceKey);$queryBuilder = $this->getQueryBuilder($locale, $locales, $user);$queryBuilder->where($this->qomFactory->childNode('node', $path));$this->appendMapping($queryBuilder, $mapping, $locale, $locales);return $this->resolveQueryBuilder($queryBuilder, $locale, $locales, $mapping, $user);}public function findByWebspaceRoot($locale, $webspaceKey, MappingInterface $mapping, UserInterface $user = null){$locales = $this->getLocalesByWebspaceKey($webspaceKey);$queryBuilder = $this->getQueryBuilder($locale, $locales, $user);$queryBuilder->where($this->qomFactory->childNode('node', $this->sessionManager->getContentPath($webspaceKey)));$this->appendMapping($queryBuilder, $mapping, $locale, $locales);return $this->resolveQueryBuilder($queryBuilder, $locale, $locales, $mapping, $user);}public function findParentsWithSiblingsByUuid($uuid,$locale,$webspaceKey,MappingInterface $mapping,UserInterface $user = null) {$path = $this->resolvePathByUuid($uuid);if (empty($webspaceKey)) {$webspaceKey = $this->nodeHelper->extractWebspaceFromPath($path);}$contentPath = $this->sessionManager->getContentPath($webspaceKey);$locales = $this->getLocalesByWebspaceKey($webspaceKey);$queryBuilder = $this->getQueryBuilder($locale, $locales, $user)->orderBy($this->qomFactory->propertyValue('node', 'jcr:path'))->where($this->qomFactory->childNode('node', $path));while (PathHelper::getPathDepth($path) > PathHelper::getPathDepth($contentPath)) {$path = PathHelper::getParentPath($path);$queryBuilder->orWhere($this->qomFactory->childNode('node', $path));}$mapping->addProperties(['order']);$this->appendMapping($queryBuilder, $mapping, $locale, $locales);$result = $this->resolveQueryBuilder($queryBuilder, $locale, $locales, $mapping, $user);return $this->generateTreeByPath($result, $uuid);}public function findByPaths(array $paths,$locale,MappingInterface $mapping,UserInterface $user = null) {$locales = $this->getLocales();$queryBuilder = $this->getQueryBuilder($locale, $locales, $user);foreach ($paths as $path) {$queryBuilder->orWhere($this->qomFactory->sameNode('node', $path));}$this->appendMapping($queryBuilder, $mapping, $locale, $locales);return $this->resolveQueryBuilder($queryBuilder, $locale, $locales, $mapping, $user);}public function findByUuids(array $uuids,$locale,MappingInterface $mapping,UserInterface $user = null) {if (0 === \count($uuids)) {return [];}$locales = $this->getLocales();$queryBuilder = $this->getQueryBuilder($locale, $locales, $user);foreach ($uuids as $uuid) {$queryBuilder->orWhere($this->qomFactory->comparison($queryBuilder->qomf()->propertyValue('node', 'jcr:uuid'),QueryObjectModelConstantsInterface::JCR_OPERATOR_EQUAL_TO,$queryBuilder->qomf()->literal($uuid)));}$this->appendMapping($queryBuilder, $mapping, $locale, $locales);$result = $this->resolveQueryBuilder($queryBuilder, $locale, $locales, $mapping, $user);\usort($result, function($a, $b) use ($uuids) {return \array_search($a->getId(), $uuids) < \array_search($b->getId(), $uuids) ? -1 : 1;});return $result;}public function findAll($locale, $webspaceKey, MappingInterface $mapping, UserInterface $user = null){$contentPath = $this->sessionManager->getContentPath($webspaceKey);$locales = $this->getLocalesByWebspaceKey($webspaceKey);$queryBuilder = $this->getQueryBuilder($locale, $locales, $user)->where($this->qomFactory->descendantNode('node', $contentPath))->orWhere($this->qomFactory->sameNode('node', $contentPath));$this->appendMapping($queryBuilder, $mapping, $locale, $locales);return $this->resolveQueryBuilder($queryBuilder, $locale, $locales, $mapping, $user);}public function findAllByPortal($locale, $portalKey, MappingInterface $mapping, UserInterface $user = null){$webspaceKey = $this->webspaceManager->findPortalByKey($portalKey)->getWebspace()->getKey();$contentPath = $this->sessionManager->getContentPath($webspaceKey);$locales = $this->getLocalesByPortalKey($portalKey);$queryBuilder = $this->getQueryBuilder($locale, $locales, $user)->where($this->qomFactory->descendantNode('node', $contentPath))->orWhere($this->qomFactory->sameNode('node', $contentPath));$this->appendMapping($queryBuilder, $mapping, $locale, $locales);return $this->resolveQueryBuilder($queryBuilder, $locale, $locales, $mapping, $user);}public function findDescendantIdsById($id){$queryBuilder = $this->getQueryBuilder();$queryBuilder->where($this->qomFactory->comparison(new PropertyValue('node', 'jcr:uuid'),'=',$this->qomFactory->literal($id)));$result = \iterator_to_array($queryBuilder->execute());if (0 === \count($result)) {return [];}$path = $result[0]->getPath();$descendantQueryBuilder = $this->getQueryBuilder()->where($this->qomFactory->descendantNode('node', $path));return \array_map(function(Row $row) {return $row->getNode()->getIdentifier();},\iterator_to_array($descendantQueryBuilder->execute()));}/*** Generates a content-tree with paths of given content array.** @param Content[] $contents** @return Content[]*/private function generateTreeByPath(array $contents, $uuid){$childrenByPath = [];foreach ($contents as $content) {$path = PathHelper::getParentPath($content->getPath());if (!isset($childrenByPath[$path])) {$childrenByPath[$path] = [];}$order = $content['order'];while (isset($childrenByPath[$path][$order])) {++$order;}$childrenByPath[$path][$order] = $content;}foreach ($contents as $content) {if (!isset($childrenByPath[$content->getPath()])) {if ($content->getId() === $uuid) {$content->setChildren([]);}continue;}\ksort($childrenByPath[$content->getPath()]);$content->setChildren(\array_values($childrenByPath[$content->getPath()]));}if (!\array_key_exists('/', $childrenByPath) || !\is_array($childrenByPath['/'])) {return [];}\ksort($childrenByPath['/']);return \array_values($childrenByPath['/']);}/*** Resolve path for node with given uuid.** @param string $uuid** @return string** @throws ItemNotFoundException*/private function resolvePathByUuid($uuid){$queryBuilder = new QueryBuilder($this->qomFactory);$queryBuilder->select('node', 'jcr:uuid', 'uuid')->from($this->qomFactory->selector('node', 'nt:unstructured'))->where($this->qomFactory->comparison($this->qomFactory->propertyValue('node', 'jcr:uuid'),'=',$this->qomFactory->literal($uuid)));$rows = $queryBuilder->execute();if (1 !== \count(\iterator_to_array($rows->getRows()))) {throw new ItemNotFoundException();}return $rows->getRows()->current()->getPath();}/*** Resolves query results to content.** @param string $locale* @param UserInterface $user** @return Content[]*/private function resolveQueryBuilder(QueryBuilder $queryBuilder,$locale,$locales,MappingInterface $mapping,UserInterface $user = null) {$result = \iterator_to_array($queryBuilder->execute());$permissions = $this->resolveResultPermissions($result, $user);return \array_values(\array_filter(\array_map(function(Row $row, $index) use ($mapping, $locale, $locales, $user, $permissions) {return $this->resolveContent($row,$locale,$locales,$mapping,$user,$permissions[$index] ?? []);},$result,\array_keys($result))));}private function resolveResultPermissions(array $result, UserInterface $user = null){$permissions = [];foreach ($result as $index => $row) {$permissions[$index] = [];$jsonPermission = $row->getValue(SecuritySubscriber::SECURITY_PERMISSION_PROPERTY);if (!$jsonPermission) {continue;}$rowPermissions = \json_decode($jsonPermission, true);foreach ($rowPermissions as $roleId => $rolePermissions) {foreach ($this->permissions as $permissionKey => $permission) {$permissions[$index][$roleId][$permissionKey] = false;}foreach ($rolePermissions as $rolePermission) {$permissions[$index][$roleId][$rolePermission] = true;}}}return $permissions;}/*** Returns QueryBuilder with basic select and where statements.** @param string $locale* @param string[] $locales* @param UserInterface $user** @return QueryBuilder*/private function getQueryBuilder($locale = null, $locales = [], UserInterface $user = null){$queryBuilder = new QueryBuilder($this->qomFactory);$queryBuilder->select('node', 'jcr:uuid', 'uuid')->addSelect('node', $this->getPropertyName('nodeType', $locale), 'nodeType')->addSelect('node', $this->getPropertyName('internal_link', $locale), 'internalLink')->addSelect('node', $this->getPropertyName('state', $locale), 'state')->addSelect('node', $this->getPropertyName('shadow-on', $locale), 'shadowOn')->addSelect('node', $this->getPropertyName('shadow-base', $locale), 'shadowBase')->addSelect('node', $this->propertyEncoder->systemName('order'), 'order')->from($this->qomFactory->selector('node', 'nt:unstructured'))->orderBy($this->qomFactory->propertyValue('node', 'sulu:order'));$this->appendSingleMapping($queryBuilder, 'template', $locales);$this->appendSingleMapping($queryBuilder, 'shadow-on', $locales);$this->appendSingleMapping($queryBuilder, 'state', $locales);$queryBuilder->addSelect('node',SecuritySubscriber::SECURITY_PERMISSION_PROPERTY,SecuritySubscriber::SECURITY_PERMISSION_PROPERTY);return $queryBuilder;}private function getPropertyName($propertyName, $locale){if ($locale) {return $this->propertyEncoder->localizedContentName($propertyName, $locale);}return $this->propertyEncoder->contentName($propertyName);}/*** Returns array of locales for given webspace key.** @param string $webspaceKey** @return string[]*/private function getLocalesByWebspaceKey($webspaceKey){$webspace = $this->webspaceManager->findWebspaceByKey($webspaceKey);return \array_map(function(Localization $localization) {return $localization->getLocale();},$webspace->getAllLocalizations());}/*** Returns array of locales for given portal key.** @param string $portalKey** @return string[]*/private function getLocalesByPortalKey($portalKey){$portal = $this->webspaceManager->findPortalByKey($portalKey);return \array_map(function(Localization $localization) {return $localization->getLocale();},$portal->getLocalizations());}/*** Returns array of locales for webspaces.** @return string[]*/private function getLocales(){return $this->webspaceManager->getAllLocales();}/*** Append mapping selects to given query-builder.** @param MappingInterface $mapping Includes array of property names* @param string $locale* @param string[] $locales*/private function appendMapping(QueryBuilder $queryBuilder, MappingInterface $mapping, $locale, $locales){if ($mapping->onlyPublished()) {$queryBuilder->andWhere($this->qomFactory->comparison($this->qomFactory->propertyValue('node',$this->propertyEncoder->localizedSystemName('state', $locale)),'=',$this->qomFactory->literal(WorkflowStage::PUBLISHED)));}$properties = $mapping->getProperties();foreach ($properties as $propertyName) {$this->appendSingleMapping($queryBuilder, $propertyName, $locales);}if ($mapping->resolveUrl()) {$this->appendUrlMapping($queryBuilder, $locales);}}/*** Append mapping selects for a single property to given query-builder.** @param string $propertyName* @param string[] $locales*/private function appendSingleMapping(QueryBuilder $queryBuilder, $propertyName, $locales){foreach ($locales as $locale) {$alias = \sprintf('%s%s', $locale, \str_replace('-', '_', \ucfirst($propertyName)));$queryBuilder->addSelect('node',$this->propertyEncoder->localizedContentName($propertyName, $locale),$alias);}}/*** Append mapping for url to given query-builder.** @param string[] $locales*/private function appendUrlMapping(QueryBuilder $queryBuilder, $locales){$structures = $this->structureManager->getStructures(Structure::TYPE_PAGE);$urlNames = [];foreach ($structures as $structure) {if (!$structure->hasTag('sulu.rlp')) {continue;}$propertyName = $structure->getPropertyByTagName('sulu.rlp')->getName();if (!\in_array($propertyName, $urlNames)) {$this->appendSingleMapping($queryBuilder, $propertyName, $locales);$urlNames[] = $propertyName;}}}/*** Resolve a single result row to a content object.** @param string $locale* @param string $locales** @return Content|null*/private function resolveContent(Row $row,$locale,$locales,MappingInterface $mapping,UserInterface $user = null,array $permissions) {$webspaceKey = $this->nodeHelper->extractWebspaceFromPath($row->getPath());$originalLocale = $locale;$availableLocales = $this->resolveAvailableLocales($row);$ghostLocale = $this->localizationFinder->findAvailableLocale($webspaceKey,$availableLocales,$locale);if (null === $ghostLocale) {$ghostLocale = \reset($availableLocales);}$type = null;if ($row->getValue('shadowOn')) {if (!$mapping->shouldHydrateShadow()) {return null;}$type = StructureType::getShadow($row->getValue('shadowBase'));} elseif (null !== $ghostLocale && $ghostLocale !== $originalLocale) {if (!$mapping->shouldHydrateGhost()) {return null;}$locale = $ghostLocale;$type = StructureType::getGhost($locale);}if (RedirectType::INTERNAL === $row->getValue('nodeType')&& $mapping->followInternalLink()&& '' !== $row->getValue('internalLink')&& $row->getValue('internalLink') !== $row->getValue('uuid')) {// TODO collect all internal link contents and query oncereturn $this->resolveInternalLinkContent($row, $locale, $webspaceKey, $mapping, $type, $user);}$shadowBase = null;if ($row->getValue('shadowOn')) {$shadowBase = $row->getValue('shadowBase');}$data = [];foreach ($mapping->getProperties() as $item) {$data[$item] = $this->resolveProperty($row, $item, $locale, $shadowBase);}$content = new Content($originalLocale,$webspaceKey,$row->getValue('uuid'),$this->resolvePath($row, $webspaceKey),$row->getValue('state'),$row->getValue('nodeType'),$this->resolveHasChildren($row), $this->resolveProperty($row, 'template', $locale, $shadowBase),$data,$permissions,$type);$content->setRow($row);if (!$content->getTemplate() || !$this->structureManager->getStructure($content->getTemplate())) {$content->setBrokenTemplate();}if ($mapping->resolveUrl()) {$url = $this->resolveUrl($row, $locale);/** @var array<string, string|null> $urls */$urls = [];\array_walk($locales,/** @var array<string, string|null> $urls */function($locale) use (&$urls, $row) {$urls[$locale] = $this->resolveUrl($row, $locale);});$content->setUrl($url);$content->setUrls($urls);}if ($mapping->resolveConcreteLocales()) {$locales = $this->resolveAvailableLocales($row);$content->setContentLocales($locales);}return $content;}/*** Resolves all available localizations for given row.** @return string[]*/private function resolveAvailableLocales(Row $row){$locales = [];foreach ($row->getValues() as $key => $value) {if (\preg_match('/^node.([a-zA-Z_]*?)Template/', $key, $matches) && '' !== $value&& !$row->getValue(\sprintf('node.%sShadow_on', $matches[1]))) {$locales[] = $matches[1];}}return $locales;}/*** Resolve a single result row which is an internal link to a content object.** @param string $locale* @param string $webspaceKey* @param MappingInterface $mapping Includes array of property names* @param StructureType $type* @param UserInterface $user** @return Content|null*/public function resolveInternalLinkContent(Row $row,$locale,$webspaceKey,MappingInterface $mapping,StructureType $type = null,UserInterface $user = null) {$linkedContent = $this->find($row->getValue('internalLink'), $locale, $webspaceKey, $mapping);if (null === $linkedContent) {return null;}$data = $linkedContent->getData();// return value of source node instead of link destination for title and non-fallback-properties$sourceNodeValueProperties = self::$nonFallbackProperties;$sourceNodeValueProperties[] = 'title';$properties = \array_intersect($sourceNodeValueProperties, \array_keys($data));foreach ($properties as $property) {$data[$property] = $this->resolveProperty($row, $property, $locale);}$resultPermissions = $this->resolveResultPermissions([$row], $user);$permissions = empty($resultPermissions) ? [] : \current($resultPermissions);$content = new Content($locale,$webspaceKey,$row->getValue('uuid'),$this->resolvePath($row, $webspaceKey),$row->getValue('state'),$row->getValue('nodeType'),$this->resolveHasChildren($row), $this->resolveProperty($row, 'template', $locale),$data,$permissions,$type);if ($mapping->resolveUrl()) {$content->setUrl($linkedContent->getUrl());$content->setUrls($linkedContent->getUrls());}if (!$content->getTemplate() || !$this->structureManager->getStructure($content->getTemplate())) {$content->setBrokenTemplate();}return $content;}/*** Resolve a property and follow shadow locale if it has one.** @param string $name* @param string $locale* @param string $shadowLocale*/private function resolveProperty(Row $row, $name, $locale, $shadowLocale = null){if (\array_key_exists(\sprintf('node.%s', $name), $row->getValues())) {return $row->getValue($name);}if (null !== $shadowLocale && !\in_array($name, self::$nonFallbackProperties)) {$locale = $shadowLocale;}$name = \sprintf('%s%s', $locale, \str_replace('-', '_', \ucfirst($name)));try {return $row->getValue($name);} catch (ItemNotFoundException $e) {// the default value of a non existing property in jackalope is an empty stringreturn '';}}/*** Resolve url property.** @param string $locale** @return string|null*/private function resolveUrl(Row $row, $locale){if (WorkflowStage::PUBLISHED !== $this->resolveProperty($row, $locale . 'State', $locale)) {return null;}$template = $this->resolveProperty($row, 'template', $locale);if (empty($template)) {return null;}$structure = $this->structureManager->getStructure($template);if (!$structure || !$structure->hasTag('sulu.rlp')) {return null;}$propertyName = $structure->getPropertyByTagName('sulu.rlp')->getName();return $this->resolveProperty($row, $propertyName, $locale);}/*** Resolves path for given row.** @param string $webspaceKey** @return string*/private function resolvePath(Row $row, $webspaceKey){return '/' . \ltrim(\str_replace($this->sessionManager->getContentPath($webspaceKey), '', $row->getPath()), '/');}/*** Resolve property has-children with given node.** @return bool*/private function resolveHasChildren(Row $row){$queryBuilder = new QueryBuilder($this->qomFactory);$queryBuilder->select('node', 'jcr:uuid', 'uuid')->from($this->qomFactory->selector('node', 'nt:unstructured'))->where($this->qomFactory->childNode('node', $row->getPath()))->setMaxResults(1);$result = $queryBuilder->execute();return \count(\iterator_to_array($result->getRows())) > 0;}public function supportsDescendantType(string $type): bool{try {$class = new \ReflectionClass($type);} catch (\ReflectionException $e) {// in case the class does not exist there is no supportreturn false;}return $class->implementsInterface(SecurityBehavior::class);}}