<?php

/*
 * This file is part of Twig.
 *
 * (c) Fabien Potencier
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

/**
 * Stores the Twig configuration.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class Twig_Environment
{
    const VERSION = '1.35.4';
    const VERSION_ID = 13504;
    const MAJOR_VERSION = 1;
    const MINOR_VERSION = 35;
    const RELEASE_VERSION = 4;
    const EXTRA_VERSION = '';

    protected $charset;
    protected $loader;
    protected $debug;
    protected $autoReload;
    protected $cache;
    protected $lexer;
    protected $parser;
    protected $compiler;
    protected $baseTemplateClass;
    protected $extensions;
    protected $parsers;
    protected $visitors;
    protected $filters;
    protected $tests;
    protected $functions;
    protected $globals;
    protected $runtimeInitialized = false;
    protected $extensionInitialized = false;
    protected $loadedTemplates;
    protected $strictVariables;
    protected $unaryOperators;
    protected $binaryOperators;
    protected $templateClassPrefix = '__TwigTemplate_';
    protected $functionCallbacks = array();
    protected $filterCallbacks = array();
    protected $staging;

    private $originalCache;
    private $bcWriteCacheFile = false;
    private $bcGetCacheFilename = false;
    private $lastModifiedExtension = 0;
    private $extensionsByClass = array();
    private $runtimeLoaders = array();
    private $runtimes = array();
    private $optionsHash;
    private $loading = array();

    /**
     * Constructor.
     *
     * Available options:
     *
     *  * debug: When set to true, it automatically set "auto_reload" to true as
     *           well (default to false).
     *
     *  * charset: The charset used by the templates (default to UTF-8).
     *
     *  * base_template_class: The base template class to use for generated
     *                         templates (default to Twig_Template).
     *
     *  * cache: An absolute path where to store the compiled templates,
     *           a Twig_Cache_Interface implementation,
     *           or false to disable compilation cache (default).
     *
     *  * auto_reload: Whether to reload the template if the original source changed.
     *                 If you don't provide the auto_reload option, it will be
     *                 determined automatically based on the debug value.
     *
     *  * strict_variables: Whether to ignore invalid variables in templates
     *                      (default to false).
     *
     *  * autoescape: Whether to enable auto-escaping (default to html):
     *                  * false: disable auto-escaping
     *                  * true: equivalent to html
     *                  * html, js: set the autoescaping to one of the supported strategies
     *                  * name: set the autoescaping strategy based on the template name extension
     *                  * PHP callback: a PHP callback that returns an escaping strategy based on the template "name"
     *
     *  * optimizations: A flag that indicates which optimizations to apply
     *                   (default to -1 which means that all optimizations are enabled;
     *                   set it to 0 to disable).
     *
     * @param Twig_LoaderInterface $loader
     * @param array                $options An array of options
     */
    public function __construct(Twig_LoaderInterface $loader = null, $options = array())
    {
        if (null !== $loader) {
            $this->setLoader($loader);
        } else {
            @trigger_error('Not passing a Twig_LoaderInterface as the first constructor argument of Twig_Environment is deprecated since version 1.21.', E_USER_DEPRECATED);
        }

        $options = array_merge(array(
            'debug' => false,
            'charset' => 'UTF-8',
            'base_template_class' => 'Twig_Template',
            'strict_variables' => false,
            'autoescape' => 'html',
            'cache' => false,
            'auto_reload' => null,
            'optimizations' => -1,
        ), $options);

        $this->debug = (bool) $options['debug'];
        $this->charset = strtoupper($options['charset']);
        $this->baseTemplateClass = $options['base_template_class'];
        $this->autoReload = null === $options['auto_reload'] ? $this->debug : (bool) $options['auto_reload'];
        $this->strictVariables = (bool) $options['strict_variables'];
        $this->setCache($options['cache']);

        $this->addExtension(new Twig_Extension_Core());
        $this->addExtension(new Twig_Extension_Escaper($options['autoescape']));
        $this->addExtension(new Twig_Extension_Optimizer($options['optimizations']));
        $this->staging = new Twig_Extension_Staging();

        // For BC
        if (is_string($this->originalCache)) {
            $r = new ReflectionMethod($this, 'writeCacheFile');
            if (__CLASS__ !== $r->getDeclaringClass()->getName()) {
                @trigger_error('The Twig_Environment::writeCacheFile method is deprecated since version 1.22 and will be removed in Twig 2.0.', E_USER_DEPRECATED);

                $this->bcWriteCacheFile = true;
            }

            $r = new ReflectionMethod($this, 'getCacheFilename');
            if (__CLASS__ !== $r->getDeclaringClass()->getName()) {
                @trigger_error('The Twig_Environment::getCacheFilename method is deprecated since version 1.22 and will be removed in Twig 2.0.', E_USER_DEPRECATED);

                $this->bcGetCacheFilename = true;
            }
        }
    }

    /**
     * Gets the base template class for compiled templates.
     *
     * @return string The base template class name
     */
    public function getBaseTemplateClass()
    {
        return $this->baseTemplateClass;
    }

    /**
     * Sets the base template class for compiled templates.
     *
     * @param string $class The base template class name
     */
    public function setBaseTemplateClass($class)
    {
        $this->baseTemplateClass = $class;
        $this->updateOptionsHash();
    }

    /**
     * Enables debugging mode.
     */
    public function enableDebug()
    {
        $this->debug = true;
        $this->updateOptionsHash();
    }

    /**
     * Disables debugging mode.
     */
    public function disableDebug()
    {
        $this->debug = false;
        $this->updateOptionsHash();
    }

    /**
     * Checks if debug mode is enabled.
     *
     * @return bool true if debug mode is enabled, false otherwise
     */
    public function isDebug()
    {
        return $this->debug;
    }

    /**
     * Enables the auto_reload option.
     */
    public function enableAutoReload()
    {
        $this->autoReload = true;
    }

    /**
     * Disables the auto_reload option.
     */
    public function disableAutoReload()
    {
        $this->autoReload = false;
    }

    /**
     * Checks if the auto_reload option is enabled.
     *
     * @return bool true if auto_reload is enabled, false otherwise
     */
    public function isAutoReload()
    {
        return $this->autoReload;
    }

    /**
     * Enables the strict_variables option.
     */
    public function enableStrictVariables()
    {
        $this->strictVariables = true;
        $this->updateOptionsHash();
    }

    /**
     * Disables the strict_variables option.
     */
    public function disableStrictVariables()
    {
        $this->strictVariables = false;
        $this->updateOptionsHash();
    }

    /**
     * Checks if the strict_variables option is enabled.
     *
     * @return bool true if strict_variables is enabled, false otherwise
     */
    public function isStrictVariables()
    {
        return $this->strictVariables;
    }

    /**
     * Gets the current cache implementation.
     *
     * @param bool $original Whether to return the original cache option or the real cache instance
     *
     * @return Twig_CacheInterface|string|false A Twig_CacheInterface implementation,
     *                                          an absolute path to the compiled templates,
     *                                          or false to disable cache
     */
    public function getCache($original = true)
    {
        return $original ? $this->originalCache : $this->cache;
    }

    /**
     * Sets the current cache implementation.
     *
     * @param Twig_CacheInterface|string|false $cache A Twig_CacheInterface implementation,
     *                                                an absolute path to the compiled templates,
     *                                                or false to disable cache
     */
    public function setCache($cache)
    {
        if (is_string($cache)) {
            $this->originalCache = $cache;
            $this->cache = new Twig_Cache_Filesystem($cache);
        } elseif (false === $cache) {
            $this->originalCache = $cache;
            $this->cache = new Twig_Cache_Null();
        } elseif (null === $cache) {
            @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);
            $this->originalCache = false;
            $this->cache = new Twig_Cache_Null();
        } elseif ($cache instanceof Twig_CacheInterface) {
            $this->originalCache = $this->cache = $cache;
        } else {
            throw new LogicException(sprintf('Cache can only be a string, false, or a Twig_CacheInterface implementation.'));
        }
    }

    /**
     * Gets the cache filename for a given template.
     *
     * @param string $name The template name
     *
     * @return string|false The cache file name or false when caching is disabled
     *
     * @deprecated since 1.22 (to be removed in 2.0)
     */
    public function getCacheFilename($name)
    {
        @trigger_error(sprintf('The %s method is deprecated since version 1.22 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED);

        $key = $this->cache->generateKey($name, $this->getTemplateClass($name));

        return !$key ? false : $key;
    }

    /**
     * Gets the template class associated with the given string.
     *
     * The generated template class is based on the following parameters:
     *
     *  * The cache key for the given template;
     *  * The currently enabled extensions;
     *  * Whether the Twig C extension is available or not;
     *  * PHP version;
     *  * Twig version;
     *  * Options with what environment was created.
     *
     * @param string   $name  The name for which to calculate the template class name
     * @param int|null $index The index if it is an embedded template
     *
     * @return string The template class name
     */
    public function getTemplateClass($name, $index = null)
    {
        $key = $this->getLoader()->getCacheKey($name).$this->optionsHash;

        return $this->templateClassPrefix.hash('sha256', $key).(null === $index ? '' : '_'.$index);
    }

    /**
     * Gets the template class prefix.
     *
     * @return string The template class prefix
     *
     * @deprecated since 1.22 (to be removed in 2.0)
     */
    public function getTemplateClassPrefix()
    {
        @trigger_error(sprintf('The %s method is deprecated since version 1.22 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED);

        return $this->templateClassPrefix;
    }

    /**
     * Renders a template.
     *
     * @param string $name    The template name
     * @param array  $context An array of parameters to pass to the template
     *
     * @return string The rendered template
     *
     * @throws Twig_Error_Loader  When the template cannot be found
     * @throws Twig_Error_Syntax  When an error occurred during compilation
     * @throws Twig_Error_Runtime When an error occurred during rendering
     */
    public function render($name, array $context = array())
    {
        return $this->loadTemplate($name)->render($context);
    }

    /**
     * Displays a template.
     *
     * @param string $name    The template name
     * @param array  $context An array of parameters to pass to the template
     *
     * @throws Twig_Error_Loader  When the template cannot be found
     * @throws Twig_Error_Syntax  When an error occurred during compilation
     * @throws Twig_Error_Runtime When an error occurred during rendering
     */
    public function display($name, array $context = array())
    {
        $this->loadTemplate($name)->display($context);
    }

    /**
     * Loads a template.
     *
     * @param string|Twig_TemplateWrapper|Twig_Template $name The template name
     *
     * @throws Twig_Error_Loader  When the template cannot be found
     * @throws Twig_Error_Runtime When a previously generated cache is corrupted
     * @throws Twig_Error_Syntax  When an error occurred during compilation
     *
     * @return Twig_TemplateWrapper
     */
    public function load($name)
    {
        if ($name instanceof Twig_TemplateWrapper) {
            return $name;
        }

        if ($name instanceof Twig_Template) {
            return new Twig_TemplateWrapper($this, $name);
        }

        return new Twig_TemplateWrapper($this, $this->loadTemplate($name));
    }

    /**
     * Loads a template internal representation.
     *
     * This method is for internal use only and should never be called
     * directly.
     *
     * @param string $name  The template name
     * @param int    $index The index if it is an embedded template
     *
     * @return Twig_TemplateInterface A template instance representing the given template name
     *
     * @throws Twig_Error_Loader  When the template cannot be found
     * @throws Twig_Error_Runtime When a previously generated cache is corrupted
     * @throws Twig_Error_Syntax  When an error occurred during compilation
     *
     * @internal
     */
    public function loadTemplate($name, $index = null)
    {
        $cls = $mainCls = $this->getTemplateClass($name);
        if (null !== $index) {
            $cls .= '_'.$index;
        }

        if (isset($this->loadedTemplates[$cls])) {
            return $this->loadedTemplates[$cls];
        }

        if (!class_exists($cls, false)) {
            if ($this->bcGetCacheFilename) {
                $key = $this->getCacheFilename($name);
            } else {
                $key = $this->cache->generateKey($name, $mainCls);
            }

            if (!$this->isAutoReload() || $this->isTemplateFresh($name, $this->cache->getTimestamp($key))) {
                $this->cache->load($key);
            }

            if (!class_exists($cls, false)) {
                $loader = $this->getLoader();
                if (!$loader instanceof Twig_SourceContextLoaderInterface) {
                    $source = new Twig_Source($loader->getSource($name), $name);
                } else {
                    $source = $loader->getSourceContext($name);
                }

                $content = $this->compileSource($source);

                if ($this->bcWriteCacheFile) {
                    $this->writeCacheFile($key, $content);
                } else {
                    $this->cache->write($key, $content);
                    $this->cache->load($key);
                }

                if (!class_exists($mainCls, false)) {
                    /* Last line of defense if either $this->bcWriteCacheFile was used,
                     * $this->cache is implemented as a no-op or we have a race condition
                     * where the cache was cleared between the above calls to write to and load from
                     * the cache.
                     */
                    eval('?>'.$content);
                }
            }

            if (!class_exists($cls, false)) {
                throw new Twig_Error_Runtime(sprintf('Failed to load Twig template "%s", index "%s": cache is corrupted.', $name, $index), -1, $source);
            }
        }

        if (!$this->runtimeInitialized) {
            $this->initRuntime();
        }

        if (isset($this->loading[$cls])) {
            throw new Twig_Error_Runtime(sprintf('Circular reference detected for Twig template "%s", path: %s.', $name, implode(' -> ', array_merge($this->loading, array($name)))));
        }

        $this->loading[$cls] = $name;

        try {
            $this->loadedTemplates[$cls] = new $cls($this);
            unset($this->loading[$cls]);
        } catch (\Exception $e) {
            unset($this->loading[$cls]);

            throw $e;
        }

        return $this->loadedTemplates[$cls];
    }

    /**
     * Creates a template from source.
     *
     * This method should not be used as a generic way to load templates.
     *
     * @param string $template The template name
     *
     * @return Twig_Template A template instance representing the given template name
     *
     * @throws Twig_Error_Loader When the template cannot be found
     * @throws Twig_Error_Syntax When an error occurred during compilation
     */
    public function createTemplate($template)
    {
        $name = sprintf('__string_template__%s', hash('sha256', $template, false));

        $loader = new Twig_Loader_Chain(array(
            new Twig_Loader_Array(array($name => $template)),
            $current = $this->getLoader(),
        ));

        $this->setLoader($loader);
        try {
            $template = $this->loadTemplate($name);
        } catch (Exception $e) {
            $this->setLoader($current);

            throw $e;
        } catch (Throwable $e) {
            $this->setLoader($current);

            throw $e;
        }
        $this->setLoader($current);

        return $template;
    }

    /**
     * Returns true if the template is still fresh.
     *
     * Besides checking the loader for freshness information,
     * this method also checks if the enabled extensions have
     * not changed.
     *
     * @param string $name The template name
     * @param int    $time The last modification time of the cached template
     *
     * @return bool true if the template is fresh, false otherwise
     */
    public function isTemplateFresh($name, $time)
    {
        if (0 === $this->lastModifiedExtension) {
            foreach ($this->extensions as $extension) {
                $r = new ReflectionObject($extension);
                if (file_exists($r->getFileName()) && ($extensionTime = filemtime($r->getFileName())) > $this->lastModifiedExtension) {
                    $this->lastModifiedExtension = $extensionTime;
                }
            }
        }

        return $this->lastModifiedExtension <= $time && $this->getLoader()->isFresh($name, $time);
    }

    /**
     * Tries to load a template consecutively from an array.
     *
     * Similar to loadTemplate() but it also accepts instances of Twig_Template and
     * Twig_TemplateWrapper, and an array of templates where each is tried to be loaded.
     *
     * @param string|Twig_Template|Twig_TemplateWrapper|array $names A template or an array of templates to try consecutively
     *
     * @return Twig_Template|Twig_TemplateWrapper
     *
     * @throws Twig_Error_Loader When none of the templates can be found
     * @throws Twig_Error_Syntax When an error occurred during compilation
     */
    public function resolveTemplate($names)
    {
        if (!is_array($names)) {
            $names = array($names);
        }

        foreach ($names as $name) {
            if ($name instanceof Twig_Template) {
                return $name;
            }

            if ($name instanceof Twig_TemplateWrapper) {
                return $name;
            }

            try {
                return $this->loadTemplate($name);
            } catch (Twig_Error_Loader $e) {
            }
        }

        if (1 === count($names)) {
            throw $e;
        }

        throw new Twig_Error_Loader(sprintf('Unable to find one of the following templates: "%s".', implode('", "', $names)));
    }

    /**
     * Clears the internal template cache.
     *
     * @deprecated since 1.18.3 (to be removed in 2.0)
     */
    public function clearTemplateCache()
    {
        @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);

        $this->loadedTemplates = array();
    }

    /**
     * Clears the template cache files on the filesystem.
     *
     * @deprecated since 1.22 (to be removed in 2.0)
     */
    public function clearCacheFiles()
    {
        @trigger_error(sprintf('The %s method is deprecated since version 1.22 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED);

        if (is_string($this->originalCache)) {
            foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->originalCache), RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
                if ($file->isFile()) {
                    @unlink($file->getPathname());
                }
            }
        }
    }

    /**
     * Gets the Lexer instance.
     *
     * @return Twig_LexerInterface
     *
     * @deprecated since 1.25 (to be removed in 2.0)
     */
    public function getLexer()
    {
        @trigger_error(sprintf('The %s() method is deprecated since version 1.25 and will be removed in 2.0.', __FUNCTION__), E_USER_DEPRECATED);

        if (null === $this->lexer) {
            $this->lexer = new Twig_Lexer($this);
        }

        return $this->lexer;
    }

    public function setLexer(Twig_LexerInterface $lexer)
    {
        $this->lexer = $lexer;
    }

    /**
     * Tokenizes a source code.
     *
     * @param string|Twig_Source $source The template source code
     * @param string             $name   The template name (deprecated)
     *
     * @return Twig_TokenStream
     *
     * @throws Twig_Error_Syntax When the code is syntactically wrong
     */
    public function tokenize($source, $name = null)
    {
        if (!$source instanceof Twig_Source) {
            @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);
            $source = new Twig_Source($source, $name);
        }

        if (null === $this->lexer) {
            $this->lexer = new Twig_Lexer($this);
        }

        return $this->lexer->tokenize($source);
    }

    /**
     * Gets the Parser instance.
     *
     * @return Twig_ParserInterface
     *
     * @deprecated since 1.25 (to be removed in 2.0)
     */
    public function getParser()
    {
        @trigger_error(sprintf('The %s() method is deprecated since version 1.25 and will be removed in 2.0.', __FUNCTION__), E_USER_DEPRECATED);

        if (null === $this->parser) {
            $this->parser = new Twig_Parser($this);
        }

        return $this->parser;
    }

    public function setParser(Twig_ParserInterface $parser)
    {
        $this->parser = $parser;
    }

    /**
     * Converts a token stream to a node tree.
     *
     * @return Twig_Node_Module
     *
     * @throws Twig_Error_Syntax When the token stream is syntactically or semantically wrong
     */
    public function parse(Twig_TokenStream $stream)
    {
        if (null === $this->parser) {
            $this->parser = new Twig_Parser($this);
        }

        return $this->parser->parse($stream);
    }

    /**
     * Gets the Compiler instance.
     *
     * @return Twig_CompilerInterface
     *
     * @deprecated since 1.25 (to be removed in 2.0)
     */
    public function getCompiler()
    {
        @trigger_error(sprintf('The %s() method is deprecated since version 1.25 and will be removed in 2.0.', __FUNCTION__), E_USER_DEPRECATED);

        if (null === $this->compiler) {
            $this->compiler = new Twig_Compiler($this);
        }

        return $this->compiler;
    }

    public function setCompiler(Twig_CompilerInterface $compiler)
    {
        $this->compiler = $compiler;
    }

    /**
     * Compiles a node and returns the PHP code.
     *
     * @return string The compiled PHP source code
     */
    public function compile(Twig_NodeInterface $node)
    {
        if (null === $this->compiler) {
            $this->compiler = new Twig_Compiler($this);
        }

        return $this->compiler->compile($node)->getSource();
    }

    /**
     * Compiles a template source code.
     *
     * @param string|Twig_Source $source The template source code
     * @param string             $name   The template name (deprecated)
     *
     * @return string The compiled PHP source code
     *
     * @throws Twig_Error_Syntax When there was an error during tokenizing, parsing or compiling
     */
    public function compileSource($source, $name = null)
    {
        if (!$source instanceof Twig_Source) {
            @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);
            $source = new Twig_Source($source, $name);
        }

        try {
            return $this->compile($this->parse($this->tokenize($source)));
        } catch (Twig_Error $e) {
            $e->setSourceContext($source);
            throw $e;
        } catch (Exception $e) {
            throw new Twig_Error_Syntax(sprintf('An exception has been thrown during the compilation of a template ("%s").', $e->getMessage()), -1, $source, $e);
        }
    }

    public function setLoader(Twig_LoaderInterface $loader)
    {
        if (!$loader instanceof Twig_SourceContextLoaderInterface && 0 !== strpos(get_class($loader), 'Mock_')) {
            @trigger_error(sprintf('Twig loader "%s" should implement Twig_SourceContextLoaderInterface since version 1.27.', get_class($loader)), E_USER_DEPRECATED);
        }

        $this->loader = $loader;
    }

    /**
     * Gets the Loader instance.
     *
     * @return Twig_LoaderInterface
     */
    public function getLoader()
    {
        if (null === $this->loader) {
            throw new LogicException('You must set a loader first.');
        }

        return $this->loader;
    }

    /**
     * Sets the default template charset.
     *
     * @param string $charset The default charset
     */
    public function setCharset($charset)
    {
        $this->charset = strtoupper($charset);
    }

    /**
     * Gets the default template charset.
     *
     * @return string The default charset
     */
    public function getCharset()
    {
        return $this->charset;
    }

    /**
     * Initializes the runtime environment.
     *
     * @deprecated since 1.23 (to be removed in 2.0)
     */
    public function initRuntime()
    {
        $this->runtimeInitialized = true;

        foreach ($this->getExtensions() as $name => $extension) {
            if (!$extension instanceof Twig_Extension_InitRuntimeInterface) {
                $m = new ReflectionMethod($extension, 'initRuntime');

                if ('Twig_Extension' !== $m->getDeclaringClass()->getName()) {
                    @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);
                }
            }

            $extension->initRuntime($this);
        }
    }

    /**
     * Returns true if the given extension is registered.
     *
     * @param string $class The extension class name
     *
     * @return bool Whether the extension is registered or not
     */
    public function hasExtension($class)
    {
        $class = ltrim($class, '\\');
        if (!isset($this->extensionsByClass[$class]) && class_exists($class, false)) {
            // For BC/FC with namespaced aliases
            $class = new ReflectionClass($class);
            $class = $class->name;
        }

        if (isset($this->extensions[$class])) {
            if ($class !== get_class($this->extensions[$class])) {
                @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);
            }

            return true;
        }

        return isset($this->extensionsByClass[$class]);
    }

    /**
     * Adds a runtime loader.
     */
    public function addRuntimeLoader(Twig_RuntimeLoaderInterface $loader)
    {
        $this->runtimeLoaders[] = $loader;
    }

    /**
     * Gets an extension by class name.
     *
     * @param string $class The extension class name
     *
     * @return Twig_ExtensionInterface
     */
    public function getExtension($class)
    {
        $class = ltrim($class, '\\');
        if (!isset($this->extensionsByClass[$class]) && class_exists($class, false)) {
            // For BC/FC with namespaced aliases
            $class = new ReflectionClass($class);
            $class = $class->name;
        }

        if (isset($this->extensions[$class])) {
            if ($class !== get_class($this->extensions[$class])) {
                @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);
            }

            return $this->extensions[$class];
        }

        if (!isset($this->extensionsByClass[$class])) {
            throw new Twig_Error_Runtime(sprintf('The "%s" extension is not enabled.', $class));
        }

        return $this->extensionsByClass[$class];
    }

    /**
     * Returns the runtime implementation of a Twig element (filter/function/test).
     *
     * @param string $class A runtime class name
     *
     * @return object The runtime implementation
     *
     * @throws Twig_Error_Runtime When the template cannot be found
     */
    public function getRuntime($class)
    {
        if (isset($this->runtimes[$class])) {
            return $this->runtimes[$class];
        }

        foreach ($this->runtimeLoaders as $loader) {
            if (null !== $runtime = $loader->load($class)) {
                return $this->runtimes[$class] = $runtime;
            }
        }

        throw new Twig_Error_Runtime(sprintf('Unable to load the "%s" runtime.', $class));
    }

    public function addExtension(Twig_ExtensionInterface $extension)
    {
        if ($this->extensionInitialized) {
            throw new LogicException(sprintf('Unable to register extension "%s" as extensions have already been initialized.', $extension->getName()));
        }

        $class = get_class($extension);
        if ($class !== $extension->getName()) {
            if (isset($this->extensions[$extension->getName()])) {
                unset($this->extensions[$extension->getName()], $this->extensionsByClass[$class]);
                @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);
            }
        }

        $this->lastModifiedExtension = 0;
        $this->extensionsByClass[$class] = $extension;
        $this->extensions[$extension->getName()] = $extension;
        $this->updateOptionsHash();
    }

    /**
     * Removes an extension by name.
     *
     * This method is deprecated and you should not use it.
     *
     * @param string $name The extension name
     *
     * @deprecated since 1.12 (to be removed in 2.0)
     */
    public function removeExtension($name)
    {
        @trigger_error(sprintf('The %s method is deprecated since version 1.12 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED);

        if ($this->extensionInitialized) {
            throw new LogicException(sprintf('Unable to remove extension "%s" as extensions have already been initialized.', $name));
        }

        $class = ltrim($name, '\\');
        if (!isset($this->extensionsByClass[$class]) && class_exists($class, false)) {
            // For BC/FC with namespaced aliases
            $class = new ReflectionClass($class);
            $class = $class->name;
        }

        if (isset($this->extensions[$class])) {
            if ($class !== get_class($this->extensions[$class])) {
                @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);
            }

            unset($this->extensions[$class]);
        }

        unset($this->extensions[$class]);
        $this->updateOptionsHash();
    }

    /**
     * Registers an array of extensions.
     *
     * @param array $extensions An array of extensions
     */
    public function setExtensions(array $extensions)
    {
        foreach ($extensions as $extension) {
            $this->addExtension($extension);
        }
    }

    /**
     * Returns all registered extensions.
     *
     * @return Twig_ExtensionInterface[] An array of extensions (keys are for internal usage only and should not be relied on)
     */
    public function getExtensions()
    {
        return $this->extensions;
    }

    public function addTokenParser(Twig_TokenParserInterface $parser)
    {
        if ($this->extensionInitialized) {
            throw new LogicException('Unable to add a token parser as extensions have already been initialized.');
        }

        $this->staging->addTokenParser($parser);
    }

    /**
     * Gets the registered Token Parsers.
     *
     * @return Twig_TokenParserBrokerInterface
     *
     * @internal
     */
    public function getTokenParsers()
    {
        if (!$this->extensionInitialized) {
            $this->initExtensions();
        }

        return $this->parsers;
    }

    /**
     * Gets registered tags.
     *
     * Be warned that this method cannot return tags defined by Twig_TokenParserBrokerInterface classes.
     *
     * @return Twig_TokenParserInterface[]
     *
     * @internal
     */
    public function getTags()
    {
        $tags = array();
        foreach ($this->getTokenParsers()->getParsers() as $parser) {
            if ($parser instanceof Twig_TokenParserInterface) {
                $tags[$parser->getTag()] = $parser;
            }
        }

        return $tags;
    }

    public function addNodeVisitor(Twig_NodeVisitorInterface $visitor)
    {
        if ($this->extensionInitialized) {
            throw new LogicException('Unable to add a node visitor as extensions have already been initialized.');
        }

        $this->staging->addNodeVisitor($visitor);
    }

    /**
     * Gets the registered Node Visitors.
     *
     * @return Twig_NodeVisitorInterface[]
     *
     * @internal
     */
    public function getNodeVisitors()
    {
        if (!$this->extensionInitialized) {
            $this->initExtensions();
        }

        return $this->visitors;
    }

    /**
     * Registers a Filter.
     *
     * @param string|Twig_SimpleFilter               $name   The filter name or a Twig_SimpleFilter instance
     * @param Twig_FilterInterface|Twig_SimpleFilter $filter
     */
    public function addFilter($name, $filter = null)
    {
        if (!$name instanceof Twig_SimpleFilter && !($filter instanceof Twig_SimpleFilter || $filter instanceof Twig_FilterInterface)) {
            throw new LogicException('A filter must be an instance of Twig_FilterInterface or Twig_SimpleFilter.');
        }

        if ($name instanceof Twig_SimpleFilter) {
            $filter = $name;
            $name = $filter->getName();
        } else {
            @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);
        }

        if ($this->extensionInitialized) {
            throw new LogicException(sprintf('Unable to add filter "%s" as extensions have already been initialized.', $name));
        }

        $this->staging->addFilter($name, $filter);
    }

    /**
     * Get a filter by name.
     *
     * Subclasses may override this method and load filters differently;
     * so no list of filters is available.
     *
     * @param string $name The filter name
     *
     * @return Twig_Filter|false A Twig_Filter instance or false if the filter does not exist
     *
     * @internal
     */
    public function getFilter($name)
    {
        if (!$this->extensionInitialized) {
            $this->initExtensions();
        }

        if (isset($this->filters[$name])) {
            return $this->filters[$name];
        }

        foreach ($this->filters as $pattern => $filter) {
            $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);

            if ($count) {
                if (preg_match('#^'.$pattern.'$#', $name, $matches)) {
                    array_shift($matches);
                    $filter->setArguments($matches);

                    return $filter;
                }
            }
        }

        foreach ($this->filterCallbacks as $callback) {
            if (false !== $filter = call_user_func($callback, $name)) {
                return $filter;
            }
        }

        return false;
    }

    public function registerUndefinedFilterCallback($callable)
    {
        $this->filterCallbacks[] = $callable;
    }

    /**
     * Gets the registered Filters.
     *
     * Be warned that this method cannot return filters defined with registerUndefinedFilterCallback.
     *
     * @return Twig_FilterInterface[]
     *
     * @see registerUndefinedFilterCallback
     *
     * @internal
     */
    public function getFilters()
    {
        if (!$this->extensionInitialized) {
            $this->initExtensions();
        }

        return $this->filters;
    }

    /**
     * Registers a Test.
     *
     * @param string|Twig_SimpleTest             $name The test name or a Twig_SimpleTest instance
     * @param Twig_TestInterface|Twig_SimpleTest $test A Twig_TestInterface instance or a Twig_SimpleTest instance
     */
    public function addTest($name, $test = null)
    {
        if (!$name instanceof Twig_SimpleTest && !($test instanceof Twig_SimpleTest || $test instanceof Twig_TestInterface)) {
            throw new LogicException('A test must be an instance of Twig_TestInterface or Twig_SimpleTest.');
        }

        if ($name instanceof Twig_SimpleTest) {
            $test = $name;
            $name = $test->getName();
        } else {
            @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);
        }

        if ($this->extensionInitialized) {
            throw new LogicException(sprintf('Unable to add test "%s" as extensions have already been initialized.', $name));
        }

        $this->staging->addTest($name, $test);
    }

    /**
     * Gets the registered Tests.
     *
     * @return Twig_TestInterface[]
     *
     * @internal
     */
    public function getTests()
    {
        if (!$this->extensionInitialized) {
            $this->initExtensions();
        }

        return $this->tests;
    }

    /**
     * Gets a test by name.
     *
     * @param string $name The test name
     *
     * @return Twig_Test|false A Twig_Test instance or false if the test does not exist
     *
     * @internal
     */
    public function getTest($name)
    {
        if (!$this->extensionInitialized) {
            $this->initExtensions();
        }

        if (isset($this->tests[$name])) {
            return $this->tests[$name];
        }

        return false;
    }

    /**
     * Registers a Function.
     *
     * @param string|Twig_SimpleFunction                 $name     The function name or a Twig_SimpleFunction instance
     * @param Twig_FunctionInterface|Twig_SimpleFunction $function
     */
    public function addFunction($name, $function = null)
    {
        if (!$name instanceof Twig_SimpleFunction && !($function instanceof Twig_SimpleFunction || $function instanceof Twig_FunctionInterface)) {
            throw new LogicException('A function must be an instance of Twig_FunctionInterface or Twig_SimpleFunction.');
        }

        if ($name instanceof Twig_SimpleFunction) {
            $function = $name;
            $name = $function->getName();
        } else {
            @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);
        }

        if ($this->extensionInitialized) {
            throw new LogicException(sprintf('Unable to add function "%s" as extensions have already been initialized.', $name));
        }

        $this->staging->addFunction($name, $function);
    }

    /**
     * Get a function by name.
     *
     * Subclasses may override this method and load functions differently;
     * so no list of functions is available.
     *
     * @param string $name function name
     *
     * @return Twig_Function|false A Twig_Function instance or false if the function does not exist
     *
     * @internal
     */
    public function getFunction($name)
    {
        if (!$this->extensionInitialized) {
            $this->initExtensions();
        }

        if (isset($this->functions[$name])) {
            return $this->functions[$name];
        }

        foreach ($this->functions as $pattern => $function) {
            $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);

            if ($count) {
                if (preg_match('#^'.$pattern.'$#', $name, $matches)) {
                    array_shift($matches);
                    $function->setArguments($matches);

                    return $function;
                }
            }
        }

        foreach ($this->functionCallbacks as $callback) {
            if (false !== $function = call_user_func($callback, $name)) {
                return $function;
            }
        }

        return false;
    }

    public function registerUndefinedFunctionCallback($callable)
    {
        $this->functionCallbacks[] = $callable;
    }

    /**
     * Gets registered functions.
     *
     * Be warned that this method cannot return functions defined with registerUndefinedFunctionCallback.
     *
     * @return Twig_FunctionInterface[]
     *
     * @see registerUndefinedFunctionCallback
     *
     * @internal
     */
    public function getFunctions()
    {
        if (!$this->extensionInitialized) {
            $this->initExtensions();
        }

        return $this->functions;
    }

    /**
     * Registers a Global.
     *
     * New globals can be added before compiling or rendering a template;
     * but after, you can only update existing globals.
     *
     * @param string $name  The global name
     * @param mixed  $value The global value
     */
    public function addGlobal($name, $value)
    {
        if ($this->extensionInitialized || $this->runtimeInitialized) {
            if (null === $this->globals) {
                $this->globals = $this->initGlobals();
            }

            if (!array_key_exists($name, $this->globals)) {
                // The deprecation notice must be turned into the following exception in Twig 2.0
                @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);
                //throw new LogicException(sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name));
            }
        }

        if ($this->extensionInitialized || $this->runtimeInitialized) {
            // update the value
            $this->globals[$name] = $value;
        } else {
            $this->staging->addGlobal($name, $value);
        }
    }

    /**
     * Gets the registered Globals.
     *
     * @return array An array of globals
     *
     * @internal
     */
    public function getGlobals()
    {
        if (!$this->runtimeInitialized && !$this->extensionInitialized) {
            return $this->initGlobals();
        }

        if (null === $this->globals) {
            $this->globals = $this->initGlobals();
        }

        return $this->globals;
    }

    /**
     * Merges a context with the defined globals.
     *
     * @param array $context An array representing the context
     *
     * @return array The context merged with the globals
     */
    public function mergeGlobals(array $context)
    {
        // we don't use array_merge as the context being generally
        // bigger than globals, this code is faster.
        foreach ($this->getGlobals() as $key => $value) {
            if (!array_key_exists($key, $context)) {
                $context[$key] = $value;
            }
        }

        return $context;
    }

    /**
     * Gets the registered unary Operators.
     *
     * @return array An array of unary operators
     *
     * @internal
     */
    public function getUnaryOperators()
    {
        if (!$this->extensionInitialized) {
            $this->initExtensions();
        }

        return $this->unaryOperators;
    }

    /**
     * Gets the registered binary Operators.
     *
     * @return array An array of binary operators
     *
     * @internal
     */
    public function getBinaryOperators()
    {
        if (!$this->extensionInitialized) {
            $this->initExtensions();
        }

        return $this->binaryOperators;
    }

    /**
     * @deprecated since 1.23 (to be removed in 2.0)
     */
    public function computeAlternatives($name, $items)
    {
        @trigger_error(sprintf('The %s method is deprecated since version 1.23 and will be removed in Twig 2.0.', __METHOD__), E_USER_DEPRECATED);

        return Twig_Error_Syntax::computeAlternatives($name, $items);
    }

    /**
     * @internal
     */
    protected function initGlobals()
    {
        $globals = array();
        foreach ($this->extensions as $name => $extension) {
            if (!$extension instanceof Twig_Extension_GlobalsInterface) {
                $m = new ReflectionMethod($extension, 'getGlobals');

                if ('Twig_Extension' !== $m->getDeclaringClass()->getName()) {
                    @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);
                }
            }

            $extGlob = $extension->getGlobals();
            if (!is_array($extGlob)) {
                throw new UnexpectedValueException(sprintf('"%s::getGlobals()" must return an array of globals.', get_class($extension)));
            }

            $globals[] = $extGlob;
        }

        $globals[] = $this->staging->getGlobals();

        return call_user_func_array('array_merge', $globals);
    }

    /**
     * @internal
     */
    protected function initExtensions()
    {
        if ($this->extensionInitialized) {
            return;
        }

        $this->parsers = new Twig_TokenParserBroker(array(), array(), false);
        $this->filters = array();
        $this->functions = array();
        $this->tests = array();
        $this->visitors = array();
        $this->unaryOperators = array();
        $this->binaryOperators = array();

        foreach ($this->extensions as $extension) {
            $this->initExtension($extension);
        }
        $this->initExtension($this->staging);
        // Done at the end only, so that an exception during initialization does not mark the environment as initialized when catching the exception
        $this->extensionInitialized = true;
    }

    /**
     * @internal
     */
    protected function initExtension(Twig_ExtensionInterface $extension)
    {
        // filters
        foreach ($extension->getFilters() as $name => $filter) {
            if ($filter instanceof Twig_SimpleFilter) {
                $name = $filter->getName();
            } else {
                @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);
            }

            $this->filters[$name] = $filter;
        }

        // functions
        foreach ($extension->getFunctions() as $name => $function) {
            if ($function instanceof Twig_SimpleFunction) {
                $name = $function->getName();
            } else {
                @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);
            }

            $this->functions[$name] = $function;
        }

        // tests
        foreach ($extension->getTests() as $name => $test) {
            if ($test instanceof Twig_SimpleTest) {
                $name = $test->getName();
            } else {
                @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);
            }

            $this->tests[$name] = $test;
        }

        // token parsers
        foreach ($extension->getTokenParsers() as $parser) {
            if ($parser instanceof Twig_TokenParserInterface) {
                $this->parsers->addTokenParser($parser);
            } elseif ($parser instanceof Twig_TokenParserBrokerInterface) {
                @trigger_error('Registering a Twig_TokenParserBrokerInterface instance is deprecated since version 1.21.', E_USER_DEPRECATED);

                $this->parsers->addTokenParserBroker($parser);
            } else {
                throw new LogicException('getTokenParsers() must return an array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances.');
            }
        }

        // node visitors
        foreach ($extension->getNodeVisitors() as $visitor) {
            $this->visitors[] = $visitor;
        }

        // operators
        if ($operators = $extension->getOperators()) {
            if (!is_array($operators)) {
                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)));
            }

            if (2 !== count($operators)) {
                throw new InvalidArgumentException(sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', get_class($extension), count($operators)));
            }

            $this->unaryOperators = array_merge($this->unaryOperators, $operators[0]);
            $this->binaryOperators = array_merge($this->binaryOperators, $operators[1]);
        }
    }

    /**
     * @deprecated since 1.22 (to be removed in 2.0)
     */
    protected function writeCacheFile($file, $content)
    {
        $this->cache->write($file, $content);
    }

    private function updateOptionsHash()
    {
        $hashParts = array_merge(
            array_keys($this->extensions),
            array(
                (int) function_exists('twig_template_get_attributes'),
                PHP_MAJOR_VERSION,
                PHP_MINOR_VERSION,
                self::VERSION,
                (int) $this->debug,
                $this->baseTemplateClass,
                (int) $this->strictVariables,
            )
        );
        $this->optionsHash = implode(':', $hashParts);
    }
}

