Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
54.26% covered (warning)
54.26%
51 / 94
20.00% covered (danger)
20.00%
3 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
router
54.26% covered (warning)
54.26%
51 / 94
20.00% covered (danger)
20.00%
3 / 15
168.05
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
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_matcher
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 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
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 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
84.21% covered (warning)
84.21%
16 / 19
0.00% covered (danger)
0.00%
0 / 1
6.14
 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()
150    {
151        return $this->get_routes();
152    }
153
154    /**
155     * {@inheritdoc}
156     */
157    public function setContext(RequestContext $context)
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        return $this->get_matcher()->match($pathinfo);
193    }
194
195    /**
196     * Gets the UrlMatcher instance associated with this Router.
197     *
198     * @return \Symfony\Component\Routing\Matcher\UrlMatcherInterface A UrlMatcherInterface instance
199     */
200    public function get_matcher()
201    {
202        if ($this->matcher !== null)
203        {
204            return $this->matcher;
205        }
206
207        $this->create_dumped_url_matcher();
208
209        return $this->matcher;
210    }
211
212    /**
213     * Creates a new dumped URL Matcher (dump it if necessary)
214     */
215    protected function create_dumped_url_matcher()
216    {
217        try
218        {
219            $cache = new ConfigCache("{$this->cache_dir}url_matcher.{$this->php_ext}", $this->debug_url_matcher);
220            if (!$cache->isFresh())
221            {
222                $dumper = new CompiledUrlMatcherDumper($this->get_routes());
223                $cache->write($dumper->dump(), $this->get_routes()->getResources());
224            }
225
226            $compiled_routes = require_once($cache->getPath());
227            $this->matcher = new CompiledUrlMatcher($compiled_routes, $this->context);
228        }
229        catch (IOException $e)
230        {
231            $this->create_new_url_matcher();
232        }
233    }
234
235    /**
236     * Creates a new URL Matcher
237     */
238    protected function create_new_url_matcher()
239    {
240        $this->matcher = new UrlMatcher($this->get_routes(), $this->context);
241    }
242
243    /**
244     * Gets the UrlGenerator instance associated with this Router.
245     *
246     * @return \Symfony\Component\Routing\Generator\UrlGeneratorInterface A UrlGeneratorInterface instance
247     */
248    public function get_generator()
249    {
250        if ($this->generator !== null)
251        {
252            return $this->generator;
253        }
254
255        $this->create_dumped_url_generator();
256
257        return $this->generator;
258    }
259
260    /**
261     * Creates a new dumped URL Generator (dump it if necessary)
262     */
263    protected function create_dumped_url_generator()
264    {
265        try
266        {
267            $cache = new ConfigCache("{$this->cache_dir}url_generator.{$this->php_ext}", $this->debug_url_generator);
268            if (!$cache->isFresh())
269            {
270                $dumper = new CompiledUrlGeneratorDumper($this->get_routes());
271                $cache->write($dumper->dump(), $this->get_routes()->getResources());
272            }
273
274            $compiled_routes = require_once($cache->getPath());
275            $this->generator = new CompiledUrlGenerator($compiled_routes, $this->context);
276        }
277        catch (IOException $e)
278        {
279            $this->create_new_url_generator();
280        }
281    }
282
283    /**
284     * Creates a new URL Generator
285     */
286    protected function create_new_url_generator()
287    {
288        $this->generator = new UrlGenerator($this->get_routes(), $this->context);
289    }
290
291    /**
292     * Replaces placeholders with service container parameter values in:
293     * - the route defaults,
294     * - the route requirements,
295     * - the route path,
296     * - the route host,
297     * - the route schemes,
298     * - the route methods.
299     *
300     * @param RouteCollection $collection
301     */
302    protected function resolveParameters(RouteCollection $collection)
303    {
304        /** @var \Symfony\Component\Routing\Route $route */
305        foreach ($collection as $route)
306        {
307            foreach ($route->getDefaults() as $name => $value)
308            {
309                $route->setDefault($name, $this->resolve($value));
310            }
311
312            $requirements = $route->getRequirements();
313            unset($requirements['_scheme']);
314            unset($requirements['_method']);
315
316            foreach ($requirements as $name => $value)
317            {
318                $route->setRequirement($name, $this->resolve($value));
319            }
320
321            $route->setPath($this->resolve($route->getPath()));
322            $route->setHost($this->resolve($route->getHost()));
323
324            $schemes = array();
325            foreach ($route->getSchemes() as $scheme)
326            {
327                $schemes = array_merge($schemes, explode('|', $this->resolve($scheme)));
328            }
329
330            $route->setSchemes($schemes);
331            $methods = array();
332            foreach ($route->getMethods() as $method)
333            {
334                $methods = array_merge($methods, explode('|', $this->resolve($method)));
335            }
336
337            $route->setMethods($methods);
338            $route->setCondition($this->resolve($route->getCondition()));
339        }
340    }
341
342    /**
343     * Recursively replaces placeholders with the service container parameters.
344     *
345     * @param mixed $value The source which might contain "%placeholders%"
346     *
347     * @return mixed The source with the placeholders replaced by the container
348     *               parameters. Arrays are resolved recursively.
349     *
350     * @throws ParameterNotFoundException When a placeholder does not exist as a container parameter
351     * @throws RuntimeException           When a container value is not a string or a numeric value
352     */
353    private function resolve($value)
354    {
355        if (is_array($value))
356        {
357            foreach ($value as $key => $val)
358            {
359                $value[$key] = $this->resolve($val);
360            }
361
362            return $value;
363        }
364
365        if (!is_string($value))
366        {
367            return $value;
368        }
369
370        $container = $this->container;
371        $escapedValue = preg_replace_callback('/%%|%([^%\s]++)%/', function ($match) use ($container, $value)
372        {
373            // skip %%
374            if (!isset($match[1]))
375            {
376                return '%%';
377            }
378
379            $resolved = $container->getParameter($match[1]);
380            if (is_string($resolved) || is_numeric($resolved))
381            {
382                return (string) $resolved;
383            }
384
385            throw new RuntimeException(sprintf(
386                    'The container parameter "%s", used in the route configuration value "%s", '.
387                    'must be a string or numeric, but it is of type %s.',
388                    $match[1],
389                    $value,
390                    gettype($resolved)
391                )
392            );
393        }, $value);
394
395        return str_replace('%%', '%', $escapedValue);
396    }
397}