Passed
Push — master ( 8cbd50...daf0b2 )
by
unknown
13:13
created

Router::matchRequest()   B

Complexity

Conditions 9
Paths 32

Size

Total Lines 39
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 27
dl 0
loc 39
rs 8.0555
c 0
b 0
f 0
cc 9
nc 32
nop 1
1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Backend\Routing;
17
18
use Psr\Http\Message\ServerRequestInterface;
19
use Symfony\Component\Routing\Matcher\UrlMatcher;
20
use Symfony\Component\Routing\RequestContext;
21
use Symfony\Component\Routing\Route as SymfonyRoute;
22
use Symfony\Component\Routing\RouteCollection as SymfonyRouteCollection;
23
use TYPO3\CMS\Backend\Routing\Exception\MethodNotAllowedException;
24
use TYPO3\CMS\Backend\Routing\Exception\ResourceNotFoundException;
25
use TYPO3\CMS\Core\SingletonInterface;
26
use TYPO3\CMS\Core\Utility\HttpUtility;
27
28
/**
29
 * Implementation of a class for adding routes, collecting throughout the Bootstrap
30
 * to register all sorts of Backend Routes, and to fetch the main Collection in order
31
 * to resolve a route (see ->match() and ->matchRequest()).
32
 *
33
 * Ideally, the Router is solely instantiated and accessed via the Bootstrap, the RequestHandler and the UriBuilder.
34
 *
35
 * See \TYPO3\CMS\Backend\Http\RequestHandler for more details on route matching() and Bootstrap->initializeBackendRouting().
36
 *
37
 * The architecture is inspired by the Symfony Routing Component.
38
 */
39
class Router implements SingletonInterface
40
{
41
    /**
42
     * All routes used in the Backend
43
     * @var SymfonyRouteCollection
44
     */
45
    protected $routeCollection;
46
47
    public function __construct()
48
    {
49
        $this->routeCollection = new SymfonyRouteCollection();
50
    }
51
    /**
52
     * Adds a new route with the identifiers
53
     *
54
     * @param string $routeIdentifier
55
     * @param Route $route
56
     */
57
    public function addRoute($routeIdentifier, $route)
58
    {
59
        $symfonyRoute = new SymfonyRoute($route->getPath(), [], [], $route->getOptions());
60
        $symfonyRoute->setMethods($route->getMethods());
61
        $this->routeCollection->add($routeIdentifier, $symfonyRoute);
62
    }
63
64
    /**
65
     * Fetch all registered routes, only use in UriBuilder
66
     *
67
     * @return Route[]
68
     */
69
    public function getRoutes(): iterable
70
    {
71
        return $this->routeCollection->getIterator();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->routeCollection->getIterator() returns the type ArrayIterator which is incompatible with the documented return type TYPO3\CMS\Backend\Routing\Route[].
Loading history...
72
    }
73
74
    /**
75
     * @internal only use in Core, this should not be exposed
76
     * @return SymfonyRouteCollection
77
     */
78
    public function getRouteCollection(): SymfonyRouteCollection
79
    {
80
        return $this->routeCollection;
81
    }
82
83
    /**
84
     * Tries to match a URL path with a set of routes.
85
     *
86
     * @param string $pathInfo The path info to be parsed
87
     * @return Route the first Route object found
88
     * @throws ResourceNotFoundException If the resource could not be found
89
     */
90
    public function match($pathInfo)
91
    {
92
        foreach ($this->routeCollection->getIterator() as $routeIdentifier => $route) {
93
            // This check is done in a simple way as there are no parameters yet (get parameters only)
94
            if ($route->getPath() === $pathInfo) {
95
                $routeResult = new Route($route->getPath(), $route->getOptions());
96
                // Store the name of the Route in the _identifier option so the token can be checked against that
97
                $routeResult->setOption('_identifier', $routeIdentifier);
98
                return $routeResult;
99
            }
100
        }
101
102
        throw new ResourceNotFoundException('The requested resource "' . $pathInfo . '" was not found.', 1425389240);
103
    }
104
105
    /**
106
     * Tries to match a URI against the registered routes
107
     *
108
     * @param ServerRequestInterface $request
109
     * @return Route the first Route object found
110
     */
111
    public function matchRequest(ServerRequestInterface $request)
112
    {
113
        $path = $request->getUri()->getPath();
114
        if (($normalizedParams = $request->getAttribute('normalizedParams')) !== null) {
115
            // Remove the directory name of the script from the path. This will usually be `/typo3` in this context.
116
            $path = substr($path, strlen(dirname($normalizedParams->getScriptName())));
117
        }
118
        if ($path === '' || $path === '/' || $path === '/index.php') {
119
            // Allow the login page to be displayed if routing is not used and on index.php
120
            // (consolidate RouteDispatcher::evaluateReferrer() when changing 'login' to something different)
121
            $path = $request->getQueryParams()['route'] ?? $request->getParsedBody()['route'] ?? '/login';
122
        }
123
        $context = new RequestContext(
124
            $path,
125
            $request->getMethod(),
126
            (string)HttpUtility::idn_to_ascii($request->getUri()->getHost()),
127
            $request->getUri()->getScheme()
128
        );
129
        try {
130
            $result = (new UrlMatcher($this->routeCollection, $context))->match($path);
131
            $matchedSymfonyRoute = $this->routeCollection->get($result['_route']);
132
            if ($matchedSymfonyRoute === null) {
133
                throw new ResourceNotFoundException('The requested resource "' . $path . '" was not found.', 1607596900);
134
            }
135
        } catch (\Symfony\Component\Routing\Exception\MethodNotAllowedException $e) {
136
            throw new MethodNotAllowedException($e->getMessage(), 1612649842);
137
        } catch (\Symfony\Component\Routing\Exception\ResourceNotFoundException $e) {
138
            throw new ResourceNotFoundException('The requested resource "' . $path . '" was not found.', 1612649840);
139
        }
140
        // Apply matched method to route
141
        $matchedOptions = $matchedSymfonyRoute->getOptions();
142
        $methods = $matchedOptions['methods'] ?? [];
143
        unset($matchedOptions['methods']);
144
        $route = new Route($matchedSymfonyRoute->getPath(), $matchedOptions);
145
        if (count($methods) > 0) {
146
            $route->setMethods($methods);
147
        }
148
        $route->setOption('_identifier', $result['_route']);
149
        return $route;
150
    }
151
}
152