diff --git a/Classes/Command/ErrorPageCommandController.php b/Classes/Command/ErrorPageCommandController.php index b4af932..45d58fc 100644 --- a/Classes/Command/ErrorPageCommandController.php +++ b/Classes/Command/ErrorPageCommandController.php @@ -5,22 +5,27 @@ use GuzzleHttp\Client; use GuzzleHttp\Psr7\StreamWrapper; -use GuzzleHttp\Psr7\Uri; -use Neos\ContentRepository\Domain\Model\NodeInterface; -use Neos\ContentRepository\Domain\Service\Context; +use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; +use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\Cli\CommandController; use Neos\Flow\Core\Bootstrap; -use Neos\Neos\Controller\Exception\NodeNotFoundException; -use Neos\Neos\Domain\Model\Domain; use Neos\Neos\Domain\Model\Site; use Neos\Neos\Domain\Repository\SiteRepository; -use Neos\Neos\Domain\Service\ContentContextFactory; -use Neos\Neos\Service\LinkingService; +use Neos\Neos\FrontendRouting\Exception\NodeNotFoundException; +use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; +use Neos\Neos\FrontendRouting\Options; +use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; use Neos\Utility\Files; use Netlogix\ErrorHandler\Configuration\ErrorHandlerConfiguration; -use Netlogix\ErrorHandler\Service\ControllerContextFactory; +use Netlogix\ErrorHandler\Service\ActionRequestFactory; use Netlogix\ErrorHandler\Service\DestinationResolver; +use Psr\Http\Message\UriInterface; use RuntimeException; use Symfony\Component\Yaml\Yaml; @@ -35,47 +40,26 @@ class ErrorPageCommandController extends CommandController { - /** - * @Flow\Inject - * @var Bootstrap - */ - protected $bootstrap; + #[Flow\Inject] + protected Bootstrap $bootstrap; - /** - * @Flow\Inject - * @var ErrorHandlerConfiguration - */ - protected $configuration; + #[Flow\Inject] + protected ErrorHandlerConfiguration $configuration; - /** - * @Flow\Inject - * @var DestinationResolver - */ - protected $destinationResolver; + #[Flow\Inject] + protected DestinationResolver $destinationResolver; - /** - * @Flow\Inject - * @var SiteRepository - */ - protected $siteRepository; + #[Flow\Inject] + protected SiteRepository $siteRepository; - /** - * @Flow\Inject - * @var ContentContextFactory - */ - protected $contentContextFactory; + #[Flow\Inject] + protected NodeUriBuilderFactory $nodeUriBuilderFactory; - /** - * @Flow\Inject - * @var LinkingService - */ - protected $linkingService; + #[Flow\Inject] + protected ContentRepositoryRegistry $contentRepositoryRegistry; - /** - * @Flow\Inject - * @var ControllerContextFactory - */ - protected $controllerContextFactory; + #[Flow\Inject] + protected ActionRequestFactory $actionRequestFactory; /** * Generate Error Pages for configured Sites @@ -144,7 +128,7 @@ public function generateCommand(bool $verbose = false) * @param bool $verbose * @throws \Exception */ - public function showConfigurationCommand() + public function showConfigurationCommand(): void { $result = [ 'Netlogix' => [ @@ -156,73 +140,35 @@ public function showConfigurationCommand() $this->output(Yaml::dump($result, 10, 2)); } - /** - * @param Site $site - * @param array $configuration - * @return Uri - * @throws NodeNotFoundException - */ - protected function getSiteUri(Site $site, array $configuration) + protected function getSiteUri(Site $site, array $configuration): UriInterface { $domain = $site->getPrimaryDomain(); if (!$domain) { throw new RuntimeException(sprintf('Site %s has no primary domain', $site->getNodeName()), 1708944032); } - $context = $this->getContextForSite($site, $domain, $configuration['dimensions']); - $source = $context->getNodeByIdentifier(substr($configuration['source'], 1)); + $contentRepository = $this->contentRepositoryRegistry->get($site->getConfiguration()->contentRepositoryId); + $contentGraph = $contentRepository->getContentGraph(WorkspaceName::fromString(WorkspaceName::WORKSPACE_NAME_LIVE)); - if (!$source instanceof NodeInterface) { - throw new NodeNotFoundException('Could not get source node in site ' . $site->getNodeName(), 1552492278); - } + $nodeAggregate = $contentGraph->findNodeAggregateById(NodeAggregateId::fromString(substr($configuration['source'], 1))); + $dimensionSpacePoint = DimensionSpacePoint::fromJsonString(json_encode($configuration['dimensions'])); + + $source = $nodeAggregate->getNodeByCoveredDimensionSpacePoint($dimensionSpacePoint); if (!$this->isNodeVisible($source)) { - throw new NodeNotFoundException('Node for site ' . $site->getNodeName() . ' is not visible', 1552492532); + throw new NodeNotFoundException('Node ' . $source->aggregateId . ' is not visible', 1552492532); } - $controllerContext = $this->controllerContextFactory->buildControllerContext(new Uri($domain->__toString())); - $nodeUri = $this->linkingService->createNodeUri($controllerContext, $source, null, null, true); + $siteDetectionResult = SiteDetectionResult::create($site->getNodeName(), $contentRepository->id); + $actionRequest = $this->actionRequestFactory->buildActionRequest($siteDetectionResult); + $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest($actionRequest); - return new Uri($nodeUri); + return $nodeUriBuilder->uriFor(NodeAddress::fromNode($source), Options::createForceAbsolute()); } - /** - * @param NodeInterface $node - * @return bool - */ - protected function isNodeVisible(NodeInterface $node): bool + protected function isNodeVisible(Node $node): bool { - $currentNode = $node; - - do { - if (!$currentNode->isVisible()) { - return false; - } - - $currentNode = $currentNode->getParent(); - } while ($currentNode !== null); - - return true; - } - - /** - * @param Site $site - * @param Domain $domain - * @param array $dimensions - * @return Context - */ - protected function getContextForSite(Site $site, Domain $domain, array $dimensions): Context - { - return $this->contentContextFactory->create([ - 'workspaceName' => 'live', - 'targetDimensions' => array_map(function (array $dimensionValues) { - return current($dimensionValues); - }, $dimensions), - 'dimensions' => $dimensions, - 'currentSite' => $site, - 'currentDomain' => $domain, - 'invisibleContentShown' => true - ]); + return !$node->tags->contain(SubtreeTag::disabled()); } } diff --git a/Classes/Configuration/ErrorHandlerConfiguration.php b/Classes/Configuration/ErrorHandlerConfiguration.php index 6caeeca..b9d0e91 100644 --- a/Classes/Configuration/ErrorHandlerConfiguration.php +++ b/Classes/Configuration/ErrorHandlerConfiguration.php @@ -5,18 +5,21 @@ namespace Netlogix\ErrorHandler\Configuration; use Generator; -use Neos\ContentRepository\Domain\Service\ContextFactoryInterface; +use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\Eel\CompilingEvaluator; -use Neos\Eel\Utility as EelUtility; use Neos\Flow\Annotations as Flow; +use Neos\Flow\Mvc\Routing\Dto\RouteParameters; +use Neos\Flow\ObjectManagement\ObjectManagerInterface; use Neos\Neos\Domain\Model\Site; -use Neos\Neos\Service\LinkingService; +use Neos\Neos\FrontendRouting\DimensionResolution\DimensionResolverFactoryInterface; +use Neos\Neos\FrontendRouting\DimensionResolution\RequestToDimensionSpacePointContext; +use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; +use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\UriInterface; use function array_filter; use function array_values; use function current; -use function explode; use function in_array; use function iterator_to_array; @@ -28,40 +31,37 @@ * source: string, * destination: string, * dimensionPathSegment?: string, - * pathPrefixes?: string[] + * pathPrefixes?: string[], + * position: int * } */ class ErrorHandlerConfiguration { - /** - * @Flow\Inject(lazy=false) - * @var CompilingEvaluator - */ - protected $eelEvaluator; - - /** - * @Flow\Inject(lazy=false) - * @var ContextFactoryInterface - */ - protected ContextFactoryInterface $contextFactory; - - /** - * @Flow\Inject(lazy=false) - * @var LinkingService - */ - protected LinkingService $linkingService; + #[Flow\Inject] + protected CompilingEvaluator $eelEvaluator; - /** - * @Flow\Inject(lazy=false) - * @var NodeBasedConfiguration - */ + #[Flow\Inject] protected NodeBasedConfiguration $nodeBasedConfiguration; - /** - * @Flow\Inject(lazy=false) - * @var SettingsBasedConfiguration - */ - protected SettingsBasedConfiguration $settingsBasedConfiguration; + #[Flow\Inject] + protected ObjectManagerInterface $objectManager; + + private function resolveDimensionSpacePointFromRequest(Site $site, string $requestPath): DimensionSpacePoint + { + $siteConfiguration = $site->getConfiguration(); + $dimensionResolverFactory = $this->objectManager->get( + $siteConfiguration->contentDimensionResolverFactoryClassName + ); + assert($dimensionResolverFactory instanceof DimensionResolverFactoryInterface); + $dimensionResolver = $dimensionResolverFactory->create($siteConfiguration->contentRepositoryId, $siteConfiguration); + $siteDetectionResult = SiteDetectionResult::create($site->getNodeName(), $siteConfiguration->contentRepositoryId); + $routeParameters = $siteDetectionResult->storeInRouteParameters(RouteParameters::createEmpty()); + + $dimensionResolverContext = RequestToDimensionSpacePointContext::fromUriPathAndRouteParametersAndResolvedSite($requestPath, $routeParameters, $site); + $dimensionResolverContext = $dimensionResolver->fromRequestToDimensionSpacePoint($dimensionResolverContext); + + return $dimensionResolverContext->resolvedDimensionSpacePoint; + } /** * @@ -77,12 +77,12 @@ class ErrorHandlerConfiguration */ public function findConfigurationForSite( Site $site, - UriInterface $uri, + ServerRequestInterface $request, int $statusCode ) { - $siteName = $site->getNodeName(); - $requestPath = ltrim($uri->getPath() ?? '', '/'); - $requestedDimensionPathSegment = current(explode('/', $requestPath, 2)); + $siteName = (string)$site->getNodeName(); + $requestPath = ltrim($request->getUri()->getPath() ?? '', '/'); + $dimensionSpacePoint = $this->resolveDimensionSpacePointFromRequest($site, $requestPath); $configurationsForSite = $this->getConfiguration(); $configurationsForSite = array_key_exists( @@ -90,62 +90,30 @@ public function findConfigurationForSite( $configurationsForSite ) ? $configurationsForSite[$siteName] : []; - $matchingStatusCodes = array_filter($configurationsForSite, + $configurationsForSite = array_filter($configurationsForSite, function (array $configuration) use ($statusCode) { return in_array($statusCode, $configuration['matchingStatusCodes'] ?? [], true); }); - $matchingDimensions = array_filter($matchingStatusCodes, - function (array $configuration) use ($requestedDimensionPathSegment) { - $dimensionPathSegment = $configuration['dimensionPathSegment'] ?? ''; - if ($dimensionPathSegment === '' && empty($configuration['dimensions'] ?? [])) { - return true; - } - - return $dimensionPathSegment === $requestedDimensionPathSegment; - }); - - $configurationWithPathPrefixes = array_filter($matchingDimensions, - function (array $configuration) { - return !empty($configuration['pathPrefixes'] ?? []); - }); + $configurationsForSite = array_filter($configurationsForSite, + function (array $configuration) use ($dimensionSpacePoint) { + $configuredDimensionSpacePoint = DimensionSpacePoint::fromArray($configuration['dimensions']); - $configurationsWithoutPathPrefixes = array_filter($matchingDimensions, - function (array $configuration) { - return empty($configuration['pathPrefixes'] ?? []); + return $configuredDimensionSpacePoint->equals($dimensionSpacePoint); }); - if (!empty($configurationWithPathPrefixes)) { - $matchingPathPrefixes = array_filter($matchingDimensions, - function (array $configuration) use ($requestPath) { - foreach ($configuration['pathPrefixes'] ?? [] as $pathPrefix) { - if (strpos($requestPath, ltrim($pathPrefix, '/')) === 0) { - return true; - } + $configurationsForSite = array_filter($configurationsForSite, + function (array $configuration) use ($requestPath) { + foreach ($configuration['pathPrefixes'] ?? [] as $pathPrefix) { + if (strpos($requestPath, ltrim($pathPrefix, '/')) === 0) { + return true; } + } - return false; - }); - - if (empty($matchingPathPrefixes)) { - return current($configurationsWithoutPathPrefixes); - } - - return current($matchingPathPrefixes); - } - - return current($configurationsWithoutPathPrefixes) ?: current($matchingStatusCodes) ?: null; - } + return false; + }); - /** - * @param string $expression - * @param array $context - * @return mixed - * @throws \Neos\Eel\Exception - */ - protected function evaluateEelExpression(string $expression, array $context) - { - return EelUtility::evaluateEelExpression($expression, $this->eelEvaluator, $context, []); + return current($configurationsForSite) ?: null; } /** @@ -162,11 +130,10 @@ public function getConfiguration() private function generateConfiguration(): Generator { $nodeBased = $this->nodeBasedConfiguration->getConfiguration(); - $settingsBased = $this->settingsBasedConfiguration->getConfiguration(); - $sites = array_keys($nodeBased + $settingsBased); + $sites = array_keys($nodeBased); foreach ($sites as $site) { - yield $site => array_values(array_merge(($nodeBased[$site] ?? []), ($settingsBased[$site] ?? []))); + yield $site => array_values($nodeBased[$site] ?? []); } } diff --git a/Classes/Configuration/NodeBasedConfiguration.php b/Classes/Configuration/NodeBasedConfiguration.php index 1730c62..216f713 100644 --- a/Classes/Configuration/NodeBasedConfiguration.php +++ b/Classes/Configuration/NodeBasedConfiguration.php @@ -6,18 +6,26 @@ use Exception; use Generator; -use GuzzleHttp\Psr7\Uri; -use Neos\ContentRepository\Domain\Model\NodeInterface; -use Neos\ContentRepository\Domain\Service\ContentDimensionCombinator; -use Neos\ContentRepository\Domain\Service\ContentDimensionPresetSourceInterface; -use Neos\ContentRepository\Domain\Service\ContextFactoryInterface; +use Neos\ContentRepository\Core\ContentRepository; +use Neos\ContentRepository\Core\Dimension\ContentDimension; +use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindRootNodeAggregatesFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; +use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Eel\FlowQuery\FlowQuery; use Neos\Flow\Annotations as Flow; use Neos\Flow\Log\ThrowableStorageInterface; -use Neos\Neos\Domain\Service\ContentContext; -use Neos\Neos\Service\LinkingService; -use Netlogix\ErrorHandler\Service\ControllerContextFactory; +use Neos\Neos\Domain\Model\Site; +use Neos\Neos\Domain\Model\SiteNodeName; +use Neos\Neos\Domain\Repository\SiteRepository; +use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; +use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; +use Netlogix\ErrorHandler\Service\ActionRequestFactory; use RuntimeException; use function array_map; @@ -33,46 +41,23 @@ */ class NodeBasedConfiguration { - /** - * @Flow\Inject(lazy=false) - * @var ContextFactoryInterface - */ - protected ContextFactoryInterface $contextFactory; - - /** - * @Flow\Inject(lazy=false) - * @var LinkingService - */ - protected LinkingService $linkingService; + #[Flow\Inject] + protected ThrowableStorageInterface $throwableStorage; - /** - * @Flow\Inject(lazy=false) - * @var ControllerContextFactory - */ - protected ControllerContextFactory $controllerContextFactory; + #[Flow\InjectConfiguration(path: 'destination')] + protected string $destination; - /** - * @Flow\Inject(lazy=false) - * @var ContentDimensionPresetSourceInterface - */ - protected ContentDimensionPresetSourceInterface $contentDimensionPresetSource; + #[Flow\Inject] + protected ContentRepositoryRegistry $contentRepositoryRegistry; - /** - * @Flow\Inject(lazy=false) - * @var ContentDimensionCombinator - */ - protected ContentDimensionCombinator $contentDimensionCombinator; + #[Flow\Inject] + protected NodeUriBuilderFactory $nodeUriBuilderFactory; - /** - * @Flow\Inject(lazy=false) - * @var ThrowableStorageInterface - */ - protected ThrowableStorageInterface $throwableStorage; + #[Flow\Inject] + protected SiteRepository $siteRepository; - /** - * @Flow\InjectConfiguration(path="destination") - */ - protected string $destination; + #[Flow\Inject] + protected ActionRequestFactory $actionRequestFactory; /** * @return array @@ -81,11 +66,12 @@ public function getConfiguration(): array { $configurationsForSite = []; $siteNodes = $this->getSiteNodes(); - $siteNodes = iterator_to_array($siteNodes, false); - foreach ($siteNodes as $siteNode) { - assert($siteNode instanceof NodeInterface); - $sitename = (string)$siteNode->getNodeName(); + foreach ($siteNodes as $siteNodeName => $siteNode) { + assert($siteNodeName instanceof SiteNodeName); + assert($siteNode instanceof Node); + + $sitename = (string)$siteNode->name; $configurationsForSite[$sitename] = $configurationsForSite[$sitename] ?? []; $errorNodes = FlowQuery::q($siteNode) @@ -93,14 +79,15 @@ public function getConfiguration(): array ->get(); foreach ($errorNodes as $errorNode) { - assert($errorNode instanceof NodeInterface); + assert($errorNode instanceof Node); try { - $errorNodeConfiguration = $this->getErrorNodeConfiguration($errorNode); + $errorNodeConfiguration = $this->getErrorNodeConfiguration($siteNodeName, $errorNode); $configurationsForSite[$sitename][json_encode($errorNodeConfiguration)] = $errorNodeConfiguration; } catch (Exception $e) { // This is used for creating error pages. // One faulty error page config must not prevent other error pages from being rendered. $this->throwableStorage->logThrowable($e); + continue; } } @@ -108,12 +95,12 @@ public function getConfiguration(): array $configurationsForSite[$sitename] = array_values($configurationsForSite[$sitename]); } - return $configurationsForSite; + return array_filter($configurationsForSite); } - public function getErrorNodeConfiguration(NodeInterface $errorNode): array + public function getErrorNodeConfiguration(SiteNodeName $siteNodeName, Node $errorNode): array { - $pathPrefixes = $this->extractPathPrefixes($errorNode); + $pathPrefixes = $this->extractPathPrefixes($siteNodeName, $errorNode); $position = 1000; foreach ($pathPrefixes as $pathPrefix) { $position -= strlen($pathPrefix); @@ -121,9 +108,8 @@ public function getErrorNodeConfiguration(NodeInterface $errorNode): array return [ 'matchingStatusCodes' => $this->extractStatusCodes($errorNode), - 'dimensions' => $this->extractDimensions($errorNode), - 'dimensionPathSegment' => $this->extractDimensionsPathSegment($errorNode), - 'source' => '#' . $errorNode->getIdentifier(), + 'dimensions' => json_decode($errorNode->dimensionSpacePoint->toJson(), true), + 'source' => '#' . $errorNode->aggregateId, 'destination' => $this->destination, 'pathPrefixes' => $pathPrefixes, 'position' => $position, @@ -134,40 +120,77 @@ public function getErrorNodeConfiguration(NodeInterface $errorNode): array * Returns all NodeInterface Site node objects currently * known to the ContentRepository context. * - * @return Generator + * @return Generator */ - protected function getSiteNodes() + protected function getSiteNodes(): iterable { - $currentContexts = $this->getContentContexts(); - foreach ($currentContexts as $contextContext) { - $rootNode = $contextContext->getRootNode(); - $sitesCollections = $rootNode->findChildNodes(); - foreach ($sitesCollections as $sitesCollection) { - yield from $sitesCollection->findChildNodes(); + $sites = $this->siteRepository->findOnline(); + + foreach ($sites as $site) { + assert($site instanceof Site); + + $contentRepository = $this->contentRepositoryRegistry->get($site->getConfiguration()->contentRepositoryId); + $contentGraph = $contentRepository->getContentGraph(WorkspaceName::fromString(WorkspaceName::WORKSPACE_NAME_LIVE)); + $rootNodeAggregates = $contentGraph->findRootNodeAggregates(FindRootNodeAggregatesFilter::create(nodeTypeName: 'Neos.Neos:Sites')); + + if ($rootNodeAggregates->count() !== 1) { + throw new RuntimeException('Expected exactly one root node aggregate for the site.', 1730410697); + } + $rootNodeAggregate = $rootNodeAggregates->first(); + + foreach ($this->combineAllDimensionSpacePoints($contentRepository) as $dimensionSpacePoint) { + $subgraph = $contentGraph->getSubgraph( + $dimensionSpacePoint, + VisibilityConstraints::frontend() + ); + + $children = $subgraph->findChildNodes( + $rootNodeAggregate->nodeAggregateId, + FindChildNodesFilter::create( + nodeTypes: 'Neos.Neos:Site' + ) + ); + + foreach ($children as $child) { + yield $site->getNodeName() => $child; + } } } } /** - * Map every dimension combination to a corresponding content context. - * - * @return ContentContext[] + * @param ContentRepository $contentRepository + * @return iterable */ - protected function getContentContexts(): array + protected function combineAllDimensionSpacePoints(ContentRepository $contentRepository): iterable { - $dimensionCombinations = $this->contentDimensionCombinator->getAllAllowedCombinations(); - foreach ($dimensionCombinations as $dimensionCombination) { - $context = $this->contextFactory->create(['dimensions' => $dimensionCombination]); - assert($context instanceof ContentContext); - $contentContextsByDimension[json_encode($dimensionCombination)] = $context; + $contentDimensions = $contentRepository->getContentDimensionSource()->getContentDimensionsOrderedByPriority(); + $combinations = []; + + foreach ($contentDimensions as $contentDimension) { + assert($contentDimension instanceof ContentDimension); + + foreach ($contentDimension->values as $contentDimensionValue) { + foreach ($contentDimensions as $otherContentDimension) { + foreach ($otherContentDimension->values as $otherContentDimensionValue) { + $coordinates = array_merge( + [(string)$contentDimension->id->jsonSerialize() => $contentDimensionValue->value], + [(string)$otherContentDimension->id->jsonSerialize() => $otherContentDimensionValue->value] + ); + ksort($coordinates); + $combinations[sha1(join('', $coordinates))] = DimensionSpacePoint::fromArray($coordinates); + } + } + } } - return array_values($contentContextsByDimension); + + return array_values($combinations); } /** * @return int[] */ - protected function extractStatusCodes(NodeInterface $errorNode): array + protected function extractStatusCodes(Node $errorNode): array { $matchingStatusCodes = $errorNode->getProperty('matchingStatusCodes'); if (is_array($matchingStatusCodes) && count($matchingStatusCodes) > 0) { @@ -181,32 +204,20 @@ protected function extractStatusCodes(NodeInterface $errorNode): array } /** - * @return array + * @return string[] */ - protected function extractDimensions(NodeInterface $errorNode): array + protected function extractPathPrefixes(SiteNodeName $siteNodeName, Node $errorNode): array { - // @phpstan-ignore-next-line - return $errorNode->getDimensions(); - } + $siteDetectionResult = SiteDetectionResult::create( + $siteNodeName, + $errorNode->contentRepositoryId + ); - protected function extractDimensionsPathSegment(NodeInterface $errorNode): string - { - $result = []; - foreach ($errorNode->getDimensions() as $singleDimensionValues) { - foreach ($singleDimensionValues as $singleDimensionValue) { - $result[] = $singleDimensionValue; - } - } - return join('-', $result); - } + $actionRequest = $this->actionRequestFactory->buildActionRequest($siteDetectionResult); + + $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest($actionRequest); + $errorUrl = $nodeUriBuilder->uriFor(NodeAddress::fromNode($errorNode)); - /** - * @return string[] - */ - protected function extractPathPrefixes(NodeInterface $errorNode): array - { - $controllerContext = $this->controllerContextFactory->buildControllerContext(new Uri()); - $errorUrl = new Uri($this->linkingService->createNodeUri($controllerContext, $errorNode)); return [dirname($errorUrl->getPath())]; } } diff --git a/Classes/Configuration/SettingsBasedConfiguration.php b/Classes/Configuration/SettingsBasedConfiguration.php deleted file mode 100644 index 465dbd6..0000000 --- a/Classes/Configuration/SettingsBasedConfiguration.php +++ /dev/null @@ -1,29 +0,0 @@ - - */ - protected $pages; - - /** - * @return array - */ - public function getConfiguration(): array - { - // @phpstan-ignore-next-line - return $this->pages ?? []; - } -} diff --git a/Classes/DataSource/ErrorPageView.php b/Classes/DataSource/ErrorPageView.php index 6bbb56f..a61ad91 100644 --- a/Classes/DataSource/ErrorPageView.php +++ b/Classes/DataSource/ErrorPageView.php @@ -4,7 +4,7 @@ namespace Netlogix\ErrorHandler\DataSource; -use Neos\ContentRepository\Domain\Model\NodeInterface; +use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\Neos\Domain\Model\Domain; use Neos\Neos\Domain\Repository\DomainRepository; use Neos\Neos\Service\DataSource\DataSourceInterface; @@ -14,25 +14,16 @@ final class ErrorPageView implements DataSourceInterface { - /** - * @var NodeBasedConfiguration - * @Flow\Inject - */ - protected $nodeBasedConfiguration; - - /** - * @var DomainRepository - * @Flow\Inject - */ - protected $domainRepository; - - /** - * @var DestinationResolver - * @Flow\Inject - */ - protected $destinationResolver; - - public function getData(NodeInterface $node = null, array $arguments = []): array + #[Flow\Inject] + protected NodeBasedConfiguration $nodeBasedConfiguration; + + #[Flow\Inject] + protected DomainRepository $domainRepository; + + #[Flow\Inject] + protected DestinationResolver $destinationResolver; + + public function getData(Node $node = null, array $arguments = []): array { if (!$node) { return []; @@ -41,14 +32,15 @@ public function getData(NodeInterface $node = null, array $arguments = []): arra if (!($domain instanceof Domain)) { return []; } - $config = $this->nodeBasedConfiguration->getErrorNodeConfiguration($node); + $config = $this->nodeBasedConfiguration->getErrorNodeConfiguration($domain->getSite()->getNodeName(), $node); + return [ 'data' => [ 'rows' => [ [ 'destination' => $this->destinationResolver->getDestinationForConfiguration( $config, - $domain->getSite()->getNodeName() + (string)$domain->getSite()->getNodeName() ), ], ] diff --git a/Classes/Handler/ProductionExceptionHandler.php b/Classes/Handler/ProductionExceptionHandler.php index bf07f59..9d094f3 100644 --- a/Classes/Handler/ProductionExceptionHandler.php +++ b/Classes/Handler/ProductionExceptionHandler.php @@ -10,7 +10,6 @@ class ProductionExceptionHandler extends FlowProductionExceptionHandler { - /** * @param int $statusCode * @param string|null $referenceCode @@ -38,14 +37,8 @@ protected function findErrorPageConfigurationForRequest(int $statusCode): ?strin return $errorPageResolver->findErrorPageForCurrentRequestAndStatusCode($statusCode); } - /** - * Override new method introduced in Flow 6.3.16 - * - * @return bool - */ protected function useCustomErrorView(): bool { return false; } - } \ No newline at end of file diff --git a/Classes/Service/ActionRequestFactory.php b/Classes/Service/ActionRequestFactory.php new file mode 100644 index 0000000..1c26b0d --- /dev/null +++ b/Classes/Service/ActionRequestFactory.php @@ -0,0 +1,58 @@ +siteRepository->findOneByNodeName($siteDetectionResult->siteNodeName); + $baseUri = new Uri((string)$site->getPrimaryDomain()); + + $httpRequest = self::buildHttpRequest($baseUri); + $httpRequest = $siteDetectionResult->storeInRequest($httpRequest); + + return ActionRequest::fromHttpRequest($httpRequest); + } + + private static function buildHttpRequest(UriInterface $uri): ServerRequest + { + return self::createHttpRequestFromGlobals($uri); + } + + private static function createHttpRequestFromGlobals(UriInterface $uri): ServerRequest + { + $_SERVER['FLOW_REWRITEURLS'] = '1'; + $fromGlobals = ServerRequest::fromGlobals(); + + return new ServerRequest( + $fromGlobals->getMethod(), + $uri, + $fromGlobals->getHeaders(), + $fromGlobals->getBody(), + $fromGlobals->getProtocolVersion(), + array_merge( + $fromGlobals->getServerParams(), + // Empty SCRIPT_NAME to prevent "./flow" in Uri + ['SCRIPT_NAME' => ''] + ) + ); + } +} \ No newline at end of file diff --git a/Classes/Service/ControllerContextFactory.php b/Classes/Service/ControllerContextFactory.php deleted file mode 100644 index 2ca46f9..0000000 --- a/Classes/Service/ControllerContextFactory.php +++ /dev/null @@ -1,64 +0,0 @@ -withAttribute( - ServerRequestAttributes::ROUTING_PARAMETERS, - RouteParameters::createEmpty()->withParameter('requestUriHost', $uri->getHost()) - ); - - $request = ActionRequest::fromHttpRequest($httpRequest); - - $uriBuilder = new UriBuilder(); - $uriBuilder->setRequest($request); - - return new ControllerContext( - $request, - new ActionResponse(), - new Arguments([]), - $uriBuilder - ); - } - - private static function createHttpRequestFromGlobals(UriInterface $uri): ServerRequest - { - $_SERVER['FLOW_REWRITEURLS'] = '1'; - $fromGlobals = ServerRequest::fromGlobals(); - - return new ServerRequest( - $fromGlobals->getMethod(), - $uri, - $fromGlobals->getHeaders(), - $fromGlobals->getBody(), - $fromGlobals->getProtocolVersion(), - array_merge( - $fromGlobals->getServerParams(), - // Empty SCRIPT_NAME to prevent "./flow" in Uri - ['SCRIPT_NAME' => ''] - ) - ); - } - -} diff --git a/Classes/Service/DestinationResolver.php b/Classes/Service/DestinationResolver.php index 5d9a1b3..4c57dbb 100644 --- a/Classes/Service/DestinationResolver.php +++ b/Classes/Service/DestinationResolver.php @@ -4,11 +4,9 @@ namespace Netlogix\ErrorHandler\Service; -use Neos\ContentRepository\Domain\Service\ContextFactoryInterface; use Neos\Eel\CompilingEvaluator; use Neos\Eel\Utility as EelUtility; use Neos\Flow\Annotations as Flow; -use Neos\Neos\Service\LinkingService; use function preg_match; @@ -18,23 +16,8 @@ */ class DestinationResolver { - /** - * @Flow\Inject(lazy=false) - * @var CompilingEvaluator - */ - protected $eelEvaluator; - - /** - * @Flow\Inject(lazy=false) - * @var ContextFactoryInterface - */ - protected ContextFactoryInterface $contextFactory; - - /** - * @Flow\Inject(lazy=false) - * @var LinkingService - */ - protected LinkingService $linkingService; + #[Flow\Inject] + protected CompilingEvaluator $eelEvaluator; /** * @param array $config @@ -46,7 +29,6 @@ public function getDestinationForConfiguration( array $config, string $siteNodeName ): string { - $dimensionPathSegment = $config['dimensionPathSegment'] ?? ''; $nodeIdentifier = preg_match( '/^#([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})$/', $config['source'] ?? '', @@ -57,8 +39,8 @@ public function getDestinationForConfiguration( return $this->evaluateEelExpression($config['destination'], [ 'site' => $siteNodeName, - 'dimensions' => $dimensionPathSegment, 'node' => $nodeIdentifier, + 'dimensions' => sha1(json_encode($config['dimensions'] ?? [])), ]); } diff --git a/Classes/Service/ErrorPageResolver.php b/Classes/Service/ErrorPageResolver.php index 0aa2043..2f6ed65 100644 --- a/Classes/Service/ErrorPageResolver.php +++ b/Classes/Service/ErrorPageResolver.php @@ -7,45 +7,23 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Core\Bootstrap; use Neos\Flow\Http\HttpRequestHandlerInterface; +use Neos\Flow\Log\ThrowableStorageInterface; use Neos\Neos\Domain\Repository\DomainRepository; use Netlogix\ErrorHandler\Configuration\ErrorHandlerConfiguration; +use Throwable; /** * @Flow\Scope("singleton") */ class ErrorPageResolver implements ProtectedContextAwareInterface { - - /** - * @var ErrorHandlerConfiguration - */ - protected $errorHandlerConfiguration; - - /** - * @var Bootstrap - */ - private $bootstrap; - - /** - * @var DomainRepository - */ - protected $domainRepository; - - /** - * @var DestinationResolver - */ - protected $destinationResolver; - public function __construct( - ErrorHandlerConfiguration $errorHandlerConfiguration, - Bootstrap $bootstrap, - DomainRepository $domainRepository, - DestinationResolver $destinationResolver + protected ErrorHandlerConfiguration $errorHandlerConfiguration, + protected Bootstrap $bootstrap, + protected DomainRepository $domainRepository, + protected DestinationResolver $destinationResolver, + protected ThrowableStorageInterface $throwableStorage, ) { - $this->errorHandlerConfiguration = $errorHandlerConfiguration; - $this->bootstrap = $bootstrap; - $this->domainRepository = $domainRepository; - $this->destinationResolver = $destinationResolver; } public function findErrorPageForCurrentRequestAndStatusCode(int $statusCode): ?string @@ -67,7 +45,7 @@ public function findErrorPageForCurrentRequestAndStatusCode(int $statusCode): ?s $configuration = $this ->errorHandlerConfiguration - ->findConfigurationForSite($currentSite, $requestHandler->getHttpRequest()->getUri(), $statusCode); + ->findConfigurationForSite($currentSite, $requestHandler->getHttpRequest(), $statusCode); if (!$configuration) { return null; @@ -76,9 +54,10 @@ public function findErrorPageForCurrentRequestAndStatusCode(int $statusCode): ?s try { return $this->destinationResolver->getDestinationForConfiguration( $configuration, - $currentDomain->getSite()->getNodeName() + (string)$currentDomain->getSite()->getNodeName() ); - } catch (\Throwable $t) { + } catch (Throwable $t) { + $this->throwableStorage->logThrowable($t); } return null; diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index 528e01a..e6eea29 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -14,15 +14,3 @@ Netlogix: ErrorHandler: destination: '${"/var/www/default/" + site + "/" + dimensions + "/" + node + ".html"}' - - pages: [] - -# 'my-site': -# - -# dimensionPathSegment: 'en_US' -# dimensions: -# language: ['en_US', 'en'] -# matchingStatusCodes: [500] -# pathPrefixes: ['/some-prefix', '/some-other-prefix'] -# source: '#550e8400-e29b-11d4-a716-446655440000' -# destination: '${"/var/www/default/" + site + "/" + dimensions + "/500.html"}' diff --git a/README.md b/README.md index 9e47029..51318ee 100644 --- a/README.md +++ b/README.md @@ -34,61 +34,6 @@ Netlogix: destination: '${"/var/www/default/" + site + "/" + dimensions + "/" + node + ".html"}' ``` -### Option 2: Provide error pages via yaml - -Provide configuration for every site and status code you need: - -```yaml -Netlogix: - ErrorHandler: - pages: - # siteNodeName of all Sites that you want to generate error pages for - - 'my-site-without-dimensions': - - - # The status codes this error page is generated for - matchingStatusCodes: [404, 410] - - # Dimensions to use for this error page. Use empty array if no dimensions are configured - dimensions: [] - - # Node identifier of documentNode to use for rendering - source: '#550e8400-e29b-11d4-a716-446655440000' - - # File path where this error page should be saved to. Available variables are `site` and `dimensions` - destination: '${"/var/www/default/mysite/errorpages/404.html"}' - - - - # You can also configure path prefixes so some site areas have different error pages. Make sure to adjust the destination path accordingly. - pathPrefixes: ['/some-special-path'] - - # The status codes this error page is generated for - matchingStatusCodes: [404, 410] - - # Dimensions to use for this error page. Use empty array if no dimensions are configured - dimensions: [] - - # Node identifier of documentNode to use for rendering - source: '#f7c8d757-391a-4a85-bdb5-81df56d5e2c0' - - # File path where this error page should be saved to. Available variables are site and dimensions - destination: '${"/var/www/default/mysite/errorpages/404-some-special-path.html"}' - - 'my-site-with-dimensions': - - - matchingStatusCodes: [500] - - # The first path segment that determines the dimensions. Use empty string if no dimensions are configured - dimensionPathSegment: 'en_US' - - # Dimensions to use for this error page. Use empty array if no dimensions are configured - dimensions: - language: ['en_US', 'en'] - - source: '#550e8400-e29b-11d4-a716-446655440000' - destination: '${"%FLOW_PATH_DATA%Persistent/ErrorPages/" + site + "-" + dimensions + "-500.html"}' -``` - ## Generate error pages To generate the static error pages, run the following Flow command: diff --git a/Resources/Private/Fusion/Root.fusion b/Resources/Private/Fusion/Root.fusion index b31ed55..28b2ca5 100644 --- a/Resources/Private/Fusion/Root.fusion +++ b/Resources/Private/Fusion/Root.fusion @@ -1,6 +1,6 @@ error = Neos.Fusion:Case { nlxErrorHandler { - @position = 'before default' + @position = 'start 1000' errorPage = ${Netlogix.ErrorPageResolver.findErrorPageForCurrentRequestAndStatusCode(statusCode)} condition = ${this.errorPage != null && File.exists(this.errorPage)} diff --git a/composer.json b/composer.json index 0739f15..f6e4b7b 100644 --- a/composer.json +++ b/composer.json @@ -4,12 +4,12 @@ "type": "neos-package", "license": "MIT", "require": { - "php": "^7.3 || ^8.0", + "php": "^8.0", "ext-curl": "*", - "guzzlehttp/guzzle": "~6.0 || ~7.0", - "neos/neos": "^7.3 || ^8.0", - "neos/flow": "^7.3.2 || ^8.0", - "symfony/yaml": "^5.1 || ^6.0 || ^7.0" + "guzzlehttp/guzzle": "^7.9", + "neos/neos": "~9.0.0", + "neos/flow": "~9.0.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" }, "autoload": { "psr-4": {