Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.64% covered (success)
97.64%
124 / 127
86.67% covered (warning)
86.67%
13 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
path_helper
97.64% covered (success)
97.64%
124 / 127
86.67% covered (warning)
86.67%
13 / 15
61
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 get_phpbb_root_path
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_adm_relative_path
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_php_ext
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 update_web_root_path
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
7.04
 remove_web_root_path
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 get_web_root_path
92.59% covered (success)
92.59%
25 / 27
0.00% covered (danger)
0.00%
0 / 1
13.07
 get_web_root_path_from_ajax_referer
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
7
 clean_url
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 glue_url_params
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 get_url_parts
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
8
 strip_url_params
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 append_url_params
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 get_valid_page
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
6
 is_router_used
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
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;
15
16use phpbb\filesystem\helper as filesystem_helper;
17
18/**
19* A class with various functions that are related to paths, files and the filesystem
20*/
21class path_helper
22{
23    /** @var \phpbb\symfony_request */
24    protected $symfony_request;
25
26    /** @var \phpbb\request\request_interface */
27    protected $request;
28
29    /** @var string */
30    protected $phpbb_root_path;
31
32    /** @var string */
33    protected $adm_relative_path;
34
35    /** @var string */
36    protected $php_ext;
37
38    /** @var string */
39    protected $web_root_path;
40
41    /** @var bool Flag whether we're in adm path */
42    protected $in_adm_path = false;
43
44    /**
45    * Constructor
46    *
47    * @param \phpbb\symfony_request $symfony_request
48    * @param \phpbb\request\request_interface $request
49    * @param string $phpbb_root_path Relative path to phpBB root
50    * @param string $php_ext PHP file extension
51    * @param mixed $adm_relative_path Relative path admin path to adm/ root
52    */
53    public function __construct(\phpbb\symfony_request $symfony_request, \phpbb\request\request_interface $request, $phpbb_root_path, $php_ext, $adm_relative_path = null)
54    {
55        $this->symfony_request = $symfony_request;
56        $this->request = $request;
57        $this->phpbb_root_path = $phpbb_root_path;
58        $this->php_ext = $php_ext;
59        $this->adm_relative_path = $adm_relative_path;
60    }
61
62    /**
63    * Get the phpBB root path
64    *
65    * @return string
66    */
67    public function get_phpbb_root_path()
68    {
69        return $this->phpbb_root_path;
70    }
71
72    /**
73    * Get the adm root path
74    *
75    * @return string
76    */
77    public function get_adm_relative_path()
78    {
79        return $this->adm_relative_path;
80    }
81
82    /**
83    * Get the php extension
84    *
85    * @return string
86    */
87    public function get_php_ext()
88    {
89        return $this->php_ext;
90    }
91
92    /**
93    * Update a web path to the correct relative root path
94    *
95    * This replaces $phpbb_root_path . some_url with
96    *    get_web_root_path() . some_url
97    *
98    * @param string $path The path to be updated
99    * @return string
100    */
101    public function update_web_root_path($path)
102    {
103        $web_root_path = $this->get_web_root_path();
104
105        // Removes the web root path if it is already present
106        if (strpos($path, $web_root_path) === 0)
107        {
108            $path = $this->phpbb_root_path . substr($path, strlen($web_root_path));
109        }
110
111        if (strpos($path, $this->phpbb_root_path) === 0)
112        {
113            $path = substr($path, strlen($this->phpbb_root_path));
114
115            if (substr($web_root_path, -8) === 'app.php/' && substr($path, 0, 7) === 'app.php')
116            {
117                $path = substr($path, 8);
118            }
119
120            $path = filesystem_helper::clean_path($web_root_path . $path);
121
122            // Further clean path if we're in adm
123            if ($this->in_adm_path && str_starts_with($path, $this->phpbb_root_path . $this->adm_relative_path))
124            {
125                $path = substr($path, strlen($this->phpbb_root_path . $this->adm_relative_path));
126            }
127        }
128
129        return $path;
130    }
131
132    /**
133    * Strips away the web root path and prepends the normal root path
134    *
135    * This replaces get_web_root_path() . some_url with
136    *    $phpbb_root_path . some_url
137    *
138    * @param string $path The path to be updated
139    * @return string
140    */
141    public function remove_web_root_path($path)
142    {
143        if (strpos($path, $this->get_web_root_path()) === 0)
144        {
145            $path = substr($path, strlen($this->get_web_root_path()));
146
147            return $this->phpbb_root_path . $path;
148        }
149
150        return $path;
151    }
152
153    /**
154    * Get a relative root path from the current URL
155    *
156    * @return string
157    */
158    public function get_web_root_path()
159    {
160        if (null !== $this->web_root_path)
161        {
162            return $this->web_root_path;
163        }
164
165        if (defined('PHPBB_USE_BOARD_URL_PATH') && PHPBB_USE_BOARD_URL_PATH)
166        {
167            return $this->web_root_path = generate_board_url() . '/';
168        }
169
170        // We do not need to escape $path_info, $request_uri and $script_name because we can not find their content in the result.
171        // Path info (e.g. /foo/bar)
172        $path_info = filesystem_helper::clean_path($this->symfony_request->getPathInfo());
173
174        // Full request URI (e.g. phpBB/app.php/foo/bar)
175        $request_uri = $this->symfony_request->getRequestUri();
176
177        // Script name URI (e.g. phpBB/app.php)
178        $script_name = $this->symfony_request->getScriptName();
179
180        /*
181        * If the path info is empty but we're using app.php, then we
182        *    might be using an empty route like app.php/ which is
183        *    supported by symfony's routing
184        */
185        if ($path_info === '/' && preg_match('/app\.' . $this->php_ext . '\/$/', $request_uri))
186        {
187            return $this->web_root_path = filesystem_helper::clean_path('./../' . $this->phpbb_root_path);
188        }
189
190        if ($path_info === '/' && defined('ADMIN_START') && preg_match('/\/' . preg_quote($this->adm_relative_path, '/') . 'index\.' . $this->php_ext . '$/', $script_name))
191        {
192            $this->in_adm_path = true;
193        }
194
195        /*
196        * If the path info is empty (single /), then we're not using
197        *    a route like app.php/foo/bar
198        */
199        if ($path_info === '/')
200        {
201            return $this->web_root_path = $this->phpbb_root_path;
202        }
203
204        /*
205        * Check AJAX request:
206        * If the current request is a AJAX we need to fix the paths.
207        * We need to get the root path based on the Referer, so we can use
208        * the generated URLs in the template of the Referer. If we do not
209        * generate the relative path based on the Referer, but based on the
210        * currently requested URL, the generated URLs will not point to the
211        * intended locations:
212        *    Referer                desired URL            desired relative root path
213        *    memberlist.php        faq.php                ./
214        *    memberlist.php        app.php/foo/bar        ./
215        *    app.php/foo            memberlist.php        ../
216        *    app.php/foo            app.php/fox            ../
217        *    app.php/foo/bar        memberlist.php        ../../
218        *    ../page.php            memberlist.php        ./phpBB/
219        *    ../sub/page.php        memberlist.php        ./../phpBB/
220        *
221        * The referer must be specified as a parameter in the query.
222        */
223        if ($this->request->is_ajax() && $this->symfony_request->get('_referer'))
224        {
225            // We need to escape $absolute_board_url because it can be partially concatenated to the result.
226            $absolute_board_url = $this->request->escape($this->symfony_request->getSchemeAndHttpHost() . $this->symfony_request->getBasePath(), true);
227
228            $referer_web_root_path = $this->get_web_root_path_from_ajax_referer(
229                $this->symfony_request->get('_referer'),
230                $absolute_board_url
231            );
232            return $this->web_root_path = $referer_web_root_path;
233        }
234
235        // How many corrections might we need?
236        $corrections = substr_count($path_info, '/');
237
238        /*
239        * If the script name (e.g. phpBB/app.php) does not exists in the
240        * requestUri (e.g. phpBB/app.php/foo/template), then we are rewriting
241        * the URL. So we must reduce the slash count by 1.
242        */
243        if (strpos($request_uri, $script_name) !== 0)
244        {
245            $corrections--;
246        }
247
248        // Prepend ../ to the phpbb_root_path as many times as / exists in path_info
249        $this->web_root_path = filesystem_helper::clean_path(
250            './' . str_repeat('../', max(0, $corrections)) . $this->phpbb_root_path
251        );
252        return $this->web_root_path;
253    }
254
255    /**
256    * Get the web root path of the referer form an ajax request
257    *
258    * @param string $absolute_referer_url
259    * @param string $absolute_board_url
260    * @return string
261    */
262    public function get_web_root_path_from_ajax_referer($absolute_referer_url, $absolute_board_url)
263    {
264        // If the board URL is in the beginning of the referer, this means
265        // we the referer is in the board URL or a subdirectory of it.
266        // So we just need to count the / (slashes) in the left over part of
267        // the referer and prepend ../ the the current root_path, to get the
268        // web root path of the referer.
269        if (strpos($absolute_referer_url, $absolute_board_url) === 0)
270        {
271            $relative_referer_path = substr($absolute_referer_url, strlen($absolute_board_url));
272            $has_params = strpos($relative_referer_path, '?');
273            if ($has_params !== false)
274            {
275                $relative_referer_path = substr($relative_referer_path, 0, $has_params);
276            }
277            $corrections = substr_count($relative_referer_path, '/');
278            return $this->phpbb_root_path . str_repeat('../', max(0, $corrections - 1));
279        }
280
281        // If not, it's a bit more complicated. We go to the parent directory
282        // of the referer until we find the remaining referer in the board URL.
283        // Foreach directory we need to add a ../ to the fixed root_path.
284        // When we finally found it, we need to remove the remaining referer
285        // from the board URL, to get the boards root path.
286        // If the then append these two strings, we get our fixed web root path.
287        $fixed_root_path = '';
288        $referer_dir = $absolute_referer_url;
289        $has_params = strpos($referer_dir, '?');
290        if ($has_params !== false)
291        {
292            $referer_dir = substr($referer_dir, 0, $has_params);
293        }
294
295        // If we do not find a slash at the end of the referer, we come
296        // from a file. So the first dirname() does not need a traversal
297        // path correction.
298        if (substr($referer_dir, -1) !== '/')
299        {
300            $referer_dir = dirname($referer_dir);
301        }
302
303        while (($dir_position = strpos($absolute_board_url, $referer_dir)) !== 0)
304        {
305            $fixed_root_path .= '../';
306            $referer_dir = dirname($referer_dir);
307
308            // Just return phpbb_root_path if we reach the top directory
309            if ($referer_dir === '.')
310            {
311                return $this->phpbb_root_path;
312            }
313        }
314
315        $fixed_root_path .= substr($absolute_board_url, strlen($referer_dir) + 1);
316        // Add trailing slash
317        return $this->phpbb_root_path . $fixed_root_path . '/';
318    }
319
320    /**
321    * Eliminates useless . and .. components from specified URL
322    *
323    * @param string $url URL to clean
324    *
325    * @return string Cleaned URL
326    */
327    public function clean_url($url)
328    {
329        $delimiter_position = strpos($url, '://');
330        // URL should contain :// but it shouldn't start with it.
331        // Do not clean URLs that do not fit these constraints.
332        if (empty($delimiter_position))
333        {
334            return $url;
335        }
336        $scheme = substr($url, 0, $delimiter_position) . '://';
337        // Add length of URL delimiter to position
338        $path = substr($url, $delimiter_position + 3);
339
340        return $scheme . filesystem_helper::clean_path($path);
341    }
342
343    /**
344    * Glue URL parameters together
345    *
346    * @param array $params URL parameters in the form of array(name => value)
347    * @return string Returns the glued string, e.g. name1=value1&amp;name2&amp;name3=value3
348    */
349    public function glue_url_params($params)
350    {
351        $_params = array();
352
353        foreach ($params as $key => $value)
354        {
355            // some parameters do not have value
356            if ($value !== null)
357            {
358                $_params[] = $key . '=' . $value;
359            }
360            else
361            {
362                $_params[] = $key;
363            }
364        }
365        return implode('&amp;', $_params);
366    }
367
368    /**
369    * Get the base and parameters of a URL
370    *
371    * @param string $url URL to break apart
372    * @param bool $is_amp Is the parameter separator &amp;. Defaults to true.
373    * @return array Returns the base and parameters in the form of array('base' => string, 'params' => array(name => value))
374    */
375    public function get_url_parts($url, $is_amp = true)
376    {
377        $separator = ($is_amp) ? '&amp;' : '&';
378        $params = array();
379
380        if (strpos($url, '?') !== false)
381        {
382            $base = substr($url, 0, strpos($url, '?'));
383            $args = substr($url, strlen($base) + 1);
384            $args = ($args) ? explode($separator, $args) : array();
385
386            foreach ($args as $argument)
387            {
388                if (empty($argument))
389                {
390                    continue;
391                }
392
393                // some parameters don't have value
394                if (strpos($argument, '=') !== false)
395                {
396                    list($key, $value) = explode('=', $argument, 2);
397                }
398                else
399                {
400                    $key = $argument;
401                    $value = null;
402                }
403
404                if ($key === '')
405                {
406                    continue;
407                }
408
409                $params[$key] = $value;
410            }
411        }
412        else
413        {
414            $base = $url;
415        }
416
417        return array(
418            'base'        => $base,
419            'params'    => $params,
420        );
421    }
422
423    /**
424    * Strip parameters from an already built URL.
425    *
426    * @param string $url URL to strip parameters from
427    * @param array|string $strip Parameters to strip.
428    * @param bool $is_amp Is the parameter separator &amp;. Defaults to true.
429    * @return string Returns the new URL.
430    */
431    public function strip_url_params($url, $strip, $is_amp = true)
432    {
433        $url_parts = $this->get_url_parts($url, $is_amp);
434        $params = $url_parts['params'];
435
436        if (!is_array($strip))
437        {
438            $strip = array($strip);
439        }
440
441        if (!empty($params))
442        {
443            // Strip the parameters off
444            foreach ($strip as $param)
445            {
446                unset($params[$param]);
447            }
448        }
449
450        return $url_parts['base'] . (($params) ? '?' . $this->glue_url_params($params) : '');
451    }
452
453    /**
454    * Append parameters to an already built URL.
455    *
456    * @param string $url URL to append parameters to
457    * @param array $new_params Parameters to add in the form of array(name => value)
458    * @param bool $is_amp Is the parameter separator &amp;. Defaults to true.
459    * @return string Returns the new URL.
460    */
461    public function append_url_params($url, $new_params, $is_amp = true)
462    {
463        $url_parts = $this->get_url_parts($url, $is_amp);
464        $params = array_merge($url_parts['params'], $new_params);
465
466        // Move the sid to the end if it's set
467        if (isset($params['sid']))
468        {
469            $sid = $params['sid'];
470            unset($params['sid']);
471            $params['sid'] = $sid;
472        }
473
474        return $url_parts['base'] . (($params) ? '?' . $this->glue_url_params($params) : '');
475    }
476
477    /**
478     * Get a valid page
479     *
480     * @param string $page The page to verify
481     * @param bool $mod_rewrite Whether mod_rewrite is enabled, default: false
482     *
483     * @return string A valid page based on given page and mod_rewrite
484     */
485    public function get_valid_page($page, $mod_rewrite = false)
486    {
487        // We need to be cautious here.
488        // On some situations, the redirect path is an absolute URL, sometimes a relative path
489        // For a relative path, let's prefix it with $phpbb_root_path to point to the correct location,
490        // else we use the URL directly.
491        $url_parts = parse_url($page);
492
493        // URL
494        if ($url_parts === false || empty($url_parts['scheme']) || empty($url_parts['host']))
495        {
496            // Remove 'app.php/' from the page, when rewrite is enabled.
497            // Treat app.php as a reserved file name and remove on mod rewrite
498            // even if it might not be in the phpBB root.
499            if ($mod_rewrite && ($app_position = strpos($page, 'app.' . $this->php_ext . '/')) !== false)
500            {
501                $page = substr($page, 0, $app_position) . substr($page, $app_position + strlen('app.' . $this->php_ext . '/'));
502            }
503
504            // Remove preceding slashes from page name and prepend root path
505            $page = $this->get_phpbb_root_path() . ltrim($page, '/\\');
506        }
507
508        return $page;
509    }
510
511    /**
512     * Tells if the router is currently in use (if the current page is a route or not)
513     *
514     * @return bool
515     */
516    public function is_router_used()
517    {
518        // Script name URI (e.g. phpBB/app.php)
519        $script_name = $this->symfony_request->getScriptName();
520
521        return basename($script_name) === 'app.' . $this->php_ext;
522    }
523}