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