1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Finder\Iterator;
13
14use Symfony\Component\Finder\Exception\AccessDeniedException;
15use Symfony\Component\Finder\SplFileInfo;
16
17/**
18 * Extends the \RecursiveDirectoryIterator to support relative paths.
19 *
20 * @author Victor Berchet <victor@suumit.com>
21 */
22class RecursiveDirectoryIterator extends \RecursiveDirectoryIterator
23{
24    /**
25     * @var bool
26     */
27    private $ignoreUnreadableDirs;
28
29    /**
30     * @var bool
31     */
32    private $rewindable;
33
34    // these 3 properties take part of the performance optimization to avoid redoing the same work in all iterations
35    private $rootPath;
36    private $subPath;
37    private $directorySeparator = '/';
38
39    /**
40     * @throws \RuntimeException
41     */
42    public function __construct(string $path, int $flags, bool $ignoreUnreadableDirs = false)
43    {
44        if ($flags & (self::CURRENT_AS_PATHNAME | self::CURRENT_AS_SELF)) {
45            throw new \RuntimeException('This iterator only support returning current as fileinfo.');
46        }
47
48        parent::__construct($path, $flags);
49        $this->ignoreUnreadableDirs = $ignoreUnreadableDirs;
50        $this->rootPath = $path;
51        if ('/' !== \DIRECTORY_SEPARATOR && !($flags & self::UNIX_PATHS)) {
52            $this->directorySeparator = \DIRECTORY_SEPARATOR;
53        }
54    }
55
56    /**
57     * Return an instance of SplFileInfo with support for relative paths.
58     *
59     * @return SplFileInfo File information
60     */
61    public function current()
62    {
63        // the logic here avoids redoing the same work in all iterations
64
65        if (null === $subPathname = $this->subPath) {
66            $subPathname = $this->subPath = (string) $this->getSubPath();
67        }
68        if ('' !== $subPathname) {
69            $subPathname .= $this->directorySeparator;
70        }
71        $subPathname .= $this->getFilename();
72
73        if ('/' !== $basePath = $this->rootPath) {
74            $basePath .= $this->directorySeparator;
75        }
76
77        return new SplFileInfo($basePath.$subPathname, $this->subPath, $subPathname);
78    }
79
80    /**
81     * @return \RecursiveIterator
82     *
83     * @throws AccessDeniedException
84     */
85    public function getChildren()
86    {
87        try {
88            $children = parent::getChildren();
89
90            if ($children instanceof self) {
91                // parent method will call the constructor with default arguments, so unreadable dirs won't be ignored anymore
92                $children->ignoreUnreadableDirs = $this->ignoreUnreadableDirs;
93
94                // performance optimization to avoid redoing the same work in all children
95                $children->rewindable = &$this->rewindable;
96                $children->rootPath = $this->rootPath;
97            }
98
99            return $children;
100        } catch (\UnexpectedValueException $e) {
101            if ($this->ignoreUnreadableDirs) {
102                // If directory is unreadable and finder is set to ignore it, a fake empty content is returned.
103                return new \RecursiveArrayIterator([]);
104            } else {
105                throw new AccessDeniedException($e->getMessage(), $e->getCode(), $e);
106            }
107        }
108    }
109
110    /**
111     * Do nothing for non rewindable stream.
112     */
113    public function rewind()
114    {
115        if (false === $this->isRewindable()) {
116            return;
117        }
118
119        parent::rewind();
120    }
121
122    /**
123     * Checks if the stream is rewindable.
124     *
125     * @return bool true when the stream is rewindable, false otherwise
126     */
127    public function isRewindable()
128    {
129        if (null !== $this->rewindable) {
130            return $this->rewindable;
131        }
132
133        if (false !== $stream = @opendir($this->getPath())) {
134            $infos = stream_get_meta_data($stream);
135            closedir($stream);
136
137            if ($infos['seekable']) {
138                return $this->rewindable = true;
139            }
140        }
141
142        return $this->rewindable = false;
143    }
144}
145