class_alias('Twig_Environment', 'Twig\Environment', false);


<?php

/*
 * This file is part of Twig.
 *
 * (c) Fabien Potencier
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

/**
 * Loads template from the filesystem.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class Twig_Loader_Filesystem implements Twig_LoaderInterface, Twig_ExistsLoaderInterface, Twig_SourceContextLoaderInterface
{
    /** Identifier of the main namespace. */
    const MAIN_NAMESPACE = '__main__';

    protected $paths = array();
    protected $cache = array();
    protected $errorCache = array();

    private $rootPath;

    /**
     * @param string|array $paths    A path or an array of paths where to look for templates
     * @param string|null  $rootPath The root path common to all relative paths (null for getcwd())
     */
    public function __construct($paths = array(), $rootPath = null)
    {
        $this->rootPath = (null === $rootPath ? getcwd() : $rootPath).DIRECTORY_SEPARATOR;
        if (false !== $realPath = realpath($rootPath)) {
            $this->rootPath = $realPath.DIRECTORY_SEPARATOR;
        }

        if ($paths) {
            $this->setPaths($paths);
        }
    }

    /**
     * Returns the paths to the templates.
     *
     * @param string $namespace A path namespace
     *
     * @return array The array of paths where to look for templates
     */
    public function getPaths($namespace = self::MAIN_NAMESPACE)
    {
        return isset($this->paths[$namespace]) ? $this->paths[$namespace] : array();
    }

    /**
     * Returns the path namespaces.
     *
     * The main namespace is always defined.
     *
     * @return array The array of defined namespaces
     */
    public function getNamespaces()
    {
        return array_keys($this->paths);
    }

    /**
     * Sets the paths where templates are stored.
     *
     * @param string|array $paths     A path or an array of paths where to look for templates
     * @param string       $namespace A path namespace
     */
    public function setPaths($paths, $namespace = self::MAIN_NAMESPACE)
    {
        if (!is_array($paths)) {
            $paths = array($paths);
        }

        $this->paths[$namespace] = array();
        foreach ($paths as $path) {
            $this->addPath($path, $namespace);
        }
    }

    /**
     * Adds a path where templates are stored.
     *
     * @param string $path      A path where to look for templates
     * @param string $namespace A path namespace
     *
     * @throws Twig_Error_Loader
     */
    public function addPath($path, $namespace = self::MAIN_NAMESPACE)
    {
        // invalidate the cache
        $this->cache = $this->errorCache = array();

        $checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path;
        if (!is_dir($checkPath)) {
            throw new Twig_Error_Loader(sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath));
        }

        $this->paths[$namespace][] = rtrim($path, '/\\');
    }

    /**
     * Prepends a path where templates are stored.
     *
     * @param string $path      A path where to look for templates
     * @param string $namespace A path namespace
     *
     * @throws Twig_Error_Loader
     */
    public function prependPath($path, $namespace = self::MAIN_NAMESPACE)
    {
        // invalidate the cache
        $this->cache = $this->errorCache = array();

        $checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path;
        if (!is_dir($checkPath)) {
            throw new Twig_Error_Loader(sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath));
        }

        $path = rtrim($path, '/\\');

        if (!isset($this->paths[$namespace])) {
            $this->paths[$namespace][] = $path;
        } else {
            array_unshift($this->paths[$namespace], $path);
        }
    }

    public function getSource($name)
    {
        @trigger_error(sprintf('Calling "getSource" on "%s" is deprecated since 1.27. Use getSourceContext() instead.', get_class($this)), E_USER_DEPRECATED);

        return file_get_contents($this->findTemplate($name));
    }

    public function getSourceContext($name)
    {
        $path = $this->findTemplate($name);

        return new Twig_Source(file_get_contents($path), $name, $path);
    }

    public function getCacheKey($name)
    {
        $path = $this->findTemplate($name);
        $len = strlen($this->rootPath);
        if (0 === strncmp($this->rootPath, $path, $len)) {
            return substr($path, $len);
        }

        return $path;
    }

    public function exists($name)
    {
        $name = $this->normalizeName($name);

        if (isset($this->cache[$name])) {
            return true;
        }

        try {
            return false !== $this->findTemplate($name, false);
        } catch (Twig_Error_Loader $exception) {
            @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);

            return false;
        }
    }

    public function isFresh($name, $time)
    {
        return filemtime($this->findTemplate($name)) < $time;
    }

    protected function findTemplate($name)
    {
        $throw = func_num_args() > 1 ? func_get_arg(1) : true;
        $name = $this->normalizeName($name);

        if (isset($this->cache[$name])) {
            return $this->cache[$name];
        }

        if (isset($this->errorCache[$name])) {
            if (!$throw) {
                return false;
            }

            throw new Twig_Error_Loader($this->errorCache[$name]);
        }

        $this->validateName($name);

        list($namespace, $shortname) = $this->parseName($name);

        if (!isset($this->paths[$namespace])) {
            $this->errorCache[$name] = sprintf('There are no registered paths for namespace "%s".', $namespace);

            if (!$throw) {
                return false;
            }

            throw new Twig_Error_Loader($this->errorCache[$name]);
        }

        foreach ($this->paths[$namespace] as $path) {
            if (!$this->isAbsolutePath($path)) {
                $path = $this->rootPath.'/'.$path;
            }

            if (is_file($path.'/'.$shortname)) {
                if (false !== $realpath = realpath($path.'/'.$shortname)) {
                    return $this->cache[$name] = $realpath;
                }

                return $this->cache[$name] = $path.'/'.$shortname;
            }
        }

        $this->errorCache[$name] = sprintf('Unable to find template "%s" (looked into: %s).', $name, implode(', ', $this->paths[$namespace]));

        if (!$throw) {
            return false;
        }

        throw new Twig_Error_Loader($this->errorCache[$name]);
    }

    protected function parseName($name, $default = self::MAIN_NAMESPACE)
    {
        if (isset($name[0]) && '@' == $name[0]) {
            if (false === $pos = strpos($name, '/')) {
                throw new Twig_Error_Loader(sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name));
            }

            $namespace = substr($name, 1, $pos - 1);
            $shortname = substr($name, $pos + 1);

            return array($namespace, $shortname);
        }

        return array($default, $name);
    }

    protected function normalizeName($name)
    {
        return preg_replace('#/{2,}#', '/', str_replace('\\', '/', (string) $name));
    }

    protected function validateName($name)
    {
        if (false !== strpos($name, "\0")) {
            throw new Twig_Error_Loader('A template name cannot contain NUL bytes.');
        }

        $name = ltrim($name, '/');
        $parts = explode('/', $name);
        $level = 0;
        foreach ($parts as $part) {
            if ('..' === $part) {
                --$level;
            } elseif ('.' !== $part) {
                ++$level;
            }

            if ($level < 0) {
                throw new Twig_Error_Loader(sprintf('Looks like you try to load a template outside configured directories (%s).', $name));
            }
        }
    }

    private function isAbsolutePath($file)
    {
        return strspn($file, '/\\', 0, 1)
            || (strlen($file) > 3 && ctype_alpha($file[0])
                && ':' === substr($file, 1, 1)
                && strspn($file, '/\\', 2, 1)
            )
            || null !== parse_url($file, PHP_URL_SCHEME)
        ;
    }
}

