[FEATURE] Allow static route resolving 15/58315/15
authorSusanne Moog <susanne.moog@typo3.org>
Tue, 18 Sep 2018 14:19:35 +0000 (16:19 +0200)
committerFrank Naegler <frank.naegler@typo3.org>
Thu, 20 Sep 2018 11:58:03 +0000 (13:58 +0200)
Resolving for predefined routes has been introduced.

Static routes can be configured on a per-site level
to provide for example robots.txt or sitemap.xml
routes.

Routes are resolved directly after site resolving
in a middleware and directly return content if found.

Static routes can be configured to deliver static text
defined in site configuration or fetch content from
a file, page or url.

The GUI uses the link wizard for providing URLs.

Resolves: #86214
Releases: master
Change-Id: I6d07529cf535a02472b2e03a763a00ff049270e8
Reviewed-on: https://review.typo3.org/58315
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Frank Naegler <frank.naegler@typo3.org>
Tested-by: Frank Naegler <frank.naegler@typo3.org>
typo3/sysext/backend/Classes/Controller/SiteConfigurationController.php
typo3/sysext/backend/Classes/Form/FormDataProvider/SiteDatabaseEditRow.php
typo3/sysext/backend/Classes/Form/FormDataProvider/SiteTcaInline.php
typo3/sysext/backend/Configuration/SiteConfiguration/site.php
typo3/sysext/backend/Configuration/SiteConfiguration/site_route.php [new file with mode: 0644]
typo3/sysext/backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf
typo3/sysext/core/Documentation/Changelog/master/Feature-86214-ImplementStaticRoutes.rst [new file with mode: 0644]
typo3/sysext/frontend/Classes/Middleware/StaticRouteResolver.php [new file with mode: 0644]
typo3/sysext/frontend/Configuration/RequestMiddlewares.php
typo3/sysext/redirects/Configuration/RequestMiddlewares.php

index 3548de2..a2939cf 100644 (file)
@@ -275,14 +275,17 @@ class SiteConfigurationController
                                 continue;
                             }
                             $type = $siteTca[$foreignTable]['columns'][$childFieldName]['config']['type'];
-                            if ($type === 'input') {
-                                $childRowData[$childFieldName] = $childFieldValue;
-                            } elseif ($type === 'select') {
-                                $childRowData[$childFieldName] = $childFieldValue;
-                            } elseif ($type === 'check') {
-                                $childRowData[$childFieldName] = (bool)$childFieldValue;
-                            } else {
-                                throw new \RuntimeException('TCA type ' . $type . ' not implemented in site handling', 1521555340);
+                            switch ($type) {
+                                case 'input':
+                                case 'select':
+                                case 'text':
+                                    $childRowData[$childFieldName] = $childFieldValue;
+                                    break;
+                                case 'check':
+                                    $childRowData[$childFieldName] = (bool)$childFieldValue;
+                                    break;
+                                default:
+                                    throw new \RuntimeException('TCA type ' . $type . ' not implemented in site handling', 1521555340);
                             }
                         }
                         $newSysSiteData[$fieldName][] = $childRowData;
index 72f21af..ae3eb2c 100644 (file)
@@ -46,7 +46,7 @@ class SiteDatabaseEditRow implements FormDataProviderInterface
             $rowData = $siteFinder->getSiteByRootPageId($siteConfigurationForPageUid)->getConfiguration();
             $result['databaseRow']['uid'] = $rowData['rootPageId'];
             $result['databaseRow']['identifier'] = $result['customData']['siteIdentifier'];
