diff --git a/Classes/Aspects/AugmentationAspect.php b/Classes/Aspects/AugmentationAspect.php
index f57ed7d88c..281e812d1f 100644
--- a/Classes/Aspects/AugmentationAspect.php
+++ b/Classes/Aspects/AugmentationAspect.php
@@ -38,24 +38,12 @@ class AugmentationAspect
*/
protected $nodeAuthorizationService;
- /**
- * @Flow\Inject
- * @var UserLocaleService
- */
- protected $userLocaleService;
-
/**
* @Flow\Inject
* @var HtmlAugmenter
*/
protected $htmlAugmenter;
- /**
- * @Flow\Inject
- * @var NodeInfoHelper
- */
- protected $nodeInfoHelper;
-
/**
* @Flow\Inject
* @var SessionInterface
@@ -126,16 +114,7 @@ public function contentElementAugmentation(JoinPointInterface $joinPoint)
$attributes['data-__neos-node-contextpath'] = $node->getContextPath();
$attributes['data-__neos-fusion-path'] = $fusionPath;
- $this->userLocaleService->switchToUILocale();
-
- $serializedNode = json_encode($this->nodeInfoHelper->renderNodeWithPropertiesAndChildrenInformation($node, $this->controllerContext));
-
- $this->userLocaleService->switchToUILocale(true);
-
- $wrappedContent = $this->htmlAugmenter->addAttributes($content, $attributes, 'div');
- $wrappedContent .= "";
-
- return $wrappedContent;
+ return $this->htmlAugmenter->addAttributes($content, $attributes);
}
/**
diff --git a/Classes/ContentRepository/Service/NodeService.php b/Classes/ContentRepository/Service/NodeService.php
index 0e725bb277..372a33867a 100644
--- a/Classes/ContentRepository/Service/NodeService.php
+++ b/Classes/ContentRepository/Service/NodeService.php
@@ -11,8 +11,13 @@
* source code.
*/
+use Doctrine\ORM\EntityManagerInterface;
+use Neos\ContentRepository\Domain\Factory\NodeFactory;
+use Neos\ContentRepository\Domain\Model\NodeData;
use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\ContentRepository\Domain\Model\Workspace;
+use Neos\ContentRepository\Domain\Repository\NodeDataRepository;
+use Neos\ContentRepository\Domain\Service\Context;
use Neos\ContentRepository\Domain\Service\ContextFactoryInterface;
use Neos\ContentRepository\Domain\Utility\NodePaths;
use Neos\Eel\FlowQuery\FlowQuery;
@@ -46,6 +51,35 @@ class NodeService
*/
protected $domainRepository;
+ /**
+ * @Flow\Inject
+ * @var NodeDataRepository
+ */
+ protected $nodeDataRepository;
+
+ /**
+ * @Flow\Inject
+ * @var EntityManagerInterface
+ */
+ protected $entityManager;
+
+ /**
+ * @Flow\Inject
+ * @var NodeFactory
+ */
+ protected $nodeFactory;
+
+ /**
+ * @var array
+ */
+ protected array $contextCache = [];
+
+ /**
+ * @Flow\InjectConfiguration(path="nodeTypeRoles.ignored", package="Neos.Neos.Ui")
+ * @var string
+ */
+ protected $ignoredNodeTypeRole;
+
/**
* Helper method to retrieve the closest document for a node
*
@@ -86,11 +120,72 @@ public function getNodeFromContextPath($contextPath, Site $site = null, Domain $
$nodePath = $nodePathAndContext['nodePath'];
$workspaceName = $nodePathAndContext['workspaceName'];
$dimensions = $nodePathAndContext['dimensions'];
+ $siteNodeName = $site ? $site->getNodeName() : explode('/', $nodePath)[2];
+
+ // Prevent reloading the same context multiple times
+ $contextHash = md5(implode('|', [$siteNodeName, $workspaceName, json_encode($dimensions), $includeAll]));
+ if (isset($this->contextCache[$contextHash])) {
+ $context = $this->contextCache[$contextHash];
+ } else {
+ $contextProperties = $this->prepareContextProperties($workspaceName, $dimensions);
+
+ if ($site === null) {
+ $site = $this->siteRepository->findOneByNodeName($siteNodeName);
+ }
+
+ if ($domain === null) {
+ $domain = $this->domainRepository->findOneBySite($site);
+ }
+ $contextProperties['currentSite'] = $site;
+ $contextProperties['currentDomain'] = $domain;
+ if ($includeAll === true) {
+ $contextProperties['invisibleContentShown'] = true;
+ $contextProperties['removedContentShown'] = true;
+ $contextProperties['inaccessibleContentShown'] = true;
+ }
+
+ $context = $this->contextFactory->create(
+ $contextProperties
+ );
+
+ $workspace = $context->getWorkspace(false);
+ if (!$workspace) {
+ return new Error(
+ sprintf('Could not convert the given source to Node object because the workspace "%s" as specified in the context node path does not exist.', $workspaceName),
+ 1451392329
+ );
+ }
+ $this->contextCache[$contextHash] = $context;
+ }
+
+ return $context->getNode($nodePath);
+ }
+
+ /**
+ * Converts given context paths to a node objects
+ *
+ * @param string[] $nodeContextPaths
+ * @return NodeInterface[]|Error
+ */
+ public function getNodesFromContextPaths(array $nodeContextPaths, Site $site = null, Domain $domain = null, $includeAll = false): array|Error
+ {
+ if (!$nodeContextPaths) {
+ return [];
+ }
+
+ $nodePaths = array_map(static function($nodeContextPath) {
+ return NodePaths::explodeContextPath($nodeContextPath)['nodePath'];
+ }, $nodeContextPaths);
+
+ $nodePathAndContext = NodePaths::explodeContextPath($nodeContextPaths[0]);
+ $nodePath = $nodePathAndContext['nodePath'];
+ $workspaceName = $nodePathAndContext['workspaceName'];
+ $dimensions = $nodePathAndContext['dimensions'];
+ $siteNodeName = explode('/', $nodePath)[2];
$contextProperties = $this->prepareContextProperties($workspaceName, $dimensions);
if ($site === null) {
- list(, , $siteNodeName) = explode('/', $nodePath);
$site = $this->siteRepository->findOneByNodeName($siteNodeName);
}
@@ -105,10 +200,7 @@ public function getNodeFromContextPath($contextPath, Site $site = null, Domain $
$contextProperties['removedContentShown'] = true;
$contextProperties['inaccessibleContentShown'] = true;
}
-
- $context = $this->contextFactory->create(
- $contextProperties
- );
+ $context = $this->contextFactory->create($contextProperties);
$workspace = $context->getWorkspace(false);
if (!$workspace) {
@@ -118,7 +210,183 @@ public function getNodeFromContextPath($contextPath, Site $site = null, Domain $
);
}
- return $context->getNode($nodePath);
+ // Query nodes and their variants from the database
+ $queryBuilder = $this->entityManager->createQueryBuilder();
+ $workspaces = $this->collectWorkspaceAndAllBaseWorkspaces($workspace);
+ $workspacesNames = array_map(static function(Workspace $workspace) { return $workspace->getName(); }, $workspaces);
+
+ // Filter by workspace and its parents
+ $queryBuilder->select('n')
+ ->from(NodeData::class, 'n')
+ ->where('n.workspace IN (:workspaces)')
+ ->andWhere('n.movedTo IS NULL')
+ ->andWhere('n.path IN (:nodePaths)')
+ ->setParameter('workspaces', $workspacesNames)
+ ->setParameter('nodePaths', $nodePaths);
+ $query = $queryBuilder->getQuery();
+ $nodeDataWithVariants = $query->getResult();
+
+ // Remove node duplicates
+ $reducedNodeData = $this->reduceNodeVariantsByWorkspacesAndDimensions($nodeDataWithVariants, $workspaces, $dimensions);
+
+ // Convert nodedata objects to nodes
+ return array_reduce($reducedNodeData, function (array $carry, NodeData $nodeData) use ($context) {
+ $node = $this->nodeFactory->createFromNodeData($nodeData, $context);
+ if ($node !== null) {
+ $carry[] = $node;
+ }
+ $context->getFirstLevelNodeCache()->setByPath($node->getPath(), $node);
+ return $carry;
+ }, []);
+ }
+
+ /**
+ * @param NodeInterface[] $parentNodes
+ */
+ public function preloadChildNodesForNodes(array $parentNodes): void
+ {
+ if (empty($parentNodes)) {
+ return;
+ }
+
+ $workspace = $parentNodes[0]->getWorkspace();
+ $context = $parentNodes[0]->getContext();
+ $dimensions = $context->getDimensions();
+
+ $parentPaths = array_map(static function(NodeInterface $parentNode) {
+ return $parentNode->getPath();
+ }, $parentNodes);
+
+ // Query nodes and their variants from the database
+ $queryBuilder = $this->entityManager->createQueryBuilder();
+ $workspaces = $this->collectWorkspaceAndAllBaseWorkspaces($workspace);
+ $workspacesNames = array_map(static function(Workspace $workspace) { return $workspace->getName(); }, $workspaces);
+
+ // Filter by workspace and its parents
+ $queryBuilder->select('n')
+ ->from(NodeData::class, 'n')
+ ->where('n.workspace IN (:workspaces)')
+ ->andWhere('n.movedTo IS NULL')
+ ->andWhere('n.parentPath IN (:parentPaths)')
+ ->setParameter('workspaces', $workspacesNames)
+ ->setParameter('parentPaths', $parentPaths);
+ $query = $queryBuilder->getQuery();
+ $nodeDataWithVariants = $query->getResult();
+
+ // Remove node duplicates
+ $reducedNodeData = $this->reduceNodeVariantsByWorkspacesAndDimensions(
+ $nodeDataWithVariants,
+ $workspaces,
+ $dimensions
+ );
+
+ // Convert nodedata objects to nodes and group them by parent path
+ $childNodesByParentPath = array_reduce($reducedNodeData, function (array $carry, NodeData $nodeData) use ($context) {
+ $node = $this->nodeFactory->createFromNodeData($nodeData, $context);
+ if ($node !== null) {
+ if (!isset($carry[$node->getParentPath()])) {
+ $carry[$node->getParentPath()] = [$node];
+ } else {
+ $carry[$node->getParentPath()][] = $node;
+ }
+ }
+ return $carry;
+ }, []);
+
+ foreach ($childNodesByParentPath as $parentPath => $childNodes) {
+ usort($childNodes, static function(NodeInterface $a, NodeInterface $b) {
+ return $a->getIndex() <=> $b->getIndex();
+ });
+ $context->getFirstLevelNodeCache()->setChildNodesByPathAndNodeTypeFilter(
+ $parentPath, '!' .
+ $this->ignoredNodeTypeRole,
+ $childNodes
+ );
+ }
+ }
+
+ /**
+ * Given an array with duplicate nodes (from different workspaces and dimensions) those are reduced to uniqueness (by node identifier)
+ * Copied from Neos\ContentRepository\Domain\Repository\NodeDataRepository
+ *
+ * @param NodeData[] $nodes NodeData result with multiple and duplicate identifiers (different nodes and redundant results for node variants with different dimensions)
+ * @param Workspace[] $workspaces
+ * @param array $dimensions
+ * @return NodeData[] Array of unique node results indexed by identifier
+ */
+ protected function reduceNodeVariantsByWorkspacesAndDimensions(array $nodes, array $workspaces, array $dimensions): array
+ {
+ $reducedNodes = [];
+
+ $minimalDimensionPositionsByIdentifier = [];
+
+ $workspaceNames = array_map(static fn (Workspace $workspace) => $workspace->getName(), $workspaces);
+
+ foreach ($nodes as $node) {
+ $nodeDimensions = $node->getDimensionValues();
+
+ // Find the position of the workspace, a smaller value means more priority
+ $workspacePosition = array_search($node->getWorkspace()->getName(), $workspaceNames);
+ if ($workspacePosition === false) {
+ throw new \Exception(sprintf(
+ 'Node workspace "%s" not found in allowed workspaces (%s), this could result from a detached workspace entity in the context.',
+ $node->getWorkspace()->getName(),
+ implode(', ', $workspaceNames)
+ ), 1718740117);
+ }
+
+ // Find positions in dimensions, add workspace in front for highest priority
+ $dimensionPositions = [];
+
+ // Special case for no dimensions
+ if ($dimensions === []) {
+ // We can just decide if the given node has no dimensions.
+ $dimensionPositions[] = ($nodeDimensions === []) ? 0 : 1;
+ }
+
+ foreach ($dimensions as $dimensionName => $dimensionValues) {
+ if (isset($nodeDimensions[$dimensionName])) {
+ foreach ($nodeDimensions[$dimensionName] as $nodeDimensionValue) {
+ $position = array_search($nodeDimensionValue, $dimensionValues);
+ if ($position === false) {
+ $position = PHP_INT_MAX;
+ }
+ $dimensionPositions[$dimensionName] = isset($dimensionPositions[$dimensionName]) ? min(
+ $dimensionPositions[$dimensionName],
+ $position
+ ) : $position;
+ }
+ } else {
+ $dimensionPositions[$dimensionName] = isset($dimensionPositions[$dimensionName]) ? min(
+ $dimensionPositions[$dimensionName],
+ PHP_INT_MAX
+ ) : PHP_INT_MAX;
+ }
+ }
+ $dimensionPositions[] = $workspacePosition;
+
+ $identifier = $node->getIdentifier();
+ // Yes, it seems to work comparing arrays that way!
+ if (!isset($minimalDimensionPositionsByIdentifier[$identifier]) || $dimensionPositions < $minimalDimensionPositionsByIdentifier[$identifier]) {
+ $reducedNodes[$identifier] = $node;
+ $minimalDimensionPositionsByIdentifier[$identifier] = $dimensionPositions;
+ }
+ }
+
+ return $reducedNodes;
+ }
+
+ /**
+ * @return Workspace[]
+ */
+ protected function collectWorkspaceAndAllBaseWorkspaces(Workspace $workspace): array
+ {
+ $workspaces = [];
+ while ($workspace !== null) {
+ $workspaces[] = $workspace;
+ $workspace = $workspace->getBaseWorkspace();
+ }
+ return $workspaces;
}
/**
diff --git a/Classes/Controller/BackendServiceController.php b/Classes/Controller/BackendServiceController.php
index a120ba48f6..f69c2342d3 100644
--- a/Classes/Controller/BackendServiceController.php
+++ b/Classes/Controller/BackendServiceController.php
@@ -532,28 +532,39 @@ public function flowQueryAction(array $chain): string
$createContext = array_shift($chain);
$finisher = array_pop($chain);
- $flowQuery = new FlowQuery(array_map(
- function ($envelope) {
- return $this->nodeService->getNodeFromContextPath($envelope['$node']);
- },
- $createContext['payload']
- ));
+ $nodeContextPaths = array_unique(array_column($createContext['payload'], '$node'));
+ $nodes = $this->nodeService->getNodesFromContextPaths($nodeContextPaths);
+
+ $flowQuery = new FlowQuery($nodes);
foreach ($chain as $operation) {
$flowQuery = call_user_func_array([$flowQuery, $operation['type']], $operation['payload']);
}
+ $nodes = array_filter($flowQuery->get());
+ $this->nodeService->preloadChildNodesForNodes(array_values($nodes));
+
$nodeInfoHelper = new NodeInfoHelper();
$result = [];
switch ($finisher['type']) {
case 'get':
- $result = $nodeInfoHelper->renderNodes(array_filter($flowQuery->get()), $this->getControllerContext());
+ $result = $nodeInfoHelper->renderNodes(
+ $nodes,
+ $this->getControllerContext()
+ );
break;
case 'getForTree':
- $result = $nodeInfoHelper->renderNodes(array_filter($flowQuery->get()), $this->getControllerContext(), true);
+ $result = $nodeInfoHelper->renderNodes(
+ $nodes,
+ $this->getControllerContext(),
+ true
+ );
break;
case 'getForTreeWithParents':
- $result = $nodeInfoHelper->renderNodesWithParents(array_filter($flowQuery->get()), $this->getControllerContext());
+ $result = $nodeInfoHelper->renderNodesWithParents(
+ $nodes,
+ $this->getControllerContext()
+ );
break;
}
diff --git a/Classes/Domain/Service/UserLocaleService.php b/Classes/Domain/Service/UserLocaleService.php
index b7dc351b24..e4451b4369 100644
--- a/Classes/Domain/Service/UserLocaleService.php
+++ b/Classes/Domain/Service/UserLocaleService.php
@@ -37,6 +37,13 @@ class UserLocaleService
*/
protected $rememberedContentLocale;
+ /**
+ * Remebered content locale for locale switching
+ *
+ * @var Locale
+ */
+ protected $rememberedUserLocale;
+
/**
* For serialization, we need to respect the UI locale, rather than the content locale
*
@@ -47,11 +54,15 @@ public function switchToUILocale($reset = false)
if ($reset === true) {
// Reset the locale
$this->i18nService->getConfiguration()->setCurrentLocale($this->rememberedContentLocale);
+ } elseif ($this->rememberedUserLocale) {
+ // Restore the local
+ $this->i18nService->getConfiguration()->setCurrentLocale($this->rememberedUserLocale);
} else {
$this->rememberedContentLocale = $this->i18nService->getConfiguration()->getCurrentLocale();
$userLocalePreference = ($this->userService->getCurrentUser() ? $this->userService->getCurrentUser()->getPreferences()->getInterfaceLanguage() : null);
$defaultLocale = $this->i18nService->getConfiguration()->getDefaultLocale();
$userLocale = $userLocalePreference ? new Locale($userLocalePreference) : $defaultLocale;
+ $this->rememberedUserLocale = $userLocale;
$this->i18nService->getConfiguration()->setCurrentLocale($userLocale);
}
}
diff --git a/Classes/FlowQueryOperations/NeosUiDefaultNodesOperation.php b/Classes/FlowQueryOperations/NeosUiDefaultNodesOperation.php
index 141da9a32a..b198f312e4 100644
--- a/Classes/FlowQueryOperations/NeosUiDefaultNodesOperation.php
+++ b/Classes/FlowQueryOperations/NeosUiDefaultNodesOperation.php
@@ -72,8 +72,9 @@ public function canEvaluate($context)
public function evaluate(FlowQuery $flowQuery, array $arguments)
{
/** @var TraversableNodeInterface $siteNode */
+ $siteNode = $flowQuery->getContext()[0];
/** @var TraversableNodeInterface $documentNode */
- list($siteNode, $documentNode) = $flowQuery->getContext();
+ $documentNode = $flowQuery->getContext()[1] ?? $siteNode;
/** @var string[] $toggledNodes */
list($baseNodeType, $loadingDepth, $toggledNodes, $clipboardNodesContextPaths) = $arguments;
diff --git a/Classes/Fusion/Helper/NodeInfoHelper.php b/Classes/Fusion/Helper/NodeInfoHelper.php
index 64d2cda626..87a10970a5 100644
--- a/Classes/Fusion/Helper/NodeInfoHelper.php
+++ b/Classes/Fusion/Helper/NodeInfoHelper.php
@@ -116,7 +116,7 @@ public function renderNode(NodeInterface $node, ControllerContext $controllerCon
/**
* @param NodeInterface $node
* @param ControllerContext|null $controllerContext
- * @param string $nodeTypeFilterOverride
+ * @param string|null $nodeTypeFilterOverride
* @return array|null
*/
public function renderNodeWithMinimalPropertiesAndChildrenInformation(NodeInterface $node, ControllerContext $controllerContext = null, string $nodeTypeFilterOverride = null)
@@ -137,15 +137,9 @@ public function renderNodeWithMinimalPropertiesAndChildrenInformation(NodeInterf
if ($controllerContext !== null) {
$nodeInfo = array_merge($nodeInfo, $this->getUriInformation($node, $controllerContext));
- if ($controllerContext->getRequest()->hasArgument('presetBaseNodeType')) {
- $presetBaseNodeType = $controllerContext->getRequest()->getArgument('presetBaseNodeType');
- }
}
- $baseNodeType = $nodeTypeFilterOverride ? $nodeTypeFilterOverride : (isset($presetBaseNodeType) ? $presetBaseNodeType : $this->defaultBaseNodeType);
- $nodeTypeFilter = $this->buildNodeTypeFilterString($this->nodeTypeStringsToList($baseNodeType), $this->nodeTypeStringsToList($this->ignoredNodeTypeRole));
-
- $nodeInfo['children'] = $this->renderChildrenInformation($node, $nodeTypeFilter);
+ $nodeInfo['children'] = $this->renderChildrenInformation($node);
$this->userLocaleService->switchToUILocale(true);
@@ -155,7 +149,7 @@ public function renderNodeWithMinimalPropertiesAndChildrenInformation(NodeInterf
/**
* @param NodeInterface $node
* @param ControllerContext|null $controllerContext
- * @param string $nodeTypeFilterOverride
+ * @param string|null $nodeTypeFilterOverride
* @return array|null
*/
public function renderNodeWithPropertiesAndChildrenInformation(NodeInterface $node, ControllerContext $controllerContext = null, string $nodeTypeFilterOverride = null)
@@ -172,13 +166,9 @@ public function renderNodeWithPropertiesAndChildrenInformation(NodeInterface $no
if ($controllerContext !== null) {
$nodeInfo = array_merge($nodeInfo, $this->getUriInformation($node, $controllerContext));
- if ($controllerContext->getRequest()->hasArgument('presetBaseNodeType')) {
- $presetBaseNodeType = $controllerContext->getRequest()->getArgument('presetBaseNodeType');
- }
}
- $baseNodeType = $nodeTypeFilterOverride ? $nodeTypeFilterOverride : (isset($presetBaseNodeType) ? $presetBaseNodeType : $this->defaultBaseNodeType);
- $nodeInfo['children'] = $this->renderChildrenInformation($node, $baseNodeType);
+ $nodeInfo['children'] = $this->renderChildrenInformation($node);
$this->userLocaleService->switchToUILocale(true);
@@ -234,26 +224,17 @@ protected function getBasicNodeInformation(NodeInterface $node): array
/**
* Get information for all children of the given parent node.
- *
- * @param NodeInterface $node
- * @param string $nodeTypeFilterString
- * @return array
*/
- protected function renderChildrenInformation(NodeInterface $node, string $nodeTypeFilterString): array
+ protected function renderChildrenInformation(NodeInterface $node): array
{
- $documentChildNodes = $node->getChildNodes($nodeTypeFilterString);
- // child nodes for content tree, must not include those nodes filtered out by `baseNodeType`
- $contentChildNodes = $node->getChildNodes($this->buildContentChildNodeFilterString());
- $childNodes = array_merge($documentChildNodes, $contentChildNodes);
+ $childNodes = $node->getChildNodes($this->buildNodeTypeFilterString([], [$this->ignoredNodeTypeRole]));
- $mapper = function (NodeInterface $childNode) {
+ return array_map(static function (NodeInterface $childNode) {
return [
'contextPath' => $childNode->getContextPath(),
- 'nodeType' => $childNode->getNodeType()->getName() // TODO: DUPLICATED; should NOT be needed!!!
+ 'nodeType' => $childNode->getNodeType()->getName(),
];
- };
-
- return array_map($mapper, $childNodes);
+ }, $childNodes);
}
/**
diff --git a/Resources/Private/Translations/nl/Main.xlf b/Resources/Private/Translations/nl/Main.xlf
index 8e8f595ad6..04c2d6dbcb 100644
--- a/Resources/Private/Translations/nl/Main.xlf
+++ b/Resources/Private/Translations/nl/Main.xlf
@@ -505,7 +505,7 @@
- Kopieer node type naar klembord
+ Kopieer node type naar klembord