class_alias('Twig_Loader_Filesystem', 'Twig\Loader\FilesystemLoader', false);


<?php

namespace abhimanyu\user\models;

use RuntimeException;
use Yii;
use yii\behaviors\TimestampBehavior;
use yii\db\ActiveRecord;
use yii\db\Expression;

/**
 * User model
 *
 * @property integer $id
 * @property string  $username
 * @property string  $password_hash
 * @property string  $password_reset_token
 * @property string  $email
 * @property string  $auth_key
 * @property string  $activation_token
 * @property integer $super_admin
 * @property integer $status
 * @property integer $created_at
 * @property integer $updated_at
 * @property string  $password write-only password
 */
class User extends ActiveRecord
{
	/** User Status - Blocked/Inactive */
	const STATUS_BLOCKED = 0;
	/** User Status - Active */
	const STATUS_ACTIVE = 1;
	/** User Status - Activation Pending */
	const STATUS_PENDING = 2;

	/** Login Type - Email and Username */
	const  LOGIN_TYPE_BOTH = 0;
	/** Login Type - Email Only */
	const  LOGIN_TYPE_EMAIL = 1;
	/** Login Type - Username Only */
	const  LOGIN_TYPE_USERNAME = 2;

	/** @var string Plain password. Used for model validation. */
	public $password;
	/** @var string Plain password. Used for model validation. */
	public $password_confirm;

