Browse code

fixed the filesystem loader with relative paths

Fabien Potencier authored on 19/10/2016 23:36:24
Showing 4 changed files
... ...
@@ -1,5 +1,6 @@
1 1
 * 1.27.0 (2016-XX-XX)
2 2
 
3
+ * fixed the filesystem loader with relative paths
3 4
  * deprecated Twig_Node::getLine() in favor of Twig_Node::getTemplateLine()
4 5
  * deprecated Twig_Template::getSource() in favor of Twig_Template::getSourceContext()
5 6
  * deprecated Twig_Node::getFilename() in favor of Twig_Node::getTemplateName()
... ...
@@ -156,6 +156,9 @@ Here is a list of the built-in loaders Twig provides:
156 156
 .. versionadded:: 1.10
157 157
     The ``prependPath()`` and support for namespaces were added in Twig 1.10.
158 158
 
159
+.. versionadded:: 1.27
160
+    Relative paths support was added in Twig 1.27.
161
+
159 162
 ``Twig_Loader_Filesystem`` loads templates from the file system. This loader
160 163
 can find templates in folders on the file system and is the preferred way to
161 164
 load them::
... ...
@@ -190,6 +193,18 @@ Namespaced templates can be accessed via the special
190 193
 
191 194
     $twig->render('@admin/index.html', array());
192 195
 
196
+``Twig_Loader_Filesystem`` support absolute and relative paths. Using relative
197
+paths is preferred as it makes the cache keys independent of the project root
198
+directory (for instance, it allows warming the cache from a build server where
199
+the directory might be different from the one used on production servers)::
200
+
201
+    $loader = new Twig_Loader_Filesystem('templates', getcwd().'/..');
202
+
203
+.. note::
204
+
205
+    When not passing the root path as a second argument, Twig uses ``getcwd()``
206
+    for relative paths.
207
+
193 208
 ``Twig_Loader_Array``
194 209
 .....................
195 210
 
... ...
@@ -23,13 +23,21 @@ class Twig_Loader_Filesystem implements Twig_LoaderInterface, Twig_ExistsLoaderI
23 23
     protected $cache = array();
24 24
     protected $errorCache = array();
25 25
 
26
+    private $rootPath;
27
+
26 28
     /**
27 29
      * Constructor.
28 30
      *
29
-     * @param string|array $paths A path or an array of paths where to look for templates
31
+     * @param string|array $paths    A path or an array of paths where to look for templates
32
+     * @param string|null  $rootPath The root path common to all relative paths (null for getcwd())
30 33
      */
