From b47b8e59106ec4d31087af25044407f5c0dd30a4 Mon Sep 17 00:00:00 2001
From: Christoph Lehmann <christoph.lehmann@networkteam.com>
Date: Thu, 28 Apr 2022 23:12:12 +0200
Subject: [PATCH] [TASK] Reduce sql queries for page link generation

Generating a page link leads to 2 sql queries.

The first query is about a general lookup and
frontend group access check. The second occurs
through PageRouter::generateUri().

By passing a fully resolved and possibly overlayed
page record (as object) to generateUri() instead of
a pageId the second query can be omitted.

Since generateUri() is public we need to make sure
the page record is completely resolved and overlayed
in order to reuse it. This is done with the new Page object.

Resolves: #97492
Releases: main, 11.5
Change-Id: I307d6e5f53d6581deb494aa123f25bde0a7ff263
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/77332
Tested-by: core-ci <typo3@b13.com>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Benni Mack <benni@typo3.org>
---
 typo3/sysext/core/Classes/Domain/Page.php     | 68 +++++++++++++++++++
 .../core/Classes/Domain/PropertyTrait.php     | 53 +++++++++++++++
 .../core/Classes/Routing/PageRouter.php       | 23 +++++--
 .../core/Classes/Routing/RouterInterface.php  |  2 +-
 .../Classes/Typolink/PageLinkBuilder.php      |  4 +-
 5 files changed, 144 insertions(+), 6 deletions(-)
 create mode 100644 typo3/sysext/core/Classes/Domain/Page.php
 create mode 100644 typo3/sysext/core/Classes/Domain/PropertyTrait.php