	/**
	 * @inheritdoc
	 */
	public static function tableName()
	{
		return '{{%user}}';
	}

	/**
	 * Finds out if password reset token is valid
	 *
	 * @param string $token password reset token
	 *
	 * @return boolean
	 */
	public static function isPasswordResetTokenValid($token)
	{
		if (empty($token)) {
			return FALSE;
		}
		$expire = Yii::$app->params['user.passwordResetTokenExpire'];
		$parts = explode('_', $token);
		$timestamp = (int)end($parts);

		return $timestamp + $expire >= time();
	}

	/**
	 * @inheritdoc
	 */
	public function behaviors()
	{
		return [
			'timestamp' => [
				'class'      => TimestampBehavior::className(),
				'attributes' => [
					ActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'],
					ActiveRecord::EVENT_BEFORE_UPDATE => ['updated_at'],
				],
				'value'      => new Expression('NOW()')
			],
		];
	}

	public function scenarios()
	{
		return [
			'register' => ['username', 'email', 'password', 'password_confirm'],
			'recover'  => ['email'],
			'reset'    => ['password', 'password_confirm'],
			'create'   => ['username', 'email', 'password'],
			'update'   => ['username', 'email', 'password'],
		];
	}

	/**
	 * @inheritdoc
	 */
	public function rules()
	{
		return [
			// username
			['username', 'required', 'on' => ['register', 'create']],
			['username', 'match', 'pattern' => '/^[a-zA-Z0-9_]\w+$/'],
			['username', 'string', 'min' => 3, 'max' => 25],
			['username', 'unique'],
			['username', 'trim'],

			// email
			['email', 'required', 'on' => ['register', 'create']],
			['email', 'email'],
			['email', 'string', 'max' => 255],
			['email', 'unique'],
			['email', 'trim'],

			// password
			['password', 'required', 'on' => ['register', 'reset', 'create']],
			['password', 'string', 'min' => 6, 'on' => ['register', 'reset', 'create', 'update']],

			// password confirm
			['password_confirm', 'required', 'on' => ['register', 'reset']],
			['password_confirm', 'compare', 'compareAttribute' => 'password'],
		];
	}