-        } elseif ($tableName === 'site_errorhandling' || $tableName === 'site_language') {
+        } elseif (in_array($tableName, ['site_errorhandling', 'site_language', 'site_route'])) {
             $siteConfigurationForPageUid = (int)($result['inlineTopMostParentUid'] ?? $result['inlineParentUid']);
             $rowData = $siteFinder->getSiteByRootPageId($siteConfigurationForPageUid)->getConfiguration();
             $parentFieldName = $result['inlineParentFieldName'];
index 3f1903f..630771e 100644 (file)
@@ -46,8 +46,8 @@ class SiteTcaInline extends AbstractDatabaseRecordProvider implements FormDataPr
             if (!$this->isInlineField($fieldConfig)) {
                 continue;
             }
-            $childTableName = $fieldConfig['config']['foreign_table'];
-            if ($childTableName !== 'site_errorhandling' && $childTableName !== 'site_language') {
+            $childTableName = $fieldConfig['config']['foreign_table'] ?? '';
+            if (!in_array($childTableName, ['site_errorhandling', 'site_language', 'site_route'])) {
                 throw new \RuntimeException('Inline relation to other tables not implemented', 1522494737);
             }
             $result['processedTca']['columns'][$fieldName]['children'] = [];
index 58e3c6b..2183f63 100644 (file)
@@ -67,12 +67,25 @@ return [
                 ],
             ],
         ],
+        'routes' => [
+            'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:site.routes',
+            'config' => [
+                'type' => 'inline',
+                'foreign_table' => 'site_route',
+                'appearance' => [
+                    'enabledControls' => [
+                        'info' => false,
+                    ],
+                ],
+            ],
+        ],
     ],
     'types' => [
         '0' => [
             'showitem' => 'identifier, rootPageId, base,
                 --div--;LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:site.tab.languages, languages,
-                --div--;LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:site.tab.errorHandling, errorHandling',
+                --div--;LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:site.tab.errorHandling, errorHandling,
+                --div--;LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:site.tab.routes, routes',
         ],
     ],
 ];
diff --git a/typo3/sysext/backend/Configuration/SiteConfiguration/site_route.php b/typo3/sysext/backend/Configuration/SiteConfiguration/site_route.php
new file mode 100644 (file)
index 0000000..da2cf0b
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+
+return [
+    'ctrl' => [
+        'label' => 'route',
+        'title' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:site_route.ctrl.title',
+        'type' => 'type',
+        'typeicon_column' => 'type',
+        'typeicon_classes' => [
+            'staticText' => 'mimetypes-text-html',
+            'uri' => 'apps-pagetree-page-content-from-page',
+        ],
+    ],
+    'columns' => [
+        'route' => [
+            'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:site_route.route',
+            'config' => [
+                'type' => 'input',
+                'eval' => 'required',
+            ],
+        ],
+        'type' => [
+            'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:site_route.type',
+            'config' => [
+                'type' => 'select',
+                'renderType' => 'selectSingle',
+                'required' => true,
+                'items' => [
+                    ['', ''],
+                    ['LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:site_route.staticText', 'staticText'],
+                    ['LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:site_route.source', 'uri']
+                ],
+            ],
+        ],
+        'content' => [
+            'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:site_route.staticText',
+            'config' => [
+                'type' => 'text',
+                'eval' => 'required',
+            ],
+        ],
+        'source' => [
+            'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:site_route.source',
+            'config' => [
+                'type' => 'input',
+                'renderType' => 'inputLink',
+                'eval' => 'required',
+                'fieldControl' => [
+                    'linkPopup' => [
+                        'options' => [
+                            'blindLinkOptions' => 'mail,spec,folder',
+                        ]
+                    ]
+                ],
+            ],
+        ]
+    ],
+    'types' => [
+        '1' => [
+            'showitem' => 'route, type',
+        ],
+        'staticText' => [
+            'showitem' => 'route, type, content',
+        ],
+        'uri' => [
+            'showitem' => 'route, type, source',
+        ]
+    ],
+];
index 309aac2..9a9fa5d 100644 (file)
                        <trans-unit id="site.errorHandling">
                                <source>Error Handling</source>
                        </trans-unit>
+                       <trans-unit id="site.routes">
+                               <source>Routes</source>
+                       </trans-unit>
                        <trans-unit id="site.tab.languages">
                                <source>Languages</source>
                        </trans-unit>
                        <trans-unit id="site.tab.errorHandling">
                                <source>Error Handling</source>
                        </trans-unit>
+                       <trans-unit id="site.tab.routes">
+                               <source>Static Routes</source>
+                       </trans-unit>
 
                        <trans-unit id="site_language.ctrl.title">
                                <source>Language Configuration for a Site</source>
                        <trans-unit id="site_errorhandling.errorPhpClassFQCN">
                                <source>ErrorHandler Class Target (FQCN)</source>
                        </trans-unit>
