Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
46.43% covered (danger)
46.43%
91 / 196
29.17% covered (danger)
29.17%
7 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
filesystem
46.43% covered (danger)
46.43%
91 / 196
29.17% covered (danger)
29.17%
7 / 24
2527.26
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 chgrp
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 chmod
59.09% covered (warning)
59.09%
13 / 22
0.00% covered (danger)
0.00%
0 / 1
33.53
 chown
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 clean_path
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 copy
33.33% covered (danger)
33.33%
1 / 3
0.00% covered (danger)
0.00%
0 / 1
3.19
 dump_file
33.33% covered (danger)
33.33%
1 / 3
0.00% covered (danger)
0.00%
0 / 1
3.19
 exists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_absolute_path
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_readable
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
56
 is_writable
35.71% covered (danger)
35.71%
5 / 14
0.00% covered (danger)
0.00%
0 / 1
74.78
 make_path_relative
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 mirror
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 mkdir
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 phpbb_chmod
59.26% covered (warning)
59.26%
32 / 54
0.00% covered (danger)
0.00%
0 / 1
112.17
 realpath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 remove
20.00% covered (danger)
20.00%
1 / 5
0.00% covered (danger)
0.00%
0 / 1
4.05
 rename
20.00% covered (danger)
20.00%
1 / 5
0.00% covered (danger)
0.00%
0 / 1
4.05
 symlink
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 touch
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 phpbb_is_writable
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
90
 phpbb_own_realpath
