1<?php
2
3/*
4 * This file is part of Twig.
5 *
6 * (c) 2009 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 * Stores the Twig configuration.
14 *
15 * @author Fabien Potencier <fabien@symfony.com>
16 */
17class Twig_Environment
18{
19    const VERSION = '1.29.0';
20    const VERSION_ID = 12900;
21    const MAJOR_VERSION = 1;
22    const MINOR_VERSION = 29;
23    const RELEASE_VERSION = 0;
24    const EXTRA_VERSION = '';
25
26    protected $charset;
27    protected $loader;
28    protected $debug;
29    protected $autoReload;
30    protected $cache;
31    protected $lexer;
32    protected $parser;
33    protected $compiler;
34    protected $baseTemplateClass;
35    protected $extensions;
36    protected $parsers;
37    protected $visitors;
38    protected $filters;
39    protected $tests;
40    protected $functions;
41    protected $globals;
42    protected $runtimeInitialized = false;
43    protected $extensionInitialized = false;
44    protected $loadedTemplates;
45    protected $strictVariables;
46    protected $unaryOperators;
47    protected $binaryOperators;
48    protected $templateClassPrefix = '__TwigTemplate_';
49    protected $functionCallbacks = array();
50    protected $filterCallbacks = array();
51    protected $staging;
52
53    private $originalCache;
54    private $bcWriteCacheFile = false;
55    private $bcGetCacheFilename = false;
56    private $lastModifiedExtension = 0;
57    private $extensionsByClass = array();
58    private $runtimeLoaders = array();
59    private $runtimes = array();
60    private $optionsHash;
61
62    /**
63     * Constructor.
64     *
65     * Available options:
66     *
67     *  * debug: When set to true, it automatically set "auto_reload" to true as
68     *           well (default to false).
69     *
70     *  * charset: The charset used by the templates (default to UTF-8).
71     *
72     *  * base_template_class: The base template class to use for generated
73     *                         templates (default to Twig_Template).
74     *
75     *  * cache: An absolute path where to store the compiled templates,
76     *           a Twig_Cache_Interface implementation,
77     *           or false to disable compilation cache (default).
78     *
79     *  * auto_reload: Whether to reload the template if the original source changed.
80     *                 If you don't provide the auto_reload option, it will be
81     *                 determined automatically based on the debug value.
82     *
83     *  * strict_variables: Whether to ignore invalid variables in templates
84     *                      (default to false).
85     *
86     *  * autoescape: Whether to enable auto-escaping (default to html):
87     *                  * false: disable auto-escaping
88     *                  * true: equivalent to html
89     *                  * html, js: set the autoescaping to one of the supported strategies
90     *                  * name: set the autoescaping strategy based on the template name extension
91     *                  * PHP callback: a PHP callback that returns an escaping strategy based on the template "name"
92     *
93     *  * optimizations: A flag that indicates which optimizations to apply
94     *                   (default to -1 which means that all optimizations are enabled;
95     *                   set it to 0 to disable).
96     *
97     * @param Twig_LoaderInterface $loader
98     * @param array                $options An array of options
99     */
100    public function __construct(Twig_LoaderInterface $loader = null, $options = array())
101    {
102        if (null !== $loader) {
103            $this->setLoader($loader);
104        } else {
105            @trigger_error('Not passing a Twig_LoaderInterface as the first constructor argument of Twig_Environment is deprecated since version 1.21.', E_USER_DEPRECATED);
106        }
107
108        $options = array_merge(array(
109            'debug' => false,
110            'charset' => 'UTF-8',
111            'base_template_class' => 'Twig_Template',
112            'strict_variables' => false,
113            'autoescape' => 'html',
114            'cache' => false,
115            'auto_reload' => null,
116            'optimizations' => -1,
117        ), $options);
118
119        $this->debug = (bool) $options['debug'];
120        $this->charset = strtoupper($options['charset']);
121        $this->baseTemplateClass = $options['base_template_class'];
122        $this->autoReload = null === $options['auto_reload'] ? $this->debug : (bool) $options['auto_reload'];
123        $this->strictVariables = (bool) $options['strict_variables'];
124        $this->setCache($options['cache']);
125
126        $this->addExtension(new Twig_Extension_Core());
127        $this->addExtension(new Twig_Extension_Escaper($options['autoescape']));
128        $this->addExtension(new Twig_Extension_Optimizer($options['optimizations']));
129        $this->staging = new Twig_Extension_Staging();
130
131        // For BC
132        if (is_string($this->originalCache)) {
133            $r = new ReflectionMethod($this, 'writeCacheFile');
134            if ($r->getDeclaringClass()->getName() !== __CLASS__) {
135                @trigger_error('The Twig_Environment::writeCacheFile method is deprecated since version 1.22 and will be removed in Twig 2.0.', E_USER_DEPRECATED);
136
137                $this->bcWriteCacheFile = true;
138            }
139
140            $r = new ReflectionMethod($this, 'getCacheFilename');
141            if ($r->getDeclaringClass()->getName() !== __CLASS__) {
142                @trigger_error('The Twig_Environment::getCacheFilename method is deprecated since version 1.22 and will be removed in Twig 2.0.', E_USER_DEPRECATED);
143
144                $this->bcGetCacheFilename = true;
145            }
146        }
147    }
148
149    /**
150     * Gets the base template class for compiled templates.
151     *
152     * @return string The base template class name
153     */
154    public function getBaseTemplateClass()
155    {
156        return $this->baseTemplateClass;
157    }
158
159    /**
160     * Sets the base template class for compiled templates.
161     *
162     * @param string $class The base template class name
163     */
164    public function setBaseTemplateClass($class)
165    {
166        $this->baseTemplateClass = $class;
167        $this->updateOptionsHash();
168    }
169
170    /**
171     * Enables debugging mode.
172     */
173    public function enableDebug()
174    {
175        $this->debug = true;
176        $this->updateOptionsHash();
177    }
178
179    /**
180     * Disables debugging mode.
181     */
182    public function disableDebug()
183    {
184        $this->debug = false;
185        $this->updateOptionsHash();
186    }
187
188    /**
189     * Checks if debug mode is enabled.
190     *
191     * @return bool true if debug mode is enabled, false otherwise
192     */
193    public function isDebug()
194    {
195        return $this->debug;
196    }
197
198    /**
199     * Enables the auto_reload option.
200     */
201    public function enableAutoReload()
202    {
203        $this->autoReload = true;
204    }
205
206    /**
207     * Disables the auto_reload option.
208     */
209    public function disableAutoReload()
210    {
211        $this->autoReload = false;
212    }
213
214    /**
215     * Checks if the auto_reload option is enabled.
216     *
217     * @return bool true if auto_reload is enabled, false otherwise
218     */
219    public function isAutoReload()
220    {
221        return $this->autoReload;
222    }
223
224    /**
225     * Enables the strict_variables option.
226     */
227    public function enableStrictVariables()
228    {
229        $this->strictVariables = true;
230        $this->updateOptionsHash();
231    }
232
233    /**
234     * Disables the strict_variables option.
235     */
236    public function disableStrictVariables()
237    {
238        $this->strictVariables = false;
239        $this->updateOptionsHash();
240    }
241
242    /**
243     * Checks if the strict_variables option is enabled.
244     *
245     * @return bool true if strict_variables is enabled, false otherwise
246     */
247    public function isStrictVariables()
248    {
249        return $this->strictVariables;
250    }
251
252    /**
253     * Gets the current cache implementation.
254     *
255     * @param bool $original Whether to return the original cache option or the real cache instance
256     *
257     * @return Twig_CacheInterface|string|false A Twig_CacheInterface implementation,
258     *                                          an absolute path to the compiled templates,
259     *                                          or false to disable cache
260     */
261    public function getCache($original = true)
262    {
263        return $original ? $this->originalCache : $this->cache;
264    }
265
266    /**
267     * Sets the current cache implementation.
268     *
269     * @param Twig_CacheInterface|string|false $cache A Twig_CacheInterface implementation,
270     *                                                an absolute path to the compiled templates,
271     *                                                or false to disable cache
272     */
273    public function setCache($cache)
274    {
275        if (is_string($cache)) {
276            $this->originalCache = $cache;
277            $this->cache = new Twig_Cache_Filesystem($cache);
278        } elseif (false === $cache) {
279            $this->originalCache = $cache;
280            $this->cache = new Twig_Cache_Null();
281        } elseif (null === $cache) {
282            @trigger_error('Using "null" as the cache strategy is deprecated since version 1.23 and will be removed in Twig 2.0.', E_USER_DEPRECATED);
283            $this->originalCache = false;
284            $this->cache = new Twig_Cache_Null();
285        } elseif ($cache instanceof Twig_CacheInterface) {
286            $this->originalCache = $this->cache = $cache;
287        } else {
288            throw new LogicException(sprintf('Cache can only be a string, false, or a Twig_CacheInterface implementation.'));
289        }
290    }
291
292    /**
293     * Gets the cache filename for a given template.
294     *
295     * @param string $name The template name
296     *
297     * @return string|false The cache file name or false when caching is disabled
298     *
299     * @deprecated since 1.22 (to be removed in 2.0)
300     */
301    public function getCacheFilename($name)
302    {
303        @trigger_error(sprintf('The %s method is deprecated since version 1.22 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED);
304
305        $key = $this->cache->generateKey($name, $this->getTemplateClass($name));
306
307        return !$key ? false : $key;
308    }
309
310    /**
311     * Gets the template class associated with the given string.
312     *
313     * The generated template class is based on the following parameters:
314     *
315     *  * The cache key for the given template;
316     *  * The currently enabled extensions;
317     *  * Whether the Twig C extension is available or not;
318     *  * PHP version;
319     *  * Twig version;
320     *  * Options with what environment was created.
321     *
322     * @param string   $name  The name for which to calculate the template class name
323     * @param int|null $index The index if it is an embedded template
324     *
325     * @return string The template class name
326     */
327    public function getTemplateClass($name, $index = null)
328    {
329        $key = $this->getLoader()->getCacheKey($name).$this->optionsHash;
330
331        return $this->templateClassPrefix.hash('sha256', $key).(null === $index ? '' : '_'.$index);
332    }
333
334    /**
335     * Gets the template class prefix.
336     *
337     * @return string The template class prefix
338     *
339     * @deprecated since 1.22 (to be removed in 2.0)
340     */
341    public function getTemplateClassPrefix()
342    {
343        @trigger_error(sprintf('The %s method is deprecated since version 1.22 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED);
344
345        return $this->templateClassPrefix;
346    }
347
348    /**
349     * Renders a template.
350     *
351     * @param string $name    The template name
352     * @param array  $context An array of parameters to pass to the template
353     *
354     * @return string The rendered template
355     *
356     * @throws Twig_Error_Loader  When the template cannot be found
357     * @throws Twig_Error_Syntax  When an error occurred during compilation
358     * @throws Twig_Error_Runtime When an error occurred during rendering
359     */
360    public function render($name, array $context = array())
361    {
362        return $this->loadTemplate($name)->render($context);
363    }
364
365    /**
366     * Displays a template.
367     *
368     * @param string $name    The template name
369     * @param array  $context An array of parameters to pass to the template
370     *
371     * @throws Twig_Error_Loader  When the template cannot be found
372     * @throws Twig_Error_Syntax  When an error occurred during compilation
373     * @throws Twig_Error_Runtime When an error occurred during rendering
374     */
375    public function display($name, array $context = array())
376    {
377        $this->loadTemplate($name)->display($context);
378    }
379
380    /**
381     * Loads a template.
382     *
383     * @param string|Twig_TemplateWrapper|Twig_Template $name The template name
384     *
385     * @return Twig_TemplateWrapper
386     */
387    public function load($name)
388    {
389        if ($name instanceof Twig_TemplateWrapper) {
390            return $name;
391        }
392
393        if ($name instanceof Twig_Template) {
394            return new Twig_TemplateWrapper($this, $name);
395        }
396
397        return new Twig_TemplateWrapper($this, $this->loadTemplate($name));
398    }
399
400    /**
401     * Loads a template internal representation.
402     *
403     * This method is for internal use only and should never be called
404     * directly.
405     *
406     * @param string $name  The template name
407     * @param int    $index The index if it is an embedded template
408     *
409     * @return Twig_TemplateInterface A template instance representing the given template name
410     *
411     * @throws Twig_Error_Loader When the template cannot be found
412     * @throws Twig_Error_Syntax When an error occurred during compilation
413     *
414     * @internal
415     */
416    public function loadTemplate($name, $index = null)
417    {
418        $cls = $this->getTemplateClass($name, $index);
419
420        if (isset($this->loadedTemplates[$cls])) {
421            return $this->loadedTemplates[$cls];
422        }
423
424        if (!class_exists($cls, false)) {
425            if ($this->bcGetCacheFilename) {
426                $key = $this->getCacheFilename($name);
427            } else {
428                $key = $this->cache->generateKey($name, $cls);
429            }
430
431            if (!$this->isAutoReload() || $this->isTemplateFresh($name, $this->cache->getTimestamp($key))) {
432                $this->cache->load($key);
433            }
434
435            if (!class_exists($cls, false)) {
436                $loader = $this->getLoader();
437                if (!$loader instanceof Twig_SourceContextLoaderInterface) {
438                    $source = new Twig_Source($loader->getSource($name), $name);
439                } else {
440                    $source = $loader->getSourceContext($name);
441                }
442
443                $content = $this->compileSource($source);
444
445                if ($this->bcWriteCacheFile) {
446                    $this->writeCacheFile($key, $content);
447                } else {
448                    $this->cache->write($key, $content);
449                    $this->cache->load($key);
450                }
451
452                if (!class_exists($cls, false)) {
453                    /* Last line of defense if either $this->bcWriteCacheFile was used,
454                     * $this->cache is implemented as a no-op or we have a race condition
455                     * where the cache was cleared between the above calls to write to and load from
456                     * the cache.
457                     */
458                    eval('?>'.$content);
459                }
460            }
461        }
462
463        if (!$this->runtimeInitialized) {
464            $this->initRuntime();
465        }
466
467        return $this->loadedTemplates[$cls] = new $cls($this);
468    }
469
470    /**
471     * Creates a template from source.
472     *
473     * This method should not be used as a generic way to load templates.
474     *
475     * @param string $template The template name
476     *
477     * @return Twig_Template A template instance representing the given template name
478     *
479     * @throws Twig_Error_Loader When the template cannot be found
480     * @throws Twig_Error_Syntax When an error occurred during compilation
481     */
482    public function createTemplate($template)
483    {
484        $name = sprintf('__string_template__%s', hash('sha256', uniqid(mt_rand(), true), false));
485
486        $loader = new Twig_Loader_Chain(array(
487            new Twig_Loader_Array(array($name => $template)),
488            $current = $this->getLoader(),
489        ));
490
491        $this->setLoader($loader);
492        try {
493            $template = $this->loadTemplate($name);
494        } catch (Exception $e) {
495            $this->setLoader($current);
496
497            throw $e;
498        } catch (Throwable $e) {
499            $this->setLoader($current);
500
501            throw $e;
502        }
503        $this->setLoader($current);
504
505        return $template;
506    }
507
508    /**
509     * Returns true if the template is still fresh.
510     *
511     * Besides checking the loader for freshness information,
512     * this method also checks if the enabled extensions have
513     * not changed.
514     *
515     * @param string $name The template name
516     * @param int    $time The last modification time of the cached template
517     *
518     * @return bool true if the template is fresh, false otherwise
519     */
520    public function isTemplateFresh($name, $time)
521    {
522        if (0 === $this->lastModifiedExtension) {
523            foreach ($this->extensions as $extension) {
524                $r = new ReflectionObject($extension);
525                if (file_exists($r->getFileName()) && ($extensionTime = filemtime($r->getFileName())) > $this->lastModifiedExtension) {
526                    $this->lastModifiedExtension = $extensionTime;
527                }
528            }
529        }
530
531        return $this->lastModifiedExtension <= $time && $this->getLoader()->isFresh($name, $time);
532    }
533
534    /**
535     * Tries to load a template consecutively from an array.
536     *
537     * Similar to loadTemplate() but it also accepts Twig_TemplateInterface instances and an array
538     * of templates where each is tried to be loaded.
539     *
540     * @param string|Twig_Template|array $names A template or an array of templates to try consecutively
541     *
542     * @return Twig_Template
543     *
544     * @throws Twig_Error_Loader When none of the templates can be found
545     * @throws Twig_Error_Syntax When an error occurred during compilation
546     */
547    public function resolveTemplate($names)
548    {
549        if (!is_array($names)) {
550            $names = array($names);
551        }
552
553        foreach ($names as $name) {
554            if ($name instanceof Twig_Template) {
555                return $name;
556            }
557
558            try {
559                return $this->loadTemplate($name);
560            } catch (Twig_Error_Loader $e) {
561            }
562        }
563
564        if (1 === count($names)) {
565            throw $e;
566        }
567
568        throw new Twig_Error_Loader(sprintf('Unable to find one of the following templates: "%s".', implode('", "', $names)));
569    }
570
571    /**
572     * Clears the internal template cache.
573     *
574     * @deprecated since 1.18.3 (to be removed in 2.0)
575     */
576    public function clearTemplateCache()
577    {
578        @trigger_error(sprintf('The %s method is deprecated since version 1.18.3 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED);
579
580        $this->loadedTemplates = array();
581    }
582
583    /**
584     * Clears the template cache files on the filesystem.
585     *
586     * @deprecated since 1.22 (to be removed in 2.0)
587     */
588    public function clearCacheFiles()
589    {
590        @trigger_error(sprintf('The %s method is deprecated since version 1.22 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED);
591
592        if (is_string($this->originalCache)) {
593            foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->originalCache), RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
594                if ($file->isFile()) {
595                    @unlink($file->getPathname());
596                }
597            }
598        }
599    }
600
601    /**
602     * Gets the Lexer instance.
603     *
604     * @return Twig_LexerInterface
605     *
606     * @deprecated since 1.25 (to be removed in 2.0)
607     */
608    public function getLexer()
609    {
610        @trigger_error(sprintf('The %s() method is deprecated since version 1.25 and will be removed in 2.0.', __FUNCTION__), E_USER_DEPRECATED);
611
612        if (null === $this->lexer) {
613            $this->lexer = new Twig_Lexer($this);
614        }
615
616        return $this->lexer;
617    }
618
619    public function setLexer(Twig_LexerInterface $lexer)
620    {
621        $this->lexer = $lexer;
622    }
623
624    /**
625     * Tokenizes a source code.
626     *
627     * @param string|Twig_Source $source The template source code
628     * @param string             $name   The template name (deprecated)
629     *
630     * @return Twig_TokenStream
631     *
632     * @throws Twig_Error_Syntax When the code is syntactically wrong
633     */
634    public function tokenize($source, $name = null)
635    {
636        if (!$source instanceof Twig_Source) {
637            @trigger_error(sprintf('Passing a string as the $source argument of %s() is deprecated since version 1.27. Pass a Twig_Source instance instead.', __METHOD__), E_USER_DEPRECATED);
638            $source = new Twig_Source($source, $name);
639        }
640
641        if (null === $this->lexer) {
642            $this->lexer = new Twig_Lexer($this);
643        }
644
645        return $this->lexer->tokenize($source);
646    }
647
648    /**
649     * Gets the Parser instance.
650     *
651     * @return Twig_ParserInterface
652     *
653     * @deprecated since 1.25 (to be removed in 2.0)
654     */
655    public function getParser()
656    {
657        @trigger_error(sprintf('The %s() method is deprecated since version 1.25 and will be removed in 2.0.', __FUNCTION__), E_USER_DEPRECATED);
658
659        if (null === $this->parser) {
660            $this->parser = new Twig_Parser($this);
661        }
662
663        return $this->parser;
664    }
665
666    public function setParser(Twig_ParserInterface $parser)
667    {
668        $this->parser = $parser;
669    }
670
671    /**
672     * Converts a token stream to a node tree.
673     *
674     * @return Twig_Node_Module
675     *
676     * @throws Twig_Error_Syntax When the token stream is syntactically or semantically wrong
677     */
678    public function parse(Twig_TokenStream $stream)
679    {
680        if (null === $this->parser) {
681            $this->parser = new Twig_Parser($this);
682        }
683
684        return $this->parser->parse($stream);
685    }
686
687    /**
688     * Gets the Compiler instance.
689     *
690     * @return Twig_CompilerInterface
691     *
692     * @deprecated since 1.25 (to be removed in 2.0)
693     */
694    public function getCompiler()
695    {
696        @trigger_error(sprintf('The %s() method is deprecated since version 1.25 and will be removed in 2.0.', __FUNCTION__), E_USER_DEPRECATED);
697
698        if (null === $this->compiler) {
699            $this->compiler = new Twig_Compiler($this);
700        }
701
702        return $this->compiler;
703    }
704
705    public function setCompiler(Twig_CompilerInterface $compiler)
706    {
707        $this->compiler = $compiler;
708    }
709
710    /**
711     * Compiles a node and returns the PHP code.
712     *
713     * @return string The compiled PHP source code
714     */
715    public function compile(Twig_NodeInterface $node)
716    {
717        if (null === $this->compiler) {
718            $this->compiler = new Twig_Compiler($this);
719        }
720
721        return $this->compiler->compile($node)->getSource();
722    }
723
724    /**
725     * Compiles a template source code.
726     *
727     * @param string|Twig_Source $source The template source code
728     * @param string             $name   The template name (deprecated)
729     *
730     * @return string The compiled PHP source code
731     *
732     * @throws Twig_Error_Syntax When there was an error during tokenizing, parsing or compiling
733     */
734    public function compileSource($source, $name = null)
735    {
736        if (!$source instanceof Twig_Source) {
737            @trigger_error(sprintf('Passing a string as the $source argument of %s() is deprecated since version 1.27. Pass a Twig_Source instance instead.', __METHOD__), E_USER_DEPRECATED);
738            $source = new Twig_Source($source, $name);
739        }
740
741        try {
742            return $this->compile($this->parse($this->tokenize($source)));
743        } catch (Twig_Error $e) {
744            $e->setSourceContext($source);
745            throw $e;
746        } catch (Exception $e) {
747            throw new Twig_Error_Syntax(sprintf('An exception has been thrown during the compilation of a template ("%s").', $e->getMessage()), -1, $source, $e);
748        }
749    }
750
751    public function setLoader(Twig_LoaderInterface $loader)
752    {
753        if (!$loader instanceof Twig_SourceContextLoaderInterface && 0 !== strpos(get_class($loader), 'Mock_Twig_LoaderInterface')) {
754            @trigger_error(sprintf('Twig loader "%s" should implement Twig_SourceContextLoaderInterface since version 1.27.', get_class($loader)), E_USER_DEPRECATED);
755        }
756
757        $this->loader = $loader;
758    }
759
760    /**
761     * Gets the Loader instance.
762     *
763     * @return Twig_LoaderInterface
764     */
765    public function getLoader()
766    {
767        if (null === $this->loader) {
768            throw new LogicException('You must set a loader first.');
769        }
770
771        return $this->loader;
772    }
773
774    /**
775     * Sets the default template charset.
776     *
777     * @param string $charset The default charset
778     */
779    public function setCharset($charset)
780    {
781        $this->charset = strtoupper($charset);
782    }
783
784    /**
785     * Gets the default template charset.
786     *
787     * @return string The default charset
788     */
789    public function getCharset()
790    {
791        return $this->charset;
792    }
793
794    /**
795     * Initializes the runtime environment.
796     *
797     * @deprecated since 1.23 (to be removed in 2.0)
798     */
799    public function initRuntime()
800    {
801        $this->runtimeInitialized = true;
802
803        foreach ($this->getExtensions() as $name => $extension) {
804            if (!$extension instanceof Twig_Extension_InitRuntimeInterface) {
805                $m = new ReflectionMethod($extension, 'initRuntime');
806
807                if ('Twig_Extension' !== $m->getDeclaringClass()->getName()) {
808                    @trigger_error(sprintf('Defining the initRuntime() method in the "%s" extension is deprecated since version 1.23. Use the `needs_environment` option to get the Twig_Environment instance in filters, functions, or tests; or explicitly implement Twig_Extension_InitRuntimeInterface if needed (not recommended).', $name), E_USER_DEPRECATED);
809                }
810            }
811
812            $extension->initRuntime($this);
813        }
814    }
815
816    /**
817     * Returns true if the given extension is registered.
818     *
819     * @param string $class The extension class name
820     *
821     * @return bool Whether the extension is registered or not
822     */
823    public function hasExtension($class)
824    {
825        $class = ltrim($class, '\\');
826        if (isset($this->extensions[$class])) {
827            if ($class !== get_class($this->extensions[$class])) {
828                @trigger_error(sprintf('Referencing the "%s" extension by its name (defined by getName()) is deprecated since 1.26 and will be removed in Twig 2.0. Use the Fully Qualified Extension Class Name instead.', $class), E_USER_DEPRECATED);
829            }
830
831            return true;
832        }
833
834        return isset($this->extensionsByClass[ltrim($class, '\\')]);
835    }
836
837    /**
838     * Adds a runtime loader.
839     */
840    public function addRuntimeLoader(Twig_RuntimeLoaderInterface $loader)
841    {
842        $this->runtimeLoaders[] = $loader;
843    }
844
845    /**
846     * Gets an extension by class name.
847     *
848     * @param string $class The extension class name
849     *
850     * @return Twig_ExtensionInterface
851     */
852    public function getExtension($class)
853    {
854        $class = ltrim($class, '\\');
855
856        if (isset($this->extensions[$class])) {
857            if ($class !== get_class($this->extensions[$class])) {
858                @trigger_error(sprintf('Referencing the "%s" extension by its name (defined by getName()) is deprecated since 1.26 and will be removed in Twig 2.0. Use the Fully Qualified Extension Class Name instead.', $class), E_USER_DEPRECATED);
859            }
860
861            return $this->extensions[$class];
862        }
863
864        if (!isset($this->extensionsByClass[$class])) {
865            throw new Twig_Error_Runtime(sprintf('The "%s" extension is not enabled.', $class));
866        }
867
868        return $this->extensionsByClass[$class];
869    }
870
871    /**
872     * Returns the runtime implementation of a Twig element (filter/function/test).
873     *
874     * @param string $class A runtime class name
875     *
876     * @return object The runtime implementation
877     *
878     * @throws Twig_Error_Runtime When the template cannot be found
879     */
880    public function getRuntime($class)
881    {
882        if (isset($this->runtimes[$class])) {
883            return $this->runtimes[$class];
884        }
885
886        foreach ($this->runtimeLoaders as $loader) {
887            if (null !== $runtime = $loader->load($class)) {
888                return $this->runtimes[$class] = $runtime;
889            }
890        }
891
892        throw new Twig_Error_Runtime(sprintf('Unable to load the "%s" runtime.', $class));
893    }
894
895    public function addExtension(Twig_ExtensionInterface $extension)
896    {
897        if ($this->extensionInitialized) {
898            throw new LogicException(sprintf('Unable to register extension "%s" as extensions have already been initialized.', $extension->getName()));
899        }
900
901        $class = get_class($extension);
902        if ($class !== $extension->getName()) {
903            if (isset($this->extensions[$extension->getName()])) {
904                unset($this->extensions[$extension->getName()], $this->extensionsByClass[$class]);
905                @trigger_error(sprintf('The possibility to register the same extension twice ("%s") is deprecated since version 1.23 and will be removed in Twig 2.0. Use proper PHP inheritance instead.', $extension->getName()), E_USER_DEPRECATED);
906            }
907        }
908
909        $this->lastModifiedExtension = 0;
910        $this->extensionsByClass[$class] = $extension;
911        $this->extensions[$extension->getName()] = $extension;
912        $this->updateOptionsHash();
913    }
914
915    /**
916     * Removes an extension by name.
917     *
918     * This method is deprecated and you should not use it.
919     *
920     * @param string $name The extension name
921     *
922     * @deprecated since 1.12 (to be removed in 2.0)
923     */
924    public function removeExtension($name)
925    {
926        @trigger_error(sprintf('The %s method is deprecated since version 1.12 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED);
927
928        if ($this->extensionInitialized) {
929            throw new LogicException(sprintf('Unable to remove extension "%s" as extensions have already been initialized.', $name));
930        }
931
932        $class = ltrim($name, '\\');
933        if (isset($this->extensions[$class])) {
934            if ($class !== get_class($this->extensions[$class])) {
935                @trigger_error(sprintf('Referencing the "%s" extension by its name (defined by getName()) is deprecated since 1.26 and will be removed in Twig 2.0. Use the Fully Qualified Extension Class Name instead.', $class), E_USER_DEPRECATED);
936            }
937
938            unset($this->extensions[$class]);
939        }
940
941        unset($this->extensions[$class]);
942        $this->updateOptionsHash();
943    }
944
945    /**
946     * Registers an array of extensions.
947     *
948     * @param array $extensions An array of extensions
949     */
950    public function setExtensions(array $extensions)
951    {
952        foreach ($extensions as $extension) {
953            $this->addExtension($extension);
954        }
955    }
956
957    /**
958     * Returns all registered extensions.
959     *
960     * @return Twig_ExtensionInterface[] An array of extensions (keys are for internal usage only and should not be relied on)
961     */
962    public function getExtensions()
963    {
964        return $this->extensions;
965    }
966
967    public function addTokenParser(Twig_TokenParserInterface $parser)
968    {
969        if ($this->extensionInitialized) {
970            throw new LogicException('Unable to add a token parser as extensions have already been initialized.');
971        }
972
973        $this->staging->addTokenParser($parser);
974    }
975
976    /**
977     * Gets the registered Token Parsers.
978     *
979     * @return Twig_TokenParserBrokerInterface
980     *
981     * @internal
982     */
983    public function getTokenParsers()
984    {
985        if (!$this->extensionInitialized) {
986            $this->initExtensions();
987        }
988
989        return $this->parsers;
990    }
991
992    /**
993     * Gets registered tags.
994     *
995     * Be warned that this method cannot return tags defined by Twig_TokenParserBrokerInterface classes.
996     *
997     * @return Twig_TokenParserInterface[]
998     *
999     * @internal
1000     */
1001    public function getTags()
1002    {
1003        $tags = array();
1004        foreach ($this->getTokenParsers()->getParsers() as $parser) {
1005            if ($parser instanceof Twig_TokenParserInterface) {
1006                $tags[$parser->getTag()] = $parser;
1007            }
1008        }
1009
1010        return $tags;
1011    }
1012
1013    public function addNodeVisitor(Twig_NodeVisitorInterface $visitor)
1014    {
1015        if ($this->extensionInitialized) {
1016            throw new LogicException('Unable to add a node visitor as extensions have already been initialized.');
1017        }
1018
1019        $this->staging->addNodeVisitor($visitor);
1020    }
1021
1022    /**
1023     * Gets the registered Node Visitors.
1024     *
1025     * @return Twig_NodeVisitorInterface[]
1026     *
1027     * @internal
1028     */
1029    public function getNodeVisitors()
1030    {
1031        if (!$this->extensionInitialized) {
1032            $this->initExtensions();
1033        }
1034
1035        return $this->visitors;
1036    }
1037
1038    /**
1039     * Registers a Filter.
1040     *
1041     * @param string|Twig_SimpleFilter               $name   The filter name or a Twig_SimpleFilter instance
1042     * @param Twig_FilterInterface|Twig_SimpleFilter $filter
1043     */
1044    public function addFilter($name, $filter = null)
1045    {
1046        if (!$name instanceof Twig_SimpleFilter && !($filter instanceof Twig_SimpleFilter || $filter instanceof Twig_FilterInterface)) {
1047            throw new LogicException('A filter must be an instance of Twig_FilterInterface or Twig_SimpleFilter.');
1048        }
1049
1050        if ($name instanceof Twig_SimpleFilter) {
1051            $filter = $name;
1052            $name = $filter->getName();
1053        } else {
1054            @trigger_error(sprintf('Passing a name as a first argument to the %s method is deprecated since version 1.21. Pass an instance of "Twig_SimpleFilter" instead when defining filter "%s".', __METHOD__, $name), E_USER_DEPRECATED);
1055        }
1056
1057        if ($this->extensionInitialized) {
1058            throw new LogicException(sprintf('Unable to add filter "%s" as extensions have already been initialized.', $name));
1059        }
1060
1061        $this->staging->addFilter($name, $filter);
1062    }
1063
1064    /**
1065     * Get a filter by name.
1066     *
1067     * Subclasses may override this method and load filters differently;
1068     * so no list of filters is available.
1069     *
1070     * @param string $name The filter name
1071     *
1072     * @return Twig_Filter|false A Twig_Filter instance or false if the filter does not exist
1073     *
1074     * @internal
1075     */
1076    public function getFilter($name)
1077    {
1078        if (!$this->extensionInitialized) {
1079            $this->initExtensions();
1080        }
1081
1082        if (isset($this->filters[$name])) {
1083            return $this->filters[$name];
1084        }
1085
1086        foreach ($this->filters as $pattern => $filter) {
1087            $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);
1088
1089            if ($count) {
1090                if (preg_match('#^'.$pattern.'$#', $name, $matches)) {
1091                    array_shift($matches);
1092                    $filter->setArguments($matches);
1093
1094                    return $filter;
1095                }
1096            }
1097        }
1098
1099        foreach ($this->filterCallbacks as $callback) {
1100            if (false !== $filter = call_user_func($callback, $name)) {
1101                return $filter;
1102            }
1103        }
1104
1105        return false;
1106    }
1107
1108    public function registerUndefinedFilterCallback($callable)
1109    {
1110        $this->filterCallbacks[] = $callable;
1111    }
1112
1113    /**
1114     * Gets the registered Filters.
1115     *
1116     * Be warned that this method cannot return filters defined with registerUndefinedFilterCallback.
1117     *
1118     * @return Twig_FilterInterface[]
1119     *
1120     * @see registerUndefinedFilterCallback
1121     *
1122     * @internal
1123     */
1124    public function getFilters()
1125    {
1126        if (!$this->extensionInitialized) {
1127            $this->initExtensions();
1128        }
1129
1130        return $this->filters;
1131    }
1132
1133    /**
1134     * Registers a Test.
1135     *
1136     * @param string|Twig_SimpleTest             $name The test name or a Twig_SimpleTest instance
1137     * @param Twig_TestInterface|Twig_SimpleTest $test A Twig_TestInterface instance or a Twig_SimpleTest instance
1138     */
1139    public function addTest($name, $test = null)
1140    {
1141        if (!$name instanceof Twig_SimpleTest && !($test instanceof Twig_SimpleTest || $test instanceof Twig_TestInterface)) {
1142            throw new LogicException('A test must be an instance of Twig_TestInterface or Twig_SimpleTest.');
1143        }
1144
1145        if ($name instanceof Twig_SimpleTest) {
1146            $test = $name;
1147            $name = $test->getName();
1148        } else {
1149            @trigger_error(sprintf('Passing a name as a first argument to the %s method is deprecated since version 1.21. Pass an instance of "Twig_SimpleTest" instead when defining test "%s".', __METHOD__, $name), E_USER_DEPRECATED);
1150        }
1151
1152        if ($this->extensionInitialized) {
1153            throw new LogicException(sprintf('Unable to add test "%s" as extensions have already been initialized.', $name));
1154        }
1155
1156        $this->staging->addTest($name, $test);
1157    }
1158
1159    /**
1160     * Gets the registered Tests.
1161     *
1162     * @return Twig_TestInterface[]
1163     *
1164     * @internal
1165     */
1166    public function getTests()
1167    {
1168        if (!$this->extensionInitialized) {
1169            $this->initExtensions();
1170        }
1171
1172        return $this->tests;
1173    }
1174
1175    /**
1176     * Gets a test by name.
1177     *
1178     * @param string $name The test name
1179     *
1180     * @return Twig_Test|false A Twig_Test instance or false if the test does not exist
1181     *
1182     * @internal
1183     */
1184    public function getTest($name)
1185    {
1186        if (!$this->extensionInitialized) {
1187            $this->initExtensions();
1188        }
1189
1190        if (isset($this->tests[$name])) {
1191            return $this->tests[$name];
1192        }
1193
1194        return false;
1195    }
1196
1197    /**
1198     * Registers a Function.
1199     *
1200     * @param string|Twig_SimpleFunction                 $name     The function name or a Twig_SimpleFunction instance
1201     * @param Twig_FunctionInterface|Twig_SimpleFunction $function
1202     */
1203    public function addFunction($name, $function = null)
1204    {
1205        if (!$name instanceof Twig_SimpleFunction && !($function instanceof Twig_SimpleFunction || $function instanceof Twig_FunctionInterface)) {
1206            throw new LogicException('A function must be an instance of Twig_FunctionInterface or Twig_SimpleFunction.');
1207        }
1208
1209        if ($name instanceof Twig_SimpleFunction) {
1210            $function = $name;
1211            $name = $function->getName();
1212        } else {
1213            @trigger_error(sprintf('Passing a name as a first argument to the %s method is deprecated since version 1.21. Pass an instance of "Twig_SimpleFunction" instead when defining function "%s".', __METHOD__, $name), E_USER_DEPRECATED);
1214        }
1215
1216        if ($this->extensionInitialized) {
1217            throw new LogicException(sprintf('Unable to add function "%s" as extensions have already been initialized.', $name));
1218        }
1219
1220        $this->staging->addFunction($name, $function);
1221    }
1222
1223    /**
1224     * Get a function by name.
1225     *
1226     * Subclasses may override this method and load functions differently;
1227     * so no list of functions is available.
1228     *
1229     * @param string $name function name
1230     *
1231     * @return Twig_Function|false A Twig_Function instance or false if the function does not exist
1232     *
1233     * @internal
1234     */
1235    public function getFunction($name)
1236    {
1237        if (!$this->extensionInitialized) {
1238            $this->initExtensions();
1239        }
1240
1241        if (isset($this->functions[$name])) {
1242            return $this->functions[$name];
1243        }
1244
1245        foreach ($this->functions as $pattern => $function) {
1246            $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);
1247
1248            if ($count) {
1249                if (preg_match('#^'.$pattern.'$#', $name, $matches)) {
1250                    array_shift($matches);
1251                    $function->setArguments($matches);
1252
1253                    return $function;
1254                }
1255            }
1256        }
1257
1258        foreach ($this->functionCallbacks as $callback) {
1259            if (false !== $function = call_user_func($callback, $name)) {
1260                return $function;
1261            }
1262        }
1263
1264        return false;
1265    }
1266
1267    public function registerUndefinedFunctionCallback($callable)
1268    {
1269        $this->functionCallbacks[] = $callable;
1270    }
1271
1272    /**
1273     * Gets registered functions.
1274     *
1275     * Be warned that this method cannot return functions defined with registerUndefinedFunctionCallback.
1276     *
1277     * @return Twig_FunctionInterface[]
1278     *
1279     * @see registerUndefinedFunctionCallback
1280     *
1281     * @internal
1282     */
1283    public function getFunctions()
1284    {
1285        if (!$this->extensionInitialized) {
1286            $this->initExtensions();
1287        }
1288
1289        return $this->functions;
1290    }
1291
1292    /**
1293     * Registers a Global.
1294     *
1295     * New globals can be added before compiling or rendering a template;
1296     * but after, you can only update existing globals.
1297     *
1298     * @param string $name  The global name
1299     * @param mixed  $value The global value
1300     */
1301    public function addGlobal($name, $value)
1302    {
1303        if ($this->extensionInitialized || $this->runtimeInitialized) {
1304            if (null === $this->globals) {
1305                $this->globals = $this->initGlobals();
1306            }
1307
1308            if (!array_key_exists($name, $this->globals)) {
1309                // The deprecation notice must be turned into the following exception in Twig 2.0
1310                @trigger_error(sprintf('Registering global variable "%s" at runtime or when the extensions have already been initialized is deprecated since version 1.21.', $name), E_USER_DEPRECATED);
1311                //throw new LogicException(sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name));
1312            }
1313        }
1314
1315        if ($this->extensionInitialized || $this->runtimeInitialized) {
1316            // update the value
1317            $this->globals[$name] = $value;
1318        } else {
1319            $this->staging->addGlobal($name, $value);
1320        }
1321    }
1322
1323    /**
1324     * Gets the registered Globals.
1325     *
1326     * @return array An array of globals
1327     *
1328     * @internal
1329     */
1330    public function getGlobals()
1331    {
1332        if (!$this->runtimeInitialized && !$this->extensionInitialized) {
1333            return $this->initGlobals();
1334        }
1335
1336        if (null === $this->globals) {
1337            $this->globals = $this->initGlobals();
1338        }
1339
1340        return $this->globals;
1341    }
1342
1343    /**
1344     * Merges a context with the defined globals.
1345     *
1346     * @param array $context An array representing the context
1347     *
1348     * @return array The context merged with the globals
1349     */
1350    public function mergeGlobals(array $context)
1351    {
1352        // we don't use array_merge as the context being generally
1353        // bigger than globals, this code is faster.
1354        foreach ($this->getGlobals() as $key => $value) {
1355            if (!array_key_exists($key, $context)) {
1356                $context[$key] = $value;
1357            }
1358        }
1359
1360        return $context;
1361    }
1362
1363    /**
1364     * Gets the registered unary Operators.
1365     *
1366     * @return array An array of unary operators
1367     *
1368     * @internal
1369     */
1370    public function getUnaryOperators()
1371    {
1372        if (!$this->extensionInitialized) {
1373            $this->initExtensions();
1374        }
1375
1376        return $this->unaryOperators;
1377    }
1378
1379    /**
1380     * Gets the registered binary Operators.
1381     *
1382     * @return array An array of binary operators
1383     *
1384     * @internal
1385     */
1386    public function getBinaryOperators()
1387    {
1388        if (!$this->extensionInitialized) {
1389            $this->initExtensions();
1390        }
1391
1392        return $this->binaryOperators;
1393    }
1394
1395    /**
1396     * @deprecated since 1.23 (to be removed in 2.0)
1397     */
1398    public function computeAlternatives($name, $items)
1399    {
1400        @trigger_error(sprintf('The %s method is deprecated since version 1.23 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED);
1401
1402        return Twig_Error_Syntax::computeAlternatives($name, $items);
1403    }
1404
1405    /**
1406     * @internal
1407     */
1408    protected function initGlobals()
1409    {
1410        $globals = array();
1411        foreach ($this->extensions as $name => $extension) {
1412            if (!$extension instanceof Twig_Extension_GlobalsInterface) {
1413                $m = new ReflectionMethod($extension, 'getGlobals');
1414
1415                if ('Twig_Extension' !== $m->getDeclaringClass()->getName()) {
1416                    @trigger_error(sprintf('Defining the getGlobals() method in the "%s" extension without explicitly implementing Twig_Extension_GlobalsInterface is deprecated since version 1.23.', $name), E_USER_DEPRECATED);
1417                }
1418            }
1419
1420            $extGlob = $extension->getGlobals();
1421            if (!is_array($extGlob)) {
1422                throw new UnexpectedValueException(sprintf('"%s::getGlobals()" must return an array of globals.', get_class($extension)));
1423            }
1424
1425            $globals[] = $extGlob;
1426        }
1427
1428        $globals[] = $this->staging->getGlobals();
1429
1430        return call_user_func_array('array_merge', $globals);
1431    }
1432
1433    /**
1434     * @internal
1435     */
1436    protected function initExtensions()
1437    {
1438        if ($this->extensionInitialized) {
1439            return;
1440        }
1441
1442        $this->extensionInitialized = true;
1443        $this->parsers = new Twig_TokenParserBroker(array(), array(), false);
1444        $this->filters = array();
1445        $this->functions = array();
1446        $this->tests = array();
1447        $this->visitors = array();
1448        $this->unaryOperators = array();
1449        $this->binaryOperators = array();
1450
1451        foreach ($this->extensions as $extension) {
1452            $this->initExtension($extension);
1453        }
1454        $this->initExtension($this->staging);
1455    }
1456
1457    /**
1458     * @internal
1459     */
1460    protected function initExtension(Twig_ExtensionInterface $extension)
1461    {
1462        // filters
1463        foreach ($extension->getFilters() as $name => $filter) {
1464            if ($filter instanceof Twig_SimpleFilter) {
1465                $name = $filter->getName();
1466            } else {
1467                @trigger_error(sprintf('Using an instance of "%s" for filter "%s" is deprecated since version 1.21. Use Twig_SimpleFilter instead.', get_class($filter), $name), E_USER_DEPRECATED);
1468            }
1469
1470            $this->filters[$name] = $filter;
1471        }
1472
1473        // functions
1474        foreach ($extension->getFunctions() as $name => $function) {
1475            if ($function instanceof Twig_SimpleFunction) {
1476                $name = $function->getName();
1477            } else {
1478                @trigger_error(sprintf('Using an instance of "%s" for function "%s" is deprecated since version 1.21. Use Twig_SimpleFunction instead.', get_class($function), $name), E_USER_DEPRECATED);
1479            }
1480
1481            $this->functions[$name] = $function;
1482        }
1483
1484        // tests
1485        foreach ($extension->getTests() as $name => $test) {
1486            if ($test instanceof Twig_SimpleTest) {
1487                $name = $test->getName();
1488            } else {
1489                @trigger_error(sprintf('Using an instance of "%s" for test "%s" is deprecated since version 1.21. Use Twig_SimpleTest instead.', get_class($test), $name), E_USER_DEPRECATED);
1490            }
1491
1492            $this->tests[$name] = $test;
1493        }
1494
1495        // token parsers
1496        foreach ($extension->getTokenParsers() as $parser) {
1497            if ($parser instanceof Twig_TokenParserInterface) {
1498                $this->parsers->addTokenParser($parser);
1499            } elseif ($parser instanceof Twig_TokenParserBrokerInterface) {
1500                @trigger_error('Registering a Twig_TokenParserBrokerInterface instance is deprecated since version 1.21.', E_USER_DEPRECATED);
1501
1502                $this->parsers->addTokenParserBroker($parser);
1503            } else {
1504                throw new LogicException('getTokenParsers() must return an array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances.');
1505            }
1506        }
1507
1508        // node visitors
1509        foreach ($extension->getNodeVisitors() as $visitor) {
1510            $this->visitors[] = $visitor;
1511        }
1512
1513        // operators
1514        if ($operators = $extension->getOperators()) {
1515            if (!is_array($operators)) {
1516                throw new InvalidArgumentException(sprintf('"%s::getOperators()" must return an array with operators, got "%s".', get_class($extension), is_object($operators) ? get_class($operators) : gettype($operators).(is_resource($operators) ? '' : '#'.$operators)));
1517            }
1518
1519            if (2 !== count($operators)) {
1520                throw new InvalidArgumentException(sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', get_class($extension), count($operators)));
1521            }
1522
1523            $this->unaryOperators = array_merge($this->unaryOperators, $operators[0]);
1524            $this->binaryOperators = array_merge($this->binaryOperators, $operators[1]);
1525        }
1526    }
1527
1528    /**
1529     * @deprecated since 1.22 (to be removed in 2.0)
1530     */
1531    protected function writeCacheFile($file, $content)
1532    {
1533        $this->cache->write($file, $content);
1534    }
1535
1536    private function updateOptionsHash()
1537    {
1538        $hashParts = array_merge(
1539            array_keys($this->extensions),
1540            array(
1541                (int) function_exists('twig_template_get_attributes'),
1542                PHP_MAJOR_VERSION,
1543                PHP_MINOR_VERSION,
1544                self::VERSION,
1545                (int) $this->debug,
1546                $this->baseTemplateClass,
1547                (int) $this->strictVariables,
1548            )
1549        );
1550        $this->optionsHash = implode(':', $hashParts);
1551    }
1552}
1553