Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
97.64% |
124 / 127 |
|
86.67% |
13 / 15 |
CRAP | |
0.00% |
0 / 1 |
path_helper | |
97.64% |
124 / 127 |
|
86.67% |
13 / 15 |
61 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
get_phpbb_root_path | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
get_adm_relative_path | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
get_php_ext | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
update_web_root_path | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
7.04 | |||
remove_web_root_path | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
get_web_root_path | |
92.59% |
25 / 27 |
|
0.00% |
0 / 1 |
13.07 | |||
get_web_root_path_from_ajax_referer | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
7 | |||
clean_url | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
glue_url_params | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
get_url_parts | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
8 | |||
strip_url_params | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
append_url_params | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
get_valid_page | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
6 | |||
is_router_used | |
100.00% |
2 / 2 |
|
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 | |
14 | namespace phpbb; |
15 | |
16 | use phpbb\filesystem\helper as filesystem_helper; |
17 | |
18 | /** |
19 | * A class with various functions that are related to paths, files and the filesystem |
20 | */ |
21 | class 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->request->header('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->request->header('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&name2&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('&', $_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 &. 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) ? '&' : '&'; |
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 &. 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 &. 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 | } |