+                       <trans-unit id="site_route.ctrl.title">
+                               <source>Routes</source>
+                       </trans-unit>
+                       <trans-unit id="site_route.type">
+                               <source>Route Type</source>
+                       </trans-unit>
+                       <trans-unit id="site_route.staticText">
+                               <source>Static Text</source>
+                       </trans-unit>
+                       <trans-unit id="site_route.source">
+                               <source>Page, File or URL</source>
+                       </trans-unit>
+                       <trans-unit id="site_route.route">
+                               <source>Static Route Name</source>
+                       </trans-unit>
                </body>
        </file>
 </xliff>
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-86214-ImplementStaticRoutes.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-86214-ImplementStaticRoutes.rst
new file mode 100644 (file)
index 0000000..33e94a1
--- /dev/null
@@ -0,0 +1,73 @@
+.. include:: ../../Includes.txt
+
+=========================================
+Feature: #86214 - Implement static routes
+=========================================
+
+See :issue:`86214`
+
+Description
+===========
+
+The site configuration module now has configuration options to add static routes on a per site basis.
+Take the following example: In a multi-site installation you want to have different robots.txt files for each site that
+should be reachable at /robots.txt on each site. You can now add a static route "robots.txt" to your site and
+define which content should be delivered.
+
+The TYPO3 SEO extension provides a sitemap for TYPO3 out of the box, but it's only reachable at a specific page type.
+To enable easier access you can now configure a static route `sitemap.xml` that maps to that page type (see example
+below).
+
+Routes can be configured as toplevel files (as in the `sitemap.xml` and `robots.txt` case) but may also be configured
+to deeper route paths (`my/deep/path/to/a/static/text` for example). Matching is done on the full path but without any
+parameters.
+
+Impact
+======
+
+Static routes can be configured via the user interface or directly in the yaml configuration.
+There are two options: deliver static text or resolve a TYPO3 URL.
+
+StaticText
+----------
+The `staticText` option allows to deliver simple text content. The text can be added through a text field directly in
+the site configuration. This is suitable for files like `robots.txt` or `humans.txt`.
+
+YAML Configuration Example :: yaml
+
+      route: robots.txt
+      type: staticText
+      content: |
+        Sitemap: https://example.com/sitemap.xml
+        User-agent: *
+        Allow: /
+        Disallow: /forbidden/
+
+TYPO3 URL (t3://)
+-----------------
+
+The type `uri` for TYPO3 URL provides the option to render either a file, page or url. Internally a request to the
+file or URL is done and its content delivered.
+
+YAML Configuration Examples :: yaml
+
+    -
+      route: sitemap.xml
+      type: uri
+      source: 't3://page?uid=1&type=1533906435'
+    -
+      route: favicon.ico
+      type: uri
+      source: 't3://file?uid=77'
+
+
+Implementation
+==============
+
+Static route resolving is implemented as a PSR-15 middleware. If the route path requested matches any one of the
+configured site routes, a response is directly generated and returned. This way there is minimal bootstrap code to
+be executed on a static route resolving request, mainly the site configuration needs to be loaded. Static routes cannot
+get parameters as the matching is done solely on the path level.
+
+
+.. index:: Frontend, ext:frontend
diff --git a/typo3/sysext/frontend/Classes/Middleware/StaticRouteResolver.php b/typo3/sysext/frontend/Classes/Middleware/StaticRouteResolver.php
new file mode 100644 (file)
index 0000000..f4b48e0
--- /dev/null
@@ -0,0 +1,117 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Frontend\Middleware;
+
+/*
+ * 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!
+ */
+
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use TYPO3\CMS\Core\Http\HtmlResponse;
+use TYPO3\CMS\Core\Http\RequestFactory;
+use TYPO3\CMS\Core\LinkHandling\LinkService;
+use TYPO3\CMS\Core\Resource\File;
+use TYPO3\CMS\Core\Routing\PageUriBuilder;
+use TYPO3\CMS\Core\Site\Entity\Site;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Resolves static routes - can return configured content directly or load content from file / urls
+ */
+class StaticRouteResolver implements MiddlewareInterface
+{
+    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+    {
+        if (($site = $request->getAttribute('site', null)) instanceof Site &&
+            ($configuration = $site->getConfiguration()['routes'] ?? null)
+        ) {
+            $path = ltrim($request->getUri()->getPath(), '/');
+            $routeNames = array_map(function (string $route) use ($site) {
+                return ltrim(trim($site->getBase()->getPath(), '/') . '/' . ltrim($route, '/'), '/');
+            }, array_column($configuration, 'route'));
+            if (in_array($path, $routeNames, true)) {
+                $key = array_search($path, $routeNames, true);
+                $routeConfig = $configuration[$key];
+                [$content, $contentType] = $this->resolveByType($request, $routeConfig['type'], $routeConfig);
+                return new HtmlResponse($content, 200, ['Content-Type' => $contentType]);
+            }
+        }
+        return $handler->handle($request);
+    }
+
+    private function getFromFile(File $file): array
+    {
+        $content = $file->getContents();
+        $contentType = $file->getMimeType();
+        return [$content, $contentType];
+    }
+
+    private function getFromUri(string $uri): array
+    {
+        $requestFactory = GeneralUtility::makeInstance(RequestFactory::class);
+        $response = $requestFactory->request($uri);
+        $contentType = 'text/plain; charset=utf-8';
+        $content = '';
+        if ($response->getStatusCode() === 200) {
+            $content = $response->getBody()->getContents();
+            $contentType = $response->getHeader('Content-Type');
+        }
+
+        return [$content, $contentType];
+    }
+
+    private function getPageUri(ServerRequestInterface $request, array $urlParams): string
+    {
+        $uriBuilder = GeneralUtility::makeInstance(PageUriBuilder::class);
+        $uri = (string)$uriBuilder->buildUri(
+            (int)$urlParams['pageuid'],
+            ['type' => $urlParams['pagetype'] ?? 0],
+            null,
+            ['language' => $request->getAttribute('language', null)],
+            PageUriBuilder::ABSOLUTE_URL
+        );
+        return $uri;
+    }
+
+    private function resolveByType(ServerRequestInterface $request, string $type, array $routeConfig): array
+    {
+        switch ($type) {
+            case 'staticText':
+                $content = $routeConfig['content'];
+                $contentType = 'text/plain; charset=utf-8';
+                break;
+            case 'uri':
+                $linkService = GeneralUtility::makeInstance(LinkService::class);
+                $urlParams = $linkService->resolve($routeConfig['source']);
+                if ($urlParams['type'] === 'url' || $urlParams['type'] === 'page') {
+                    $uri = $urlParams['url'] ?? $this->getPageUri($request, $urlParams);
+                    [$content, $contentType] = $this->getFromUri($uri);
+                } elseif ($urlParams['type'] === 'file') {
+                    [$content, $contentType] = $this->getFromFile($urlParams['file']);
+                } else {
+                    throw new \InvalidArgumentException('Can only handle URIs of type page, url or file.', 1537348076);
+                }
+
+                break;
+            default:
+                throw new \InvalidArgumentException(
+                    'Can only handle static file configurations with type uri or staticText.',
+                    1537348083
+                );
+        }
+        return [$content, $contentType];
+    }
+}
index d286fee..c331fbe 100644 (file)
@@ -88,6 +88,15 @@ return [
                 'typo3/cms-frontend/page-resolver'
             ]
         ],
+        'typo3/cms-frontend/static-route-resolver' => [
+            'target' => \TYPO3\CMS\Frontend\Middleware\StaticRouteResolver::class,
+            'after' => [
+                'typo3/cms-frontend/site',
+            ],
+            'before' => [
+                'typo3/cms-frontend/page-resolver'
+            ]
+        ],
         'typo3/cms-frontend/page-resolver' => [
             'target' => \TYPO3\CMS\Frontend\Middleware\PageResolver::class,
             'after' => [
index a649b18..b7e195c 100644 (file)
@@ -13,6 +13,7 @@ return [
             'after' => [
                 'typo3/cms-frontend/tsfe',
                 'typo3/cms-frontend/authentication',
+                'typo3/cms-frontend/static-route-resolver',
             ],
         ],
     ],