	public function attributeLabels()
	{
		return [
			'username'         => 'Username',
			'password_hash'    => 'Password',
			'password_confirm' => 'Confirm Password',
			'email'            => 'Email'
		];
	}

	/**
	 * Validates password
	 *
	 * @param string $password password to validate
	 *
	 * @return boolean if password provided is valid for current user
	 */
	public function validatePassword($password)
	{
		return Yii::$app->security->validatePassword($password, $this->password_hash);
	}

	public function beforeSave($insert)
	{
		if ($insert) {
			$this->generateAuthKey();
			$this->generateActivationToken();
		}

		if ($this->password)
			$this->setPassword($this->password);

		return parent::beforeSave($insert);
	}

	/**
	 * Generates "remember me" authentication key
	 */
	public function generateAuthKey()
	{
		$this->auth_key = Yii::$app->security->generateRandomString();
	}

	/**
	 * Generates account activation token
	 */
	public function generateActivationToken()
	{
		$this->activation_token = Yii::$app->security->generateRandomString(24);
	}

	/**
	 * Generates password hash from password and sets it to the model
	 *
	 * @param string $password
	 */
	public function setPassword($password)
	{
		$this->password_hash = Yii::$app->security->generatePasswordHash($password);
	}

	public function afterSave($insert, $changedAttributes)
	{
		if ($insert) {
			$profile = Yii::createObject([
				'class' => Profile::className(),
				'uid'   => $this->id
			]);
			$profile->save(FALSE);
		}

		parent::afterSave($insert, $changedAttributes);
	}

