/** * Builds the array of Menu items for this variant menu */ protected function buildItems() { $menuItems = []; $targetDimensionsToMatch = []; $allDimensionPresets = $this->configurationContentDimensionPresetSource->getAllPresets(); $includeAllPresets = $this->getIncludeAllPresets(); $pinnedDimensionValues = $this->getPresets(); $pinnedDimensionName = $this->getDimension(); if ($pinnedDimensionName !== null) { $targetDimensionsToMatch = $this->currentNode->getContext()->getTargetDimensions(); unset($targetDimensionsToMatch[$pinnedDimensionName]); } foreach ($this->contentDimensionCombinator->getAllAllowedCombinations() as $allowedCombination) { $targetDimensions = $this->calculateTargetDimensionsForCombination($allowedCombination); if ($pinnedDimensionName !== null && is_array($pinnedDimensionValues)) { if (!in_array($targetDimensions[$pinnedDimensionName], $pinnedDimensionValues)) { continue; } } // skip variants not matching the current target dimensions (except the dimension this menu covers) if ($targetDimensionsToMatch !== []) { foreach ($targetDimensionsToMatch as $dimensionName => $dimensionValue) { if ($targetDimensions[$dimensionName] !== $dimensionValue) { continue 2; } } } $nodeInDimensions = $this->getNodeInDimensions($allowedCombination, $targetDimensions); // no match, so we look further... if ($nodeInDimensions === null && $includeAllPresets) { $nodeInDimensions = $this->findAcceptableNode($allowedCombination, $allDimensionPresets); } if ($nodeInDimensions !== null && $this->isNodeHidden($nodeInDimensions)) { $nodeInDimensions = null; } // determine metadata for target dimensions of node array_walk($targetDimensions, function (&$dimensionValue, $dimensionName, $allDimensionPresets) use($pinnedDimensionName) { $dimensionValue = ['value' => $dimensionValue, 'label' => $allDimensionPresets[$dimensionName]['presets'][$dimensionValue]['label'], 'isPinnedDimension' => $pinnedDimensionName === null || $dimensionName == $pinnedDimensionName ? true : false]; }, $allDimensionPresets); if ($pinnedDimensionName === null) { $itemLabel = $nodeInDimensions->getLabel(); } else { $itemLabel = $targetDimensions[$pinnedDimensionName]['label']; } $menuItems[] = ['node' => $nodeInDimensions, 'state' => $this->calculateItemState($nodeInDimensions), 'label' => $itemLabel, 'dimensions' => $allowedCombination, 'targetDimensions' => $targetDimensions]; } // sort/limit according to configured "presets" if needed if ($pinnedDimensionName !== null && is_array($pinnedDimensionValues)) { $sortedMenuItems = []; foreach ($pinnedDimensionValues as $pinnedDimensionValue) { foreach ($menuItems as $menuItemKey => $menuItem) { if ($menuItem['targetDimensions'][$pinnedDimensionName]['value'] === $pinnedDimensionValue) { $sortedMenuItems[$menuItemKey] = $menuItem; } } } return $sortedMenuItems; } return $menuItems; }
/** * @param string $workspaceName * @param integer $limit * @param callable $callback * @return integer */ protected function indexWorkspace($workspaceName, $limit = null, callable $callback = null) { $count = 0; $combinations = $this->contentDimensionCombinator->getAllAllowedCombinations(); if ($combinations === []) { $count += $this->indexWorkspaceWithDimensions($workspaceName, [], $limit, $callback); } else { foreach ($combinations as $combination) { $count += $this->indexWorkspaceWithDimensions($workspaceName, $combination, $limit, $callback); } } return $count; }
/** * Generate missing URI path segments * * This generates URI path segment properties for all document nodes which don't have * a path segment set yet. * * @param string $workspaceName * @param boolean $dryRun * @return void */ public function generateUriPathSegments($workspaceName, $dryRun) { $baseContext = $this->createContext($workspaceName, []); $baseContextSitesNode = $baseContext->getNode(SiteService::SITES_ROOT_PATH); if (!$baseContextSitesNode) { $this->output->outputLine('<error>Could not find "' . SiteService::SITES_ROOT_PATH . '" root node</error>'); return; } $baseContextSiteNodes = $baseContextSitesNode->getChildNodes(); if ($baseContextSiteNodes === []) { $this->output->outputLine('<error>Could not find any site nodes in "' . SiteService::SITES_ROOT_PATH . '" root node</error>'); return; } foreach ($this->dimensionCombinator->getAllAllowedCombinations() as $dimensionCombination) { $flowQuery = new FlowQuery($baseContextSiteNodes); $siteNodes = $flowQuery->context(['dimensions' => $dimensionCombination, 'targetDimensions' => []])->get(); if (count($siteNodes) > 0) { $this->output->outputLine('Checking for nodes with missing URI path segment in dimension "%s"', array(trim(NodePaths::generateContextPath('', '', $dimensionCombination), '@;'))); foreach ($siteNodes as $siteNode) { $this->generateUriPathSegmentsForNode($siteNode, $dryRun); } } } $this->persistenceManager->persistAll(); }
/** * Performs checks for disallowed child nodes according to the node's auto-create configuration and constraints * and removes them if found. * * @param string $workspaceName * @param boolean $dryRun Simulate? * @return void */ protected function removeDisallowedChildNodes($workspaceName, $dryRun) { $this->output->outputLine('Checking for disallowed child nodes ...'); /** @var \Doctrine\ORM\QueryBuilder $queryBuilder */ $queryBuilder = $this->entityManager->createQueryBuilder(); /** @var \TYPO3\TYPO3CR\Domain\Model\Workspace $workspace */ $workspace = $this->workspaceRepository->findByIdentifier($workspaceName); $nodes = array(); $nodeExceptionCount = 0; $removeDisallowedChildNodes = function (NodeInterface $node) use(&$removeDisallowedChildNodes, &$nodes, &$nodeExceptionCount, $queryBuilder) { try { foreach ($node->getChildNodes() as $childNode) { /** @var $childNode NodeInterface */ if (!$childNode->isAutoCreated() && !$node->isNodeTypeAllowedAsChildNode($childNode->getNodeType())) { $nodes[] = $childNode; $parent = $node->isAutoCreated() ? $node->getParent() : $node; $this->output->outputLine('Found disallowed node named "%s" (%s) in "%s", child of node "%s" (%s)', array($childNode->getName(), $childNode->getNodeType()->getName(), $childNode->getPath(), $parent->getName(), $parent->getNodeType()->getName())); } else { $removeDisallowedChildNodes($childNode); } } } catch (\Exception $e) { $nodeExceptionCount++; } }; // TODO: Performance could be improved by a search for all child node data instead of looping over all contexts foreach ($this->contentDimensionCombinator->getAllAllowedCombinations() as $dimensionConfiguration) { $context = $this->createContext($workspace->getName(), $dimensionConfiguration); $removeDisallowedChildNodes($context->getRootNode()); } $disallowedChildNodesCount = count($nodes); if ($disallowedChildNodesCount > 0) { $this->output->outputLine(); if (!$dryRun) { $self = $this; $this->askBeforeExecutingTask('Do you want to remove all disallowed child nodes?', function () use($self, $nodes, $disallowedChildNodesCount, $workspaceName) { foreach ($nodes as $node) { $self->removeNodeAndChildNodesInWorkspaceByPath($node->getPath(), $workspaceName); } $self->output->outputLine('Removed %s disallowed node%s.', array($disallowedChildNodesCount, $disallowedChildNodesCount > 1 ? 's' : '')); }); } else { $this->output->outputLine('Found %s disallowed node%s to be removed.', array($disallowedChildNodesCount, $disallowedChildNodesCount > 1 ? 's' : '')); } if ($nodeExceptionCount > 0) { $this->output->outputLine(); $this->output->outputLine('%s error%s occurred during child node traversing.', array($nodeExceptionCount, $nodeExceptionCount > 1 ? 's' : '')); } $this->output->outputLine(); } }
/** * Generate missing URI path segments * * This generates URI path segment properties for all document nodes which don't have * a path segment set yet. * * @param string $workspaceName * @param boolean $dryRun * @return void */ public function generateUriPathSegments($workspaceName, $dryRun) { $baseContext = $this->createContext($workspaceName, []); $baseContextSiteNodes = $baseContext->getNode('/sites')->getChildNodes(); if ($baseContextSiteNodes === []) { return; } foreach ($this->dimensionCombinator->getAllAllowedCombinations() as $dimensionCombination) { $flowQuery = new FlowQuery($baseContextSiteNodes); $siteNodes = $flowQuery->context(['dimensions' => $dimensionCombination, 'targetDimensions' => []])->get(); if (count($siteNodes) > 0) { $this->output->outputLine('Searching for nodes with missing URI path segment in dimension "%s"', array(trim(NodePaths::generateContextPath('', '', $dimensionCombination), '@;'))); foreach ($siteNodes as $siteNode) { $this->generateUriPathSegmentsForNode($siteNode, $dryRun); } } } }
/** * Remove nodes with invalid dimension values * * This removes nodes which have dimension values not fitting to the current dimension configuration * * @param string $workspaceName Name of the workspace to consider * @param boolean $dryRun Simulate? * @return void */ public function removeNodesWithInvalidDimensions($workspaceName, $dryRun) { $this->output->outputLine('Checking for nodes with invalid dimensions ...'); $allowedDimensionCombinations = $this->contentDimensionCombinator->getAllAllowedCombinations(); $nodesArray = $this->collectNodesWithInvalidDimensions($workspaceName, $allowedDimensionCombinations); if ($nodesArray === []) { return; } if (!$dryRun) { $self = $this; $this->output->outputLine(); $this->output->outputLine('Nodes with invalid dimension values found.' . PHP_EOL . 'You might solve this by migrating them to your current dimension configuration or by removing them.'); $this->askBeforeExecutingTask(sprintf('Do you want to remove %s node%s with invalid dimensions now?', count($nodesArray), count($nodesArray) > 1 ? 's' : ''), function () use($self, $nodesArray, $workspaceName) { foreach ($nodesArray as $nodeArray) { $self->removeNode($nodeArray['identifier'], $nodeArray['dimensionsHash']); } $self->output->outputLine('Removed %s node%s with invalid dimension values.', array(count($nodesArray), count($nodesArray) > 1 ? 's' : '')); }); } else { $this->output->outputLine('Found %s node%s with invalid dimension values to be removed.', array(count($nodesArray), count($nodesArray) > 1 ? 's' : '')); } $this->output->outputLine(); }
/** * index this node, and add it to the current bulk request. * * @param NodeInterface $node * @param string $targetWorkspaceName In case this is triggered during publishing, a workspace name will be passed in * @return void * @throws \TYPO3\TYPO3CR\Search\Exception\IndexingException */ public function indexNode(NodeInterface $node, $targetWorkspaceName = null) { $indexer = function (NodeInterface $node, $targetWorkspaceName = null) { $contextPath = $node->getContextPath(); if ($this->settings['indexAllWorkspaces'] === false) { // we are only supposed to index the live workspace. // We need to check the workspace at two occasions; checking the // $targetWorkspaceName and the workspace name of the node's context as fallback if ($targetWorkspaceName !== null && $targetWorkspaceName !== 'live') { return; } if ($targetWorkspaceName === null && $node->getContext()->getWorkspaceName() !== 'live') { return; } } if ($targetWorkspaceName !== null) { $contextPath = str_replace($node->getContext()->getWorkspace()->getName(), $targetWorkspaceName, $contextPath); } $contextPathHash = sha1($contextPath); $nodeType = $node->getNodeType(); $mappingType = $this->getIndex()->findType(NodeTypeMappingBuilder::convertNodeTypeNameToMappingName($nodeType)); // Remove document with the same contextPathHash but different NodeType, required after NodeType change $this->logger->log(sprintf('NodeIndexer: Removing node %s from index (if node type changed from %s). ID: %s', $contextPath, $node->getNodeType()->getName(), $contextPathHash), LOG_DEBUG, null, 'ElasticSearch (CR)'); $this->getIndex()->request('DELETE', '/_query', [], json_encode(['query' => ['bool' => ['must' => ['ids' => ['values' => [$contextPathHash]]], 'must_not' => ['term' => ['_type' => NodeTypeMappingBuilder::convertNodeTypeNameToMappingName($node->getNodeType()->getName())]]]]])); if ($node->isRemoved()) { // TODO: handle deletion from the fulltext index as well $mappingType->deleteDocumentById($contextPathHash); $this->logger->log(sprintf('NodeIndexer: Removed node %s from index (node flagged as removed). ID: %s', $contextPath, $contextPathHash), LOG_DEBUG, null, 'ElasticSearch (CR)'); return; } $logger = $this->logger; $fulltextIndexOfNode = []; $nodePropertiesToBeStoredInIndex = $this->extractPropertiesAndFulltext($node, $fulltextIndexOfNode, function ($propertyName) use($logger, $contextPathHash) { $logger->log(sprintf('NodeIndexer (%s) - Property "%s" not indexed because no configuration found.', $contextPathHash, $propertyName), LOG_DEBUG, null, 'ElasticSearch (CR)'); }); $document = new ElasticSearchDocument($mappingType, $nodePropertiesToBeStoredInIndex, $contextPathHash); $documentData = $document->getData(); if ($targetWorkspaceName !== null) { $documentData['__workspace'] = $targetWorkspaceName; } $dimensionCombinations = $node->getContext()->getDimensions(); if (is_array($dimensionCombinations)) { $documentData['__dimensionCombinations'] = $dimensionCombinations; $documentData['__dimensionCombinationHash'] = md5(json_encode($dimensionCombinations)); } if ($this->isFulltextEnabled($node)) { if ($this->isFulltextRoot($node)) { // for fulltext root documents, we need to preserve the "__fulltext" field. That's why we use the // "update" API instead of the "index" API, with a custom script internally; as we // shall not delete the "__fulltext" part of the document if it has any. $this->currentBulkRequest[] = [['update' => ['_type' => $document->getType()->getName(), '_id' => $document->getId()]], ['script' => ' fulltext = (ctx._source.containsKey("__fulltext") ? ctx._source.__fulltext : new LinkedHashMap()); fulltextParts = (ctx._source.containsKey("__fulltextParts") ? ctx._source.__fulltextParts : new LinkedHashMap()); ctx._source = newData; ctx._source.__fulltext = fulltext; ctx._source.__fulltextParts = fulltextParts ', 'params' => ['newData' => $documentData], 'upsert' => $documentData, 'lang' => 'groovy']]; } else { // non-fulltext-root documents can be indexed as-they-are $this->currentBulkRequest[] = [['index' => ['_type' => $document->getType()->getName(), '_id' => $document->getId()]], $documentData]; } $this->updateFulltext($node, $fulltextIndexOfNode, $targetWorkspaceName); } $this->logger->log(sprintf('NodeIndexer: Added / updated node %s. ID: %s Context: %s', $contextPath, $contextPathHash, json_encode($node->getContext()->getProperties())), LOG_DEBUG, null, 'ElasticSearch (CR)'); }; $dimensionCombinations = $this->contentDimensionCombinator->getAllAllowedCombinations(); $workspaceName = $targetWorkspaceName ?: 'live'; $nodeIdentifier = $node->getIdentifier(); if ($dimensionCombinations !== []) { foreach ($dimensionCombinations as $combination) { $context = $this->contextFactory->create(['workspaceName' => $workspaceName, 'dimensions' => $combination]); $node = $context->getNodeByIdentifier($nodeIdentifier); if ($node !== null) { $indexer($node, $targetWorkspaceName); } } } else { $context = $this->contextFactory->create(['workspaceName' => $workspaceName]); $node = $context->getNodeByIdentifier($nodeIdentifier); if ($node !== null) { $indexer($node, $targetWorkspaceName); } } }
/** * @param string $workspaceName * @return void */ protected function indexWorkspace($workspaceName) { $combinations = $this->contentDimensionCombinator->getAllAllowedCombinations(); if ($combinations === array()) { $this->indexWorkspaceWithDimensions($workspaceName); } else { foreach ($combinations as $combination) { $this->indexWorkspaceWithDimensions($workspaceName, $combination); } } }