Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
61.22% covered (warning)
61.22%
60 / 98
26.67% covered (danger)
26.67%
4 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
router
61.22% covered (warning)
61.22%
60 / 98
26.67% covered (danger)
26.67%
4 / 15
122.19
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 get_routes
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 getRouteCollection
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setContext
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 getContext
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 generate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 match
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 get_matcher
50.00% covered (danger)
50.00%
2 / 4
0.00% covered (danger)
0.00%
0 / 1
2.50
 create_dumped_url_matcher
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 create_new_url_matcher
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_generator
50.00% covered (danger)
50.00%
2 / 4
0.00% covered (danger)
0.00%
0 / 1
2.50
 create_dumped_url_generator
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 create_new_url_generator
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 resolveParameters
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
6.01
 resolve
50.00% covered (danger)
50.00%
12 / 24
0.00% covered (danger)
0.00%
0 / 1
13.12
1<?php
2/**
3*
4* This file is part of the phpBB Forum Software package.
5*
6* @copyright (c) phpBB Limited <https://www.phpbb.com>
7* @license       GNU General Public License, version 2 (GPL-2.0)
8*
9* For full copyright and license information, please see
10* the docs/CREDITS.txt file.
11*
12*/
13
14namespace phpbb\routing;
15
16use phpbb\routing\resources_locator\resources_locator_interface;
17use Symfony\Component\Config\ConfigCache;
18use Symfony\Component\Config\Loader\LoaderInterface;
19use Symfony\Component\DependencyInjection\ContainerInterface;
20use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
21use Symfony\Component\DependencyInjection\Exception\RuntimeException;
22use Symfony\Component\Filesystem\Exception\IOException;
23use Symfony\Component\Routing\Generator\Dumper\CompiledUrlGeneratorDumper;
24use Symfony\Component\Routing\Generator\CompiledUrlGenerator;
25use Symfony\Component\Routing\Generator\UrlGenerator;
26use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper;
27use Symfony\Component\Routing\Matcher\CompiledUrlMatcher;
28use Symfony\Component\Routing\Matcher\UrlMatcher;
29use Symfony\Component\Routing\RequestContext;
30use Symfony\Component\Routing\RouteCollection;
31use Symfony\Component\Routing\RouterInterface;
32
33/**
34 * Integration of all pieces of the routing system for easier use.
35 */
36class router implements RouterInterface
37{
38    /**
39     * @var ContainerInterface
40     */
41    protected $container;
42
43    /**
44     * @var resources_locator_interface
45     */
46    protected $resources_locator;
47
48    /**
49     * @var LoaderInterface
50     */
51    protected $loader;
52
53    /**
54     * PHP file extensions
55     *
56     * @var string
57     */
58    protected $php_ext;
59
60    /**
61     * @var \Symfony\Component\Routing\Matcher\UrlMatcherInterface
62     */
63    protected $matcher;
64
65    /**
66     * @var \Symfony\Component\Routing\Generator\UrlGeneratorInterface
67     */
68    protected $generator;
69
70    /**
71     * @var RequestContext
72     */
73    protected $context;
74
75    /**
76     * @var RouteCollection
77     */
78    protected $route_collection;
79
80    /**
81     * @var string
82     */
83    protected $cache_dir;
84
85    /**
86     * @var string
87     */
88    protected $debug_url_generator;
89
90    /**
91     * @var string
92     */
93    protected $debug_url_matcher;
94
95    /**
96     * Construct method
97     *
98     * @param ContainerInterface $container DI container
99     * @param resources_locator_interface $resources_locator Resources locator
100     * @param LoaderInterface $loader Resources loader
101     * @param string $php_ext PHP file extension
102     * @param string $cache_dir phpBB cache directory
103     * @param string $debug_url_generator Debug url generator
104     * @param string $debug_url_matcher Debug url matcher
105     */
106    public function __construct(ContainerInterface $container, resources_locator_interface $resources_locator, LoaderInterface $loader, string $php_ext, string $cache_dir, string $debug_url_generator, string $debug_url_matcher)
107    {
108        $this->container            = $container;
109        $this->resources_locator    = $resources_locator;
110        $this->loader                = $loader;
111        $this->php_ext                = $php_ext;
112        $this->context                = new RequestContext();
113        $this->cache_dir            = $cache_dir;
114        $this->debug_url_generator    = $debug_url_generator;
115        $this->debug_url_matcher    = $debug_url_matcher;
116    }
117
118    /**
119     * Get the list of routes
120     *
121     * @return RouteCollection Get the route collection
122     */
123    public function get_routes()
124    {
125        if ($this->route_collection === null /*|| $this->route_collection->count() === 0*/)
126        {
127            $this->route_collection = new RouteCollection;
128            foreach ($this->resources_locator->locate_resources() as $resource)
129            {
130                if (is_array($resource))
131                {
132                    $this->route_collection->addCollection($this->loader->load($resource[0], $resource[1]));
133                }
134                else
135                {
136                    $this->route_collection->addCollection($this->loader->load($resource));
137                }
138            }
139
140            $this->resolveParameters($this->route_collection);
141        }
142
143        return $this->route_collection;
144    }
145
146    /**
147     * {@inheritdoc}
148     */
149    public function getRouteCollection(): RouteCollection
150    {
151        return $this->get_routes();
152    }
153
154    /**
155     * {@inheritdoc}
156     */
157    public function setContext(RequestContext $context): void
158    {
159        $this->context = $context;
160
161        if ($this->matcher !== null)
162        {
163            $this->get_matcher()->setContext($context);
164        }
165        if ($this->generator !== null)
166        {
167            $this->get_generator()->setContext($context);
168        }
169    }
170
171    /**
172     * {@inheritdoc}
173     */
174    public function getContext(): RequestContext
175    {
176        return $this->context;
177    }
178
179    /**
180     * {@inheritdoc}
181     */
182    public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string
183    {
184        return $this->get_generator()->generate($name, $parameters, $referenceType);
185    }
186
187    /**
188     * {@inheritdoc}
189     */
190    public function match(string $pathinfo): array
191    {
192        // Remove query string
193        $pathinfo = parse_url($pathinfo, PHP_URL_PATH);
194
195        if ($pathinfo === null)
196        {
197            throw new RuntimeException('Malformed pathinfo given to the router.');
198        }
199
200        // Remove app.php if present
201        $pathinfo = preg_replace('/^\/app\.php\//', '/', $pathinfo);
202
203        return $this->get_matcher()->match($pathinfo);
204    }
205
206    /**
207     * Gets the UrlMatcher instance associated with this Router.
208     *
209     * @return \Symfony\Component\Routing\Matcher\UrlMatcherInterface A UrlMatcherInterface instance
210     */
211    public function get_matcher()
212    {
213        if ($this->matcher !== null)
214        {
215            return $this->matcher;
216        }
217
218        $this->create_dumped_url_matcher();
219
220        return $this->matcher;
221    }
222
223    /**
224     * Creates a new dumped URL Matcher (dump it if necessary)
225     */
226    protected function create_dumped_url_matcher()
227    {
228        try
229        {
230            $cache = new ConfigCache("{$this->cache_dir}url_matcher.{$this->php_ext}", $this->debug_url_matcher);
231            if (!$cache->isFresh())
232            {
233                $dumper = new CompiledUrlMatcherDumper($this->get_routes());
234                $cache->write($dumper->dump(), $this->get_routes()->getResources());
235            }
236
237            $compiled_routes = require_once($cache->getPath());
238            $this->matcher = new CompiledUrlMatcher($compiled_routes, $this->context);
239        }
240        catch (IOException $e)
241        {
242            $this->create_new_url_matcher();
243        }
244    }
245
246    /**
247     * Creates a new URL Matcher
248     */
249    protected function create_new_url_matcher()
250    {
251        $this->matcher = new UrlMatcher($this->get_routes(), $this->context);
252    }
253
254    /**
255     * Gets the UrlGenerator instance associated with this Router.
256     *
257     * @return \Symfony\Component\Routing\Generator\UrlGeneratorInterface A UrlGeneratorInterface instance
258     */
259    public function get_generator()
260    {
261        if ($this->generator !== null)
262        {
263            return $this->generator;
264        }
265
266        $this->create_dumped_url_generator();
267
268        return $this->generator;
269    }
270
271    /**
272     * Creates a new dumped URL Generator (dump it if necessary)
273     */
274    protected function create_dumped_url_generator()
275    {
276        try
277        {
278            $cache = new ConfigCache("{$this->cache_dir}url_generator.{$this->php_ext}", $this->debug_url_generator);
279            if (!$cache->isFresh())
280            {
281                $dumper = new CompiledUrlGeneratorDumper($this->get_routes());
282                $cache->write($dumper->dump(), $this->get_routes()->getResources());
283            }
284
285            $compiled_routes = require_once($cache->getPath());
286            $this->generator = new CompiledUrlGenerator($compiled_routes, $this->context);
287        }
288        catch (IOException $e)
289        {
290            $this->create_new_url_generator();
291        }
292    }
293
294    /**
295     * Creates a new URL Generator
296     */
297    protected function create_new_url_generator()
298    {
299        $this->generator = new UrlGenerator($this->get_routes(), $this->context);
300    }
301
302    /**
303     * Replaces placeholders with service container parameter values in:
304     * - the route defaults,
305     * - the route requirements,
306     * - the route path,
307     * - the route host,
308     * - the route schemes,
309     * - the route methods.
310     *
311     * @param RouteCollection $collection
312     */
313    protected function resolveParameters(RouteCollection $collection)
314    {
315        /** @var \Symfony\Component\Routing\Route $route */
316        foreach ($collection as $route)
317        {
318            foreach ($route->getDefaults() as $name => $value)
319            {
320                $route->setDefault($name, $this->resolve($value));
321            }
322
323            $requirements = $route->getRequirements();
324            unset($requirements['_scheme']);
325            unset($requirements['_method']);
326
327            foreach ($requirements as $name => $value)
328            {
329                $route->setRequirement($name, $this->resolve($value));
330            }
331
332            $route->setPath($this->resolve($route->getPath()));
333            $route->setHost($this->resolve($route->getHost()));
334
335            $schemes = array();
336            foreach ($route->getSchemes() as $scheme)
337            {
338                $schemes = array_merge($schemes, explode('|', $this->resolve($scheme)));
339            }
340
341            $route->setSchemes($schemes);
342            $methods = array();
343            foreach ($route->getMethods() as $method)
344            {
345                $methods = array_merge($methods, explode('|', $this->resolve($method)));
346            }
347
348            $route->setMethods($methods);
349            $route->setCondition($this->resolve($route->getCondition()));
350        }
351    }
352
353    /**
354     * Recursively replaces placeholders with the service container parameters.
355     *
356     * @param mixed $value The source which might contain "%placeholders%"
357     *
358     * @return mixed The source with the placeholders replaced by the container
359     *               parameters. Arrays are resolved recursively.
360     *
361     * @throws ParameterNotFoundException When a placeholder does not exist as a container parameter
362     * @throws RuntimeException           When a container value is not a string or a numeric value
363     */
364    private function resolve($value)
365    {
366        if (is_array($value))
367        {
368            foreach ($value as $key => $val)
369            {
370                $value[$key] = $this->resolve($val);
371            }
372
373            return $value;
374        }
375
376        if (!is_string($value))
377        {
378            return $value;
379        }
380
381        $container = $this->container;
382        $escapedValue = preg_replace_callback('/%%|%([^%\s]++)%/', function ($match) use ($container, $value)
383        {
384            // skip %%
385            if (!isset($match[1]))
386            {
387                return '%%';
388            }
389
390            $resolved = $container->getParameter($match[1]);
391            if (is_string($resolved) || is_numeric($resolved))
392            {
393                return (string) $resolved;
394            }
395
396            throw new RuntimeException(sprintf(
397                    'The container parameter "%s", used in the route configuration value "%s", '.
398                    'must be a string or numeric, but it is of type %s.',
399                    $match[1],
400                    $value,
401                    gettype($resolved)
402                )
403            );
404        }, $value);
405
406        return str_replace('%%', '%', $escapedValue);
407    }
408}