	/**
	 * Gets user profile
	 *
	 * @return Profile
	 */
	public function getProfile()
	{
		return $this->hasOne(Profile::className(), ['uid' => 'id']);
	}

	/**
	 * This method is used to register new user account.
	 *
	 * @param bool $isSuperAdmin
	 * @param int  $status
	 *
	 * @return bool
	 */
	public function register($isSuperAdmin = FALSE, $status = 1)
	{
		if ($this->getIsNewRecord() == FALSE) {
			throw new RuntimeException('Calling "' . __CLASS__ . '::' . __METHOD__ . '" on existing user');
		}

		// Set to 1 if isSuperAdmin is true else set to 0
		$this->super_admin = $isSuperAdmin ? 1 : 0;

		// Set status
		$this->status = $status;

		// Save user data to the database
		if ($this->save()) {
			return TRUE;
		}

		return FALSE;
	}

	/**
	 * @return bool Whether the user is an admin or not.
	 */
	public function getIsAdmin()
	{
		return $this->super_admin == 1;
	}

	public function create($isSuperAdmin = FALSE)
	{
		if ($this->getIsNewRecord() == FALSE) {
			throw new \RuntimeException('Calling "' . __CLASS__ . '::' . __METHOD__ . '" on existing user');
		}

		// Set to 1 if isSuperAdmin is true else set to 0
		$this->super_admin = $isSuperAdmin ? 1 : 0;

		// Set status
		$this->status = User::STATUS_PENDING;

		// Save user data to the database
		if ($this->save()) {
			return TRUE;
		}

		return FALSE;
	}

	/**
	 * Returns user's status
	 *
	 * @return null|string
	 */
	public function getIsStatus()
	{
		switch ($this->status) {
			case User::STATUS_PENDING:
				return '
Pending
';
			case User::STATUS_ACTIVE:
				return '
Active
';
			case User::STATUS_BLOCKED:
				return '
Blocked
';
		}

		return NULL;
	}

	/**
	 * Returns TRUE if user is confirmed else FALSE
	 *
	 * @return bool
	 */
	public function getIsConfirmed()
	{
		return $this->status != User::STATUS_PENDING;
	}

	/**
	 * Returns TRUE if user is blocked else FALSE
	 *
	 * @return bool
	 */
	public function getIsBlocked()
	{
		return $this->status == User::STATUS_BLOCKED;
	}

	/**
	 * Confirms user and sets status to ACTIVE
	 */
	public function confirm()
	{
		$this->status = User::STATUS_ACTIVE;
		if ($this->save(FALSE))
			return TRUE;

		return FALSE;
	}

	/**
	 * Blocks the user and sets the status to BLOCKED
	 */
	public function block()
	{
		$this->status = User::STATUS_BLOCKED;

		if ($this->save(FALSE))
			return TRUE;

		return FALSE;
	}

	/**
	 * Unblocks the user and sets the status to ACTIVE
	 */
	public function unblock()
	{
		$this->status = User::STATUS_ACTIVE;

		if ($this->save(FALSE))
			return TRUE;

		return FALSE;
	}
}


<?php

namespace Kirby\Twig;

use Exception;
use Str;
use Tpl;

/**
 * Main Kirby Twig Plugin class, hopefully with a stable API.
 *
 * @package  Kirby Twig Plugin
 * @author   Florens Verschelde <florens@fvsch.com>
 */
class Plugin
{
    /** @var bool - flag to make sure we only register once */
    static private $registered = false;

    /**
     * Register the template component and load the `twig` helper function
     * @return bool
     * @throws Exception
     */
    static public function register()
    {
        // only register once
        if (static::$registered === true) {
            return true;
        }
        $kirby = kirby();
        if (!class_exists('Kirby\Component\Template')) {
            throw new Exception('The Kirby Twig plugin requires Kirby 2.3 or higher. Current version: ' . $kirby->version());
        }
        if (!class_exists('Twig_Environment')) {
            require_once __DIR__.'/../lib/Twig/lib/Twig/Autoloader.php';
            \Twig_Autoloader::register();
        }
        $kirby->set('component', 'template', 'Kirby\Twig\TwigComponent');
        if (is_executable('twig') === false) {
            require_once __DIR__ . '/helpers.php';
        }
        return static::$registered = true;
    }

    /**
     * Renders a Twig template string or template file
     * Can be used in Kirby controllers and PHP templates
     *
     *  * Example usage:
     *
     *     <?php echo twig('Hello {{ who }}', ['who'=>'World']) ?>
     *     <?php echo twig('@snippets/header.twig', ['title'=>'Home page']) ?>
     *
     * Note: in Twig templates, you should use the `include` tag or function instead.
     *
     * @param string $template - path or template string to render
     * @param array  $userData - data to pass as variables to the template
     * @return string
     */
    static public function render($template, $userData)
    {
        if (!is_string($template)) return '';
        $path = strlen($template) <= 256 ? trim($template) : '';
        $data = array_merge(Tpl::$data, is_array($userData) ? $userData : []);
        $twig = TwigEnv::instance();

        // treat template as a path only if it *looks like* a Twig template path
        if (Str::startsWith($path, '@') || Str::endsWith(strtolower($path), '.twig')) {
            return $twig->renderPath($path, $data);
        }
        return $twig->renderString($template, $data);
    }
}


<?php