73.33% covered (warning)
73.33%
22 / 30
0.00% covered (danger)
0.00%
0 / 1
17.72
 to_iterator
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 resolve_path
100.00% covered (success)
100.00%
1 / 1
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\filesystem;
15
16use Symfony\Component\Filesystem\Exception\IOException;
17use phpbb\filesystem\exception\filesystem_exception;
18
19/**
20 * A class with various functions that are related to paths, files and the filesystem
21 */
22class filesystem implements filesystem_interface
23{
24    /**
25     * Store some information about file ownership for phpBB's chmod function
26     *
27     * @var array
28     */
29    protected $chmod_info;
30
31    /**
32     * Stores current working directory
33     *
34     * @var string|bool|null        current working directory or false if it cannot be recovered
35     */
36    protected $working_directory;
37
38    /**
39     * Symfony's Filesystem component
40     *
41     * @var \Symfony\Component\Filesystem\Filesystem
42     */
43    protected $symfony_filesystem;
44
45    /**
46     * Constructor
47     */
48    public function __construct()
49    {
50        $this->chmod_info            = array();
51        $this->symfony_filesystem    = new \Symfony\Component\Filesystem\Filesystem();
52        $this->working_directory    = null;
53    }
54
55    /**
56     * {@inheritdoc}
57     */
58    public function chgrp($files, $group, $recursive = false)
59    {
60        try
61        {
62            $this->symfony_filesystem->chgrp($files, $group, $recursive);
63        }
64        catch (IOException $e)
65        {
66            // Try to recover filename
67            // By the time this is written that is at the end of the message
68            $error = trim($e->getMessage());
69            $file = substr($error, strrpos($error, ' '));
70
71            throw new filesystem_exception('FILESYSTEM_CANNOT_CHANGE_FILE_GROUP', $file, array(), $e);
72        }
73    }
74
75    /**
76     * {@inheritdoc}
77     */
78    public function chmod($files, $perms = null, $recursive = false, $force_chmod_link = false)
79    {
80        if (is_null($perms))
81        {
82            // Default to read permission for compatibility reasons
83            $perms = self::CHMOD_READ;
84        }
85
86        // Check if we got a permission flag
87        if ($perms > self::CHMOD_ALL)
88        {
89            $file_perm = $perms;
90
91            // Extract permissions
92            //$owner = ($file_perm >> 6) & 7; // This will be ignored
93            $group = ($file_perm >> 3) & 7;
94            $other = ($file_perm >> 0) & 7;
95
96            // Does any permissions provided? if so we add execute bit for directories
97            $group = ($group !== 0) ? ($group | self::CHMOD_EXECUTE) : $group;
98            $other = ($other !== 0) ? ($other | self::CHMOD_EXECUTE) : $other;
99
100            // Compute directory permissions
101            $dir_perm = (self::CHMOD_ALL << 6) + ($group << 3) + ($other << 3);
102        }
103        else
104        {
105            // Add execute bit to owner if execute bit is among perms
106            $owner_perm    = (self::CHMOD_READ | self::CHMOD_WRITE) | ($perms & self::CHMOD_EXECUTE);
107            $file_perm    = ($owner_perm << 6) + ($perms << 3) + ($perms << 0);
108
109            // Compute directory permissions
110            $perm = ($perms !== 0) ? ($perms | self::CHMOD_EXECUTE) : $perms;
111            $dir_perm = (($owner_perm | self::CHMOD_EXECUTE) << 6) + ($perm << 3) + ($perm << 0);
112        }
113
114        // Symfony's filesystem component does not support extra execution flags on directories
115        // so we need to implement it again
116        foreach ($this->to_iterator($files) as $file)
117        {
118            if ($recursive && is_dir($file) && !is_link($file))
119            {
120                $this->chmod(new \FilesystemIterator($file), $perms, true);
121            }
122
123            // Don't chmod links as mostly those require 0777 and that cannot be changed
124            if (is_dir($file) || (is_link($file) && $force_chmod_link))
125            {
126                if (true !== @chmod($file, $dir_perm))
127                {
128                    throw new filesystem_exception('FILESYSTEM_CANNOT_CHANGE_FILE_PERMISSIONS', $file,  array());
129                }
130            }
131            else if (is_file($file))
132            {
133                if (true !== @chmod($file, $file_perm))
134                {
135                    throw new filesystem_exception('FILESYSTEM_CANNOT_CHANGE_FILE_PERMISSIONS', $file,  array());
136                }
137            }
138        }
139    }
140
141    /**
142     * {@inheritdoc}
143     */
144    public function chown($files, $user, $recursive = false)
145    {
146        try
147        {
148            $this->symfony_filesystem->chown($files, $user, $recursive);
149        }
150        catch (IOException $e)
151        {
152            // Try to recover filename
153            // By the time this is written that is at the end of the message
154            $error = trim($e->getMessage());
155            $file = substr($error, strrpos($error, ' '));
156
157            throw new filesystem_exception('FILESYSTEM_CANNOT_CHANGE_FILE_GROUP', $file, array(), $e);
158        }
159    }
160
161    /**
162     * {@inheritdoc}
163     */
164    public function clean_path($path)
165    {
166        return helper::clean_path($path);
167    }
168
169    /**
170     * {@inheritdoc}
171     */
172    public function copy($origin_file, $target_file, $override = false)
173    {
174        try
175        {
176            $this->symfony_filesystem->copy($origin_file, $target_file, $override);
177        }
178        catch (IOException $e)
179        {
180            throw new filesystem_exception('FILESYSTEM_CANNOT_COPY_FILES', '', array(), $e);
181        }
182    }
183
184    /**
185     * {@inheritdoc}
186     */
187    public function dump_file($filename, $content)
188    {
189        try
190        {
191            $this->symfony_filesystem->dumpFile($filename, $content);
192        }
193        catch (IOException $e)
194        {
195            throw new filesystem_exception('FILESYSTEM_CANNOT_DUMP_FILE', $filename, array(), $e);
196        }
197    }
198
199    /**
200     * {@inheritdoc}
201     */
202    public function exists($files)
203    {
204        return $this->symfony_filesystem->exists($files);
205    }
206
207    /**
208     * {@inheritdoc}
209     */
210    public function is_absolute_path($path)
211    {
212        return helper::is_absolute_path($path);
213    }
214
215    /**
216     * {@inheritdoc}
217     */
218    public function is_readable($files, $recursive = false)
219    {
220        foreach ($this->to_iterator($files) as $file)
221        {
222            if ($recursive && is_dir($file) && !is_link($file))
223            {
224                if (!$this->is_readable(new \FilesystemIterator($file), true))
225                {
226                    return false;
227                }
228            }
229
230            if (!is_readable($file))
231            {
232                return false;
233            }
234        }
235
236        return true;
237    }
238
239    /**
240     * {@inheritdoc}
241     */
242    public function is_writable($files, $recursive = false)
243    {
244        if (defined('PHP_WINDOWS_VERSION_MAJOR') || !function_exists('is_writable'))
245        {
246            foreach ($this->to_iterator($files) as $file)
247            {
248                if ($recursive && is_dir($file) && !is_link($file))
249                {
250                    if (!$this->is_writable(new \FilesystemIterator($file), true))
251                    {
252                        return false;
253                    }
254                }
255
256                if (!$this->phpbb_is_writable($file))
257                {
258                    return false;
259                }
260            }
261        }
262        else
263        {
264            // use built in is_writable
265            foreach ($this->to_iterator($files) as $file)
266            {
267                if ($recursive && is_dir($file) && !is_link($file))
268                {
269                    if (!$this->is_writable(new \FilesystemIterator($file), true))
270                    {
271                        return false;
272                    }
273                }
274
275                if (!is_writable($file))
276                {
277                    return false;
278                }
279            }
280        }
281
282        return true;
283    }
284
285    /**
286     * {@inheritdoc}
287     */
288    public function make_path_relative($end_path, $start_path)
289    {
290        return helper::make_path_relative($end_path, $start_path);
291    }
292
293    /**
294     * {@inheritdoc}
295     */
296    public function mirror($origin_dir, $target_dir, \Traversable $iterator = null, $options = array())
297    {
298        try
299        {
300            $this->symfony_filesystem->mirror($origin_dir, $target_dir, $iterator, $options);
301        }
302        catch (IOException $e)
303        {
304            $msg = $e->getMessage();
305            $filename = substr($msg, strpos($msg, '"'), strrpos($msg, '"'));
306
307            throw new filesystem_exception('FILESYSTEM_CANNOT_MIRROR_DIRECTORY', $filename, array(), $e);
308        }
309    }
310
311    /**
312     * {@inheritdoc}
313     */
314    public function mkdir($dirs, $mode = 0777)
315    {
316        try
317        {
318            $this->symfony_filesystem->mkdir($dirs, $mode);
319        }
320        catch (IOException $e)
321        {
322            $msg = $e->getMessage();
323            $filename = substr($msg, strpos($msg, '"'), strrpos($msg, '"'));
324
325            throw new filesystem_exception('FILESYSTEM_CANNOT_CREATE_DIRECTORY', $filename, array(), $e);
326        }
327    }
328
329    /**
330     * {@inheritdoc}
331     */
332    public function phpbb_chmod($file, $perms = null, $recursive = false, $force_chmod_link = false)
333    {
334        if (is_null($perms))
335        {
336            // Default to read permission for compatibility reasons
337            $perms = self::CHMOD_READ;
338        }
339
340        if (empty($this->chmod_info))
341        {
342            if (!function_exists('fileowner') || !function_exists('filegroup'))
343            {
344                $this->chmod_info['process'] = false;
345            }
346            else
347            {
348                $common_php_owner    = @fileowner(__FILE__);
349                $common_php_group    = @filegroup(__FILE__);
350
351                // And the owner and the groups PHP is running under.
352                $php_uid    = (function_exists('posix_getuid')) ? @posix_getuid() : false;
353                $php_gids    = (function_exists('posix_getgroups')) ? @posix_getgroups() : false;
354
355                // If we are unable to get owner/group, then do not try to set them by guessing
356                if (!$php_uid || empty($php_gids) || !$common_php_owner || !$common_php_group)
357                {
358                    $this->chmod_info['process'] = false;
359                }
360                else
361                {
362                    $this->chmod_info = array(
363                        'process'        => true,
364                        'common_owner'    => $common_php_owner,
365                        'common_group'    => $common_php_group,
366                        'php_uid'        => $php_uid,
367                        'php_gids'        => $php_gids,
368                    );
369                }
370            }
371        }
372
373        if ($this->chmod_info['process'])
374        {
375            try
376            {
377                foreach ($this->to_iterator($file) as $current_file)
378                {
379                    $file_uid = @fileowner($current_file);
380                    $file_gid = @filegroup($current_file);
381
382                    // Change owner
383                    if (is_writable($file) && $file_uid !== $this->chmod_info['common_owner'])
384                    {
385                        $this->chown($current_file, $this->chmod_info['common_owner'], $recursive);
386                    }
387
388                    // Change group
389                    if (is_writable($file) && $file_gid !== $this->chmod_info['common_group'])
390                    {
391                        $this->chgrp($current_file, $this->chmod_info['common_group'], $recursive);
392                    }
393
394                    clearstatcache();
395                    $file_uid = @fileowner($current_file);
396                    $file_gid = @filegroup($current_file);
397                }
398            }
399            catch (filesystem_exception $e)
400            {
401                $this->chmod_info['process'] = false;
402            }
403        }
404
405        // Still able to process?
406        if ($this->chmod_info['process'])
407        {
408            if ($file_uid === $this->chmod_info['php_uid'])
409            {
410                $php = 'owner';
411            }
412            else if (in_array($file_gid, $this->chmod_info['php_gids']))
413            {
414                $php = 'group';
415            }
416            else
417            {
418                // Since we are setting the everyone bit anyway, no need to do expensive operations
419                $this->chmod_info['process'] = false;
420            }
421        }
422
423        // We are not able to determine or change something
424        if (!$this->chmod_info['process'])
425        {
426            $php = 'other';
427        }
428
429        switch ($php)
430        {
431            case 'owner':
432                try
433                {
434                    $this->chmod($file, $perms, $recursive, $force_chmod_link);
435                    clearstatcache();
436                    if ($this->is_readable($file) && $this->is_writable($file))
437                    {
438                        break;
439                    }
440                }
441                catch (filesystem_exception $e)
442                {
443                    // Do nothing
444                }
445            case 'group':
446                try
447                {
448                    $this->chmod($file, $perms, $recursive, $force_chmod_link);
449                    clearstatcache();
450                    if ((!($perms & self::CHMOD_READ) || $this->is_readable($file, $recursive)) && (!($perms & self::CHMOD_WRITE) || $this->is_writable($file, $recursive)))
451                    {
452                        break;
453                    }
454                }
455                catch (filesystem_exception $e)
456                {
457                    // Do nothing
458                }
459            case 'other':
460            default:
461                $this->chmod($file, $perms, $recursive, $force_chmod_link);
462            break;
463        }
464    }
465
466    /**
467     * {@inheritdoc}
468     */
469    public function realpath($path)
470    {
471        return helper::realpath($path);
472    }
473
474    /**
475     * {@inheritdoc}
476     */
477    public function remove($files)
478    {
479        try
480        {
481            $this->symfony_filesystem->remove($files);
482        }
483        catch (IOException $e)
484        {
485            // Try to recover filename
486            // By the time this is written that is at the end of the message
487            $error = trim($e->getMessage());
488            $file = substr($error, strrpos($error, ' '));
489
490            throw new filesystem_exception('FILESYSTEM_CANNOT_DELETE_FILES', $file, array(), $e);
491        }
492    }
493
494    /**
495     * {@inheritdoc}
496     */
497    public function rename($origin, $target, $overwrite = false)
498    {
499        try
500        {
501            $this->symfony_filesystem->rename($origin, $target, $overwrite);
502        }
503        catch (IOException $e)
504        {
505            $msg = $e->getMessage();
506            $filename = substr($msg, strpos($msg, '"'), strrpos($msg, '"'));
507
508            throw new filesystem_exception('FILESYSTEM_CANNOT_RENAME_FILE', $filename, array(), $e);
509        }
510    }
511
512    /**
513     * {@inheritdoc}
514     */
515    public function symlink($origin_dir, $target_dir, $copy_on_windows = false)
516    {
517        try
518        {
519            $this->symfony_filesystem->symlink($origin_dir, $target_dir, $copy_on_windows);
520        }
521        catch (IOException $e)
522        {
523            throw new filesystem_exception('FILESYSTEM_CANNOT_CREATE_SYMLINK', $origin_dir, array(), $e);
524        }
525    }
526
527    /**
528     * {@inheritdoc}
529     */
530    public function touch($files, $time = null, $access_time = null)
531    {
532        try
533        {
534            $this->symfony_filesystem->touch($files, $time, $access_time);
535        }
536        catch (IOException $e)
537        {
538            // Try to recover filename
539            // By the time this is written that is at the end of the message
540            $error = trim($e->getMessage());
541            $file = substr($error, strrpos($error, ' '));
542
543            throw new filesystem_exception('FILESYSTEM_CANNOT_TOUCH_FILES', $file, array(), $e);
544        }
545    }
546
547    /**
548     * phpBB's implementation of is_writable
549     *
550     * @todo Investigate if is_writable is still buggy
551     *
552     * @param string    $file    file/directory to check if writable
553     *
554     * @return bool    true if the given path is writable
555     */
556    protected function phpbb_is_writable($file)
557    {
558        if (file_exists($file))
559        {
560            // Canonicalise path to absolute path
561            $file = $this->realpath($file);
562
563            if (is_dir($file))
564            {
565                // Test directory by creating a file inside the directory
566                $result = @tempnam($file, 'i_w');
567
568                if (is_string($result) && file_exists($result))
569                {
570                    unlink($result);
571
572                    // Ensure the file is actually in the directory (returned realpathed)
573                    return (strpos($result, $file) === 0) ? true : false;
574                }
575            }
576            else
577            {
578                $handle = new \SplFileInfo($file);
579
580                // Returns TRUE if writable, FALSE otherwise
581                return $handle->isWritable();
582            }
583        }
584        else
585        {
586            // file does not exist test if we can write to the directory
587            $dir = dirname($file);
588
589            if (file_exists($dir) && is_dir($dir) && $this->phpbb_is_writable($dir))
590            {
591                return true;
592            }
593        }
594
595        return false;
596    }
597
598    /**
599     * Try to resolve real path when PHP's realpath fails to do so
600     *
601     * @deprecated 3.3.0-a1 (To be removed: 4.0.0)
602     *
603     * @param ?string    $path
604     * @return bool|string
605     */
606    protected function phpbb_own_realpath($path)
607    {
608        // Replace all directory separators with '/'
609        $path = str_replace(DIRECTORY_SEPARATOR, '/', $path ?: '');
610
611        $is_absolute_path = false;
612        $path_prefix = '';
613
614        if ($this->is_absolute_path($path))
615        {
616            $is_absolute_path = true;
617        }
618        else
619        {
620            // Resolve working directory and store it
621            if (is_null($this->working_directory))
622            {
623                if (function_exists('getcwd'))
624                {
625                    $this->working_directory = str_replace(DIRECTORY_SEPARATOR, '/', getcwd());
626                }
627
628                //
629                // From this point on we really just guessing
630                // If chdir were called we screwed
631                //
632                else if (function_exists('debug_backtrace'))
633                {
634                    $call_stack = debug_backtrace(0);
635                    $this->working_directory = str_replace(DIRECTORY_SEPARATOR, '/', dirname($call_stack[max(0, count($call_stack) - 1)]['file']));
636                }
637                else
638                {
639                    //
640                    // Assuming that the working directory is phpBB root
641                    // we could use this as a fallback, when phpBB will use controllers
642                    // everywhere this will be a safe assumption
643                    //
644                    //$dir_parts = explode(DIRECTORY_SEPARATOR, __DIR__);
645                    //$namespace_parts = explode('\\', trim(__NAMESPACE__, '\\'));
646
647                    //$namespace_part_count = count($namespace_parts);
648
649                    // Check if we still loading from root
650                    //if (array_slice($dir_parts, -$namespace_part_count) === $namespace_parts)
651                    //{
652                    //    $this->working_directory = implode('/', array_slice($dir_parts, 0, -$namespace_part_count));
653                    //}
654                    //else
655                    //{
656                    //    $this->working_directory = false;
657                    //}
658
659                    $this->working_directory = false;
660                }
661            }
662
663            if ($this->working_directory !== false)
664            {
665                $is_absolute_path = true;
666                $path = $this->working_directory . '/' . $path;
667            }
668        }
669
670        if ($is_absolute_path)
671        {
672            if (defined('PHP_WINDOWS_VERSION_MAJOR'))
673            {
674                $path_prefix = $path[0] . ':';
675                $path = substr($path, 2);
676            }
677            else
678            {
679                $path_prefix = '';
680            }
681        }
682
683        $resolved_path = $this->resolve_path($path, $path_prefix, $is_absolute_path);
684        if ($resolved_path === false)
685        {
686            return false;
687        }
688
689        $resolved_path = (string) $resolved_path;
690
691        if (!@file_exists($resolved_path) || (!@is_dir($resolved_path . '/') && !is_file($resolved_path)))
692        {
693            return false;
694        }
695
696        // Return OS specific directory separators
697        $resolved = str_replace('/', DIRECTORY_SEPARATOR, $resolved_path);
698
699        // Check for DIRECTORY_SEPARATOR at the end (and remove it!)
700        if (substr($resolved, -1) === DIRECTORY_SEPARATOR)
701        {
702            return substr($resolved, 0, -1);
703        }
704
705        return $resolved;
706    }
707
708    /**
709     * Convert file(s) to \Traversable object
710     *
711     * This is the same function as Symfony's toIterator, but that is private
712     * so we cannot use it.
713     *
714     * @param string|array|\Traversable    $files    filename/list of filenames
715     * @return \Traversable
716     */
717    protected function to_iterator($files)
718    {
719        if (!$files instanceof \Traversable)
720        {
721            $files = new \ArrayObject(is_array($files) ? $files : array($files));
722        }
723
724        return $files;
725    }
726
727    /**
728     * Try to resolve symlinks in path
729     *
730     * @deprecated 3.3.0-a1 (To be removed: 4.0.0)
731     *
732     * @param string    $path            The path to resolve
733     * @param string    $prefix            The path prefix (on windows the drive letter)
734     * @param bool         $absolute        Whether or not the path is absolute
735     * @param bool        $return_array    Whether or not to return path parts
736     *
737     * @return string|array|bool    returns the resolved path or an array of parts of the path if $return_array is true
738     *                                 or false if path cannot be resolved
739     */
740    protected function resolve_path($path, $prefix = '', $absolute = false, $return_array = false)
741    {
742        return helper::resolve_path($path, $prefix, $absolute, $return_array);
743    }
744}