diff --git a/typo3/sysext/core/Classes/Domain/Page.php b/typo3/sysext/core/Classes/Domain/Page.php
new file mode 100644
index 000000000000..dec1ab0ae2dd
--- /dev/null
+++ b/typo3/sysext/core/Classes/Domain/Page.php
@@ -0,0 +1,68 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Domain;
+
+/**
+ * @internal not part of public API, as this needs to be streamlined and proven
+ */
+class Page implements \ArrayAccess
+{
+    use PropertyTrait;
+
+    protected array $specialPropertyNames = [
+        '_language',
+        '_LOCALIZED_UID',
+        '_MP_PARAM',
+        '_ORIG_uid',
+        '_ORIG_pid',
+        '_SHORTCUT_ORIGINAL_PAGE_UID',
+        '_PAGES_OVERLAY',
+        '_PAGES_OVERLAY_UID',
+        '_PAGES_OVERLAY_LANGUAGE',
+        '_PAGES_OVERLAY_REQUESTEDLANGUAGE',
+    ];
+
+    protected array $specialProperties = [];
+
+    public function __construct(array $properties)
+    {
+        foreach ($properties as $propertyName => $propertyValue) {
+            if (isset($this->specialPropertyNames[$propertyName])) {
+                $this->specialProperties[$propertyName] = $propertyValue;
+            } else {
+                $this->properties[$propertyName] = $propertyValue;
+            }
+        }
+    }
+
+    public function getLanguageId(): int
+    {
+        return $this->specialProperties['_language'] ?? $this->specialProperties['_PAGES_OVERLAY_LANGUAGE'] ?? $this->properties['sys_language_uid'];
+    }
+
+    public function getPageId(): int
+    {
+        $pageId = isset($this->properties['l10n_parent']) && $this->properties['l10n_parent'] > 0 ? $this->properties['l10n_parent'] : $this->properties['uid'];
+        return (int)$pageId;
+    }
+
+    public function toArray(): array
+    {
+        return $this->properties;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Domain/PropertyTrait.php b/typo3/sysext/core/Classes/Domain/PropertyTrait.php
new file mode 100644
index 000000000000..bbf0ab3765d4
--- /dev/null
+++ b/typo3/sysext/core/Classes/Domain/PropertyTrait.php
@@ -0,0 +1,53 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Domain;
+
+/**
+ * @internal not part of public API, as this needs to be streamlined and proven
+ */
+trait PropertyTrait
+{
+    /**
+     * @var array<string, mixed>
+     */
+    protected array $properties = [];
+
+    #[\ReturnTypeWillChange]
+    public function offsetExists($offset)
+    {
+        return isset($this->properties[$offset]);
+    }
+
+    #[\ReturnTypeWillChange]
+    public function offsetGet($offset)
+    {
+        return $this->properties[$offset] ?? null;
+    }
+
+    #[\ReturnTypeWillChange]
+    public function offsetSet($offset, $value)
+    {
+        $this->properties[$offset] = $value;
+    }
+
+    #[\ReturnTypeWillChange]
+    public function offsetUnset($offset)
+    {
+        unset($this->properties[$offset]);
+    }
+}
diff --git a/typo3/sysext/core/Classes/Routing/PageRouter.php b/typo3/sysext/core/Classes/Routing/PageRouter.php
index 56ec9d393bb2..60b1c4b0c093 100644
--- a/typo3/sysext/core/Classes/Routing/PageRouter.php
+++ b/typo3/sysext/core/Classes/Routing/PageRouter.php
@@ -24,6 +24,7 @@ use Symfony\Component\Routing\Exception\ResourceNotFoundException;
 use Symfony\Component\Routing\RequestContext;
 use TYPO3\CMS\Core\Context\Context;
 use TYPO3\CMS\Core\Context\LanguageAspectFactory;
+use TYPO3\CMS\Core\Domain\Page;
 use TYPO3\CMS\Core\Domain\Repository\PageRepository;
 use TYPO3\CMS\Core\Exception\SiteNotFoundException;
 use TYPO3\CMS\Core\Http\Uri;
@@ -224,9 +225,9 @@ class PageRouter implements RouterInterface
     }
 
     /**
-     * API for generating a page where the $route parameter is typically an array (page record) or the page ID
+     * API for generating a page uri where the $route parameter is typically an array (a page record) or the page ID
      *
-     * @param array|string|int $route
+     * @param array|string|int|Page $route
      * @param array $parameters an array of query parameters which can be built into the URI path, also consider the special handling of "_language"
      * @param string $fragment additional #my-fragment part
      * @param string $type see the RouterInterface for possible types
@@ -249,7 +250,9 @@ class PageRouter implements RouterInterface
         }
 
         $pageId = 0;
-        if (is_array($route)) {
+        if ($route instanceof Page) {
+            $pageId = $route->getPageId();
+        } elseif (is_array($route)) {
             $pageId = (int)$route['uid'];
         } elseif (is_scalar($route)) {
             $pageId = (int)$route;
@@ -258,7 +261,19 @@ class PageRouter implements RouterInterface
         $context = clone $this->context;
         $context->setAspect('language', LanguageAspectFactory::createFromSiteLanguage($language));
         $pageRepository = GeneralUtility::makeInstance(PageRepository::class, $context);
-        $page = $pageRepository->getPage($pageId, true);
+
+        if ($route instanceof Page) {
+            $page = $route->toArray();
+        } elseif (is_array($route)
+            // Check 3rd party input $route for basic requirements
+            && isset($route['uid'], $route['sys_language_uid'], $route['l10n_parent'], $route['slug'])
+            && (int)$route['sys_language_uid'] === $language->getLanguageId()
+            && ((int)$route['l10n_parent'] === 0 || ($route['_PAGES_OVERLAY'] ?? false))
+        ) {
+            $page = $route;
+        } else {
+            $page = $pageRepository->getPage($pageId, true);
+        }
         $pagePath = $page['slug'] ?? '';
 
         if ($parameters['MP'] ?? '') {
diff --git a/typo3/sysext/core/Classes/Routing/RouterInterface.php b/typo3/sysext/core/Classes/Routing/RouterInterface.php
index 800a115315a6..fb0ff3f49903 100644
--- a/typo3/sysext/core/Classes/Routing/RouterInterface.php
+++ b/typo3/sysext/core/Classes/Routing/RouterInterface.php
@@ -47,7 +47,7 @@ interface RouterInterface
     /**
      * Builds a URI based on the $route and the given parameters.
      *
-     * @param string|array|int $route either the route name, or for pages it is usually the array of a page record, or the page ID
+     * @param string|array|int|\ArrayAccess $route either the route name, or for pages it is usually the array of a page record, or the page ID
      * @param array $parameters query parameters, specially reserved parameters are usually prefixed with "_"
      * @param string $fragment the section/fragment www.example.com/page/#fragment, WITHOUT the hash
      * @param string $type see the constants above.
diff --git a/typo3/sysext/frontend/Classes/Typolink/PageLinkBuilder.php b/typo3/sysext/frontend/Classes/Typolink/PageLinkBuilder.php
index cf0515deb6b5..9706abda2bac 100644
--- a/typo3/sysext/frontend/Classes/Typolink/PageLinkBuilder.php
+++ b/typo3/sysext/frontend/Classes/Typolink/PageLinkBuilder.php
@@ -26,6 +26,7 @@ use TYPO3\CMS\Core\Context\LanguageAspectFactory;
 use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
+use TYPO3\CMS\Core\Domain\Page;
 use TYPO3\CMS\Core\Domain\Repository\PageRepository;
 use TYPO3\CMS\Core\Exception\Page\RootLineException;
 use TYPO3\CMS\Core\Exception\SiteNotFoundException;
@@ -480,6 +481,7 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
 
         $targetPageId = (int)($page['l10n_parent'] > 0 ? $page['l10n_parent'] : $page['uid']);
         $queryParameters['_language'] = $siteLanguageOfTargetPage;
+        $pageObject = new Page($page);
 
         if ($fragment
             && $useAbsoluteUrl === false
@@ -493,7 +495,7 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
         } else {
             try {
                 $uri = $siteOfTargetPage->getRouter()->generateUri(
-                    $targetPageId,
+                    $pageObject,
                     $queryParameters,
                     $fragment,
                     $useAbsoluteUrl ? RouterInterface::ABSOLUTE_URL : RouterInterface::ABSOLUTE_PATH
-- 
GitLab