/*
 * This file is part of Twig.
 *
 * (c) Fabien Potencier
 * (c) Armin Ronacher
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

/**
 * Compiles a node to PHP code.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class Twig_Compiler implements Twig_CompilerInterface
{
    protected $lastLine;
    protected $source;
    protected $indentation;
    protected $env;
    protected $debugInfo = array();
    protected $sourceOffset;
    protected $sourceLine;
    protected $filename;
    private $varNameSalt = 0;

    public function __construct(Twig_Environment $env)
    {
        $this->env = $env;
    }

    /**
     * @deprecated since 1.25 (to be removed in 2.0)
     */
    public function getFilename()
    {
        @trigger_error(sprintf('The %s() method is deprecated since version 1.25 and will be removed in 2.0.', __FUNCTION__), E_USER_DEPRECATED);

        return $this->filename;
    }

    /**
     * Returns the environment instance related to this compiler.
     *
     * @return Twig_Environment
     */
    public function getEnvironment()
    {
        return $this->env;
    }

    /**
     * Gets the current PHP code after compilation.
     *
     * @return string The PHP code
     */
    public function getSource()
    {
        return $this->source;
    }

    /**
     * Compiles a node.
     *
     * @param Twig_NodeInterface $node        The node to compile
     * @param int                $indentation The current indentation
     *
     * @return $this
     */
    public function compile(Twig_NodeInterface $node, $indentation = 0)
    {
        $this->lastLine = null;
        $this->source = '';
        $this->debugInfo = array();
        $this->sourceOffset = 0;
        // source code starts at 1 (as we then increment it when we encounter new lines)
        $this->sourceLine = 1;
        $this->indentation = $indentation;
        $this->varNameSalt = 0;

        if ($node instanceof Twig_Node_Module) {
            // to be removed in 2.0
            $this->filename = $node->getTemplateName();
        }

        $node->compile($this);

        return $this;
    }

    public function subcompile(Twig_NodeInterface $node, $raw = true)
    {
        if (false === $raw) {
            $this->source .= str_repeat(' ', $this->indentation * 4);
        }

        $node->compile($this);

        return $this;
    }

    /**
     * Adds a raw string to the compiled code.
     *
     * @param string $string The string
     *
     * @return $this
     */
    public function raw($string)
    {
        $this->source .= $string;

        return $this;
    }

    /**
     * Writes a string to the compiled code by adding indentation.
     *
     * @return $this
     */
    public function write()
    {
        $strings = func_get_args();
        foreach ($strings as $string) {
            $this->source .= str_repeat(' ', $this->indentation * 4).$string;
        }

        return $this;
    }

    /**
     * Appends an indentation to the current PHP code after compilation.
     *
     * @return $this
     *
     * @deprecated since 1.27 (to be removed in 2.0).
     */
    public function addIndentation()
    {
        @trigger_error('The '.__METHOD__.' method is deprecated since version 1.27 and will be removed in 2.0. Use write(\'\') instead.', E_USER_DEPRECATED);

        $this->source .= str_repeat(' ', $this->indentation * 4);

        return $this;
    }

    /**
     * Adds a quoted string to the compiled code.
     *
     * @param string $value The string
     *
     * @return $this
     */
    public function string($value)
    {
        $this->source .= sprintf('"%s"', addcslashes($value, "\0\t\"\$\\"));

        return $this;
    }

    /**
     * Returns a PHP representation of a given value.
     *
     * @param mixed $value The value to convert
     *
     * @return $this
     */
    public function repr($value)
    {
        if (is_int($value) || is_float($value)) {
            if (false !== $locale = setlocale(LC_NUMERIC, '0')) {
                setlocale(LC_NUMERIC, 'C');
            }

            $this->raw($value);

            if (false !== $locale) {
                setlocale(LC_NUMERIC, $locale);
            }
        } elseif (null === $value) {
            $this->raw('null');
        } elseif (is_bool($value)) {
            $this->raw($value ? 'true' : 'false');
        } elseif (is_array($value)) {
            $this->raw('array(');
            $first = true;
            foreach ($value as $key => $v) {
                if (!$first) {
                    $this->raw(', ');
                }
                $first = false;
                $this->repr($key);
                $this->raw(' => ');
                $this->repr($v);
            }
            $this->raw(')');
        } else {
            $this->string($value);
        }

        return $this;
    }

    /**
     * Adds debugging information.
     *
     * @return $this
     */
    public function addDebugInfo(Twig_NodeInterface $node)
    {
        if ($node->getTemplateLine() != $this->lastLine) {
            $this->write(sprintf("// line %d\n", $node->getTemplateLine()));

            // when mbstring.func_overload is set to 2
            // mb_substr_count() replaces substr_count()
            // but they have different signatures!
            if (((int) ini_get('mbstring.func_overload')) & 2) {
                @trigger_error('Support for having "mbstring.func_overload" different from 0 is deprecated version 1.29 and will be removed in 2.0.', E_USER_DEPRECATED);

                // this is much slower than the "right" version
                $this->sourceLine += mb_substr_count(mb_substr($this->source, $this->sourceOffset), "\n");
            } else {
                $this->sourceLine += substr_count($this->source, "\n", $this->sourceOffset);
            }
            $this->sourceOffset = strlen($this->source);
            $this->debugInfo[$this->sourceLine] = $node->getTemplateLine();

            $this->lastLine = $node->getTemplateLine();
        }

        return $this;
    }

    public function getDebugInfo()
    {
        ksort($this->debugInfo);

        return $this->debugInfo;
    }

    /**
     * Indents the generated code.
     *
     * @param int $step The number of indentation to add
     *
     * @return $this
     */
    public function indent($step = 1)
    {
        $this->indentation += $step;

        return $this;
    }

    /**
     * Outdents the generated code.
     *
     * @param int $step The number of indentation to remove
     *
     * @return $this
     *
     * @throws LogicException When trying to outdent too much so the indentation would become negative
     */
    public function outdent($step = 1)
    {
        // can't outdent by more steps than the current indentation level
        if ($this->indentation < $step) {
            throw new LogicException('Unable to call outdent() as the indentation would become negative.');
        }

        $this->indentation -= $step;

        return $this;
    }

    public function getVarName()
    {
        return sprintf('__internal_%s', hash('sha256', __METHOD__.$this->varNameSalt++));
    }
}

class_alias('Twig_Compiler', 'Twig\Compiler', false);
class_exists('Twig_Node');


<?php

/*
 * This file is part of Twig.
 *
 * (c) Fabien Potencier
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

@trigger_error('The Twig_Autoloader class is deprecated since version 1.21 and will be removed in 2.0. Use Composer instead.', E_USER_DEPRECATED);

/**
 * Autoloads Twig classes.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @deprecated since 1.21 and will be removed in 2.0. Use Composer instead. 2.0.
 */
class Twig_Autoloader
{
    /**
     * Registers Twig_Autoloader as an SPL autoloader.
     *
     * @param bool $prepend whether to prepend the autoloader or not
     */
    public static function register($prepend = false)
    {
        @trigger_error('Using Twig_Autoloader is deprecated since version 1.21. Use Composer instead.', E_USER_DEPRECATED);

        if (PHP_VERSION_ID < 50300) {
            spl_autoload_register(array(__CLASS__, 'autoload'));
        } else {
            spl_autoload_register(array(__CLASS__, 'autoload'), true, $prepend);
        }
    }

    /**
     * Handles autoloading of classes.
     *
     * @param string $class a class name
     */
    public static function autoload($class)
    {
        if (0 !== strpos($class, 'Twig')) {
            return;
        }

        if (is_file($file = dirname(__FILE__).'/../'.str_replace(array('_', "\0"), array('/', ''), $class).'.php')) {
            require $file;
        }
    }
}


<?php

namespace abhimanyu\user\controllers;

use abhimanyu\user\models\AccountLoginForm;
use abhimanyu\user\models\SocialAccount;
use abhimanyu\user\UserModule;
use Yii;
use yii\authclient\AuthAction;
use yii\authclient\ClientInterface;
use yii\filters\AccessControl;
use yii\filters\VerbFilter;
use yii\helpers\Url;
use yii\web\Controller;

class AuthController extends Controller
{
	public function behaviors()
	{
		return [
			'access' => [
				'class' => AccessControl::className(),
				'rules' => [
					[
						'allow'   => TRUE,
						'actions' => ['login', 'auth'],
						'roles'   => ['?']
					],
					[
						'allow'   => TRUE,
						'actions' => ['logout'],
						'roles'   => ['@']
					]
				]
			],
			'verbs'  => [
				'class'   => VerbFilter::className(),
				'actions' => [
					'logout' => ['post']
				]
			]
		];
	}

	public function actions()
	{
		return [
			'auth' => [
				'class'           => AuthAction::className(),
				'successCallback' => [$this, 'authenticate'],
			]
		];
	}

	public function authenticate(ClientInterface $client)
	{
		$attributes = $client->getUserAttributes();
		$provider = $client->getId();
		$clientId = $attributes['id'];

		$model = SocialAccount::find()->where(['provider' => $provider, 'client_id' => $clientId])->one();

		if ($model === NULL) {
			$model->save(FALSE);
		}

		if (NULL === ($user = $model->getUser())) {
			$this->action->successUrl = Url::to(['/user/registration/connect', 'account_id' => $model->id]);
		} else {
			Yii::$app->user->login($user, UserModule::$rememberMeDuration);
		}
	}

	/**
	 * Displays the login page.
	 *
	 * @return string|\yii\web\Response
	 */
	public function actionLogin()
	{
		// If the user is logged in, redirect to dashboard
		if (!Yii::$app->user->isGuest)
			return $this->redirect(Yii::$app->user->returnUrl);

		$model = new AccountLoginForm();

		if ($model->load(Yii::$app->request->post()) && $model->validate() && $model->login())
			return $this->redirect(Yii::$app->user->returnUrl);

		return $this->render('login', [
			'model'              => $model,
			'canRegister'        => UserModule::$canRegister,
			'canRecoverPassword' => UserModule::$canRecoverPassword
		]);
	}

	/**
	 * Logs the user out and then redirects to the homepage.
	 *
	 * @return \yii\web\Response
	 */
	public function actionLogout()
	{
		Yii::$app->user->logout();

		return $this->goHome();
	}
}


<?php

	namespace abhimanyu\user\models;

	use abhimanyu\user\Mailer;
	use Yii;
	use yii\base\Model;

	class AccountRecoverPasswordForm extends Model
	{
		public $verifyCode;
		public $email;

		public function rules()
		{
			return [
				// email
				['email', 'required'],
				['email', 'email'],
				['email', 'exist', 'targetClass' => User::className(), 'message' => 'Email not found in the system!'],
			];
		}

		public function attributeLabels()
		{
			return [
				'email' => 'Email'
			];
		}

		/**
		 * Sends recovery message.
		 */
		public function recoverPassword()
		{
			$user = User::findOne(['email' => $this->email]);

			if ($user != NULL) {
				$user->password_reset_token = Yii::$app->getSecurity()->generateRandomString() . '_' . time();

				$user->save(FALSE);
			}

			// Sends recovery mail
			Mailer::sendRecoveryMessage($user);
			Yii::$app->session->setFlash('info', 'You will receive an email with instructions on how to reset your password in a few minutes.');
		}
	}


<?php

namespace Kirby\Twig;

use C;
use Closure;
use Escape;
use Kirby;
use Response;
use Tpl;
use Twig_Environment;
use Twig_Loader_Filesystem;
use Twig_SimpleFunction;
use Twig_SimpleFilter;
use Twig_Extension_Debug;
use Twig_Error;
use Twig_Error_Loader;

/**
 * Wrapper for the Twig_Environment class, setting up an instance
 * with Kirby-specific configuration.
 *
 * @package  Kirby Twig Plugin
 * @author   Florens Verschelde <florens@fvsch.com>
 */
class TwigEnv
{
    /** @var Twig_Environment */
    public $twig = null;

    /** @var boolean */
    public $debug = false;

    /** @var TwigEnv */
    private static $instance = null;

