1<?php
2
3/*
4 * This file is part of Twig.
5 *
6 * (c) Fabien Potencier
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 Twig\Loader;
13
14use Twig\Error\LoaderError;
15use Twig\Source;
16
17/**
18 * Loads template from the filesystem.
19 *
20 * @author Fabien Potencier <fabien@symfony.com>
21 */
22class FilesystemLoader implements LoaderInterface, ExistsLoaderInterface, SourceContextLoaderInterface
23{
24    /** Identifier of the main namespace. */
25    public const MAIN_NAMESPACE = '__main__';
26
27    protected $paths = [];
28    protected $cache = [];
29    protected $errorCache = [];
30
31    private $rootPath;
32
33    /**
34     * @param string|array $paths    A path or an array of paths where to look for templates
35     * @param string|null  $rootPath The root path common to all relative paths (null for getcwd())
36     */
37    public function __construct($paths = [], string $rootPath = null)
38    {
39        $this->rootPath = (null === $rootPath ? getcwd() : $rootPath).\DIRECTORY_SEPARATOR;
40        if (null !== $rootPath && false !== ($realPath = realpath($rootPath))) {
41            $this->rootPath = $realPath.\DIRECTORY_SEPARATOR;
42        }
43
44        if ($paths) {
45            $this->setPaths($paths);
46        }
47    }
48
49    /**
50     * Returns the paths to the templates.
51     *
52     * @param string $namespace A path namespace
53     *
54     * @return array The array of paths where to look for templates
55     */
56    public function getPaths($namespace = self::MAIN_NAMESPACE)
57    {
58        return isset($this->paths[$namespace]) ? $this->paths[$namespace] : [];
59    }
60
61    /**
62     * Returns the path namespaces.
63     *
64     * The main namespace is always defined.
65     *
66     * @return array The array of defined namespaces
67     */
68    public function getNamespaces()
69    {
70        return array_keys($this->paths);
71    }
72
73    /**
74     * Sets the paths where templates are stored.
75     *
76     * @param string|array $paths     A path or an array of paths where to look for templates
77     * @param string       $namespace A path namespace
78     */
79    public function setPaths($paths, $namespace = self::MAIN_NAMESPACE)
80    {
81        if (!\is_array($paths)) {
82            $paths = [$paths];
83        }
84
85        $this->paths[$namespace] = [];
86        foreach ($paths as $path) {
87            $this->addPath($path, $namespace);
88        }
89    }
90
91    /**
92     * Adds a path where templates are stored.
93     *
94     * @param string $path      A path where to look for templates
95     * @param string $namespace A path namespace
96     *
97     * @throws LoaderError
98     */
99    public function addPath($path, $namespace = self::MAIN_NAMESPACE)
100    {
101        // invalidate the cache
102        $this->cache = $this->errorCache = [];
103
104        $checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path;
105        if (!is_dir($checkPath)) {
106            throw new LoaderError(sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath));
107        }
108
109        $this->paths[$namespace][] = rtrim($path, '/\\');
110    }
111
112    /**
113     * Prepends a path where templates are stored.
114     *
115     * @param string $path      A path where to look for templates
116     * @param string $namespace A path namespace
117     *
118     * @throws LoaderError
119     */
120    public function prependPath($path, $namespace = self::MAIN_NAMESPACE)
121    {
122        // invalidate the cache
123        $this->cache = $this->errorCache = [];
124
125        $checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path;
126        if (!is_dir($checkPath)) {
127            throw new LoaderError(sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath));
128        }
129
130        $path = rtrim($path, '/\\');
131
132        if (!isset($this->paths[$namespace])) {
133            $this->paths[$namespace][] = $path;
134        } else {
135            array_unshift($this->paths[$namespace], $path);
136        }
137    }
138
139    public function getSourceContext($name)
140    {
141        if (null === ($path = $this->findTemplate($name)) || false === $path) {
142            return new Source('', $name, '');
143        }
144
145        return new Source(file_get_contents($path), $name, $path);
146    }
147
148    public function getCacheKey($name)
149    {
150        if (null === ($path = $this->findTemplate($name)) || false === $path) {
151            return '';
152        }
153        $len = \strlen($this->rootPath);
154        if (0 === strncmp($this->rootPath, $path, $len)) {
155            return substr($path, $len);
156        }
157
158        return $path;
159    }
160
161    public function exists($name)
162    {
163        $name = $this->normalizeName($name);
164
165        if (isset($this->cache[$name])) {
166            return true;
167        }
168
169        return null !== ($path = $this->findTemplate($name, false)) && false !== $path;
170    }
171
172    public function isFresh($name, $time)
173    {
174        // false support to be removed in 3.0
175        if (null === ($path = $this->findTemplate($name)) || false === $path) {
176            return false;
177        }
178
179        return filemtime($path) < $time;
180    }
181
182    /**
183     * Checks if the template can be found.
184     *
185     * In Twig 3.0, findTemplate must return a string or null (returning false won't work anymore).
186     *
187     * @param string $name  The template name
188     * @param bool   $throw Whether to throw an exception when an error occurs
189     *
190     * @return string|false|null The template name or false/null
191     */
192    protected function findTemplate($name, $throw = true)
193    {
194        $name = $this->normalizeName($name);
195
196        if (isset($this->cache[$name])) {
197            return $this->cache[$name];
198        }
199
200        if (isset($this->errorCache[$name])) {
201            if (!$throw) {
202                return false;
203            }
204
205            throw new LoaderError($this->errorCache[$name]);
206        }
207
208        try {
209            $this->validateName($name);
210
211            list($namespace, $shortname) = $this->parseName($name);
212        } catch (LoaderError $e) {
213            if (!$throw) {
214                return false;
215            }
216
217            throw $e;
218        }
219
220        if (!isset($this->paths[$namespace])) {
221            $this->errorCache[$name] = sprintf('There are no registered paths for namespace "%s".', $namespace);
222
223            if (!$throw) {
224                return false;
225            }
226
227            throw new LoaderError($this->errorCache[$name]);
228        }
229
230        foreach ($this->paths[$namespace] as $path) {
231            if (!$this->isAbsolutePath($path)) {
232                $path = $this->rootPath.$path;
233            }
234
235            if (is_file($path.'/'.$shortname)) {
236                if (false !== $realpath = realpath($path.'/'.$shortname)) {
237                    return $this->cache[$name] = $realpath;
238                }
239
240                return $this->cache[$name] = $path.'/'.$shortname;
241            }
242        }
243
244        $this->errorCache[$name] = sprintf('Unable to find template "%s" (looked into: %s).', $name, implode(', ', $this->paths[$namespace]));
245
246        if (!$throw) {
247            return false;
248        }
249
250        throw new LoaderError($this->errorCache[$name]);
251    }
252
253    private function normalizeName($name)
254    {
255        return preg_replace('#/{2,}#', '/', str_replace('\\', '/', (string) $name));
256    }
257
258    private function parseName($name, $default = self::MAIN_NAMESPACE)
259    {
260        if (isset($name[0]) && '@' == $name[0]) {
261            if (false === $pos = strpos($name, '/')) {
262                throw new LoaderError(sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name));
263            }
264
265            $namespace = substr($name, 1, $pos - 1);
266            $shortname = substr($name, $pos + 1);
267
268            return [$namespace, $shortname];
269        }
270
271        return [$default, $name];
272    }
273
274    private function validateName($name)
275    {
276        if (false !== strpos($name, "\0")) {
277            throw new LoaderError('A template name cannot contain NUL bytes.');
278        }
279
280        $name = ltrim($name, '/');
281        $parts = explode('/', $name);
282        $level = 0;
283        foreach ($parts as $part) {
284            if ('..' === $part) {
285                --$level;
286            } elseif ('.' !== $part) {
287                ++$level;
288            }
289
290            if ($level < 0) {
291                throw new LoaderError(sprintf('Looks like you try to load a template outside configured directories (%s).', $name));
292            }
293        }
294    }
295
296    private function isAbsolutePath($file)
297    {
298        return strspn($file, '/\\', 0, 1)
299            || (\strlen($file) > 3 && ctype_alpha($file[0])
300                && ':' === $file[1]
301                && strspn($file, '/\\', 2, 1)
302            )
303            || null !== parse_url($file, \PHP_URL_SCHEME)
304        ;
305    }
306}
307
308class_alias('Twig\Loader\FilesystemLoader', 'Twig_Loader_Filesystem');
309