@@ -23,17 +23,25 @@ class Twig_Loader_Filesystem implements Twig_LoaderInterface, Twig_ExistsLoaderI
2323 protected $ cache = array ();
2424 protected $ errorCache = array ();
2525
26+ private $ useRealpath = false ;
2627 private $ rootPath ;
2728
2829 /**
2930 * @param string|array $paths A path or an array of paths where to look for templates
3031 * @param string|null $rootPath The root path common to all relative paths (null for getcwd())
3132 */
32- public function __construct ($ paths = array (), $ rootPath = null )
33+ public function __construct ($ paths = array (), $ rootPath = null , $ useRealpath = false )
3334 {
34- $ this ->rootPath = (null === $ rootPath ? getcwd () : $ rootPath ).DIRECTORY_SEPARATOR ;
35- if (false !== $ realPath = realpath ($ rootPath )) {
36- $ this ->rootPath = $ realPath .DIRECTORY_SEPARATOR ;
35+ $ this ->useRealpath = $ useRealpath ;
36+
37+ $ this ->rootPath = (null === $ rootPath ? getcwd () : $ rootPath );
38+ if ($ this ->rootPath ) {
39+ // realpath() usage for backward compatibility only
40+ if ($ useRealpath && false !== ($ realPath = realpath ($ this ->rootPath ))) {
41+ $ this ->rootPath = $ realPath .DIRECTORY_SEPARATOR ;
42+ } else {
43+ $ this ->rootPath = self ::normalizePath ($ this ->rootPath ).DIRECTORY_SEPARATOR ;
44+ }
3745 }
3846
3947 if ($ paths ) {
@@ -214,12 +222,12 @@ protected function findTemplate($name)
214222 $ path = $ this ->rootPath .'/ ' .$ path ;
215223 }
216224
217- if (is_file ($ path .'/ ' .$ shortname )) {
218- if (false !== $ realpath = realpath ($ path .'/ ' .$ shortname )) {
219- return $ this ->cache [$ name ] = $ realpath ;
225+ $ filename = $ path .'/ ' .$ shortname ;
226+ if (is_file ($ filename )) {
227+ if ($ this ->useRealpath && false !== ($ realPath = realpath ($ filename ))) {
228+ $ filename = $ realPath ;
220229 }
221-
222- return $ this ->cache [$ name ] = $ path .'/ ' .$ shortname ;
230+ return $ this ->cache [$ name ] = self ::normalizePath ($ filename );
223231 }
224232 }
225233
@@ -275,6 +283,48 @@ protected function validateName($name)
275283 }
276284 }
277285
286+ /**
287+ * Normalize a path by removing redundant '..', '.' and '/' and thus preventing the
288+ * need of using the realpath() function that may come with some side effects such
289+ * as breaking out open_basedir configuration by attempting to following symlinks
290+ *
291+ * @param string $string
292+ * @param bool $removeTrailingSlash
293+ * @return string
294+ */
295+ static public function normalizePath ($ string )
296+ {
297+ // Handle windows gracefully
298+ if (\DIRECTORY_SEPARATOR !== '/ ' ) {
299+ $ string = \str_replace (\DIRECTORY_SEPARATOR , '/ ' , $ string );
300+ }
301+ // Also tests some special cases we can't really do anything with
302+ if (false === \strpos ($ string , '/ ' ) || '/ ' === $ string || '. ' === $ string || '.. ' === $ string ) {
303+ return $ string ;
304+ }
305+ // This is supposedly invalid, but an empty string is an empty string
306+ if ('' === ($ string = \rtrim ($ string , '/ ' ))) {
307+ return '' ;
308+ }
309+
310+ $ scheme = null ;
311+ if (\strpos ($ string , ':// ' )) {
312+ list ($ scheme , $ string ) = \explode (':// ' , $ string , 2 );
313+ }
314+
315+ // Matches useless '.' repetitions
316+ $ string = \preg_replace ('@^\./|(/\.)+/|/\.$@ ' , '/ ' , $ string );
317+
318+ $ count = 0 ;
319+ do {
320+ // string such as '//' can be generated by the first regex, hence the second
321+ $ string = \preg_replace ('@[^/]+/+\.\.(/+|$)@ ' , '$2 ' , \preg_replace ('@//+@ ' , '/ ' , $ string ), -1 , $ count );
322+ } while ($ count );
323+
324+ // rtrim() a second time because preg_replace() could leave a trailing '/'
325+ return ($ scheme ? ($ scheme .':// ' ) : '' ).\rtrim ($ string , '/ ' );
326+ }
327+
278328 private function isAbsolutePath ($ file )
279329 {
280330 return strspn ($ file , '/ \\' , 0 , 1 )
0 commit comments