/** * Filter a node by the current context. * Will either return the node or NULL if it is not permitted in current context. * * @param NodeInterface $node * @param Context $context * @return \TYPO3\TYPO3CR\Domain\Model\Node|NULL */ protected function filterNodeByContext(NodeInterface $node, Context $context) { if (!$context->isRemovedContentShown() && $node->isRemoved()) { return NULL; } if (!$context->isInvisibleContentShown() && !$node->isVisible()) { return NULL; } if (!$context->isInaccessibleContentShown() && !$node->isAccessible()) { return NULL; } return $node; }
/** * Move the given node instance to the target workspace * * If no target node variant (having the same dimension values) exists in the target workspace, the node that * is published will be used as a new node variant in the target workspace. * * @param NodeInterface $node The node to publish * @param Workspace $targetWorkspace The workspace to publish to * @return void */ protected function moveNodeVariantToTargetWorkspace(NodeInterface $node, Workspace $targetWorkspace) { $nodeData = $node->getNodeData(); $movedShadowNodeData = $this->nodeDataRepository->findOneByMovedTo($nodeData); if ($movedShadowNodeData instanceof NodeData && $movedShadowNodeData->isRemoved()) { $this->nodeDataRepository->remove($movedShadowNodeData); } if ($targetWorkspace->getBaseWorkspace() === null && $node->isRemoved()) { $this->nodeDataRepository->remove($nodeData); } else { $nodeData->setWorkspace($targetWorkspace); $nodeData->setLastPublicationDateTime($this->now); $this->nodeService->cleanUpProperties($node); } $node->setNodeDataIsMatchingContext(null); }
/** * 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) { $contextPath = $node->getContextPath(); 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->getIndex()->request('DELETE', '/_query', array(), json_encode(['query' => ['bool' => ['must' => ['ids' => ['values' => [$contextPathHash]]], 'must_not' => ['term' => ['_type' => str_replace('.', '/', $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 = array(); $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; } 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[] = array(array('update' => array('_type' => $document->getType()->getName(), '_id' => $document->getId())), array('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' => array('newData' => $documentData), 'upsert' => $documentData, 'lang' => 'groovy')); } else { // non-fulltext-root documents can be indexed as-they-are $this->currentBulkRequest[] = array(array('index' => array('_type' => $document->getType()->getName(), '_id' => $document->getId())), $documentData); } $this->updateFulltext($node, $fulltextIndexOfNode, $targetWorkspaceName); } $this->logger->log(sprintf('NodeIndexer: Added / updated node %s. ID: %s', $contextPath, $contextPathHash), LOG_DEBUG, NULL, 'ElasticSearch (CR)'); }
/** * Collects CSS class names used for styling editable elements in the Neos backend. * * @param NodeInterface $node * @return array */ protected function collectEditingClassNames(NodeInterface $node) { $classNames = []; if ($node->getNodeType()->isOfType('TYPO3.Neos:ContentCollection')) { // This is needed since the backend relies on this class (should not be necessary) $classNames[] = 'neos-contentcollection'; } else { $classNames[] = 'neos-contentelement'; } if ($node->isRemoved()) { $classNames[] = 'neos-contentelement-removed'; } if ($node->isHidden()) { $classNames[] = 'neos-contentelement-hidden'; } if ($this->isInlineEditable($node) === false) { $classNames[] = 'neos-not-inline-editable'; } return $classNames; }
/** * 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); } } }
/** * Filter a node by the current context. * Will either return the node or NULL if it is not permitted in current context. * * @param NodeInterface $node * @param Context $context * @return \TYPO3\TYPO3CR\Domain\Model\NodeInterface|NULL */ protected function filterNodeByContext(NodeInterface $node, Context $context) { $this->securityContext->withoutAuthorizationChecks(function () use(&$node, $context) { if (!$context->isRemovedContentShown() && $node->isRemoved()) { $node = null; return; } if (!$context->isInvisibleContentShown() && !$node->isVisible()) { $node = null; return; } if (!$context->isInaccessibleContentShown() && !$node->isAccessible()) { $node = null; } }); return $node; }
/** * Remove all properties not configured in the current Node Type. * This will not do anything on Nodes marked as removed as those could be queued up for deletion * which contradicts updates (that would be necessary to remove the properties). * * @param NodeInterface $node * @return void */ public function cleanUpProperties(NodeInterface $node) { if ($node->isRemoved() === false) { $nodeData = $node->getNodeData(); $nodeTypeProperties = $node->getNodeType()->getProperties(); foreach ($node->getProperties() as $name => $value) { if (!isset($nodeTypeProperties[$name])) { $nodeData->removeProperty($name); } } } }
/** * Replace the node data of a node instance with a given target node data * * The node data of the node that is published will be removed and the existing node data inside the target * workspace is updated to the changes and will be injected into the node instance. If the node was marked as * removed, both node data are removed. * * @param NodeInterface $node The node instance with node data to be published * @param NodeData $targetNodeData The existing node data in the target workspace * @return void */ protected function replaceNodeData(NodeInterface $node, NodeData $targetNodeData) { $sourceNodeData = $node->getNodeData(); if ($node->isRemoved() === TRUE) { $this->nodeDataRepository->remove($targetNodeData); } else { $targetNodeData->similarize($node->getNodeData()); $targetNodeData->setPath($node->getPath(), FALSE); $node->setNodeData($targetNodeData); } $this->nodeDataRepository->remove($sourceNodeData); }
/** * Wrap the $content identified by $node with the needed markup for * the backend. * $parameters can be used to further pass parameters to the content element. * * @param \TYPO3\TYPO3CR\Domain\Model\NodeInterface $node * @param string $typoscriptPath * @param string $content * @param boolean $isPage * @return string */ public function wrapContentObject(\TYPO3\TYPO3CR\Domain\Model\NodeInterface $node, $typoscriptPath, $content, $isPage = FALSE) { $contentType = $node->getContentType(); $tagBuilder = new \TYPO3\Fluid\Core\ViewHelper\TagBuilder('div'); $tagBuilder->forceClosingTag(TRUE); if (!$node->isRemoved()) { $tagBuilder->setContent($content); } if (!$isPage) { $cssClasses = array('t3-contentelement'); $cssClasses[] = str_replace(array(':', '.'), '-', strtolower($contentType->getName())); if ($node->isHidden()) { $cssClasses[] = 't3-contentelement-hidden'; } if ($node->isRemoved()) { $cssClasses[] = 't3-contentelement-removed'; } $tagBuilder->addAttribute('class', implode(' ', $cssClasses)); $tagBuilder->addAttribute('id', 'c' . $node->getIdentifier()); } try { $this->accessDecisionManager->decideOnResource('TYPO3_TYPO3_Backend_BackendController'); } catch (\TYPO3\FLOW3\Security\Exception\AccessDeniedException $e) { return $tagBuilder->render(); } $tagBuilder->addAttribute('typeof', 'typo3:' . $contentType->getName()); $tagBuilder->addAttribute('about', $node->getContextPath()); $this->addScriptTag($tagBuilder, '__workspacename', $node->getWorkspace()->getName()); $this->addScriptTag($tagBuilder, '_removed', $node->isRemoved() ? 'true' : 'false', 'boolean'); $this->addScriptTag($tagBuilder, '_typoscriptPath', $typoscriptPath); foreach ($contentType->getProperties() as $propertyName => $propertyConfiguration) { $dataType = isset($propertyConfiguration['type']) ? $propertyConfiguration['type'] : 'string'; if ($propertyName[0] === '_') { $propertyValue = \TYPO3\FLOW3\Reflection\ObjectAccess::getProperty($node, substr($propertyName, 1)); } else { $propertyValue = $node->getProperty($propertyName); } // Serialize boolean values to String if (isset($propertyConfiguration['type']) && $propertyConfiguration['type'] === 'boolean') { $propertyValue = $propertyValue ? 'true' : 'false'; } // Serialize date values to String if ($propertyValue !== NULL && isset($propertyConfiguration['type']) && $propertyConfiguration['type'] === 'date') { $propertyValue = $propertyValue->format('Y-m-d'); } // Serialize objects to JSON strings if (is_object($propertyValue) && $propertyValue !== NULL && isset($propertyConfiguration['type']) && $this->objectManager->isRegistered($propertyConfiguration['type'])) { $gettableProperties = \TYPO3\FLOW3\Reflection\ObjectAccess::getGettableProperties($propertyValue); $convertedProperties = array(); foreach ($gettableProperties as $key => $value) { if (is_object($value)) { $entityIdentifier = $this->persistenceManager->getIdentifierByObject($value); if ($entityIdentifier !== NULL) { $value = $entityIdentifier; } } $convertedProperties[$key] = $value; } $propertyValue = json_encode($convertedProperties); $dataType = 'jsonEncoded'; } $this->addScriptTag($tagBuilder, $propertyName, $propertyValue, $dataType); } if (!$isPage) { // add CSS classes $this->addScriptTag($tagBuilder, '__contenttype', $contentType->getName()); } else { $tagBuilder->addAttribute('id', 't3-page-metainformation'); $tagBuilder->addAttribute('data-__sitename', $this->nodeRepository->getContext()->getCurrentSite()->getName()); $tagBuilder->addAttribute('data-__siteroot', sprintf('/sites/%s@%s', $this->nodeRepository->getContext()->getCurrentSite()->getNodeName(), $this->nodeRepository->getContext()->getWorkspace()->getName())); } return $tagBuilder->render(); }