Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.47% covered (success)
92.47%
135 / 146
76.19% covered (warning)
76.19%
16 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
finder
92.47% covered (success)
92.47%
135 / 146
76.19% covered (warning)
76.19%
16 / 21
62.59
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
3
 set_extensions
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 core_path
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 suffix
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 extension_suffix
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 core_suffix
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 prefix
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 extension_prefix
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 core_prefix
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 directory
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 extension_directory
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 core_directory
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 sanitise_directory
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 get_classes
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 get_classes_from_files
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 get_directories
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_files
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 find_with_root_path
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 find
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 find_from_extension
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 find_from_paths
93.22% covered (success)
93.22%
55 / 59
0.00% covered (danger)
0.00%
0 / 1
27.23
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\finder;
15
16use phpbb\cache\service;
17use phpbb\filesystem\helper as filesystem_helper;
18
19/**
20* The finder provides a simple way to locate files in the core and a set of extensions
21*/
22class finder
23{
24    protected $extensions;
25    protected $phpbb_root_path;
26    protected $cache;
27    protected $use_cache;
28    protected $php_ext;
29
30    /**
31    * The cache variable name used to store $this->cached_queries in $this->cache.
32    *
33    * Allows the use of multiple differently configured finders with the same cache.
34    * @var string
35    */
36    protected $cache_name;
37
38    /**
39    * An associative array, containing all search parameters set in methods.
40    * @var    array
41    */
42    protected $query;
43
44    /**
45    * A map from md5 hashes of serialized queries to their previously retrieved
46    * results.
47    * @var    array
48    */
49    protected $cached_queries;
50
51    /**
52    * Creates a new finder instance with its dependencies
53    *
54    * @param string $phpbb_root_path Path to the phpbb root directory
55    * @param service|null $cache A cache instance or null
56    * @param bool $use_cache Flag whether cache should be used
57    * @param string $php_ext php file extension
58    * @param string $cache_name The name of the cache variable, defaults to
59    *                            _ext_finder
60    */
61    public function __construct(?service $cache, bool $use_cache, string $phpbb_root_path, string $php_ext, string $cache_name = '_ext_finder')
62    {
63        $this->phpbb_root_path = $phpbb_root_path;
64        $this->cache = $cache;
65        $this->use_cache = $use_cache;
66        $this->php_ext = $php_ext;
67        $this->cache_name = $cache_name;
68
69        $this->query = array(
70            'core_path' => false,
71            'core_suffix' => false,
72            'core_prefix' => false,
73            'core_directory' => false,
74            'extension_suffix' => false,
75            'extension_prefix' => false,
76            'extension_directory' => false,
77        );
78        $this->extensions = array();
79
80        $this->cached_queries = $this->cache ? ($this->cache->get($this->cache_name) ?: []) : [];
81    }
82
83    /**
84    * Set the array of extensions
85    *
86    * @param array $extensions        A list of extensions that should be searched as well
87    * @param bool $replace_list        Should the list be emptied before adding the extensions
88    * @return \phpbb\finder\finder This object for chaining calls
89    */
90    public function set_extensions(array $extensions, $replace_list = true)
91    {
92        if ($replace_list)
93        {
94            $this->extensions = array();
95        }
96
97        foreach ($extensions as $ext_name)
98        {
99            $this->extensions[$ext_name] = $this->phpbb_root_path . 'ext/' . $ext_name . '/';
100        }
101        return $this;
102    }
103
104    /**
105    * Sets a core path to be searched in addition to extensions
106    *
107    * @param string $core_path The path relative to phpbb_root_path
108    * @return \phpbb\finder\finder This object for chaining calls
109    */
110    public function core_path($core_path)
111    {
112        $this->query['core_path'] = $core_path;
113        return $this;
114    }
115
116    /**
117    * Sets the suffix all files found in extensions and core must match.
118    *
119    * There is no default file extension, so to find PHP files only, you will
120    * have to specify .php as a suffix. However when using get_classes, the .php
121    * file extension is automatically added to suffixes.
122    *
123    * @param string $suffix A filename suffix
124    * @return \phpbb\finder\finder This object for chaining calls
125    */
126    public function suffix($suffix)
127    {
128        $this->core_suffix($suffix);
129        $this->extension_suffix($suffix);
130        return $this;
131    }
132
133    /**
134    * Sets a suffix all files found in extensions must match
135    *
136    * There is no default file extension, so to find PHP files only, you will
137    * have to specify .php as a suffix. However when using get_classes, the .php
138    * file extension is automatically added to suffixes.
139    *
140    * @param string $extension_suffix A filename suffix
141    * @return \phpbb\finder\finder This object for chaining calls
142    */
143    public function extension_suffix($extension_suffix)
144    {
145        $this->query['extension_suffix'] = $extension_suffix;
146        return $this;
147    }
148
149    /**
150    * Sets a suffix all files found in the core path must match
151    *
152    * There is no default file extension, so to find PHP files only, you will
153    * have to specify .php as a suffix. However when using get_classes, the .php
154    * file extension is automatically added to suffixes.
155    *
156    * @param string $core_suffix A filename suffix
157    * @return \phpbb\finder\finder This object for chaining calls
158    */
159    public function core_suffix($core_suffix)
160    {
161        $this->query['core_suffix'] = $core_suffix;
162        return $this;
163    }
164
165    /**
166    * Sets the prefix all files found in extensions and core must match
167    *
168    * @param string $prefix A filename prefix
169    * @return \phpbb\finder\finder This object for chaining calls
170    */
171    public function prefix($prefix)
172    {
173        $this->core_prefix($prefix);
174        $this->extension_prefix($prefix);
175        return $this;
176    }
177
178    /**
179    * Sets a prefix all files found in extensions must match
180    *
181    * @param string $extension_prefix A filename prefix
182    * @return \phpbb\finder\finder This object for chaining calls
183    */
184    public function extension_prefix($extension_prefix)
185    {
186        $this->query['extension_prefix'] = $extension_prefix;
187        return $this;
188    }
189
190    /**
191    * Sets a prefix all files found in the core path must match
192    *
193    * @param string $core_prefix A filename prefix
194    * @return \phpbb\finder\finder This object for chaining calls
195    */
196    public function core_prefix($core_prefix)
197    {
198        $this->query['core_prefix'] = $core_prefix;
199        return $this;
200    }
201
202    /**
203    * Sets a directory all files found in extensions and core must be contained in
204    *
205    * Automatically sets the core_directory if its value does not differ from
206    * the current directory.
207    *
208    * @param string $directory
209    * @return \phpbb\finder\finder This object for chaining calls
210    */
211    public function directory($directory)
212    {
213        $this->core_directory($directory);
214        $this->extension_directory($directory);
215        return $this;
216    }
217
218    /**
219    * Sets a directory all files found in extensions must be contained in
220    *
221    * @param string $extension_directory
222    * @return \phpbb\finder\finder This object for chaining calls
223    */
224    public function extension_directory($extension_directory)
225    {
226        $this->query['extension_directory'] = $this->sanitise_directory($extension_directory);
227        return $this;
228    }
229
230    /**
231    * Sets a directory all files found in the core path must be contained in
232    *
233    * @param string $core_directory
234    * @return \phpbb\finder\finder This object for chaining calls
235    */
236    public function core_directory($core_directory)
237    {
238        $this->query['core_directory'] = $this->sanitise_directory($core_directory);
239        return $this;
240    }
241
242    /**
243    * Removes occurrences of /./ and makes sure path ends without trailing slash
244    *
245    * @param string $directory A directory pattern
246    * @return string A cleaned up directory pattern
247    */
248    protected function sanitise_directory($directory)
249    {
250        $directory = filesystem_helper::clean_path($directory);
251        $dir_len = strlen($directory);
252
253        if ($dir_len > 1 && $directory[$dir_len - 1] === '/')
254        {
255            $directory = substr($directory, 0, -1);
256        }
257
258        return $directory;
259    }
260
261    /**
262    * Finds classes matching the configured options if they follow phpBB naming rules.
263    *
264    * The php file extension is automatically added to suffixes.
265    *
266    * Note: If a file is matched but contains a class name not following the
267    * phpBB naming rules an incorrect class name will be returned.
268    *
269    * @param bool $cache Whether the result should be cached
270    * @return array An array of found class names
271    */
272    public function get_classes($cache = true)
273    {
274        $this->query['extension_suffix'] .= '.' . $this->php_ext;
275        $this->query['core_suffix'] .= '.' . $this->php_ext;
276
277        $files = $this->find($cache, false);
278
279        return $this->get_classes_from_files($files);
280    }
281
282    /**
283    * Get class names from a list of files
284    *
285    * @param array $files Array of files (from find())
286    * @return array Array of class names
287    */
288    public function get_classes_from_files($files)
289    {
290        $classes = array();
291        foreach ($files as $file => $ext_name)
292        {
293            $class = substr($file, 0, -strlen('.' . $this->php_ext));
294            if ($ext_name === '/' && preg_match('#^includes/#', $file))
295            {
296                $class = preg_replace('#^includes/#', '', $class);
297                $classes[] = 'phpbb_' . str_replace('/', '_', $class);
298            }
299            else
300            {
301                $class = preg_replace('#^ext/#', '', $class);
302                $classes[] = '\\' . str_replace('/', '\\', $class);
303            }
304        }
305        return $classes;
306    }
307
308    /**
309    * Finds all directories matching the configured options
310    *
311    * @param bool $cache Whether the result should be cached
312    * @param bool $extension_keys Whether the result should have extension name as array key
313    * @return array An array of paths to found directories
314    */
315    public function get_directories($cache = true, $extension_keys = false)
316    {
317        return $this->find_with_root_path($cache, true, $extension_keys);
318    }
319
320    /**
321    * Finds all files matching the configured options.
322    *
323    * @param bool $cache Whether the result should be cached
324    * @return array An array of paths to found files
325    */
326    public function get_files($cache = true)
327    {
328        return $this->find_with_root_path($cache, false);
329    }
330
331    /**
332    * A wrapper around the general find which prepends a root path to results
333    *
334    * @param bool $cache Whether the result should be cached
335    * @param bool $is_dir Directories will be returned when true, only files
336    *                    otherwise
337    * @param bool $extension_keys If true, result will be associative array
338    *                    with extension name as key
339    * @return array An array of paths to found items
340    */
341    protected function find_with_root_path($cache = true, $is_dir = false, $extension_keys = false)
342    {
343        $items = $this->find($cache, $is_dir);
344
345        $result = array();
346        foreach ($items as $item => $ext_name)
347        {
348            if ($extension_keys)
349            {
350                $result[$ext_name] = $this->phpbb_root_path . $item;
351            }
352            else
353            {
354                $result[] = $this->phpbb_root_path . $item;
355            }
356        }
357
358        return $result;
359    }
360
361    /**
362    * Finds all file system entries matching the configured options
363    *
364    * @param bool $cache Whether the result should be cached
365    * @param bool $is_dir Directories will be returned when true, only files
366    *                     otherwise
367    * @return array An array of paths to found items
368    */
369    public function find($cache = true, $is_dir = false)
370    {
371        $extensions = $this->extensions;
372        if ($this->query['core_path'])
373        {
374            $extensions['/'] = $this->phpbb_root_path . $this->query['core_path'];
375        }
376
377        $files = array();
378        $file_list = $this->find_from_paths($extensions, $cache, $is_dir);
379
380        foreach ($file_list as $file)
381        {
382            $files[$file['named_path']] = $file['ext_name'];
383        }
384
385        return $files;
386    }
387
388    /**
389    * Finds all file system entries matching the configured options for one
390    * specific extension
391    *
392    * @param string $extension_name Name of the extension
393    * @param string $extension_path Relative path to the extension root directory
394    * @param bool $cache Whether the result should be cached
395    * @param bool $is_dir Directories will be returned when true, only files
396    *                     otherwise
397    * @return array An array of paths to found items
398    */
399    public function find_from_extension($extension_name, $extension_path, $cache = true, $is_dir = false)
400    {
401        $extensions = array(
402            $extension_name => $extension_path,
403        );
404
405        $files = array();
406        $file_list = $this->find_from_paths($extensions, $cache, $is_dir);
407
408        foreach ($file_list as $file)
409        {
410            $files[$file['named_path']] = $file['ext_name'];
411        }
412
413        return $files;
414    }
415
416    /**
417    * Finds all file system entries matching the configured options from
418    * an array of paths
419    *
420    * @param array $extensions Array of extensions (name => full relative path)
421    * @param bool $cache Whether the result should be cached
422    * @param bool $is_dir Directories will be returned when true, only files
423    *                     otherwise
424    * @return array An array of paths to found items
425    */
426    public function find_from_paths($extensions, $cache = true, $is_dir = false)
427    {
428        $this->query['is_dir'] = $is_dir;
429        $query = md5(serialize($this->query) . serialize($extensions));
430
431        if ($this->use_cache && $cache && isset($this->cached_queries[$query]))
432        {
433            return $this->cached_queries[$query];
434        }
435
436        $files = array();
437
438        foreach ($extensions as $name => $path)
439        {
440            $ext_name = $name;
441
442            if (!file_exists($path))
443            {
444                continue;
445            }
446
447            if ($name === '/')
448            {
449                $location = $this->query['core_path'];
450                $name = '';
451                $suffix = $this->query['core_suffix'];
452                $prefix = $this->query['core_prefix'];
453                $directory = $this->query['core_directory'];
454            }
455            else
456            {
457                $location = 'ext/';
458                $name .= '/';
459                $suffix = $this->query['extension_suffix'];
460                $prefix = $this->query['extension_prefix'];
461                $directory = $this->query['extension_directory'];
462            }
463
464            // match only first directory if leading slash is given
465            if ($directory === '/')
466            {
467                $directory_pattern = '^' . preg_quote(DIRECTORY_SEPARATOR, '#');
468            }
469            else if ($directory && $directory[0] === '/')
470            {
471                if (!$is_dir)
472                {
473                    $path .= substr($directory, 1);
474                }
475                $directory_pattern = '^' . preg_quote(str_replace('/', DIRECTORY_SEPARATOR, $directory) . DIRECTORY_SEPARATOR, '#');
476            }
477            else
478            {
479                $directory_pattern = preg_quote(DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $directory) . DIRECTORY_SEPARATOR, '#');
480            }
481            if ($is_dir)
482            {
483                $directory_pattern .= '$';
484            }
485            $directory_pattern = '#' . $directory_pattern . '#';
486
487            if (is_dir($path))
488            {
489                $iterator = new \phpbb\finder\recursive_path_iterator(
490                    $path,
491                    \RecursiveIteratorIterator::SELF_FIRST
492                );
493
494                foreach ($iterator as $file_info)
495                {
496                    $filename = $file_info->getFilename();
497
498                    if ($file_info->isDir() == $is_dir)
499                    {
500                        if ($is_dir)
501                        {
502                            $relative_path = $iterator->getInnerIterator()->getSubPath() . DIRECTORY_SEPARATOR . basename($filename) . DIRECTORY_SEPARATOR;
503                            if ($relative_path[0] !== DIRECTORY_SEPARATOR)
504                            {
505                                $relative_path = DIRECTORY_SEPARATOR . $relative_path;
506                            }
507                        }
508                        else
509                        {
510                            $relative_path = $iterator->getInnerIterator()->getSubPathname();
511                            if ($directory && $directory[0] === '/')
512                            {
513                                $relative_path = str_replace('/', DIRECTORY_SEPARATOR, $directory) . DIRECTORY_SEPARATOR . $relative_path;
514                            }
515                            else
516                            {
517                                $relative_path = DIRECTORY_SEPARATOR . $relative_path;
518                            }
519                        }
520
521                        if ((!$suffix || substr($relative_path, -strlen($suffix)) === $suffix) &&
522                            (!$prefix || substr($filename, 0, strlen($prefix)) === $prefix) &&
523                            (!$directory || preg_match($directory_pattern, $relative_path)))
524                        {
525                            $files[] = array(
526                                'named_path'    => str_replace(DIRECTORY_SEPARATOR, '/', $location . $name . substr($relative_path, 1)),
527                                'ext_name'        => $ext_name,
528                                'path'            => str_replace(array(DIRECTORY_SEPARATOR, $this->phpbb_root_path), array('/', ''), $file_info->getPath()) . '/',
529                                'filename'        => $filename,
530                            );
531                        }
532                    }
533                }
534            }
535        }
536
537        if ($cache && $this->cache)
538        {
539            $this->cached_queries[$query] = $files;
540            $this->cache->put($this->cache_name, $this->cached_queries);
541        }
542
543        return $files;
544    }
545}