1<?php
2
3/*
4 * This file is part of Mustache.php.
5 *
6 * (c) 2010-2017 Justin Hileman
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12/**
13 * A Mustache implementation in PHP.
14 *
15 * {@link http://defunkt.github.com/mustache}
16 *
17 * Mustache is a framework-agnostic logic-less templating language. It enforces separation of view
18 * logic from template files. In fact, it is not even possible to embed logic in the template.
19 *
20 * This is very, very rad.
21 *
22 * @author Justin Hileman {@link http://justinhileman.com}
23 */
24class Mustache_Engine
25{
26    const VERSION        = '2.13.0';
27    const SPEC_VERSION   = '1.1.2';
28
29    const PRAGMA_FILTERS      = 'FILTERS';
30    const PRAGMA_BLOCKS       = 'BLOCKS';
31    const PRAGMA_ANCHORED_DOT = 'ANCHORED-DOT';
32
33    // Known pragmas
34    private static $knownPragmas = array(
35        self::PRAGMA_FILTERS      => true,
36        self::PRAGMA_BLOCKS       => true,
37        self::PRAGMA_ANCHORED_DOT => true,
38    );
39
40    // Template cache
41    private $templates = array();
42
43    // Environment
44    private $templateClassPrefix = '__Mustache_';
45    private $cache;
46    private $lambdaCache;
47    private $cacheLambdaTemplates = false;
48    private $loader;
49    private $partialsLoader;
50    private $helpers;
51    private $escape;
52    private $entityFlags = ENT_COMPAT;
53    private $charset = 'UTF-8';
54    private $logger;
55    private $strictCallables = false;
56    private $pragmas = array();
57    private $delimiters;
58
59    // Services
60    private $tokenizer;
61    private $parser;
62    private $compiler;
63
64    /**
65     * Mustache class constructor.
66     *
67     * Passing an $options array allows overriding certain Mustache options during instantiation:
68     *
69     *     $options = array(
70     *         // The class prefix for compiled templates. Defaults to '__Mustache_'.
71     *         'template_class_prefix' => '__MyTemplates_',
72     *
73     *         // A Mustache cache instance or a cache directory string for compiled templates.
74     *         // Mustache will not cache templates unless this is set.
75     *         'cache' => dirname(__FILE__).'/tmp/cache/mustache',
76     *
77     *         // Override default permissions for cache files. Defaults to using the system-defined umask. It is
78     *         // *strongly* recommended that you configure your umask properly rather than overriding permissions here.
79     *         'cache_file_mode' => 0666,
80     *
81     *         // Optionally, enable caching for lambda section templates. This is generally not recommended, as lambda
82     *         // sections are often too dynamic to benefit from caching.
83     *         'cache_lambda_templates' => true,
84     *
85     *         // Customize the tag delimiters used by this engine instance. Note that overriding here changes the
86     *         // delimiters used to parse all templates and partials loaded by this instance. To override just for a
87     *         // single template, use an inline "change delimiters" tag at the start of the template file:
88     *         //
89     *         //     {{=<% %>=}}
90     *         //
91     *         'delimiters' => '<% %>',
92     *
93     *         // A Mustache template loader instance. Uses a StringLoader if not specified.
94     *         'loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'),
95     *
96     *         // A Mustache loader instance for partials.
97     *         'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'),
98     *
99     *         // An array of Mustache partials. Useful for quick-and-dirty string template loading, but not as
100     *         // efficient or lazy as a Filesystem (or database) loader.
101     *         'partials' => array('foo' => file_get_contents(dirname(__FILE__).'/views/partials/foo.mustache')),
102     *
103     *         // An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order
104     *         // sections), or any other valid Mustache context value. They will be prepended to the context stack,
105     *         // so they will be available in any template loaded by this Mustache instance.
106     *         'helpers' => array('i18n' => function ($text) {
107     *             // do something translatey here...
108     *         }),
109     *
110     *         // An 'escape' callback, responsible for escaping double-mustache variables.
111     *         'escape' => function ($value) {
112     *             return htmlspecialchars($buffer, ENT_COMPAT, 'UTF-8');
113     *         },
114     *
115     *         // Type argument for `htmlspecialchars`.  Defaults to ENT_COMPAT.  You may prefer ENT_QUOTES.
116     *         'entity_flags' => ENT_QUOTES,
117     *
118     *         // Character set for `htmlspecialchars`. Defaults to 'UTF-8'. Use 'UTF-8'.
119     *         'charset' => 'ISO-8859-1',
120     *
121     *         // A Mustache Logger instance. No logging will occur unless this is set. Using a PSR-3 compatible
122     *         // logging library -- such as Monolog -- is highly recommended. A simple stream logger implementation is
123     *         // available as well:
124     *         'logger' => new Mustache_Logger_StreamLogger('php://stderr'),
125     *
126     *         // Only treat Closure instances and invokable classes as callable. If true, values like
127     *         // `array('ClassName', 'methodName')` and `array($classInstance, 'methodName')`, which are traditionally
128     *         // "callable" in PHP, are not called to resolve variables for interpolation or section contexts. This
129     *         // helps protect against arbitrary code execution when user input is passed directly into the template.
130     *         // This currently defaults to false, but will default to true in v3.0.
131     *         'strict_callables' => true,
132     *
133     *         // Enable pragmas across all templates, regardless of the presence of pragma tags in the individual
134     *         // templates.
135     *         'pragmas' => [Mustache_Engine::PRAGMA_FILTERS],
136     *     );
137     *
138     * @throws Mustache_Exception_InvalidArgumentException If `escape` option is not callable
139     *
140     * @param array $options (default: array())
141     */
142    public function __construct(array $options = array())
143    {
144        if (isset($options['template_class_prefix'])) {
145            if ((string) $options['template_class_prefix'] === '') {
146                throw new Mustache_Exception_InvalidArgumentException('Mustache Constructor "template_class_prefix" must not be empty');
147            }
148
149            $this->templateClassPrefix = $options['template_class_prefix'];
150        }
151
152        if (isset($options['cache'])) {
153            $cache = $options['cache'];
154
155            if (is_string($cache)) {
156                $mode  = isset($options['cache_file_mode']) ? $options['cache_file_mode'] : null;
157                $cache = new Mustache_Cache_FilesystemCache($cache, $mode);
158            }
159
160            $this->setCache($cache);
161        }
162
163        if (isset($options['cache_lambda_templates'])) {
164            $this->cacheLambdaTemplates = (bool) $options['cache_lambda_templates'];
165        }
166
167        if (isset($options['loader'])) {
168            $this->setLoader($options['loader']);
169        }
170
171        if (isset($options['partials_loader'])) {
172            $this->setPartialsLoader($options['partials_loader']);
173        }
174
175        if (isset($options['partials'])) {
176            $this->setPartials($options['partials']);
177        }
178
179        if (isset($options['helpers'])) {
180            $this->setHelpers($options['helpers']);
181        }
182
183        if (isset($options['escape'])) {
184            if (!is_callable($options['escape'])) {
185                throw new Mustache_Exception_InvalidArgumentException('Mustache Constructor "escape" option must be callable');
186            }
187
188            $this->escape = $options['escape'];
189        }
190
191        if (isset($options['entity_flags'])) {
192            $this->entityFlags = $options['entity_flags'];
193        }
194
195        if (isset($options['charset'])) {
196            $this->charset = $options['charset'];
197        }
198
199        if (isset($options['logger'])) {
200            $this->setLogger($options['logger']);
201        }
202
203        if (isset($options['strict_callables'])) {
204            $this->strictCallables = $options['strict_callables'];
205        }
206
207        if (isset($options['delimiters'])) {
208            $this->delimiters = $options['delimiters'];
209        }
210
211        if (isset($options['pragmas'])) {
212            foreach ($options['pragmas'] as $pragma) {
213                if (!isset(self::$knownPragmas[$pragma])) {
214                    throw new Mustache_Exception_InvalidArgumentException(sprintf('Unknown pragma: "%s".', $pragma));
215                }
216                $this->pragmas[$pragma] = true;
217            }
218        }
219    }
220
221    /**
222     * Shortcut 'render' invocation.
223     *
224     * Equivalent to calling `$mustache->loadTemplate($template)->render($context);`
225     *
226     * @see Mustache_Engine::loadTemplate
227     * @see Mustache_Template::render
228     *
229     * @param string $template
230     * @param mixed  $context  (default: array())
231     *
232     * @return string Rendered template
233     */
234    public function render($template, $context = array())
235    {
236        return $this->loadTemplate($template)->render($context);
237    }
238
239    /**
240     * Get the current Mustache escape callback.
241     *
242     * @return callable|null
243     */
244    public function getEscape()
245    {
246        return $this->escape;
247    }
248
249    /**
250     * Get the current Mustache entitity type to escape.
251     *
252     * @return int
253     */
254    public function getEntityFlags()
255    {
256        return $this->entityFlags;
257    }
258
259    /**
260     * Get the current Mustache character set.
261     *
262     * @return string
263     */
264    public function getCharset()
265    {
266        return $this->charset;
267    }
268
269    /**
270     * Get the current globally enabled pragmas.
271     *
272     * @return array
273     */
274    public function getPragmas()
275    {
276        return array_keys($this->pragmas);
277    }
278
279    /**
280     * Set the Mustache template Loader instance.
281     *
282     * @param Mustache_Loader $loader
283     */
284    public function setLoader(Mustache_Loader $loader)
285    {
286        $this->loader = $loader;
287    }
288
289    /**
290     * Get the current Mustache template Loader instance.
291     *
292     * If no Loader instance has been explicitly specified, this method will instantiate and return
293     * a StringLoader instance.
294     *
295     * @return Mustache_Loader
296     */
297    public function getLoader()
298    {
299        if (!isset($this->loader)) {
300            $this->loader = new Mustache_Loader_StringLoader();
301        }
302
303        return $this->loader;
304    }
305
306    /**
307     * Set the Mustache partials Loader instance.
308     *
309     * @param Mustache_Loader $partialsLoader
310     */
311    public function setPartialsLoader(Mustache_Loader $partialsLoader)
312    {
313        $this->partialsLoader = $partialsLoader;
314    }
315
316    /**
317     * Get the current Mustache partials Loader instance.
318     *
319     * If no Loader instance has been explicitly specified, this method will instantiate and return
320     * an ArrayLoader instance.
321     *
322     * @return Mustache_Loader
323     */
324    public function getPartialsLoader()
325    {
326        if (!isset($this->partialsLoader)) {
327            $this->partialsLoader = new Mustache_Loader_ArrayLoader();
328        }
329
330        return $this->partialsLoader;
331    }
332
333    /**
334     * Set partials for the current partials Loader instance.
335     *
336     * @throws Mustache_Exception_RuntimeException If the current Loader instance is immutable
337     *
338     * @param array $partials (default: array())
339     */
340    public function setPartials(array $partials = array())
341    {
342        if (!isset($this->partialsLoader)) {
343            $this->partialsLoader = new Mustache_Loader_ArrayLoader();
344        }
345
346        if (!$this->partialsLoader instanceof Mustache_Loader_MutableLoader) {
347            throw new Mustache_Exception_RuntimeException('Unable to set partials on an immutable Mustache Loader instance');
348        }
349
350        $this->partialsLoader->setTemplates($partials);
351    }
352
353    /**
354     * Set an array of Mustache helpers.
355     *
356     * An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order sections), or
357     * any other valid Mustache context value. They will be prepended to the context stack, so they will be available in
358     * any template loaded by this Mustache instance.
359     *
360     * @throws Mustache_Exception_InvalidArgumentException if $helpers is not an array or Traversable
361     *
362     * @param array|Traversable $helpers
363     */
364    public function setHelpers($helpers)
365    {
366        if (!is_array($helpers) && !$helpers instanceof Traversable) {
367            throw new Mustache_Exception_InvalidArgumentException('setHelpers expects an array of helpers');
368        }
369
370        $this->getHelpers()->clear();
371
372        foreach ($helpers as $name => $helper) {
373            $this->addHelper($name, $helper);
374        }
375    }
376
377    /**
378     * Get the current set of Mustache helpers.
379     *
380     * @see Mustache_Engine::setHelpers
381     *
382     * @return Mustache_HelperCollection
383     */
384    public function getHelpers()
385    {
386        if (!isset($this->helpers)) {
387            $this->helpers = new Mustache_HelperCollection();
388        }
389
390        return $this->helpers;
391    }
392
393    /**
394     * Add a new Mustache helper.
395     *
396     * @see Mustache_Engine::setHelpers
397     *
398     * @param string $name
399     * @param mixed  $helper
400     */
401    public function addHelper($name, $helper)
402    {
403        $this->getHelpers()->add($name, $helper);
404    }
405
406    /**
407     * Get a Mustache helper by name.
408     *
409     * @see Mustache_Engine::setHelpers
410     *
411     * @param string $name
412     *
413     * @return mixed Helper
414     */
415    public function getHelper($name)
416    {
417        return $this->getHelpers()->get($name);
418    }
419
420    /**
421     * Check whether this Mustache instance has a helper.
422     *
423     * @see Mustache_Engine::setHelpers
424     *
425     * @param string $name
426     *
427     * @return bool True if the helper is present
428     */
429    public function hasHelper($name)
430    {
431        return $this->getHelpers()->has($name);
432    }
433
434    /**
435     * Remove a helper by name.
436     *
437     * @see Mustache_Engine::setHelpers
438     *
439     * @param string $name
440     */
441    public function removeHelper($name)
442    {
443        $this->getHelpers()->remove($name);
444    }
445
446    /**
447     * Set the Mustache Logger instance.
448     *
449     * @throws Mustache_Exception_InvalidArgumentException If logger is not an instance of Mustache_Logger or Psr\Log\LoggerInterface
450     *
451     * @param Mustache_Logger|Psr\Log\LoggerInterface $logger
452     */
453    public function setLogger($logger = null)
454    {
455        if ($logger !== null && !($logger instanceof Mustache_Logger || is_a($logger, 'Psr\\Log\\LoggerInterface'))) {
456            throw new Mustache_Exception_InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.');
457        }
458
459        if ($this->getCache()->getLogger() === null) {
460            $this->getCache()->setLogger($logger);
461        }
462
463        $this->logger = $logger;
464    }
465
466    /**
467     * Get the current Mustache Logger instance.
468     *
469     * @return Mustache_Logger|Psr\Log\LoggerInterface
470     */
471    public function getLogger()
472    {
473        return $this->logger;
474    }
475
476    /**
477     * Set the Mustache Tokenizer instance.
478     *
479     * @param Mustache_Tokenizer $tokenizer
480     */
481    public function setTokenizer(Mustache_Tokenizer $tokenizer)
482    {
483        $this->tokenizer = $tokenizer;
484    }
485
486    /**
487     * Get the current Mustache Tokenizer instance.
488     *
489     * If no Tokenizer instance has been explicitly specified, this method will instantiate and return a new one.
490     *
491     * @return Mustache_Tokenizer
492     */
493    public function getTokenizer()
494    {
495        if (!isset($this->tokenizer)) {
496            $this->tokenizer = new Mustache_Tokenizer();
497        }
498
499        return $this->tokenizer;
500    }
501
502    /**
503     * Set the Mustache Parser instance.
504     *
505     * @param Mustache_Parser $parser
506     */
507    public function setParser(Mustache_Parser $parser)
508    {
509        $this->parser = $parser;
510    }
511
512    /**
513     * Get the current Mustache Parser instance.
514     *
515     * If no Parser instance has been explicitly specified, this method will instantiate and return a new one.
516     *
517     * @return Mustache_Parser
518     */
519    public function getParser()
520    {
521        if (!isset($this->parser)) {
522            $this->parser = new Mustache_Parser();
523        }
524
525        return $this->parser;
526    }
527
528    /**
529     * Set the Mustache Compiler instance.
530     *
531     * @param Mustache_Compiler $compiler
532     */
533    public function setCompiler(Mustache_Compiler $compiler)
534    {
535        $this->compiler = $compiler;
536    }
537
538    /**
539     * Get the current Mustache Compiler instance.
540     *
541     * If no Compiler instance has been explicitly specified, this method will instantiate and return a new one.
542     *
543     * @return Mustache_Compiler
544     */
545    public function getCompiler()
546    {
547        if (!isset($this->compiler)) {
548            $this->compiler = new Mustache_Compiler();
549        }
550
551        return $this->compiler;
552    }
553
554    /**
555     * Set the Mustache Cache instance.
556     *
557     * @param Mustache_Cache $cache
558     */
559    public function setCache(Mustache_Cache $cache)
560    {
561        if (isset($this->logger) && $cache->getLogger() === null) {
562            $cache->setLogger($this->getLogger());
563        }
564
565        $this->cache = $cache;
566    }
567
568    /**
569     * Get the current Mustache Cache instance.
570     *
571     * If no Cache instance has been explicitly specified, this method will instantiate and return a new one.
572     *
573     * @return Mustache_Cache
574     */
575    public function getCache()
576    {
577        if (!isset($this->cache)) {
578            $this->setCache(new Mustache_Cache_NoopCache());
579        }
580
581        return $this->cache;
582    }
583
584    /**
585     * Get the current Lambda Cache instance.
586     *
587     * If 'cache_lambda_templates' is enabled, this is the default cache instance. Otherwise, it is a NoopCache.
588     *
589     * @see Mustache_Engine::getCache
590     *
591     * @return Mustache_Cache
592     */
593    protected function getLambdaCache()
594    {
595        if ($this->cacheLambdaTemplates) {
596            return $this->getCache();
597        }
598
599        if (!isset($this->lambdaCache)) {
600            $this->lambdaCache = new Mustache_Cache_NoopCache();
601        }
602
603        return $this->lambdaCache;
604    }
605
606    /**
607     * Helper method to generate a Mustache template class.
608     *
609     * This method must be updated any time options are added which make it so
610     * the same template could be parsed and compiled multiple different ways.
611     *
612     * @param string|Mustache_Source $source
613     *
614     * @return string Mustache Template class name
615     */
616    public function getTemplateClassName($source)
617    {
618        // For the most part, adding a new option here should do the trick.
619        //
620        // Pick a value here which is unique for each possible way the template
621        // could be compiled... but not necessarily unique per option value. See
622        // escape below, which only needs to differentiate between 'custom' and
623        // 'default' escapes.
624        //
625        // Keep this list in alphabetical order :)
626        $chunks = array(
627            'charset'         => $this->charset,
628            'delimiters'      => $this->delimiters ? $this->delimiters : '{{ }}',
629            'entityFlags'     => $this->entityFlags,
630            'escape'          => isset($this->escape) ? 'custom' : 'default',
631            'key'             => ($source instanceof Mustache_Source) ? $source->getKey() : 'source',
632            'pragmas'         => $this->getPragmas(),
633            'strictCallables' => $this->strictCallables,
634            'version'         => self::VERSION,
635        );
636
637        $key = json_encode($chunks);
638
639        // Template Source instances have already provided their own source key. For strings, just include the whole
640        // source string in the md5 hash.
641        if (!$source instanceof Mustache_Source) {
642            $key .= "\n" . $source;
643        }
644
645        return $this->templateClassPrefix . md5($key);
646    }
647
648    /**
649     * Load a Mustache Template by name.
650     *
651     * @param string $name
652     *
653     * @return Mustache_Template
654     */
655    public function loadTemplate($name)
656    {
657        return $this->loadSource($this->getLoader()->load($name));
658    }
659
660    /**
661     * Load a Mustache partial Template by name.
662     *
663     * This is a helper method used internally by Template instances for loading partial templates. You can most likely
664     * ignore it completely.
665     *
666     * @param string $name
667     *
668     * @return Mustache_Template
669     */
670    public function loadPartial($name)
671    {
672        try {
673            if (isset($this->partialsLoader)) {
674                $loader = $this->partialsLoader;
675            } elseif (isset($this->loader) && !$this->loader instanceof Mustache_Loader_StringLoader) {
676                $loader = $this->loader;
677            } else {
678                throw new Mustache_Exception_UnknownTemplateException($name);
679            }
680
681            return $this->loadSource($loader->load($name));
682        } catch (Mustache_Exception_UnknownTemplateException $e) {
683            // If the named partial cannot be found, log then return null.
684            $this->log(
685                Mustache_Logger::WARNING,
686                'Partial not found: "{name}"',
687                array('name' => $e->getTemplateName())
688            );
689        }
690    }
691
692    /**
693     * Load a Mustache lambda Template by source.
694     *
695     * This is a helper method used by Template instances to generate subtemplates for Lambda sections. You can most
696     * likely ignore it completely.
697     *
698     * @param string $source
699     * @param string $delims (default: null)
700     *
701     * @return Mustache_Template
702     */
703    public function loadLambda($source, $delims = null)
704    {
705        if ($delims !== null) {
706            $source = $delims . "\n" . $source;
707        }
708
709        return $this->loadSource($source, $this->getLambdaCache());
710    }
711
712    /**
713     * Instantiate and return a Mustache Template instance by source.
714     *
715     * Optionally provide a Mustache_Cache instance. This is used internally by Mustache_Engine::loadLambda to respect
716     * the 'cache_lambda_templates' configuration option.
717     *
718     * @see Mustache_Engine::loadTemplate
719     * @see Mustache_Engine::loadPartial
720     * @see Mustache_Engine::loadLambda
721     *
722     * @param string|Mustache_Source $source
723     * @param Mustache_Cache         $cache  (default: null)
724     *
725     * @return Mustache_Template
726     */
727    private function loadSource($source, Mustache_Cache $cache = null)
728    {
729        $className = $this->getTemplateClassName($source);
730
731        if (!isset($this->templates[$className])) {
732            if ($cache === null) {
733                $cache = $this->getCache();
734            }
735
736            if (!class_exists($className, false)) {
737                if (!$cache->load($className)) {
738                    $compiled = $this->compile($source);
739                    $cache->cache($className, $compiled);
740                }
741            }
742
743            $this->log(
744                Mustache_Logger::DEBUG,
745                'Instantiating template: "{className}"',
746                array('className' => $className)
747            );
748
749            $this->templates[$className] = new $className($this);
750        }
751
752        return $this->templates[$className];
753    }
754
755    /**
756     * Helper method to tokenize a Mustache template.
757     *
758     * @see Mustache_Tokenizer::scan
759     *
760     * @param string $source
761     *
762     * @return array Tokens
763     */
764    private function tokenize($source)
765    {
766        return $this->getTokenizer()->scan($source, $this->delimiters);
767    }
768
769    /**
770     * Helper method to parse a Mustache template.
771     *
772     * @see Mustache_Parser::parse
773     *
774     * @param string $source
775     *
776     * @return array Token tree
777     */
778    private function parse($source)
779    {
780        $parser = $this->getParser();
781        $parser->setPragmas($this->getPragmas());
782
783        return $parser->parse($this->tokenize($source));
784    }
785
786    /**
787     * Helper method to compile a Mustache template.
788     *
789     * @see Mustache_Compiler::compile
790     *
791     * @param string|Mustache_Source $source
792     *
793     * @return string generated Mustache template class code
794     */
795    private function compile($source)
796    {
797        $name = $this->getTemplateClassName($source);
798
799        $this->log(
800            Mustache_Logger::INFO,
801            'Compiling template to "{className}" class',
802            array('className' => $name)
803        );
804
805        if ($source instanceof Mustache_Source) {
806            $source = $source->getSource();
807        }
808        $tree = $this->parse($source);
809
810        $compiler = $this->getCompiler();
811        $compiler->setPragmas($this->getPragmas());
812
813        return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags);
814    }
815
816    /**
817     * Add a log record if logging is enabled.
818     *
819     * @param int    $level   The logging level
820     * @param string $message The log message
821     * @param array  $context The log context
822     */
823    private function log($level, $message, array $context = array())
824    {
825        if (isset($this->logger)) {
826            $this->logger->log($level, $message, $context);
827        }
828    }
829}
830