31
-    public function __construct($paths = array())
34
+    public function __construct($paths = array(), $rootPath = null)
32 35
     {
36
+        $this->rootPath = (null === $rootPath ? getcwd() : $rootPath).DIRECTORY_SEPARATOR;
37
+        if (false !== $realPath = realpath($rootPath)) {
38
+            $this->rootPath = $realPath.DIRECTORY_SEPARATOR;
39
+        }
40
+
33 41
         if ($paths) {
34 42
             $this->setPaths($paths);
35 43
         }
... ...
@@ -81,7 +89,7 @@ class Twig_Loader_Filesystem implements Twig_LoaderInterface, Twig_ExistsLoaderI
81 89
      * Adds a path where templates are stored.
82 90
      *
83 91
      * @param string $path      A path where to look for templates
84
-     * @param string $namespace A path name
92
+     * @param string $namespace A path namespace
85 93
      *
86 94
      * @throws Twig_Error_Loader
87 95
      */
... ...
@@ -90,8 +98,9 @@ class Twig_Loader_Filesystem implements Twig_LoaderInterface, Twig_ExistsLoaderI
90 98
         // invalidate the cache
91 99
         $this->cache = $this->errorCache = array();
92 100
 
93
-        if (!is_dir($path)) {
94
-            throw new Twig_Error_Loader(sprintf('The "%s" directory does not exist.', $path));
101
+        $checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path;
102
+        if (!is_dir($checkPath)) {
103
+            throw new Twig_Error_Loader(sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath));
95 104
         }
96 105
 
97 106
         $this->paths[$namespace][] = rtrim($path, '/\\');
... ...
@@ -101,7 +110,7 @@ class Twig_Loader_Filesystem implements Twig_LoaderInterface, Twig_ExistsLoaderI
101 110
      * Prepends a path where templates are stored.
102 111
      *
103 112
      * @param string $path      A path where to look for templates
104
-     * @param string $namespace A path name
113
+     * @param string $namespace A path namespace
105 114
      *
106 115
      * @throws Twig_Error_Loader
107 116
      */
... ...
@@ -110,8 +119,9 @@ class Twig_Loader_Filesystem implements Twig_LoaderInterface, Twig_ExistsLoaderI
110 119
         // invalidate the cache
111 120
         $this->cache = $this->errorCache = array();
112 121
 
113
-        if (!is_dir($path)) {
114
-            throw new Twig_Error_Loader(sprintf('The "%s" directory does not exist.', $path));
122
+        $checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path;
123
+        if (!is_dir($checkPath)) {
124
+            throw new Twig_Error_Loader(sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath));
115 125
         }
116 126
 
117 127
         $path = rtrim($path, '/\\');
... ...
@@ -146,7 +156,13 @@ class Twig_Loader_Filesystem implements Twig_LoaderInterface, Twig_ExistsLoaderI
146 156
      */
147 157
     public function getCacheKey($name)
148 158
     {
149
-        return $this->findTemplate($name);
159
+        $path = $this->findTemplate($name);
160
+        $len = strlen($this->rootPath);
161
+        if (0 === strncmp($this->rootPath, $path, $len)) {
162
+            return substr($path, $len);
163
+        }
164
+
165
+        return $path;
150 166
     }
151 167
 
152 168
     /**
... ...
@@ -207,8 +223,16 @@ class Twig_Loader_Filesystem implements Twig_LoaderInterface, Twig_ExistsLoaderI
207 223
         }
208 224
 
209 225
         foreach ($this->paths[$namespace] as $path) {
226
+            if (!$this->isAbsolutePath($path)) {
227
+                $path = $this->rootPath.'/'.$path;
228
+            }
229
+
210 230
             if (is_file($path.'/'.$shortname)) {
211
-                return $this->cache[$name] = $this->normalizePath($path.'/'.$shortname);
231
+                if (false !== $realpath = realpath($path.'/'.$shortname)) {
232
+                    return $this->cache[$name] = $realpath;
233
+                }
234
+
235
+                return $this->cache[$name] = $path.'/'.$shortname;
212 236
             }
213 237
         }
214 238
 
... ...
@@ -264,19 +288,14 @@ class Twig_Loader_Filesystem implements Twig_LoaderInterface, Twig_ExistsLoaderI
264 288
         }
265 289
     }
266 290
 
267
-    private function normalizePath($path)
291
+    private function isAbsolutePath($file)
268 292
     {
269
-        $parts = explode('/', str_replace('\\', '/', $path));
270
-        $hasProto = false !== strpos($path, '://');
271
-        $new = array();
272
-        foreach ($parts as $i => $part) {
273
-            if ('..' === $part) {
274
-                array_pop($new);
275
-            } elseif ('.' !== $part && ('' !== $part || 0 === $i || $hasProto && $i < 3)) {
276
-                $new[] = $part;
277
-            }
278
-        }
279
-
280
-        return implode('/', $new);
293
+        return strspn($file, '/\\', 0, 1)
294
+            || (strlen($file) > 3 && ctype_alpha($file[0])
295
+                && substr($file, 1, 1) === ':'
296
+                && strspn($file, '/\\', 2, 1)
297
+            )
298
+            || null !== parse_url($file, PHP_URL_SCHEME)
299
+        ;
281 300
     }
282 301
 }
... ...
@@ -62,9 +62,9 @@ class Twig_Tests_Loader_FilesystemTest extends PHPUnit_Framework_TestCase
62 62
     /**
63 63
      * @dataProvider getBasePaths
64 64
      */
65
-    public function testPaths($basePath)
65
+    public function testPaths($basePath, $cacheKey, $rootPath)
66 66
     {
67
-        $loader = new Twig_Loader_Filesystem(array($basePath.'/normal', $basePath.'/normal_bis'));
67
+        $loader = new Twig_Loader_Filesystem(array($basePath.'/normal', $basePath.'/normal_bis'), $rootPath);
68 68
         $loader->setPaths(array($basePath.'/named', $basePath.'/named_bis'), 'named');
69 69
         $loader->addPath($basePath.'/named_ter', 'named');
70 70
         $loader->addPath($basePath.'/normal_ter');
... ...
@@ -87,7 +87,7 @@ class Twig_Tests_Loader_FilesystemTest extends PHPUnit_Framework_TestCase
87 87
         ), $loader->getPaths('named'));
88 88
 
89 89
         // do not use realpath here as it would make the test unuseful
90
-        $this->assertEquals(str_replace('\\', '/', $basePath.'/named_quater/named_absolute.html'), str_replace('\\', '/', $loader->getCacheKey('@named/named_absolute.html')));
90
+        $this->assertEquals($cacheKey, str_replace('\\', '/', $loader->getCacheKey('@named/named_absolute.html')));
91 91
         $this->assertEquals("path (final)\n", $loader->getSource('index.html'));
92 92
         $this->assertEquals("path (final)\n", $loader->getSource('@__main__/index.html'));
93 93
         $this->assertEquals("named path (final)\n", $loader->getSource('@named/index.html'));
... ...
@@ -96,8 +96,31 @@ class Twig_Tests_Loader_FilesystemTest extends PHPUnit_Framework_TestCase
96 96
     public function getBasePaths()
97 97
     {
98 98
         return array(
99
-            array(dirname(__FILE__).'/Fixtures'),
100
-            array('test/Twig/Tests/Loader/Fixtures'),
99
+            array(
100
+                dirname(__FILE__).'/Fixtures',
101
+                'test/Twig/Tests/Loader/Fixtures/named_quater/named_absolute.html',
102
+                null,
103
+            ),
104
+            array(
105
+                dirname(__FILE__).'/Fixtures/../Fixtures',
106
+                'test/Twig/Tests/Loader/Fixtures/named_quater/named_absolute.html',
107
+                null,
108
+            ),
109
+            array(
110
+                'test/Twig/Tests/Loader/Fixtures',
111
+                'test/Twig/Tests/Loader/Fixtures/named_quater/named_absolute.html',
112
+                getcwd(),
113
+            ),
114
+            array(
115
+                'Fixtures',
116
+                'Fixtures/named_quater/named_absolute.html',
117
+                getcwd().'/test/Twig/Tests/Loader',
118
+            ),
119
+            array(
120
+                'Fixtures',
121
+                'Fixtures/named_quater/named_absolute.html',
122
+                getcwd().'/test/../test/Twig/Tests/Loader',
123
+            ),
101 124
         );
102 125
     }
103 126