    /**
     * Kirby helper functions to expose as simple Twig functions
     *
     * We're exposing all helper functions documented in
     * https://getkirby.com/docs/cheatsheet#helpers
     * with just a few exceptions (sending email, saving files…)
     *
     * Prefix the function name with '*' to mark the
     * function's output as safe (avoiding HTML escaping).
     *
     * @var array
     */
    private $defaultFunctions = [
        '*attr',
        '*brick',
        // Get config value
        'c::get',
        // Skipping: call - Allows calling any PHP function
        'csrf',
        '*css',
        // Skipping: dump - Twig has one, and its ouput seems buggy anyway (prints the result twice?)
        // Skipping: e, ecco - Twig syntax is simple: {{ condition ? 'a' : 'b' }}
        // Skipping: email - Send emails from controllers, not templates
        '*esc',
        '*excerpt',
        'get',
        '*gist',
        'go',
        'gravatar',
        '*h', '*html',
        '*image',
        'invalid',
        // Get locale-specific config (or translation)
        'l::get',
        '*js',
        'kirby',
        '*kirbytag',
        '*kirbytext',
        // Skipping: l - We're adding it manually
        '*markdown',
        'memory',
        '*multiline',
        'page',
        'pages',
        'param',
        'params',
        // From the Patterns plugin - similar to snippet
        '*pattern',
        // Skipping: r - Same reason as for ecco/e
        'site',
        'size',
        '*smartypants',
        '*snippet',
        // Skipping: structure - For writing data to pages, not for display
        // Skipping: textfile - For making content file names
        'thisUrl',
        '*thumb',
        '*twitter',
        // Skipping: upload - Manage uploading from a controller
        'u', 'url',
        '*vimeo',
        '*widont',
        '*xml',
        'yaml',
        '*youtube',
    ];

    /**
     * Cache of $kirby->roots()->templates()
     * @var string
     */
    private $templateDir = null;

    /**
     * Prepare the Twig environment
     * @throws Twig_Error_Loader
     */
    public function __construct()
    {
        $kirby = Kirby::instance();
        $this->debug = $kirby->get('option', 'debug');
        $this->templateDir = $this->normalizePath($kirby->roots()->templates());

        // Get environment & user config
        $options = [
            'core' => [
                'debug' => $this->debug,
                'strict_variables' => c::get('twig.strict', $this->debug),
                'autoescape' => c::get('twig.autoescape', true),
                'cache' => false
            ],
            'namespace' => [
                'templates' => $this->templateDir,
                'snippets' => $this->normalizePath($kirby->roots->snippets()),
                'plugins' => $this->normalizePath($kirby->roots->plugins()),
                'assets' => $this->normalizePath($kirby->roots->assets())
            ],
            'function' => $this->cleanNames(array_merge(
                $this->defaultFunctions,
                c::get('twig.env.functions', [])
            )),
            'filter' => $this->cleanNames(c::get('twig.env.filters', []))
        ];

        // Set cache directory
        if (c::get('twig.cache')) {
            $options['core']['cache'] = $kirby->roots()->cache() . '/twig';
        }

        // Look at 'twig.abc.xYz' options to find namespaces, functions & filters
        foreach (array_keys(c::$data) as $key) {
            $p = '/^twig\.(env\.)?([a-z]+)\.(\*?[a-zA-Z][a-zA-Z0-9_\-]*)$/';
            if (preg_match($p, $key, $m) === 1 && array_key_exists($m[2], $options)) {
                $options[ $m[2] ][ $m[3] ] = c::get($key);
            }
        }

        // Set up the template loader
        $loader = new Twig_Loader_Filesystem($this->templateDir);
        $canSkip = ['snippets', 'plugins', 'assets'];
        foreach ($options['namespace'] as $key=>$path) {
            if (!is_string($path)) continue;
            if (in_array($key, $canSkip) && !file_exists($path)) continue;
            $loader->addPath($path, $key);
        }

        // Start up Twig
        $this->twig = new Twig_Environment($loader, $options['core']);

        // Enable Twig’s dump function
        $this->twig->addExtension(new Twig_Extension_Debug());

        // Plug in functions and filters
        foreach ($options['function'] as $name => $func) {
            $this->addCallable('function', $name, $func);
        }
        foreach ($options['filter'] as $name => $func) {
            $this->addCallable('filter', $name, $func);
        }

        // Make sure the instance is stored / overwritten
        static::$instance = $this;
    }

    /**
     * Return a new instance or the cached instance if it exists
     * @return TwigEnv
     */
    public static function instance()
    {
        if (is_null(static::$instance)) {
            static::$instance = new static;
        }
        return static::$instance;
    }

    /**
     * Render a Twig template from a file path,
     * similarly to how Tpl::load renders a PHP template
     * @param string $filePath
     * @param array $tplData
     * @param bool $return
     * @param bool $isPage
     * @return string
     * @throws Twig_Error
     */
    public function renderPath($filePath='', $tplData=[], $return=true, $isPage=false)
    {
        // Remove the start of the templates path, since Twig asks
        // for a path starting from one of the registered directories.
        $path = $this->normalizePath($filePath);
        $prefix = $this->templateDir . '/';
        if (strpos($path, $prefix) === 0) {
            $path = substr($path, strlen($prefix));
        }

        try {
            $content = $this->twig->render($path, $tplData);
        }
        catch (Twig_Error $err) {
            $content = $this->error($err, $isPage);
        }

        // Mimicking the API of Tpl::load and how it's called by
        // Kirby\Component\Template::render.
        if ($return) return $content;
        echo $content;
    }

    /**
     * Render a Twig template from a string
     * @param  string $tplString
     * @param  array $tplData
     * @return string
     * @throws Twig_Error
     * @throws \Exception
     * @throws \Throwable
     */
    public function renderString($tplString='', $tplData=[])
    {
        try {
            return $this->twig->createTemplate($tplString)->render($tplData);
        }
        catch (Twig_Error $err) {
            return $this->error($err, false, $tplString);
        }
    }

    /**
     * Handle Twig errors, with different scenarios depending on if we're
     * rendering a full page or a fragment (e.g. when using the `twig` helper),
     * and if we're in debug mode or not.
     *
     *        | Page mode            | Fragment mode
     * -------|----------------------| --------------
     * Debug: | Custom error page    | Error message
     * -------|----------------------| --------------
     * Prod:  | Standard error page, | Empty string
     *        | or let error through |
     *
     * @param  Twig_Error $err
     * @param  boolean    $isPage
     * @param  string     $templateString
     * @return string|Response
     * @throws Twig_Error
     */
    private function error(Twig_Error $err, $isPage=false, $templateString=null)
    {
        if (!$this->debug) {
            if (!$isPage) return '';
            // Debug mode off: show the site's error page
            try {
                $kirby = Kirby::instance();
                $page = $kirby->site()->page($kirby->get('option', 'error'));
                if ($page) return $kirby->render($page);
            }
            // avoid loops
            catch (Twig_Error $err2) {
            }
            // Error page didn't exist or was buggy: rethrow the initial error
            // Can result in the 'fatal.php' white error page (in Kirby 2.4+
            // with Whoops active), or an empty response (white page).
            // That’s consistent with errors for e.g. missing base templates.
            throw $err;
        }

        // Gather information
        $name = $err->getTemplateName();
        $line = $err->getTemplateLine();
        $msg  = $err->getRawMessage();
        $path = null;
        $code = $templateString ? $templateString : '';
        if (!$templateString) {
            try {
                $source = $this->twig->getLoader()->getSourceContext($name);
                $path = $source->getPath();
                $code = $source->getCode();
            }
            catch (Twig_Error $err2) {}
        }

        // When returning a HTML fragment
        if (!$isPage && $this->debug) {
            $info = get_class($err) . ', line ' . $line . ' of ' .
                ($templateString ? 'template string:' : $name);
            $src  = $this->getSourceExcerpt($code, $line, 1, false);
            return 'Error: ' . $info . "\n" .
                '
'.$src.'
' . "\n" .
                '➡ ' . $msg . "
\n";
        }

        // When rendering a full page with Twig: make a custom error page
        // Note for Kirby 2.4+: we don't use the Whoops error page because
        // it's not possible to surface Twig source code in it's stack trace
        // and code viewer. Whoops will only show the PHP method calls going
        // in in the Twig library. That's a know — but unresolved — issue.
        // https://github.com/filp/whoops/issues/167
        // https://github.com/twigphp/Twig/issues/1347
        // So we roll our own.
        $html = Tpl::load(__DIR__ . '/errorpage.php', [
            'title' => get_class($err),
            'subtitle' => 'Line ' . $line . ' of ' . ($path ? $path : $name),
            'message' => $msg,
            'code' => $this->getSourceExcerpt($code, $line, 6, true)
        ]);
        return new Response($html, 'html', 500);
    }

    /**
     * Extract a few lines of source code from a source string
     * @param string $source
     * @param int    $line
     * @param int    $plus
     * @param bool   $format
     * @return string
     */
    private function getSourceExcerpt($source='', $line=1, $plus=1, $format=false)
    {
        $excerpt = [];
        $twig  = Escape::html($source);
        $lines = preg_split("/(\r\n|\n|\r)/", $twig);
        $start = max(1, $line - $plus);
        $limit = min(count($lines), $line + $plus);
        for ($i = $start - 1; $i < $limit; $i++) {
            if ($format) {
                $attr = 'data-line="'.($i+1).'"';
                if ($i === $line - 1) $excerpt[] = "$lines[$i]";
                else $excerpt[] = "$lines[$i]";
            }
            else {
                $excerpt[] = $lines[$i];
            }
        }
        return implode("\n", $excerpt);
    }

    /**
     * Normalize file paths, especially for Windows slashes
     */
    private function normalizePath($path)
    {
        $clean = trim(preg_replace('#[\\\/]+#', '/', $path));
        return rtrim($clean, '/');
    }

    /**
     * Clean up function names for use in Twig templates
     * Returns ['twig name' => 'callable name']
     * @param  array $source
     * @return array
     */
    private function cleanNames($source)
    {
        $names = [];
        foreach ($source as $name) {
            if (!is_string($name)) continue;
            $key = str_replace('::', '__', $name);
            $names[$key] = trim($name, '*');
        }
        return $names;
    }

    /**
     * Expose a function to the Twig environment as a function or filter
     * @param string $type
     * @param string $name
     * @param string|Closure $func
     */
    private function addCallable($type='function', $name, $func)
    {
        if (!is_string($name) || !is_callable($func)) {
            return;
        }
        $twname = trim($name, '*');
        $params = [];
        if (strpos($name, '*') === 0) {
            $params['is_safe'] = ['html'];
        }
        if ($type === 'function') {
            $this->twig->addFunction(new Twig_SimpleFunction($twname, $func, $params));
        }
        if ($type === 'filter') {
            $this->twig->addFilter(new Twig_SimpleFilter($twname, $func, $params));
        }
    }
}