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     * @param string $path
41     * @param int    $flags
42     * @param bool   $ignoreUnreadableDirs
43     *
44     * @throws \RuntimeException
45     */
46    public function __construct($path, $flags, $ignoreUnreadableDirs = false)
47    {
48        if ($flags & (self::CURRENT_AS_PATHNAME | self::CURRENT_AS_SELF)) {
49            throw new \RuntimeException('This iterator only support returning current as fileinfo.');
50        }
51
52        parent::__construct($path, $flags);
53        $this->ignoreUnreadableDirs = $ignoreUnreadableDirs;
54        $this->rootPath = $path;
55        if ('/' !== \DIRECTORY_SEPARATOR && !($flags & self::UNIX_PATHS)) {
56            $this->directorySeparator = \DIRECTORY_SEPARATOR;
57        }
58    }
59
60    /**
61     * Return an instance of SplFileInfo with support for relative paths.
62     *
63     * @return SplFileInfo File information
64     */
65    public function current()
66    {
67        // the logic here avoids redoing the same work in all iterations
68
69        if (null === $subPathname = $this->subPath) {
70            $subPathname = $this->subPath = (string) $this->getSubPath();
71        }
72        if ('' !== $subPathname) {
73            $subPathname .= $this->directorySeparator;
74        }
75        $subPathname .= $this->getFilename();
76
77        if ('/' !== $basePath = $this->rootPath) {
78            $basePath .= $this->directorySeparator;
79        }
80
81        return new SplFileInfo($basePath.$subPathname, $this->subPath, $subPathname);
82    }
83
84    /**
85     * @return \RecursiveIterator
86     *
87     * @throws AccessDeniedException
88     */
89    public function getChildren()
90    {
91        try {
92            $children = parent::getChildren();
93
94            if ($children instanceof self) {
95                // parent method will call the constructor with default arguments, so unreadable dirs won't be ignored anymore
96                $children->ignoreUnreadableDirs = $this->ignoreUnreadableDirs;
97
98                // performance optimization to avoid redoing the same work in all children
99                $children->rewindable = &$this->rewindable;
100                $children->rootPath = $this->rootPath;
101            }
102
103            return $children;
104        } catch (\UnexpectedValueException $e) {
105            if ($this->ignoreUnreadableDirs) {
106                // If directory is unreadable and finder is set to ignore it, a fake empty content is returned.
107                return new \RecursiveArrayIterator([]);
108            } else {
109                throw new AccessDeniedException($e->getMessage(), $e->getCode(), $e);
110            }
111        }
112    }
113
114    /**
115     * Do nothing for non rewindable stream.
116     */
117    public function rewind()
118    {
119        if (false === $this->isRewindable()) {
120            return;
121        }
122
123        // @see https://bugs.php.net/68557
124        if (\PHP_VERSION_ID < 50523 || \PHP_VERSION_ID >= 50600 && \PHP_VERSION_ID < 50607) {
125            parent::next();
126        }
127
128        parent::rewind();
129    }
130
131    /**
132     * Checks if the stream is rewindable.
133     *
134     * @return bool true when the stream is rewindable, false otherwise
135     */
136    public function isRewindable()
137    {
138        if (null !== $this->rewindable) {
139            return $this->rewindable;
140        }
141
142        // workaround for an HHVM bug, should be removed when https://github.com/facebook/hhvm/issues/7281 is fixed
143        if ('' === $this->getPath()) {
144            return $this->rewindable = false;
145        }
146
147        if (false !== $stream = @opendir($this->getPath())) {
148            $infos = stream_get_meta_data($stream);
149            closedir($stream);
150
151            if ($infos['seekable']) {
152                return $this->rewindable = true;
153            }
154        }
155
156        return $this->rewindable = false;
157    }
158}
159