summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Mustache/Autoloader.php157
-rw-r--r--Mustache/Cache.php43
-rw-r--r--Mustache/Cache/AbstractCache.php60
-rw-r--r--Mustache/Cache/FilesystemCache.php161
-rw-r--r--Mustache/Cache/NoopCache.php47
-rw-r--r--Mustache/Compiler.php1104
-rw-r--r--Mustache/Context.php391
-rw-r--r--Mustache/Engine.php1420
-rw-r--r--Mustache/Exception.php18
-rw-r--r--Mustache/Exception/InvalidArgumentException.php18
-rw-r--r--Mustache/Exception/LogicException.php18
-rw-r--r--Mustache/Exception/RuntimeException.php18
-rw-r--r--Mustache/Exception/SyntaxException.php41
-rw-r--r--Mustache/Exception/UnknownFilterException.php38
-rw-r--r--Mustache/Exception/UnknownHelperException.php38
-rw-r--r--Mustache/Exception/UnknownTemplateException.php38
-rw-r--r--Mustache/HelperCollection.php340
-rw-r--r--Mustache/LICENSE35
-rw-r--r--Mustache/LambdaHelper.php76
-rw-r--r--Mustache/Loader.php53
-rw-r--r--Mustache/Loader/ArrayLoader.php158
-rw-r--r--Mustache/Loader/CascadingLoader.php69
-rw-r--r--Mustache/Loader/FilesystemLoader.php253
-rw-r--r--Mustache/Loader/InlineLoader.php123
-rw-r--r--Mustache/Loader/MutableLoader.php63
-rw-r--r--Mustache/Loader/ProductionFilesystemLoader.php86
-rw-r--r--Mustache/Loader/StringLoader.php81
-rw-r--r--Mustache/Logger.php126
-rw-r--r--Mustache/Logger/AbstractLogger.php121
-rw-r--r--Mustache/Logger/StreamLogger.php194
-rw-r--r--Mustache/Parser.php473
-rw-r--r--Mustache/Source.php40
-rw-r--r--Mustache/Source/FilesystemSource.php77
-rw-r--r--Mustache/Template.php329
-rw-r--r--Mustache/Tokenizer.php694
-rw-r--r--config.php.example17
-rw-r--r--inc/crypto.inc.php6
-rw-r--r--inc/database.inc.php422
-rw-r--r--inc/image.inc.php7
-rw-r--r--inc/message.inc.php24
-rw-r--r--inc/render.inc.php21
-rw-r--r--inc/session.inc.php43
-rw-r--r--inc/shibauth.inc.php202
-rw-r--r--inc/user.inc.php97
-rw-r--r--inc/util.inc.php40
-rw-r--r--index.php1
-rw-r--r--modules/adduser.inc.php11
-rw-r--r--modules/agb.inc.php1
-rw-r--r--modules/images.inc.php51
-rw-r--r--modules/main.inc.php25
-rw-r--r--modules/register.inc.php12
-rw-r--r--modules/suitelogin.inc.php31
-rw-r--r--pam.php33
-rw-r--r--shib/api.php166
-rw-r--r--templates/agb/_page.html227
-rw-r--r--templates/image-list.html51
-rw-r--r--templates/main-menu.html1
-rw-r--r--templates/main/deploy.html22
-rw-r--r--templates/main/logged-in.html2
-rw-r--r--templates/sharemode/deploy.html2
60 files changed, 5928 insertions, 2588 deletions
diff --git a/Mustache/Autoloader.php b/Mustache/Autoloader.php
index 707a0ff..e8ea3f4 100644
--- a/Mustache/Autoloader.php
+++ b/Mustache/Autoloader.php
@@ -1,69 +1,88 @@
-<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Mustache class autoloader.
- */
-class Mustache_Autoloader
-{
-
- private $baseDir;
-
- /**
- * Autoloader constructor.
- *
- * @param string $baseDir Mustache library base directory (default: dirname(__FILE__).'/..')
- */
- public function __construct($baseDir = null)
- {
- if ($baseDir === null) {
- $this->baseDir = dirname(__FILE__).'/..';
- } else {
- $this->baseDir = rtrim($baseDir, '/');
- }
- }
-
- /**
- * Register a new instance as an SPL autoloader.
- *
- * @param string $baseDir Mustache library base directory (default: dirname(__FILE__).'/..')
- *
- * @return Mustache_Autoloader Registered Autoloader instance
- */
- public static function register($baseDir = null)
- {
- $loader = new self($baseDir);
- spl_autoload_register(array($loader, 'autoload'));
-
- return $loader;
- }
-
- /**
- * Autoload Mustache classes.
- *
- * @param string $class
- */
- public function autoload($class)
- {
- if ($class[0] === '\\') {
- $class = substr($class, 1);
- }
-
- if (strpos($class, 'Mustache') !== 0) {
- return;
- }
-
- $file = sprintf('%s/%s.php', $this->baseDir, str_replace('_', '/', $class));
- if (is_file($file)) {
- require $file;
- }
- }
-}
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache class autoloader.
+ */
+class Mustache_Autoloader
+{
+ private $baseDir;
+
+ /**
+ * An array where the key is the baseDir and the key is an instance of this
+ * class.
+ *
+ * @var array
+ */
+ private static $instances;
+
+ /**
+ * Autoloader constructor.
+ *
+ * @param string $baseDir Mustache library base directory (default: dirname(__FILE__).'/..')
+ */
+ public function __construct($baseDir = null)
+ {
+ if ($baseDir === null) {
+ $baseDir = dirname(__FILE__) . '/..';
+ }
+
+ // realpath doesn't always work, for example, with stream URIs
+ $realDir = realpath($baseDir);
+ if (is_dir($realDir)) {
+ $this->baseDir = $realDir;
+ } else {
+ $this->baseDir = $baseDir;
+ }
+ }
+
+ /**
+ * Register a new instance as an SPL autoloader.
+ *
+ * @param string $baseDir Mustache library base directory (default: dirname(__FILE__).'/..')
+ *
+ * @return Mustache_Autoloader Registered Autoloader instance
+ */
+ public static function register($baseDir = null)
+ {
+ $key = $baseDir ? $baseDir : 0;
+
+ if (!isset(self::$instances[$key])) {
+ self::$instances[$key] = new self($baseDir);
+ }
+
+ $loader = self::$instances[$key];
+ spl_autoload_register(array($loader, 'autoload'));
+
+ return $loader;
+ }
+
+ /**
+ * Autoload Mustache classes.
+ *
+ * @param string $class
+ */
+ public function autoload($class)
+ {
+ if ($class[0] === '\\') {
+ $class = substr($class, 1);
+ }
+
+ if (strpos($class, 'Mustache') !== 0) {
+ return;
+ }
+
+ $file = sprintf('%s/%s.php', $this->baseDir, str_replace('_', '/', $class));
+ if (is_file($file)) {
+ require $file;
+ }
+ }
+}
diff --git a/Mustache/Cache.php b/Mustache/Cache.php
new file mode 100644
index 0000000..3292efa
--- /dev/null
+++ b/Mustache/Cache.php
@@ -0,0 +1,43 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Cache interface.
+ *
+ * Interface for caching and loading Mustache_Template classes
+ * generated by the Mustache_Compiler.
+ */
+interface Mustache_Cache
+{
+ /**
+ * Load a compiled Mustache_Template class from cache.
+ *
+ * @param string $key
+ *
+ * @return bool indicates successfully class load
+ */
+ public function load($key);
+
+ /**
+ * Cache and load a compiled Mustache_Template class.
+ *
+ * @param string $key
+ * @param string $value
+ */
+ public function cache($key, $value);
+
+ /**
+ * Set a logger instance.
+ *
+ * @param Mustache_Logger|Psr\Log\LoggerInterface $logger
+ */
+ public function setLogger($logger = null);
+}
diff --git a/Mustache/Cache/AbstractCache.php b/Mustache/Cache/AbstractCache.php
new file mode 100644
index 0000000..281038f
--- /dev/null
+++ b/Mustache/Cache/AbstractCache.php
@@ -0,0 +1,60 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Abstract Mustache Cache class.
+ *
+ * Provides logging support to child implementations.
+ *
+ * @abstract
+ */
+abstract class Mustache_Cache_AbstractCache implements Mustache_Cache
+{
+ private $logger = null;
+
+ /**
+ * Get the current logger instance.
+ *
+ * @return Mustache_Logger|Psr\Log\LoggerInterface
+ */
+ public function getLogger()
+ {
+ return $this->logger;
+ }
+
+ /**
+ * Set a logger instance.
+ *
+ * @param Mustache_Logger|Psr\Log\LoggerInterface $logger
+ */
+ public function setLogger($logger = null)
+ {
+ if ($logger !== null && !($logger instanceof Mustache_Logger || is_a($logger, 'Psr\\Log\\LoggerInterface'))) {
+ throw new Mustache_Exception_InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.');
+ }
+
+ $this->logger = $logger;
+ }
+
+ /**
+ * Add a log record if logging is enabled.
+ *
+ * @param string $level The logging level
+ * @param string $message The log message
+ * @param array $context The log context
+ */
+ protected function log($level, $message, array $context = array())
+ {
+ if (isset($this->logger)) {
+ $this->logger->log($level, $message, $context);
+ }
+ }
+}
diff --git a/Mustache/Cache/FilesystemCache.php b/Mustache/Cache/FilesystemCache.php
new file mode 100644
index 0000000..3e742b7
--- /dev/null
+++ b/Mustache/Cache/FilesystemCache.php
@@ -0,0 +1,161 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Cache filesystem implementation.
+ *
+ * A FilesystemCache instance caches Mustache Template classes from the filesystem by name:
+ *
+ * $cache = new Mustache_Cache_FilesystemCache(dirname(__FILE__).'/cache');
+ * $cache->cache($className, $compiledSource);
+ *
+ * The FilesystemCache benefits from any opcode caching that may be setup in your environment. So do that, k?
+ */
+class Mustache_Cache_FilesystemCache extends Mustache_Cache_AbstractCache
+{
+ private $baseDir;
+ private $fileMode;
+
+ /**
+ * Filesystem cache constructor.
+ *
+ * @param string $baseDir Directory for compiled templates
+ * @param int $fileMode Override default permissions for cache files. Defaults to using the system-defined umask
+ */
+ public function __construct($baseDir, $fileMode = null)
+ {
+ $this->baseDir = $baseDir;
+ $this->fileMode = $fileMode;
+ }
+
+ /**
+ * Load the class from cache using `require_once`.
+ *
+ * @param string $key
+ *
+ * @return bool
+ */
+ public function load($key)
+ {
+ $fileName = $this->getCacheFilename($key);
+ if (!is_file($fileName)) {
+ return false;
+ }
+
+ require_once $fileName;
+
+ return true;
+ }
+
+ /**
+ * Cache and load the compiled class.
+ *
+ * @param string $key
+ * @param string $value
+ */
+ public function cache($key, $value)
+ {
+ $fileName = $this->getCacheFilename($key);
+
+ $this->log(
+ Mustache_Logger::DEBUG,
+ 'Writing to template cache: "{fileName}"',
+ array('fileName' => $fileName)
+ );
+
+ $this->writeFile($fileName, $value);
+ $this->load($key);
+ }
+
+ /**
+ * Build the cache filename.
+ * Subclasses should override for custom cache directory structures.
+ *
+ * @param string $name
+ *
+ * @return string
+ */
+ protected function getCacheFilename($name)
+ {
+ return sprintf('%s/%s.php', $this->baseDir, $name);
+ }
+
+ /**
+ * Create cache directory.
+ *
+ * @throws Mustache_Exception_RuntimeException If unable to create directory
+ *
+ * @param string $fileName
+ *
+ * @return string
+ */
+ private function buildDirectoryForFilename($fileName)
+ {
+ $dirName = dirname($fileName);
+ if (!is_dir($dirName)) {
+ $this->log(
+ Mustache_Logger::INFO,
+ 'Creating Mustache template cache directory: "{dirName}"',
+ array('dirName' => $dirName)
+ );
+
+ @mkdir($dirName, 0777, true);
+ // @codeCoverageIgnoreStart
+ if (!is_dir($dirName)) {
+ throw new Mustache_Exception_RuntimeException(sprintf('Failed to create cache directory "%s".', $dirName));
+ }
+ // @codeCoverageIgnoreEnd
+ }
+
+ return $dirName;
+ }
+
+ /**
+ * Write cache file.
+ *
+ * @throws Mustache_Exception_RuntimeException If unable to write file
+ *
+ * @param string $fileName
+ * @param string $value
+ */
+ private function writeFile($fileName, $value)
+ {
+ $dirName = $this->buildDirectoryForFilename($fileName);
+
+ $this->log(
+ Mustache_Logger::DEBUG,
+ 'Caching compiled template to "{fileName}"',
+ array('fileName' => $fileName)
+ );
+
+ $tempFile = tempnam($dirName, basename($fileName));
+ if (false !== @file_put_contents($tempFile, $value)) {
+ if (@rename($tempFile, $fileName)) {
+ $mode = isset($this->fileMode) ? $this->fileMode : (0666 & ~umask());
+ @chmod($fileName, $mode);
+
+ return;
+ }
+
+ // @codeCoverageIgnoreStart
+ $this->log(
+ Mustache_Logger::ERROR,
+ 'Unable to rename Mustache temp cache file: "{tempName}" -> "{fileName}"',
+ array('tempName' => $tempFile, 'fileName' => $fileName)
+ );
+ // @codeCoverageIgnoreEnd
+ }
+
+ // @codeCoverageIgnoreStart
+ throw new Mustache_Exception_RuntimeException(sprintf('Failed to write cache file "%s".', $fileName));
+ // @codeCoverageIgnoreEnd
+ }
+}
diff --git a/Mustache/Cache/NoopCache.php b/Mustache/Cache/NoopCache.php
new file mode 100644
index 0000000..ed9eec9
--- /dev/null
+++ b/Mustache/Cache/NoopCache.php
@@ -0,0 +1,47 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Cache in-memory implementation.
+ *
+ * The in-memory cache is used for uncached lambda section templates. It's also useful during development, but is not
+ * recommended for production use.
+ */
+class Mustache_Cache_NoopCache extends Mustache_Cache_AbstractCache
+{
+ /**
+ * Loads nothing. Move along.
+ *
+ * @param string $key
+ *
+ * @return bool
+ */
+ public function load($key)
+ {
+ return false;
+ }
+
+ /**
+ * Loads the compiled Mustache Template class without caching.
+ *
+ * @param string $key
+ * @param string $value
+ */
+ public function cache($key, $value)
+ {
+ $this->log(
+ Mustache_Logger::WARNING,
+ 'Template cache disabled, evaluating "{className}" class at runtime',
+ array('className' => $key)
+ );
+ eval('?>' . $value);
+ }
+}
diff --git a/Mustache/Compiler.php b/Mustache/Compiler.php
index dd5307d..2b0d1f9 100644
--- a/Mustache/Compiler.php
+++ b/Mustache/Compiler.php
@@ -1,386 +1,718 @@
-<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Mustache Compiler class.
- *
- * This class is responsible for turning a Mustache token parse tree into normal PHP source code.
- */
-class Mustache_Compiler
-{
-
- private $sections;
- private $source;
- private $indentNextLine;
- private $customEscape;
- private $charset;
-
- /**
- * Compile a Mustache token parse tree into PHP source code.
- *
- * @param string $source Mustache Template source code
- * @param string $tree Parse tree of Mustache tokens
- * @param string $name Mustache Template class name
- * @param bool $customEscape (default: false)
- * @param string $charset (default: 'UTF-8')
- *
- * @return string Generated PHP source code
- */
- public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8')
- {
- $this->sections = array();
- $this->source = $source;
- $this->indentNextLine = true;
- $this->customEscape = $customEscape;
- $this->charset = $charset;
-
- return $this->writeCode($tree, $name);
- }
-
- /**
- * Helper function for walking the Mustache token parse tree.
- *
- * @throws InvalidArgumentException upon encountering unknown token types.
- *
- * @param array $tree Parse tree of Mustache tokens
- * @param int $level (default: 0)
- *
- * @return string Generated PHP source code
- */
- private function walk(array $tree, $level = 0)
- {
- $code = '';
- $level++;
- foreach ($tree as $node) {
- switch ($node[Mustache_Tokenizer::TYPE]) {
- case Mustache_Tokenizer::T_SECTION:
- $code .= $this->section(
- $node[Mustache_Tokenizer::NODES],
- $node[Mustache_Tokenizer::NAME],
- $node[Mustache_Tokenizer::INDEX],
- $node[Mustache_Tokenizer::END],
- $node[Mustache_Tokenizer::OTAG],
- $node[Mustache_Tokenizer::CTAG],
- $level
- );
- break;
-
- case Mustache_Tokenizer::T_INVERTED:
- $code .= $this->invertedSection(
- $node[Mustache_Tokenizer::NODES],
- $node[Mustache_Tokenizer::NAME],
- $level
- );
- break;
-
- case Mustache_Tokenizer::T_PARTIAL:
- case Mustache_Tokenizer::T_PARTIAL_2:
- $code .= $this->partial(
- $node[Mustache_Tokenizer::NAME],
- isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
- $level
- );
- break;
-
- case Mustache_Tokenizer::T_UNESCAPED:
- case Mustache_Tokenizer::T_UNESCAPED_2:
- $code .= $this->variable($node[Mustache_Tokenizer::NAME], false, $level);
- break;
-
- case Mustache_Tokenizer::T_COMMENT:
- break;
-
- case Mustache_Tokenizer::T_ESCAPED:
- $code .= $this->variable($node[Mustache_Tokenizer::NAME], true, $level);
- break;
-
- case Mustache_Tokenizer::T_TEXT:
- $code .= $this->text($node[Mustache_Tokenizer::VALUE], $level);
- break;
-
- default:
- throw new InvalidArgumentException('Unknown node type: '.json_encode($node));
- }
- }
-
- return $code;
- }
-
- const KLASS = '<?php
-
- class %s extends Mustache_Template
- {
- public function renderInternal(Mustache_Context $context, $indent = \'\', $escape = false)
- {
- $buffer = \'\';
- %s
-
- if ($escape) {
- return %s;
- } else {
- return $buffer;
- }
- }
- %s
- }';
-
- /**
- * Generate Mustache Template class PHP source.
- *
- * @param array $tree Parse tree of Mustache tokens
- * @param string $name Mustache Template class name
- *
- * @return string Generated PHP source code
- */
- private function writeCode($tree, $name)
- {
- $code = $this->walk($tree);
- $sections = implode("\n", $this->sections);
-
- return sprintf($this->prepare(self::KLASS, 0, false), $name, $code, $this->getEscape('$buffer'), $sections);
- }
-
- const SECTION_CALL = '
- // %s section
- $buffer .= $this->section%s($context, $indent, $context->%s(%s));
- ';
-
- const SECTION = '
- private function section%s(Mustache_Context $context, $indent, $value) {
- $buffer = \'\';
- if (!is_string($value) && is_callable($value)) {
- $source = %s;
- $buffer .= $this->mustache
- ->loadLambda((string) call_user_func($value, $source)%s)
- ->renderInternal($context, $indent);
- } elseif (!empty($value)) {
- $values = $this->isIterable($value) ? $value : array($value);
- foreach ($values as $value) {
- $context->push($value);%s
- $context->pop();
- }
- }
-
- return $buffer;
- }';
-
- /**
- * Generate Mustache Template section PHP source.
- *
- * @param array $nodes Array of child tokens
- * @param string $id Section name
- * @param int $start Section start offset
- * @param int $end Section end offset
- * @param string $otag Current Mustache opening tag
- * @param string $ctag Current Mustache closing tag
- * @param int $level
- *
- * @return string Generated section PHP source code
- */
- private function section($nodes, $id, $start, $end, $otag, $ctag, $level)
- {
- $method = $this->getFindMethod($id);
- $id = var_export($id, true);
- $source = var_export(substr($this->source, $start, $end - $start), true);
-
- if ($otag !== '{{' || $ctag !== '}}') {
- $delims = ', '.var_export(sprintf('{{= %s %s =}}', $otag, $ctag), true);
- } else {
- $delims = '';
- }
-
- $key = ucfirst(md5($delims."\n".$source));
-
- if (!isset($this->sections[$key])) {
- $this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $source, $delims, $this->walk($nodes, 2));
- }
-
- return sprintf($this->prepare(self::SECTION_CALL, $level), $id, $key, $method, $id);
- }
-
- const INVERTED_SECTION = '
- // %s inverted section
- $value = $context->%s(%s);
- if (empty($value)) {
- %s
- }';
-
- /**
- * Generate Mustache Template inverted section PHP source.
- *
- * @param array $nodes Array of child tokens
- * @param string $id Section name
- * @param int $level
- *
- * @return string Generated inverted section PHP source code
- */
- private function invertedSection($nodes, $id, $level)
- {
- $method = $this->getFindMethod($id);
- $id = var_export($id, true);
-
- return sprintf($this->prepare(self::INVERTED_SECTION, $level), $id, $method, $id, $this->walk($nodes, $level));
- }
-
- const PARTIAL = '
- if ($partial = $this->mustache->loadPartial(%s)) {
- $buffer .= $partial->renderInternal($context, %s);
- }
- ';
-
- /**
- * Generate Mustache Template partial call PHP source.
- *
- * @param string $id Partial name
- * @param string $indent Whitespace indent to apply to partial
- * @param int $level
- *
- * @return string Generated partial call PHP source code
- */
- private function partial($id, $indent, $level)
- {
- return sprintf(
- $this->prepare(self::PARTIAL, $level),
- var_export($id, true),
- var_export($indent, true)
- );
- }
-
- const VARIABLE = '
- $value = $context->%s(%s);
- if (!is_string($value) && is_callable($value)) {
- $value = $this->mustache
- ->loadLambda((string) call_user_func($value))
- ->renderInternal($context, $indent);
- }
- $buffer .= %s%s;
- ';
-
- /**
- * Generate Mustache Template variable interpolation PHP source.
- *
- * @param string $id Variable name
- * @param boolean $escape Escape the variable value for output?
- * @param int $level
- *
- * @return string Generated variable interpolation PHP source
- */
- private function variable($id, $escape, $level)
- {
- $method = $this->getFindMethod($id);
- $id = ($method !== 'last') ? var_export($id, true) : '';
- $value = $escape ? $this->getEscape() : '$value';
-
- return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $this->flushIndent(), $value);
- }
-
- const LINE = '$buffer .= "\n";';
- const TEXT = '$buffer .= %s%s;';
-
- /**
- * Generate Mustache Template output Buffer call PHP source.
- *
- * @param string $text
- * @param int $level
- *
- * @return string Generated output Buffer call PHP source
- */
- private function text($text, $level)
- {
- if ($text === "\n") {
- $this->indentNextLine = true;
-
- return $this->prepare(self::LINE, $level);
- } else {
- return sprintf($this->prepare(self::TEXT, $level), $this->flushIndent(), var_export($text, true));
- }
- }
-
- /**
- * Prepare PHP source code snippet for output.
- *
- * @param string $text
- * @param int $bonus Additional indent level (default: 0)
- * @param boolean $prependNewline Prepend a newline to the snippet? (default: true)
- *
- * @return string PHP source code snippet
- */
- private function prepare($text, $bonus = 0, $prependNewline = true)
- {
- $text = ($prependNewline ? "\n" : '').trim($text);
- if ($prependNewline) {
- $bonus++;
- }
-
- return preg_replace("/\n( {8})?/", "\n".str_repeat(" ", $bonus * 4), $text);
- }
-
- const DEFAULT_ESCAPE = 'htmlspecialchars(%s, ENT_COMPAT, %s)';
- const CUSTOM_ESCAPE = 'call_user_func($this->mustache->getEscape(), %s)';
-
- /**
- * Get the current escaper.
- *
- * @param string $value (default: '$value')
- *
- * @return string Either a custom callback, or an inline call to `htmlspecialchars`
- */
- private function getEscape($value = '$value')
- {
- if ($this->customEscape) {
- return sprintf(self::CUSTOM_ESCAPE, $value);
- } else {
- return sprintf(self::DEFAULT_ESCAPE, $value, var_export($this->charset, true));
- }
- }
-
- /**
- * Select the appropriate Context `find` method for a given $id.
- *
- * The return value will be one of `find`, `findDot` or `last`.
- *
- * @see Mustache_Context::find
- * @see Mustache_Context::findDot
- * @see Mustache_Context::last
- *
- * @param string $id Variable name
- *
- * @return string `find` method name
- */
- private function getFindMethod($id)
- {
- if ($id === '.') {
- return 'last';
- } elseif (strpos($id, '.') === false) {
- return 'find';
- } else {
- return 'findDot';
- }
- }
-
- const LINE_INDENT = '$indent . ';
-
- /**
- * Get the current $indent prefix to write to the buffer.
- *
- * @return string "$indent . " or ""
- */
- private function flushIndent()
- {
- if ($this->indentNextLine) {
- $this->indentNextLine = false;
-
- return self::LINE_INDENT;
- } else {
- return '';
- }
- }
-}
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Compiler class.
+ *
+ * This class is responsible for turning a Mustache token parse tree into normal PHP source code.
+ */
+class Mustache_Compiler
+{
+ private $pragmas;
+ private $defaultPragmas = array();
+ private $sections;
+ private $blocks;
+ private $source;
+ private $indentNextLine;
+ private $customEscape;
+ private $entityFlags;
+ private $charset;
+ private $strictCallables;
+
+ /**
+ * Compile a Mustache token parse tree into PHP source code.
+ *
+ * @param string $source Mustache Template source code
+ * @param string $tree Parse tree of Mustache tokens
+ * @param string $name Mustache Template class name
+ * @param bool $customEscape (default: false)
+ * @param string $charset (default: 'UTF-8')
+ * @param bool $strictCallables (default: false)
+ * @param int $entityFlags (default: ENT_COMPAT)
+ *
+ * @return string Generated PHP source code
+ */
+ public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false, $entityFlags = ENT_COMPAT)
+ {
+ $this->pragmas = $this->defaultPragmas;
+ $this->sections = array();
+ $this->blocks = array();
+ $this->source = $source;
+ $this->indentNextLine = true;
+ $this->customEscape = $customEscape;
+ $this->entityFlags = $entityFlags;
+ $this->charset = $charset;
+ $this->strictCallables = $strictCallables;
+
+ return $this->writeCode($tree, $name);
+ }
+
+ /**
+ * Enable pragmas across all templates, regardless of the presence of pragma
+ * tags in the individual templates.
+ *
+ * @internal Users should set global pragmas in Mustache_Engine, not here :)
+ *
+ * @param string[] $pragmas
+ */
+ public function setPragmas(array $pragmas)
+ {
+ $this->pragmas = array();
+ foreach ($pragmas as $pragma) {
+ $this->pragmas[$pragma] = true;
+ }
+ $this->defaultPragmas = $this->pragmas;
+ }
+
+ /**
+ * Helper function for walking the Mustache token parse tree.
+ *
+ * @throws Mustache_Exception_SyntaxException upon encountering unknown token types
+ *
+ * @param array $tree Parse tree of Mustache tokens
+ * @param int $level (default: 0)
+ *
+ * @return string Generated PHP source code
+ */
+ private function walk(array $tree, $level = 0)
+ {
+ $code = '';
+ $level++;
+ foreach ($tree as $node) {
+ switch ($node[Mustache_Tokenizer::TYPE]) {
+ case Mustache_Tokenizer::T_PRAGMA:
+ $this->pragmas[$node[Mustache_Tokenizer::NAME]] = true;
+ break;
+
+ case Mustache_Tokenizer::T_SECTION:
+ $code .= $this->section(
+ $node[Mustache_Tokenizer::NODES],
+ $node[Mustache_Tokenizer::NAME],
+ isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(),
+ $node[Mustache_Tokenizer::INDEX],
+ $node[Mustache_Tokenizer::END],
+ $node[Mustache_Tokenizer::OTAG],
+ $node[Mustache_Tokenizer::CTAG],
+ $level
+ );
+ break;
+
+ case Mustache_Tokenizer::T_INVERTED:
+ $code .= $this->invertedSection(
+ $node[Mustache_Tokenizer::NODES],
+ $node[Mustache_Tokenizer::NAME],
+ isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(),
+ $level
+ );
+ break;
+
+ case Mustache_Tokenizer::T_PARTIAL:
+ $code .= $this->partial(
+ $node[Mustache_Tokenizer::NAME],
+ isset($node[Mustache_Tokenizer::DYNAMIC]) ? $node[Mustache_Tokenizer::DYNAMIC] : false,
+ isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
+ $level
+ );
+ break;
+
+ case Mustache_Tokenizer::T_PARENT:
+ $code .= $this->parent(
+ $node[Mustache_Tokenizer::NAME],
+ isset($node[Mustache_Tokenizer::DYNAMIC]) ? $node[Mustache_Tokenizer::DYNAMIC] : false,
+ isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
+ $node[Mustache_Tokenizer::NODES],
+ $level
+ );
+ break;
+
+ case Mustache_Tokenizer::T_BLOCK_ARG:
+ $code .= $this->blockArg(
+ $node[Mustache_Tokenizer::NODES],
+ $node[Mustache_Tokenizer::NAME],
+ $node[Mustache_Tokenizer::INDEX],
+ $node[Mustache_Tokenizer::END],
+ $node[Mustache_Tokenizer::OTAG],
+ $node[Mustache_Tokenizer::CTAG],
+ $level
+ );
+ break;
+
+ case Mustache_Tokenizer::T_BLOCK_VAR:
+ $code .= $this->blockVar(
+ $node[Mustache_Tokenizer::NODES],
+ $node[Mustache_Tokenizer::NAME],
+ $node[Mustache_Tokenizer::INDEX],
+ $node[Mustache_Tokenizer::END],
+ $node[Mustache_Tokenizer::OTAG],
+ $node[Mustache_Tokenizer::CTAG],
+ $level
+ );
+ break;
+
+ case Mustache_Tokenizer::T_COMMENT:
+ break;
+
+ case Mustache_Tokenizer::T_ESCAPED:
+ case Mustache_Tokenizer::T_UNESCAPED:
+ case Mustache_Tokenizer::T_UNESCAPED_2:
+ $code .= $this->variable(
+ $node[Mustache_Tokenizer::NAME],
+ isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(),
+ $node[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_ESCAPED,
+ $level
+ );
+ break;
+
+ case Mustache_Tokenizer::T_TEXT:
+ $code .= $this->text($node[Mustache_Tokenizer::VALUE], $level);
+ break;
+
+ default:
+ throw new Mustache_Exception_SyntaxException(sprintf('Unknown token type: %s', $node[Mustache_Tokenizer::TYPE]), $node);
+ }
+ }
+
+ return $code;
+ }
+
+ const KLASS = '<?php
+
+ class %s extends Mustache_Template
+ {
+ private $lambdaHelper;%s
+
+ public function renderInternal(Mustache_Context $context, $indent = \'\')
+ {
+ $this->lambdaHelper = new Mustache_LambdaHelper($this->mustache, $context);
+ $buffer = \'\';
+ %s
+
+ return $buffer;
+ }
+ %s
+ %s
+ }';
+
+ const KLASS_NO_LAMBDAS = '<?php
+
+ class %s extends Mustache_Template
+ {%s
+ public function renderInternal(Mustache_Context $context, $indent = \'\')
+ {
+ $buffer = \'\';
+ %s
+
+ return $buffer;
+ }
+ }';
+
+ const STRICT_CALLABLE = 'protected $strictCallables = true;';
+
+ /**
+ * Generate Mustache Template class PHP source.
+ *
+ * @param array $tree Parse tree of Mustache tokens
+ * @param string $name Mustache Template class name
+ *
+ * @return string Generated PHP source code
+ */
+ private function writeCode($tree, $name)
+ {
+ $code = $this->walk($tree);
+ $sections = implode("\n", $this->sections);
+ $blocks = implode("\n", $this->blocks);
+ $klass = empty($this->sections) && empty($this->blocks) ? self::KLASS_NO_LAMBDAS : self::KLASS;
+
+ $callable = $this->strictCallables ? $this->prepare(self::STRICT_CALLABLE) : '';
+
+ return sprintf($this->prepare($klass, 0, false, true), $name, $callable, $code, $sections, $blocks);
+ }
+
+ const BLOCK_VAR = '
+ $blockFunction = $context->findInBlock(%s);
+ if (is_callable($blockFunction)) {
+ $buffer .= call_user_func($blockFunction, $context);
+ %s}
+ ';
+
+ const BLOCK_VAR_ELSE = '} else {%s';
+
+ /**
+ * Generate Mustache Template inheritance block variable PHP source.
+ *
+ * @param array $nodes Array of child tokens
+ * @param string $id Section name
+ * @param int $start Section start offset
+ * @param int $end Section end offset
+ * @param string $otag Current Mustache opening tag
+ * @param string $ctag Current Mustache closing tag
+ * @param int $level
+ *
+ * @return string Generated PHP source code
+ */
+ private function blockVar($nodes, $id, $start, $end, $otag, $ctag, $level)
+ {
+ $id = var_export($id, true);
+
+ $else = $this->walk($nodes, $level);
+ if ($else !== '') {
+ $else = sprintf($this->prepare(self::BLOCK_VAR_ELSE, $level + 1, false, true), $else);
+ }
+
+ return sprintf($this->prepare(self::BLOCK_VAR, $level), $id, $else);
+ }
+
+ const BLOCK_ARG = '%s => array($this, \'block%s\'),';
+
+ /**
+ * Generate Mustache Template inheritance block argument PHP source.
+ *
+ * @param array $nodes Array of child tokens
+ * @param string $id Section name
+ * @param int $start Section start offset
+ * @param int $end Section end offset
+ * @param string $otag Current Mustache opening tag
+ * @param string $ctag Current Mustache closing tag
+ * @param int $level
+ *
+ * @return string Generated PHP source code
+ */
+ private function blockArg($nodes, $id, $start, $end, $otag, $ctag, $level)
+ {
+ $key = $this->block($nodes);
+ $id = var_export($id, true);
+
+ return sprintf($this->prepare(self::BLOCK_ARG, $level), $id, $key);
+ }
+
+ const BLOCK_FUNCTION = '
+ public function block%s($context)
+ {
+ $indent = $buffer = \'\';%s
+
+ return $buffer;
+ }
+ ';
+
+ /**
+ * Generate Mustache Template inheritance block function PHP source.
+ *
+ * @param array $nodes Array of child tokens
+ *
+ * @return string key of new block function
+ */
+ private function block($nodes)
+ {
+ $code = $this->walk($nodes, 0);
+ $key = ucfirst(md5($code));
+
+ if (!isset($this->blocks[$key])) {
+ $this->blocks[$key] = sprintf($this->prepare(self::BLOCK_FUNCTION, 0), $key, $code);
+ }
+
+ return $key;
+ }
+
+ const SECTION_CALL = '
+ $value = $context->%s(%s);%s
+ $buffer .= $this->section%s($context, $indent, $value);
+ ';
+
+ const SECTION = '
+ private function section%s(Mustache_Context $context, $indent, $value)
+ {
+ $buffer = \'\';
+
+ if (%s) {
+ $source = %s;
+ $result = (string) call_user_func($value, $source, %s);
+ if (strpos($result, \'{{\') === false) {
+ $buffer .= $result;
+ } else {
+ $buffer .= $this->mustache
+ ->loadLambda($result%s)
+ ->renderInternal($context);
+ }
+ } elseif (!empty($value)) {
+ $values = $this->isIterable($value) ? $value : array($value);
+ foreach ($values as $value) {
+ $context->push($value);
+ %s
+ $context->pop();
+ }
+ }
+
+ return $buffer;
+ }
+ ';
+
+ /**
+ * Generate Mustache Template section PHP source.
+ *
+ * @param array $nodes Array of child tokens
+ * @param string $id Section name
+ * @param string[] $filters Array of filters
+ * @param int $start Section start offset
+ * @param int $end Section end offset
+ * @param string $otag Current Mustache opening tag
+ * @param string $ctag Current Mustache closing tag
+ * @param int $level
+ *
+ * @return string Generated section PHP source code
+ */
+ private function section($nodes, $id, $filters, $start, $end, $otag, $ctag, $level)
+ {
+ $source = var_export(substr($this->source, $start, $end - $start), true);
+ $callable = $this->getCallable();
+
+ if ($otag !== '{{' || $ctag !== '}}') {
+ $delimTag = var_export(sprintf('{{= %s %s =}}', $otag, $ctag), true);
+ $helper = sprintf('$this->lambdaHelper->withDelimiters(%s)', $delimTag);
+ $delims = ', ' . $delimTag;
+ } else {
+ $helper = '$this->lambdaHelper';
+ $delims = '';
+ }
+
+ $key = ucfirst(md5($delims . "\n" . $source));
+
+ if (!isset($this->sections[$key])) {
+ $this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $callable, $source, $helper, $delims, $this->walk($nodes, 2));
+ }
+
+ $method = $this->getFindMethod($id);
+ $id = var_export($id, true);
+ $filters = $this->getFilters($filters, $level);
+
+ return sprintf($this->prepare(self::SECTION_CALL, $level), $method, $id, $filters, $key);
+ }
+
+ const INVERTED_SECTION = '
+ $value = $context->%s(%s);%s
+ if (empty($value)) {
+ %s
+ }
+ ';
+
+ /**
+ * Generate Mustache Template inverted section PHP source.
+ *
+ * @param array $nodes Array of child tokens
+ * @param string $id Section name
+ * @param string[] $filters Array of filters
+ * @param int $level
+ *
+ * @return string Generated inverted section PHP source code
+ */
+ private function invertedSection($nodes, $id, $filters, $level)
+ {
+ $method = $this->getFindMethod($id);
+ $id = var_export($id, true);
+ $filters = $this->getFilters($filters, $level);
+
+ return sprintf($this->prepare(self::INVERTED_SECTION, $level), $method, $id, $filters, $this->walk($nodes, $level));
+ }
+
+ const DYNAMIC_NAME = '$this->resolveValue($context->%s(%s), $context)';
+
+ /**
+ * Generate Mustache Template dynamic name resolution PHP source.
+ *
+ * @param string $id Tag name
+ * @param bool $dynamic True if the name is dynamic
+ *
+ * @return string Dynamic name resolution PHP source code
+ */
+ private function resolveDynamicName($id, $dynamic)
+ {
+ if (!$dynamic) {
+ return var_export($id, true);
+ }
+
+ $method = $this->getFindMethod($id);
+ $id = ($method !== 'last') ? var_export($id, true) : '';
+
+ // TODO: filters?
+
+ return sprintf(self::DYNAMIC_NAME, $method, $id);
+ }
+
+ const PARTIAL_INDENT = ', $indent . %s';
+ const PARTIAL = '
+ if ($partial = $this->mustache->loadPartial(%s)) {
+ $buffer .= $partial->renderInternal($context%s);
+ }
+ ';
+
+ /**
+ * Generate Mustache Template partial call PHP source.
+ *
+ * @param string $id Partial name
+ * @param bool $dynamic Partial name is dynamic
+ * @param string $indent Whitespace indent to apply to partial
+ * @param int $level
+ *
+ * @return string Generated partial call PHP source code
+ */
+ private function partial($id, $dynamic, $indent, $level)
+ {
+ if ($indent !== '') {
+ $indentParam = sprintf(self::PARTIAL_INDENT, var_export($indent, true));
+ } else {
+ $indentParam = '';
+ }
+
+ return sprintf(
+ $this->prepare(self::PARTIAL, $level),
+ $this->resolveDynamicName($id, $dynamic),
+ $indentParam
+ );
+ }
+
+ const PARENT = '
+ if ($parent = $this->mustache->loadPartial(%s)) {
+ $context->pushBlockContext(array(%s
+ ));
+ $buffer .= $parent->renderInternal($context, $indent);
+ $context->popBlockContext();
+ }
+ ';
+
+ const PARENT_NO_CONTEXT = '
+ if ($parent = $this->mustache->loadPartial(%s)) {
+ $buffer .= $parent->renderInternal($context, $indent);
+ }
+ ';
+
+ /**
+ * Generate Mustache Template inheritance parent call PHP source.
+ *
+ * @param string $id Parent tag name
+ * @param bool $dynamic Tag name is dynamic
+ * @param string $indent Whitespace indent to apply to parent
+ * @param array $children Child nodes
+ * @param int $level
+ *
+ * @return string Generated PHP source code
+ */
+ private function parent($id, $dynamic, $indent, array $children, $level)
+ {
+ $realChildren = array_filter($children, array(__CLASS__, 'onlyBlockArgs'));
+ $partialName = $this->resolveDynamicName($id, $dynamic);
+
+ if (empty($realChildren)) {
+ return sprintf($this->prepare(self::PARENT_NO_CONTEXT, $level), $partialName);
+ }
+
+ return sprintf(
+ $this->prepare(self::PARENT, $level),
+ $partialName,
+ $this->walk($realChildren, $level + 1)
+ );
+ }
+
+ /**
+ * Helper method for filtering out non-block-arg tokens.
+ *
+ * @param array $node
+ *
+ * @return bool True if $node is a block arg token
+ */
+ private static function onlyBlockArgs(array $node)
+ {
+ return $node[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_BLOCK_ARG;
+ }
+
+ const VARIABLE = '
+ $value = $this->resolveValue($context->%s(%s), $context);%s
+ $buffer .= %s($value === null ? \'\' : %s);
+ ';
+
+ /**
+ * Generate Mustache Template variable interpolation PHP source.
+ *
+ * @param string $id Variable name
+ * @param string[] $filters Array of filters
+ * @param bool $escape Escape the variable value for output?
+ * @param int $level
+ *
+ * @return string Generated variable interpolation PHP source
+ */
+ private function variable($id, $filters, $escape, $level)
+ {
+ $method = $this->getFindMethod($id);
+ $id = ($method !== 'last') ? var_export($id, true) : '';
+ $filters = $this->getFilters($filters, $level);
+ $value = $escape ? $this->getEscape() : '$value';
+
+ return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $filters, $this->flushIndent(), $value);
+ }
+
+ const FILTER = '
+ $filter = $context->%s(%s);
+ if (!(%s)) {
+ throw new Mustache_Exception_UnknownFilterException(%s);
+ }
+ $value = call_user_func($filter, $value);%s
+ ';
+
+ /**
+ * Generate Mustache Template variable filtering PHP source.
+ *
+ * @param string[] $filters Array of filters
+ * @param int $level
+ *
+ * @return string Generated filter PHP source
+ */
+ private function getFilters(array $filters, $level)
+ {
+ if (empty($filters)) {
+ return '';
+ }
+
+ $name = array_shift($filters);
+ $method = $this->getFindMethod($name);
+ $filter = ($method !== 'last') ? var_export($name, true) : '';
+ $callable = $this->getCallable('$filter');
+ $msg = var_export($name, true);
+
+ return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $callable, $msg, $this->getFilters($filters, $level));
+ }
+
+ const LINE = '$buffer .= "\n";';
+ const TEXT = '$buffer .= %s%s;';
+
+ /**
+ * Generate Mustache Template output Buffer call PHP source.
+ *
+ * @param string $text
+ * @param int $level
+ *
+ * @return string Generated output Buffer call PHP source
+ */
+ private function text($text, $level)
+ {
+ $indentNextLine = (substr($text, -1) === "\n");
+ $code = sprintf($this->prepare(self::TEXT, $level), $this->flushIndent(), var_export($text, true));
+ $this->indentNextLine = $indentNextLine;
+
+ return $code;
+ }
+
+ /**
+ * Prepare PHP source code snippet for output.
+ *
+ * @param string $text
+ * @param int $bonus Additional indent level (default: 0)
+ * @param bool $prependNewline Prepend a newline to the snippet? (default: true)
+ * @param bool $appendNewline Append a newline to the snippet? (default: false)
+ *
+ * @return string PHP source code snippet
+ */
+ private function prepare($text, $bonus = 0, $prependNewline = true, $appendNewline = false)
+ {
+ $text = ($prependNewline ? "\n" : '') . trim($text);
+ if ($prependNewline) {
+ $bonus++;
+ }
+ if ($appendNewline) {
+ $text .= "\n";
+ }
+
+ return preg_replace("/\n( {8})?/", "\n" . str_repeat(' ', $bonus * 4), $text);
+ }
+
+ const DEFAULT_ESCAPE = 'htmlspecialchars(%s, %s, %s)';
+ const CUSTOM_ESCAPE = 'call_user_func($this->mustache->getEscape(), %s)';
+
+ /**
+ * Get the current escaper.
+ *
+ * @param string $value (default: '$value')
+ *
+ * @return string Either a custom callback, or an inline call to `htmlspecialchars`
+ */
+ private function getEscape($value = '$value')
+ {
+ if ($this->customEscape) {
+ return sprintf(self::CUSTOM_ESCAPE, $value);
+ }
+
+ return sprintf(self::DEFAULT_ESCAPE, $value, var_export($this->entityFlags, true), var_export($this->charset, true));
+ }
+
+ /**
+ * Select the appropriate Context `find` method for a given $id.
+ *
+ * The return value will be one of `find`, `findDot`, `findAnchoredDot` or `last`.
+ *
+ * @see Mustache_Context::find
+ * @see Mustache_Context::findDot
+ * @see Mustache_Context::last
+ *
+ * @param string $id Variable name
+ *
+ * @return string `find` method name
+ */
+ private function getFindMethod($id)
+ {
+ if ($id === '.') {
+ return 'last';
+ }
+
+ if (isset($this->pragmas[Mustache_Engine::PRAGMA_ANCHORED_DOT]) && $this->pragmas[Mustache_Engine::PRAGMA_ANCHORED_DOT]) {
+ if (substr($id, 0, 1) === '.') {
+ return 'findAnchoredDot';
+ }
+ }
+
+ if (strpos($id, '.') === false) {
+ return 'find';
+ }
+
+ return 'findDot';
+ }
+
+ const IS_CALLABLE = '!is_string(%s) && is_callable(%s)';
+ const STRICT_IS_CALLABLE = 'is_object(%s) && is_callable(%s)';
+
+ /**
+ * Helper function to compile strict vs lax "is callable" logic.
+ *
+ * @param string $variable (default: '$value')
+ *
+ * @return string "is callable" logic
+ */
+ private function getCallable($variable = '$value')
+ {
+ $tpl = $this->strictCallables ? self::STRICT_IS_CALLABLE : self::IS_CALLABLE;
+
+ return sprintf($tpl, $variable, $variable);
+ }
+
+ const LINE_INDENT = '$indent . ';
+
+ /**
+ * Get the current $indent prefix to write to the buffer.
+ *
+ * @return string "$indent . " or ""
+ */
+ private function flushIndent()
+ {
+ if (!$this->indentNextLine) {
+ return '';
+ }
+
+ $this->indentNextLine = false;
+
+ return self::LINE_INDENT;
+ }
+}
diff --git a/Mustache/Context.php b/Mustache/Context.php
index 7bc7571..69c02e0 100644
--- a/Mustache/Context.php
+++ b/Mustache/Context.php
@@ -1,149 +1,242 @@
-<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Mustache Template rendering Context.
- */
-class Mustache_Context
-{
- private $stack = array();
-
- /**
- * Mustache rendering Context constructor.
- *
- * @param mixed $context Default rendering context (default: null)
- */
- public function __construct($context = null)
- {
- if ($context !== null) {
- $this->stack = array($context);
- }
- }
-
- /**
- * Push a new Context frame onto the stack.
- *
- * @param mixed $value Object or array to use for context
- */
- public function push($value)
- {
- array_push($this->stack, $value);
- }
-
- /**
- * Pop the last Context frame from the stack.
- *
- * @return mixed Last Context frame (object or array)
- */
- public function pop()
- {
- return array_pop($this->stack);
- }
-
- /**
- * Get the last Context frame.
- *
- * @return mixed Last Context frame (object or array)
- */
- public function last()
- {
- return end($this->stack);
- }
-
- /**
- * Find a variable in the Context stack.
- *
- * Starting with the last Context frame (the context of the innermost section), and working back to the top-level
- * rendering context, look for a variable with the given name:
- *
- * * If the Context frame is an associative array which contains the key $id, returns the value of that element.
- * * If the Context frame is an object, this will check first for a public method, then a public property named
- * $id. Failing both of these, it will try `__isset` and `__get` magic methods.
- * * If a value named $id is not found in any Context frame, returns an empty string.
- *
- * @param string $id Variable name
- *
- * @return mixed Variable value, or '' if not found
- */
- public function find($id)
- {
- return $this->findVariableInStack($id, $this->stack);
- }
-
- /**
- * Find a 'dot notation' variable in the Context stack.
- *
- * Note that dot notation traversal bubbles through scope differently than the regular find method. After finding
- * the initial chunk of the dotted name, each subsequent chunk is searched for only within the value of the previous
- * result. For example, given the following context stack:
- *
- * $data = array(
- * 'name' => 'Fred',
- * 'child' => array(
- * 'name' => 'Bob'
- * ),
- * );
- *
- * ... and the Mustache following template:
- *
- * {{ child.name }}
- *
- * ... the `name` value is only searched for within the `child` value of the global Context, not within parent
- * Context frames.
- *
- * @param string $id Dotted variable selector
- *
- * @return mixed Variable value, or '' if not found
- */
- public function findDot($id)
- {
- $chunks = explode('.', $id);
- $first = array_shift($chunks);
- $value = $this->findVariableInStack($first, $this->stack);
-
- foreach ($chunks as $chunk) {
- if ($value === '') {
- return $value;
- }
-
- $value = $this->findVariableInStack($chunk, array($value));
- }
-
- return $value;
- }
-
- /**
- * Helper function to find a variable in the Context stack.
- *
- * @see Mustache_Context::find
- *
- * @param string $id Variable name
- * @param array $stack Context stack
- *
- * @return mixed Variable value, or '' if not found
- */
- private function findVariableInStack($id, array $stack)
- {
- for ($i = count($stack) - 1; $i >= 0; $i--) {
- if (is_object($stack[$i])) {
- if (method_exists($stack[$i], $id)) {
- return $stack[$i]->$id();
- } elseif (isset($stack[$i]->$id)) {
- return $stack[$i]->$id;
- }
- } elseif (is_array($stack[$i]) && array_key_exists($id, $stack[$i])) {
- return $stack[$i][$id];
- }
- }
-
- return '';
- }
-}
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template rendering Context.
+ */
+class Mustache_Context
+{
+ private $stack = array();
+ private $blockStack = array();
+
+ /**
+ * Mustache rendering Context constructor.
+ *
+ * @param mixed $context Default rendering context (default: null)
+ */
+ public function __construct($context = null)
+ {
+ if ($context !== null) {
+ $this->stack = array($context);
+ }
+ }
+
+ /**
+ * Push a new Context frame onto the stack.
+ *
+ * @param mixed $value Object or array to use for context
+ */
+ public function push($value)
+ {
+ array_push($this->stack, $value);
+ }
+
+ /**
+ * Push a new Context frame onto the block context stack.
+ *
+ * @param mixed $value Object or array to use for block context
+ */
+ public function pushBlockContext($value)
+ {
+ array_push($this->blockStack, $value);
+ }
+
+ /**
+ * Pop the last Context frame from the stack.
+ *
+ * @return mixed Last Context frame (object or array)
+ */
+ public function pop()
+ {
+ return array_pop($this->stack);
+ }
+
+ /**
+ * Pop the last block Context frame from the stack.
+ *
+ * @return mixed Last block Context frame (object or array)
+ */
+ public function popBlockContext()
+ {
+ return array_pop($this->blockStack);
+ }
+
+ /**
+ * Get the last Context frame.
+ *
+ * @return mixed Last Context frame (object or array)
+ */
+ public function last()
+ {
+ return end($this->stack);
+ }
+
+ /**
+ * Find a variable in the Context stack.
+ *
+ * Starting with the last Context frame (the context of the innermost section), and working back to the top-level
+ * rendering context, look for a variable with the given name:
+ *
+ * * If the Context frame is an associative array which contains the key $id, returns the value of that element.
+ * * If the Context frame is an object, this will check first for a public method, then a public property named
+ * $id. Failing both of these, it will try `__isset` and `__get` magic methods.
+ * * If a value named $id is not found in any Context frame, returns an empty string.
+ *
+ * @param string $id Variable name
+ *
+ * @return mixed Variable value, or '' if not found
+ */
+ public function find($id)
+ {
+ return $this->findVariableInStack($id, $this->stack);
+ }
+
+ /**
+ * Find a 'dot notation' variable in the Context stack.
+ *
+ * Note that dot notation traversal bubbles through scope differently than the regular find method. After finding
+ * the initial chunk of the dotted name, each subsequent chunk is searched for only within the value of the previous
+ * result. For example, given the following context stack:
+ *
+ * $data = array(
+ * 'name' => 'Fred',
+ * 'child' => array(
+ * 'name' => 'Bob'
+ * ),
+ * );
+ *
+ * ... and the Mustache following template:
+ *
+ * {{ child.name }}
+ *
+ * ... the `name` value is only searched for within the `child` value of the global Context, not within parent
+ * Context frames.
+ *
+ * @param string $id Dotted variable selector
+ *
+ * @return mixed Variable value, or '' if not found
+ */
+ public function findDot($id)
+ {
+ $chunks = explode('.', $id);
+ $first = array_shift($chunks);
+ $value = $this->findVariableInStack($first, $this->stack);
+
+ foreach ($chunks as $chunk) {
+ if ($value === '') {
+ return $value;
+ }
+
+ $value = $this->findVariableInStack($chunk, array($value));
+ }
+
+ return $value;
+ }
+
+ /**
+ * Find an 'anchored dot notation' variable in the Context stack.
+ *
+ * This is the same as findDot(), except it looks in the top of the context
+ * stack for the first value, rather than searching the whole context stack
+ * and starting from there.
+ *
+ * @see Mustache_Context::findDot
+ *
+ * @throws Mustache_Exception_InvalidArgumentException if given an invalid anchored dot $id
+ *
+ * @param string $id Dotted variable selector
+ *
+ * @return mixed Variable value, or '' if not found
+ */
+ public function findAnchoredDot($id)
+ {
+ $chunks = explode('.', $id);
+ $first = array_shift($chunks);
+ if ($first !== '') {
+ throw new Mustache_Exception_InvalidArgumentException(sprintf('Unexpected id for findAnchoredDot: %s', $id));
+ }
+
+ $value = $this->last();
+
+ foreach ($chunks as $chunk) {
+ if ($value === '') {
+ return $value;
+ }
+
+ $value = $this->findVariableInStack($chunk, array($value));
+ }
+
+ return $value;
+ }
+
+ /**
+ * Find an argument in the block context stack.
+ *
+ * @param string $id
+ *
+ * @return mixed Variable value, or '' if not found
+ */
+ public function findInBlock($id)
+ {
+ foreach ($this->blockStack as $context) {
+ if (array_key_exists($id, $context)) {
+ return $context[$id];
+ }
+ }
+
+ return '';
+ }
+
+ /**
+ * Helper function to find a variable in the Context stack.
+ *
+ * @see Mustache_Context::find
+ *
+ * @param string $id Variable name
+ * @param array $stack Context stack
+ *
+ * @return mixed Variable value, or '' if not found
+ */
+ private function findVariableInStack($id, array $stack)
+ {
+ for ($i = count($stack) - 1; $i >= 0; $i--) {
+ $frame = &$stack[$i];
+
+ switch (gettype($frame)) {
+ case 'object':
+ if (!($frame instanceof Closure)) {
+ // Note that is_callable() *will not work here*
+ // See https://github.com/bobthecow/mustache.php/wiki/Magic-Methods
+ if (method_exists($frame, $id)) {
+ return $frame->$id();
+ }
+
+ if (isset($frame->$id)) {
+ return $frame->$id;
+ }
+
+ if ($frame instanceof ArrayAccess && isset($frame[$id])) {
+ return $frame[$id];
+ }
+ }
+ break;
+
+ case 'array':
+ if (array_key_exists($id, $frame)) {
+ return $frame[$id];
+ }
+ break;
+ }
+ }
+
+ return '';
+ }
+}
diff --git a/Mustache/Engine.php b/Mustache/Engine.php
index ca909bc..7a31ac0 100644
--- a/Mustache/Engine.php
+++ b/Mustache/Engine.php
@@ -1,589 +1,831 @@
-<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * A Mustache implementation in PHP.
- *
- * {@link http://defunkt.github.com/mustache}
- *
- * Mustache is a framework-agnostic logic-less templating language. It enforces separation of view
- * logic from template files. In fact, it is not even possible to embed logic in the template.
- *
- * This is very, very rad.
- *
- * @author Justin Hileman {@link http://justinhileman.com}
- */
-class Mustache_Engine
-{
- const VERSION = '2.0.2';
- const SPEC_VERSION = '1.1.2';
-
- // Template cache
- private $templates = array();
-
- // Environment
- private $templateClassPrefix = '__Mustache_';
- private $cache = null;
- private $loader;
- private $partialsLoader;
- private $helpers;
- private $escape;
- private $charset = 'UTF-8';
-
- /**
- * Mustache class constructor.
- *
- * Passing an $options array allows overriding certain Mustache options during instantiation:
- *
- * $options = array(
- * // The class prefix for compiled templates. Defaults to '__Mustache_'
- * 'template_class_prefix' => '__MyTemplates_',
- *
- * // A cache directory for compiled templates. Mustache will not cache templates unless this is set
- * 'cache' => dirname(__FILE__).'/tmp/cache/mustache',
- *
- * // A Mustache template loader instance. Uses a StringLoader if not specified
- * 'loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'),
- *
- * // A Mustache loader instance for partials.
- * 'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'),
- *
- * // An array of Mustache partials. Useful for quick-and-dirty string template loading, but not as
- * // efficient or lazy as a Filesystem (or database) loader.
- * 'partials' => array('foo' => file_get_contents(dirname(__FILE__).'/views/partials/foo.mustache')),
- *
- * // An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order
- * // sections), or any other valid Mustache context value. They will be prepended to the context stack,
- * // so they will be available in any template loaded by this Mustache instance.
- * 'helpers' => array('i18n' => function($text) {
- * // do something translatey here...
- * }),
- *
- * // An 'escape' callback, responsible for escaping double-mustache variables.
- * 'escape' => function($value) {
- * return htmlspecialchars($buffer, ENT_COMPAT, 'UTF-8');
- * },
- *
- * // character set for `htmlspecialchars`. Defaults to 'UTF-8'
- * 'charset' => 'ISO-8859-1',
- * );
- *
- * @param array $options (default: array())
- */
- public function __construct(array $options = array())
- {
- if (isset($options['template_class_prefix'])) {
- $this->templateClassPrefix = $options['template_class_prefix'];
- }
-
- if (isset($options['cache'])) {
- $this->cache = $options['cache'];
- }
-
- if (isset($options['loader'])) {
- $this->setLoader($options['loader']);
- }
-
- if (isset($options['partials_loader'])) {
- $this->setPartialsLoader($options['partials_loader']);
- }
-
- if (isset($options['partials'])) {
- $this->setPartials($options['partials']);
- }
-
- if (isset($options['helpers'])) {
- $this->setHelpers($options['helpers']);
- }
-
- if (isset($options['escape'])) {
- if (!is_callable($options['escape'])) {
- throw new InvalidArgumentException('Mustache Constructor "escape" option must be callable');
- }
-
- $this->escape = $options['escape'];
- }
-
- if (isset($options['charset'])) {
- $this->charset = $options['charset'];
- }
- }
-
- /**
- * Shortcut 'render' invocation.
- *
- * Equivalent to calling `$mustache->loadTemplate($template)->render($data);`
- *
- * @see Mustache_Engine::loadTemplate
- * @see Mustache_Template::render
- *
- * @param string $template
- * @param mixed $data
- *
- * @return string Rendered template
- */
- public function render($template, $data)
- {
- return $this->loadTemplate($template)->render($data);
- }
-
- /**
- * Get the current Mustache escape callback.
- *
- * @return mixed Callable or null
- */
- public function getEscape()
- {
- return $this->escape;
- }
-
- /**
- * Get the current Mustache character set.
- *
- * @return string
- */
- public function getCharset()
- {
- return $this->charset;
- }
-
- /**
- * Set the Mustache template Loader instance.
- *
- * @param Mustache_Loader $loader
- */
- public function setLoader(Mustache_Loader $loader)
- {
- $this->loader = $loader;
- }
-
- /**
- * Get the current Mustache template Loader instance.
- *
- * If no Loader instance has been explicitly specified, this method will instantiate and return
- * a StringLoader instance.
- *
- * @return Mustache_Loader
- */
- public function getLoader()
- {
- if (!isset($this->loader)) {
- $this->loader = new Mustache_Loader_StringLoader;
- }
-
- return $this->loader;
- }
-
- /**
- * Set the Mustache partials Loader instance.
- *
- * @param Mustache_Loader $partialsLoader
- */
- public function setPartialsLoader(Mustache_Loader $partialsLoader)
- {
- $this->partialsLoader = $partialsLoader;
- }
-
- /**
- * Get the current Mustache partials Loader instance.
- *
- * If no Loader instance has been explicitly specified, this method will instantiate and return
- * an ArrayLoader instance.
- *
- * @return Mustache_Loader
- */
- public function getPartialsLoader()
- {
- if (!isset($this->partialsLoader)) {
- $this->partialsLoader = new Mustache_Loader_ArrayLoader;
- }
-
- return $this->partialsLoader;
- }
-
- /**
- * Set partials for the current partials Loader instance.
- *
- * @throws RuntimeException If the current Loader instance is immutable
- *
- * @param array $partials (default: array())
- */
- public function setPartials(array $partials = array())
- {
- $loader = $this->getPartialsLoader();
- if (!$loader instanceof Mustache_Loader_MutableLoader) {
- throw new RuntimeException('Unable to set partials on an immutable Mustache Loader instance');
- }
-
- $loader->setTemplates($partials);
- }
-
- /**
- * Set an array of Mustache helpers.
- *
- * An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order sections), or
- * any other valid Mustache context value. They will be prepended to the context stack, so they will be available in
- * any template loaded by this Mustache instance.
- *
- * @throws InvalidArgumentException if $helpers is not an array or Traversable
- *
- * @param array|Traversable $helpers
- */
- public function setHelpers($helpers)
- {
- if (!is_array($helpers) && !$helpers instanceof Traversable) {
- throw new InvalidArgumentException('setHelpers expects an array of helpers');
- }
-
- $this->getHelpers()->clear();
-
- foreach ($helpers as $name => $helper) {
- $this->addHelper($name, $helper);
- }
- }
-
- /**
- * Get the current set of Mustache helpers.
- *
- * @see Mustache_Engine::setHelpers
- *
- * @return Mustache_HelperCollection
- */
- public function getHelpers()
- {
- if (!isset($this->helpers)) {
- $this->helpers = new Mustache_HelperCollection;
- }
-
- return $this->helpers;
- }
-
- /**
- * Add a new Mustache helper.
- *
- * @see Mustache_Engine::setHelpers
- *
- * @param string $name
- * @param mixed $helper
- */
- public function addHelper($name, $helper)
- {
- $this->getHelpers()->add($name, $helper);
- }
-
- /**
- * Get a Mustache helper by name.
- *
- * @see Mustache_Engine::setHelpers
- *
- * @param string $name
- *
- * @return mixed Helper
- */
- public function getHelper($name)
- {
- return $this->getHelpers()->get($name);
- }
-
- /**
- * Check whether this Mustache instance has a helper.
- *
- * @see Mustache_Engine::setHelpers
- *
- * @param string $name
- *
- * @return boolean True if the helper is present
- */
- public function hasHelper($name)
- {
- return $this->getHelpers()->has($name);
- }
-
- /**
- * Remove a helper by name.
- *
- * @see Mustache_Engine::setHelpers
- *
- * @param string $name
- */
- public function removeHelper($name)
- {
- $this->getHelpers()->remove($name);
- }
-
- /**
- * Set the Mustache Tokenizer instance.
- *
- * @param Mustache_Tokenizer $tokenizer
- */
- public function setTokenizer(Mustache_Tokenizer $tokenizer)
- {
- $this->tokenizer = $tokenizer;
- }
-
- /**
- * Get the current Mustache Tokenizer instance.
- *
- * If no Tokenizer instance has been explicitly specified, this method will instantiate and return a new one.
- *
- * @return Mustache_Tokenizer
- */
- public function getTokenizer()
- {
- if (!isset($this->tokenizer)) {
- $this->tokenizer = new Mustache_Tokenizer;
- }
-
- return $this->tokenizer;
- }
-
- /**
- * Set the Mustache Parser instance.
- *
- * @param Mustache_Parser $parser
- */
- public function setParser(Mustache_Parser $parser)
- {
- $this->parser = $parser;
- }
-
- /**
- * Get the current Mustache Parser instance.
- *
- * If no Parser instance has been explicitly specified, this method will instantiate and return a new one.
- *
- * @return Mustache_Parser
- */
- public function getParser()
- {
- if (!isset($this->parser)) {
- $this->parser = new Mustache_Parser;
- }
-
- return $this->parser;
- }
-
- /**
- * Set the Mustache Compiler instance.
- *
- * @param Mustache_Compiler $compiler
- */
- public function setCompiler(Mustache_Compiler $compiler)
- {
- $this->compiler = $compiler;
- }
-
- /**
- * Get the current Mustache Compiler instance.
- *
- * If no Compiler instance has been explicitly specified, this method will instantiate and return a new one.
- *
- * @return Mustache_Compiler
- */
- public function getCompiler()
- {
- if (!isset($this->compiler)) {
- $this->compiler = new Mustache_Compiler;
- }
-
- return $this->compiler;
- }
-
- /**
- * Helper method to generate a Mustache template class.
- *
- * @param string $source
- *
- * @return string Mustache Template class name
- */
- public function getTemplateClassName($source)
- {
- return $this->templateClassPrefix . md5(sprintf(
- 'version:%s,escape:%s,charset:%s,source:%s',
- self::VERSION,
- isset($this->escape) ? 'custom' : 'default',
- $this->charset,
- $source
- ));
- }
-
- /**
- * Load a Mustache Template by name.
- *
- * @param string $name
- *
- * @return Mustache_Template
- */
- public function loadTemplate($name)
- {
- return $this->loadSource($this->getLoader()->load($name));
- }
-
- /**
- * Load a Mustache partial Template by name.
- *
- * This is a helper method used internally by Template instances for loading partial templates. You can most likely
- * ignore it completely.
- *
- * @param string $name
- *
- * @return Mustache_Template
- */
- public function loadPartial($name)
- {
- try {
- return $this->loadSource($this->getPartialsLoader()->load($name));
- } catch (InvalidArgumentException $e) {
- // If the named partial cannot be found, return null.
- }
- }
-
- /**
- * Load a Mustache lambda Template by source.
- *
- * This is a helper method used by Template instances to generate subtemplates for Lambda sections. You can most
- * likely ignore it completely.
- *
- * @param string $source
- * @param string $delims (default: null)
- *
- * @return Mustache_Template
- */
- public function loadLambda($source, $delims = null)
- {
- if ($delims !== null) {
- $source = $delims . "\n" . $source;
- }
-
- return $this->loadSource($source);
- }
-
- /**
- * Instantiate and return a Mustache Template instance by source.
- *
- * @see Mustache_Engine::loadTemplate
- * @see Mustache_Engine::loadPartial
- * @see Mustache_Engine::loadLambda
- *
- * @param string $source
- *
- * @return Mustache_Template
- */
- private function loadSource($source)
- {
- $className = $this->getTemplateClassName($source);
-
- if (!isset($this->templates[$className])) {
- if (!class_exists($className, false)) {
- if ($fileName = $this->getCacheFilename($source)) {
- if (!is_file($fileName)) {
- $this->writeCacheFile($fileName, $this->compile($source));
- }
-
- require_once $fileName;
- } else {
- eval('?>'.$this->compile($source));
- }
- }
-
- $this->templates[$className] = new $className($this);
- }
-
- return $this->templates[$className];
- }
-
- /**
- * Helper method to tokenize a Mustache template.
- *
- * @see Mustache_Tokenizer::scan
- *
- * @param string $source
- *
- * @return array Tokens
- */
- private function tokenize($source)
- {
- return $this->getTokenizer()->scan($source);
- }
-
- /**
- * Helper method to parse a Mustache template.
- *
- * @see Mustache_Parser::parse
- *
- * @param string $source
- *
- * @return array Token tree
- */
- private function parse($source)
- {
- return $this->getParser()->parse($this->tokenize($source));
- }
-
- /**
- * Helper method to compile a Mustache template.
- *
- * @see Mustache_Compiler::compile
- *
- * @param string $source
- *
- * @return string generated Mustache template class code
- */
- private function compile($source)
- {
- $tree = $this->parse($source);
- $name = $this->getTemplateClassName($source);
-
- return $this->getCompiler()->compile($source, $tree, $name, isset($this->escape), $this->charset);
- }
-
- /**
- * Helper method to generate a Mustache Template class cache filename.
- *
- * @param string $source
- *
- * @return string Mustache Template class cache filename
- */
- private function getCacheFilename($source)
- {
- if ($this->cache) {
- return sprintf('%s/%s.php', $this->cache, $this->getTemplateClassName($source));
- }
- }
-
- /**
- * Helper method to dump a generated Mustache Template subclass to the file cache.
- *
- * @throws RuntimeException if unable to write to $fileName.
- *
- * @param string $fileName
- * @param string $source
- *
- * @codeCoverageIgnore
- */
- private function writeCacheFile($fileName, $source)
- {
- if (!is_dir(dirname($fileName))) {
- mkdir(dirname($fileName), 0777, true);
- }
-
- $tempFile = tempnam(dirname($fileName), basename($fileName));
- if (false !== @file_put_contents($tempFile, $source)) {
- if (@rename($tempFile, $fileName)) {
- chmod($fileName, 0644);
-
- return;
- }
- }
-
- throw new RuntimeException(sprintf('Failed to write cache file "%s".', $fileName));
- }
-}
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A Mustache implementation in PHP.
+ *
+ * {@link http://defunkt.github.com/mustache}
+ *
+ * Mustache is a framework-agnostic logic-less templating language. It enforces separation of view
+ * logic from template files. In fact, it is not even possible to embed logic in the template.
+ *
+ * This is very, very rad.
+ *
+ * @author Justin Hileman {@link http://justinhileman.com}
+ */
+class Mustache_Engine
+{
+ const VERSION = '2.14.2';
+ const SPEC_VERSION = '1.3.0';
+
+ const PRAGMA_FILTERS = 'FILTERS';
+ const PRAGMA_BLOCKS = 'BLOCKS';
+ const PRAGMA_ANCHORED_DOT = 'ANCHORED-DOT';
+ const PRAGMA_DYNAMIC_NAMES = 'DYNAMIC-NAMES';
+
+ // Known pragmas
+ private static $knownPragmas = array(
+ self::PRAGMA_FILTERS => true,
+ self::PRAGMA_BLOCKS => true,
+ self::PRAGMA_ANCHORED_DOT => true,
+ self::PRAGMA_DYNAMIC_NAMES => true,
+ );
+
+ // Template cache
+ private $templates = array();
+
+ // Environment
+ private $templateClassPrefix = '__Mustache_';
+ private $cache;
+ private $lambdaCache;
+ private $cacheLambdaTemplates = false;
+ private $loader;
+ private $partialsLoader;
+ private $helpers;
+ private $escape;
+ private $entityFlags = ENT_COMPAT;
+ private $charset = 'UTF-8';
+ private $logger;
+ private $strictCallables = false;
+ private $pragmas = array();
+ private $delimiters;
+
+ // Services
+ private $tokenizer;
+ private $parser;
+ private $compiler;
+
+ /**
+ * Mustache class constructor.
+ *
+ * Passing an $options array allows overriding certain Mustache options during instantiation:
+ *
+ * $options = array(
+ * // The class prefix for compiled templates. Defaults to '__Mustache_'.
+ * 'template_class_prefix' => '__MyTemplates_',
+ *
+ * // A Mustache cache instance or a cache directory string for compiled templates.
+ * // Mustache will not cache templates unless this is set.
+ * 'cache' => dirname(__FILE__).'/tmp/cache/mustache',
+ *
+ * // Override default permissions for cache files. Defaults to using the system-defined umask. It is
+ * // *strongly* recommended that you configure your umask properly rather than overriding permissions here.
+ * 'cache_file_mode' => 0666,
+ *
+ * // Optionally, enable caching for lambda section templates. This is generally not recommended, as lambda
+ * // sections are often too dynamic to benefit from caching.
+ * 'cache_lambda_templates' => true,
+ *
+ * // Customize the tag delimiters used by this engine instance. Note that overriding here changes the
+ * // delimiters used to parse all templates and partials loaded by this instance. To override just for a
+ * // single template, use an inline "change delimiters" tag at the start of the template file:
+ * //
+ * // {{=<% %>=}}
+ * //
+ * 'delimiters' => '<% %>',
+ *
+ * // A Mustache template loader instance. Uses a StringLoader if not specified.
+ * 'loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'),
+ *
+ * // A Mustache loader instance for partials.
+ * 'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'),
+ *
+ * // An array of Mustache partials. Useful for quick-and-dirty string template loading, but not as
+ * // efficient or lazy as a Filesystem (or database) loader.
+ * 'partials' => array('foo' => file_get_contents(dirname(__FILE__).'/views/partials/foo.mustache')),
+ *
+ * // An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order
+ * // sections), or any other valid Mustache context value. They will be prepended to the context stack,
+ * // so they will be available in any template loaded by this Mustache instance.
+ * 'helpers' => array('i18n' => function ($text) {
+ * // do something translatey here...
+ * }),
+ *
+ * // An 'escape' callback, responsible for escaping double-mustache variables.
+ * 'escape' => function ($value) {
+ * return htmlspecialchars($buffer, ENT_COMPAT, 'UTF-8');
+ * },
+ *
+ * // Type argument for `htmlspecialchars`. Defaults to ENT_COMPAT. You may prefer ENT_QUOTES.
+ * 'entity_flags' => ENT_QUOTES,
+ *
+ * // Character set for `htmlspecialchars`. Defaults to 'UTF-8'. Use 'UTF-8'.
+ * 'charset' => 'ISO-8859-1',
+ *
+ * // A Mustache Logger instance. No logging will occur unless this is set. Using a PSR-3 compatible
+ * // logging library -- such as Monolog -- is highly recommended. A simple stream logger implementation is
+ * // available as well:
+ * 'logger' => new Mustache_Logger_StreamLogger('php://stderr'),
+ *
+ * // Only treat Closure instances and invokable classes as callable. If true, values like
+ * // `array('ClassName', 'methodName')` and `array($classInstance, 'methodName')`, which are traditionally
+ * // "callable" in PHP, are not called to resolve variables for interpolation or section contexts. This
+ * // helps protect against arbitrary code execution when user input is passed directly into the template.
+ * // This currently defaults to false, but will default to true in v3.0.
+ * 'strict_callables' => true,
+ *
+ * // Enable pragmas across all templates, regardless of the presence of pragma tags in the individual
+ * // templates.
+ * 'pragmas' => [Mustache_Engine::PRAGMA_FILTERS],
+ * );
+ *
+ * @throws Mustache_Exception_InvalidArgumentException If `escape` option is not callable
+ *
+ * @param array $options (default: array())
+ */
+ public function __construct(array $options = array())
+ {
+ if (isset($options['template_class_prefix'])) {
+ if ((string) $options['template_class_prefix'] === '') {
+ throw new Mustache_Exception_InvalidArgumentException('Mustache Constructor "template_class_prefix" must not be empty');
+ }
+
+ $this->templateClassPrefix = $options['template_class_prefix'];
+ }
+
+ if (isset($options['cache'])) {
+ $cache = $options['cache'];
+
+ if (is_string($cache)) {
+ $mode = isset($options['cache_file_mode']) ? $options['cache_file_mode'] : null;
+ $cache = new Mustache_Cache_FilesystemCache($cache, $mode);
+ }
+
+ $this->setCache($cache);
+ }
+
+ if (isset($options['cache_lambda_templates'])) {
+ $this->cacheLambdaTemplates = (bool) $options['cache_lambda_templates'];
+ }
+
+ if (isset($options['loader'])) {
+ $this->setLoader($options['loader']);
+ }
+
+ if (isset($options['partials_loader'])) {
+ $this->setPartialsLoader($options['partials_loader']);
+ }
+
+ if (isset($options['partials'])) {
+ $this->setPartials($options['partials']);
+ }
+
+ if (isset($options['helpers'])) {
+ $this->setHelpers($options['helpers']);
+ }
+
+ if (isset($options['escape'])) {
+ if (!is_callable($options['escape'])) {
+ throw new Mustache_Exception_InvalidArgumentException('Mustache Constructor "escape" option must be callable');
+ }
+
+ $this->escape = $options['escape'];
+ }
+
+ if (isset($options['entity_flags'])) {
+ $this->entityFlags = $options['entity_flags'];
+ }
+
+ if (isset($options['charset'])) {
+ $this->charset = $options['charset'];
+ }
+
+ if (isset($options['logger'])) {
+ $this->setLogger($options['logger']);
+ }
+
+ if (isset($options['strict_callables'])) {
+ $this->strictCallables = $options['strict_callables'];
+ }
+
+ if (isset($options['delimiters'])) {
+ $this->delimiters = $options['delimiters'];
+ }
+
+ if (isset($options['pragmas'])) {
+ foreach ($options['pragmas'] as $pragma) {
+ if (!isset(self::$knownPragmas[$pragma])) {
+ throw new Mustache_Exception_InvalidArgumentException(sprintf('Unknown pragma: "%s".', $pragma));
+ }
+ $this->pragmas[$pragma] = true;
+ }
+ }
+ }
+
+ /**
+ * Shortcut 'render' invocation.
+ *
+ * Equivalent to calling `$mustache->loadTemplate($template)->render($context);`
+ *
+ * @see Mustache_Engine::loadTemplate
+ * @see Mustache_Template::render
+ *
+ * @param string $template
+ * @param mixed $context (default: array())
+ *
+ * @return string Rendered template
+ */
+ public function render($template, $context = array())
+ {
+ return $this->loadTemplate($template)->render($context);
+ }
+
+ /**
+ * Get the current Mustache escape callback.
+ *
+ * @return callable|null
+ */
+ public function getEscape()
+ {
+ return $this->escape;
+ }
+
+ /**
+ * Get the current Mustache entitity type to escape.
+ *
+ * @return int
+ */
+ public function getEntityFlags()
+ {
+ return $this->entityFlags;
+ }
+
+ /**
+ * Get the current Mustache character set.
+ *
+ * @return string
+ */
+ public function getCharset()
+ {
+ return $this->charset;
+ }
+
+ /**
+ * Get the current globally enabled pragmas.
+ *
+ * @return array
+ */
+ public function getPragmas()
+ {
+ return array_keys($this->pragmas);
+ }
+
+ /**
+ * Set the Mustache template Loader instance.
+ *
+ * @param Mustache_Loader $loader
+ */
+ public function setLoader(Mustache_Loader $loader)
+ {
+ $this->loader = $loader;
+ }
+
+ /**
+ * Get the current Mustache template Loader instance.
+ *
+ * If no Loader instance has been explicitly specified, this method will instantiate and return
+ * a StringLoader instance.
+ *
+ * @return Mustache_Loader
+ */
+ public function getLoader()
+ {
+ if (!isset($this->loader)) {
+ $this->loader = new Mustache_Loader_StringLoader();
+ }
+
+ return $this->loader;
+ }
+
+ /**
+ * Set the Mustache partials Loader instance.
+ *
+ * @param Mustache_Loader $partialsLoader
+ */
+ public function setPartialsLoader(Mustache_Loader $partialsLoader)
+ {
+ $this->partialsLoader = $partialsLoader;
+ }
+
+ /**
+ * Get the current Mustache partials Loader instance.
+ *
+ * If no Loader instance has been explicitly specified, this method will instantiate and return
+ * an ArrayLoader instance.
+ *
+ * @return Mustache_Loader
+ */
+ public function getPartialsLoader()
+ {
+ if (!isset($this->partialsLoader)) {
+ $this->partialsLoader = new Mustache_Loader_ArrayLoader();
+ }
+
+ return $this->partialsLoader;
+ }
+
+ /**
+ * Set partials for the current partials Loader instance.
+ *
+ * @throws Mustache_Exception_RuntimeException If the current Loader instance is immutable
+ *
+ * @param array $partials (default: array())
+ */
+ public function setPartials(array $partials = array())
+ {
+ if (!isset($this->partialsLoader)) {
+ $this->partialsLoader = new Mustache_Loader_ArrayLoader();
+ }
+
+ if (!$this->partialsLoader instanceof Mustache_Loader_MutableLoader) {
+ throw new Mustache_Exception_RuntimeException('Unable to set partials on an immutable Mustache Loader instance');
+ }
+
+ $this->partialsLoader->setTemplates($partials);
+ }
+
+ /**
+ * Set an array of Mustache helpers.
+ *
+ * An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order sections), or
+ * any other valid Mustache context value. They will be prepended to the context stack, so they will be available in
+ * any template loaded by this Mustache instance.
+ *
+ * @throws Mustache_Exception_InvalidArgumentException if $helpers is not an array or Traversable
+ *
+ * @param array|Traversable $helpers
+ */
+ public function setHelpers($helpers)
+ {
+ if (!is_array($helpers) && !$helpers instanceof Traversable) {
+ throw new Mustache_Exception_InvalidArgumentException('setHelpers expects an array of helpers');
+ }
+
+ $this->getHelpers()->clear();
+
+ foreach ($helpers as $name => $helper) {
+ $this->addHelper($name, $helper);
+ }
+ }
+
+ /**
+ * Get the current set of Mustache helpers.
+ *
+ * @see Mustache_Engine::setHelpers
+ *
+ * @return Mustache_HelperCollection
+ */
+ public function getHelpers()
+ {
+ if (!isset($this->helpers)) {
+ $this->helpers = new Mustache_HelperCollection();
+ }
+
+ return $this->helpers;
+ }
+
+ /**
+ * Add a new Mustache helper.
+ *
+ * @see Mustache_Engine::setHelpers
+ *
+ * @param string $name
+ * @param mixed $helper
+ */
+ public function addHelper($name, $helper)
+ {
+ $this->getHelpers()->add($name, $helper);
+ }
+
+ /**
+ * Get a Mustache helper by name.
+ *
+ * @see Mustache_Engine::setHelpers
+ *
+ * @param string $name
+ *
+ * @return mixed Helper
+ */
+ public function getHelper($name)
+ {
+ return $this->getHelpers()->get($name);
+ }
+
+ /**
+ * Check whether this Mustache instance has a helper.
+ *
+ * @see Mustache_Engine::setHelpers
+ *
+ * @param string $name
+ *
+ * @return bool True if the helper is present
+ */
+ public function hasHelper($name)
+ {
+ return $this->getHelpers()->has($name);
+ }
+
+ /**
+ * Remove a helper by name.
+ *
+ * @see Mustache_Engine::setHelpers
+ *
+ * @param string $name
+ */
+ public function removeHelper($name)
+ {
+ $this->getHelpers()->remove($name);
+ }
+
+ /**
+ * Set the Mustache Logger instance.
+ *
+ * @throws Mustache_Exception_InvalidArgumentException If logger is not an instance of Mustache_Logger or Psr\Log\LoggerInterface
+ *
+ * @param Mustache_Logger|Psr\Log\LoggerInterface $logger
+ */
+ public function setLogger($logger = null)
+ {
+ if ($logger !== null && !($logger instanceof Mustache_Logger || is_a($logger, 'Psr\\Log\\LoggerInterface'))) {
+ throw new Mustache_Exception_InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.');
+ }
+
+ if ($this->getCache()->getLogger() === null) {
+ $this->getCache()->setLogger($logger);
+ }
+
+ $this->logger = $logger;
+ }
+
+ /**
+ * Get the current Mustache Logger instance.
+ *
+ * @return Mustache_Logger|Psr\Log\LoggerInterface
+ */
+ public function getLogger()
+ {
+ return $this->logger;
+ }
+
+ /**
+ * Set the Mustache Tokenizer instance.
+ *
+ * @param Mustache_Tokenizer $tokenizer
+ */
+ public function setTokenizer(Mustache_Tokenizer $tokenizer)
+ {
+ $this->tokenizer = $tokenizer;
+ }
+
+ /**
+ * Get the current Mustache Tokenizer instance.
+ *
+ * If no Tokenizer instance has been explicitly specified, this method will instantiate and return a new one.
+ *
+ * @return Mustache_Tokenizer
+ */
+ public function getTokenizer()
+ {
+ if (!isset($this->tokenizer)) {
+ $this->tokenizer = new Mustache_Tokenizer();
+ }
+
+ return $this->tokenizer;
+ }
+
+ /**
+ * Set the Mustache Parser instance.
+ *
+ * @param Mustache_Parser $parser
+ */
+ public function setParser(Mustache_Parser $parser)
+ {
+ $this->parser = $parser;
+ }
+
+ /**
+ * Get the current Mustache Parser instance.
+ *
+ * If no Parser instance has been explicitly specified, this method will instantiate and return a new one.
+ *
+ * @return Mustache_Parser
+ */
+ public function getParser()
+ {
+ if (!isset($this->parser)) {
+ $this->parser = new Mustache_Parser();
+ }
+
+ return $this->parser;
+ }
+
+ /**
+ * Set the Mustache Compiler instance.
+ *
+ * @param Mustache_Compiler $compiler
+ */
+ public function setCompiler(Mustache_Compiler $compiler)
+ {
+ $this->compiler = $compiler;
+ }
+
+ /**
+ * Get the current Mustache Compiler instance.
+ *
+ * If no Compiler instance has been explicitly specified, this method will instantiate and return a new one.
+ *
+ * @return Mustache_Compiler
+ */
+ public function getCompiler()
+ {
+ if (!isset($this->compiler)) {
+ $this->compiler = new Mustache_Compiler();
+ }
+
+ return $this->compiler;
+ }
+
+ /**
+ * Set the Mustache Cache instance.
+ *
+ * @param Mustache_Cache $cache
+ */
+ public function setCache(Mustache_Cache $cache)
+ {
+ if (isset($this->logger) && $cache->getLogger() === null) {
+ $cache->setLogger($this->getLogger());
+ }
+
+ $this->cache = $cache;
+ }
+
+ /**
+ * Get the current Mustache Cache instance.
+ *
+ * If no Cache instance has been explicitly specified, this method will instantiate and return a new one.
+ *
+ * @return Mustache_Cache
+ */
+ public function getCache()
+ {
+ if (!isset($this->cache)) {
+ $this->setCache(new Mustache_Cache_NoopCache());
+ }
+
+ return $this->cache;
+ }
+
+ /**
+ * Get the current Lambda Cache instance.
+ *
+ * If 'cache_lambda_templates' is enabled, this is the default cache instance. Otherwise, it is a NoopCache.
+ *
+ * @see Mustache_Engine::getCache
+ *
+ * @return Mustache_Cache
+ */
+ protected function getLambdaCache()
+ {
+ if ($this->cacheLambdaTemplates) {
+ return $this->getCache();
+ }
+
+ if (!isset($this->lambdaCache)) {
+ $this->lambdaCache = new Mustache_Cache_NoopCache();
+ }
+
+ return $this->lambdaCache;
+ }
+
+ /**
+ * Helper method to generate a Mustache template class.
+ *
+ * This method must be updated any time options are added which make it so
+ * the same template could be parsed and compiled multiple different ways.
+ *
+ * @param string|Mustache_Source $source
+ *
+ * @return string Mustache Template class name
+ */
+ public function getTemplateClassName($source)
+ {
+ // For the most part, adding a new option here should do the trick.
+ //
+ // Pick a value here which is unique for each possible way the template
+ // could be compiled... but not necessarily unique per option value. See
+ // escape below, which only needs to differentiate between 'custom' and
+ // 'default' escapes.
+ //
+ // Keep this list in alphabetical order :)
+ $chunks = array(
+ 'charset' => $this->charset,
+ 'delimiters' => $this->delimiters ? $this->delimiters : '{{ }}',
+ 'entityFlags' => $this->entityFlags,
+ 'escape' => isset($this->escape) ? 'custom' : 'default',
+ 'key' => ($source instanceof Mustache_Source) ? $source->getKey() : 'source',
+ 'pragmas' => $this->getPragmas(),
+ 'strictCallables' => $this->strictCallables,
+ 'version' => self::VERSION,
+ );
+
+ $key = json_encode($chunks);
+
+ // Template Source instances have already provided their own source key. For strings, just include the whole
+ // source string in the md5 hash.
+ if (!$source instanceof Mustache_Source) {
+ $key .= "\n" . $source;
+ }
+
+ return $this->templateClassPrefix . md5($key);
+ }
+
+ /**
+ * Load a Mustache Template by name.
+ *
+ * @param string $name
+ *
+ * @return Mustache_Template
+ */
+ public function loadTemplate($name)
+ {
+ return $this->loadSource($this->getLoader()->load($name));
+ }
+
+ /**
+ * Load a Mustache partial Template by name.
+ *
+ * This is a helper method used internally by Template instances for loading partial templates. You can most likely
+ * ignore it completely.
+ *
+ * @param string $name
+ *
+ * @return Mustache_Template
+ */
+ public function loadPartial($name)
+ {
+ try {
+ if (isset($this->partialsLoader)) {
+ $loader = $this->partialsLoader;
+ } elseif (isset($this->loader) && !$this->loader instanceof Mustache_Loader_StringLoader) {
+ $loader = $this->loader;
+ } else {
+ throw new Mustache_Exception_UnknownTemplateException($name);
+ }
+
+ return $this->loadSource($loader->load($name));
+ } catch (Mustache_Exception_UnknownTemplateException $e) {
+ // If the named partial cannot be found, log then return null.
+ $this->log(
+ Mustache_Logger::WARNING,
+ 'Partial not found: "{name}"',
+ array('name' => $e->getTemplateName())
+ );
+ }
+ }
+
+ /**
+ * Load a Mustache lambda Template by source.
+ *
+ * This is a helper method used by Template instances to generate subtemplates for Lambda sections. You can most
+ * likely ignore it completely.
+ *
+ * @param string $source
+ * @param string $delims (default: null)
+ *
+ * @return Mustache_Template
+ */
+ public function loadLambda($source, $delims = null)
+ {
+ if ($delims !== null) {
+ $source = $delims . "\n" . $source;
+ }
+
+ return $this->loadSource($source, $this->getLambdaCache());
+ }
+
+ /**
+ * Instantiate and return a Mustache Template instance by source.
+ *
+ * Optionally provide a Mustache_Cache instance. This is used internally by Mustache_Engine::loadLambda to respect
+ * the 'cache_lambda_templates' configuration option.
+ *
+ * @see Mustache_Engine::loadTemplate
+ * @see Mustache_Engine::loadPartial
+ * @see Mustache_Engine::loadLambda
+ *
+ * @param string|Mustache_Source $source
+ * @param Mustache_Cache $cache (default: null)
+ *
+ * @return Mustache_Template
+ */
+ private function loadSource($source, Mustache_Cache $cache = null)
+ {
+ $className = $this->getTemplateClassName($source);
+
+ if (!isset($this->templates[$className])) {
+ if ($cache === null) {
+ $cache = $this->getCache();
+ }
+
+ if (!class_exists($className, false)) {
+ if (!$cache->load($className)) {
+ $compiled = $this->compile($source);
+ $cache->cache($className, $compiled);
+ }
+ }
+
+ $this->log(
+ Mustache_Logger::DEBUG,
+ 'Instantiating template: "{className}"',
+ array('className' => $className)
+ );
+
+ $this->templates[$className] = new $className($this);
+ }
+
+ return $this->templates[$className];
+ }
+
+ /**
+ * Helper method to tokenize a Mustache template.
+ *
+ * @see Mustache_Tokenizer::scan
+ *
+ * @param string $source
+ *
+ * @return array Tokens
+ */
+ private function tokenize($source)
+ {
+ return $this->getTokenizer()->scan($source, $this->delimiters);
+ }
+
+ /**
+ * Helper method to parse a Mustache template.
+ *
+ * @see Mustache_Parser::parse
+ *
+ * @param string $source
+ *
+ * @return array Token tree
+ */
+ private function parse($source)
+ {
+ $parser = $this->getParser();
+ $parser->setPragmas($this->getPragmas());
+
+ return $parser->parse($this->tokenize($source));
+ }
+
+ /**
+ * Helper method to compile a Mustache template.
+ *
+ * @see Mustache_Compiler::compile
+ *
+ * @param string|Mustache_Source $source
+ *
+ * @return string generated Mustache template class code
+ */
+ private function compile($source)
+ {
+ $name = $this->getTemplateClassName($source);
+
+ $this->log(
+ Mustache_Logger::INFO,
+ 'Compiling template to "{className}" class',
+ array('className' => $name)
+ );
+
+ if ($source instanceof Mustache_Source) {
+ $source = $source->getSource();
+ }
+ $tree = $this->parse($source);
+
+ $compiler = $this->getCompiler();
+ $compiler->setPragmas($this->getPragmas());
+
+ return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags);
+ }
+
+ /**
+ * Add a log record if logging is enabled.
+ *
+ * @param int $level The logging level
+ * @param string $message The log message
+ * @param array $context The log context
+ */
+ private function log($level, $message, array $context = array())
+ {
+ if (isset($this->logger)) {
+ $this->logger->log($level, $message, $context);
+ }
+ }
+}
diff --git a/Mustache/Exception.php b/Mustache/Exception.php
new file mode 100644
index 0000000..d4001a9
--- /dev/null
+++ b/Mustache/Exception.php
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A Mustache Exception interface.
+ */
+interface Mustache_Exception
+{
+ // This space intentionally left blank.
+}
diff --git a/Mustache/Exception/InvalidArgumentException.php b/Mustache/Exception/InvalidArgumentException.php
new file mode 100644
index 0000000..becf2ed
--- /dev/null
+++ b/Mustache/Exception/InvalidArgumentException.php
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Invalid argument exception.
+ */
+class Mustache_Exception_InvalidArgumentException extends InvalidArgumentException implements Mustache_Exception
+{
+ // This space intentionally left blank.
+}
diff --git a/Mustache/Exception/LogicException.php b/Mustache/Exception/LogicException.php
new file mode 100644
index 0000000..b2424d6
--- /dev/null
+++ b/Mustache/Exception/LogicException.php
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Logic exception.
+ */
+class Mustache_Exception_LogicException extends LogicException implements Mustache_Exception
+{
+ // This space intentionally left blank.
+}
diff --git a/Mustache/Exception/RuntimeException.php b/Mustache/Exception/RuntimeException.php
new file mode 100644
index 0000000..b6369f4
--- /dev/null
+++ b/Mustache/Exception/RuntimeException.php
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Runtime exception.
+ */
+class Mustache_Exception_RuntimeException extends RuntimeException implements Mustache_Exception
+{
+ // This space intentionally left blank.
+}
diff --git a/Mustache/Exception/SyntaxException.php b/Mustache/Exception/SyntaxException.php
new file mode 100644
index 0000000..b1879a3
--- /dev/null
+++ b/Mustache/Exception/SyntaxException.php
@@ -0,0 +1,41 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache syntax exception.
+ */
+class Mustache_Exception_SyntaxException extends LogicException implements Mustache_Exception
+{
+ protected $token;
+
+ /**
+ * @param string $msg
+ * @param array $token
+ * @param Exception $previous
+ */
+ public function __construct($msg, array $token, Exception $previous = null)
+ {
+ $this->token = $token;
+ if (version_compare(PHP_VERSION, '5.3.0', '>=')) {
+ parent::__construct($msg, 0, $previous);
+ } else {
+ parent::__construct($msg); // @codeCoverageIgnore
+ }
+ }
+
+ /**
+ * @return array
+ */
+ public function getToken()
+ {
+ return $this->token;
+ }
+}
diff --git a/Mustache/Exception/UnknownFilterException.php b/Mustache/Exception/UnknownFilterException.php
new file mode 100644
index 0000000..0651c17
--- /dev/null
+++ b/Mustache/Exception/UnknownFilterException.php
@@ -0,0 +1,38 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Unknown filter exception.
+ */
+class Mustache_Exception_UnknownFilterException extends UnexpectedValueException implements Mustache_Exception
+{
+ protected $filterName;
+
+ /**
+ * @param string $filterName
+ * @param Exception $previous
+ */
+ public function __construct($filterName, Exception $previous = null)
+ {
+ $this->filterName = $filterName;
+ $message = sprintf('Unknown filter: %s', $filterName);
+ if (version_compare(PHP_VERSION, '5.3.0', '>=')) {
+ parent::__construct($message, 0, $previous);
+ } else {
+ parent::__construct($message); // @codeCoverageIgnore
+ }
+ }
+
+ public function getFilterName()
+ {
+ return $this->filterName;
+ }
+}
diff --git a/Mustache/Exception/UnknownHelperException.php b/Mustache/Exception/UnknownHelperException.php
new file mode 100644
index 0000000..193be78
--- /dev/null
+++ b/Mustache/Exception/UnknownHelperException.php
@@ -0,0 +1,38 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Unknown helper exception.
+ */
+class Mustache_Exception_UnknownHelperException extends InvalidArgumentException implements Mustache_Exception
+{
+ protected $helperName;
+
+ /**
+ * @param string $helperName
+ * @param Exception $previous
+ */
+ public function __construct($helperName, Exception $previous = null)
+ {
+ $this->helperName = $helperName;
+ $message = sprintf('Unknown helper: %s', $helperName);
+ if (version_compare(PHP_VERSION, '5.3.0', '>=')) {
+ parent::__construct($message, 0, $previous);
+ } else {
+ parent::__construct($message); // @codeCoverageIgnore
+ }
+ }
+
+ public function getHelperName()
+ {
+ return $this->helperName;
+ }
+}
diff --git a/Mustache/Exception/UnknownTemplateException.php b/Mustache/Exception/UnknownTemplateException.php
new file mode 100644
index 0000000..32a778a
--- /dev/null
+++ b/Mustache/Exception/UnknownTemplateException.php
@@ -0,0 +1,38 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Unknown template exception.
+ */
+class Mustache_Exception_UnknownTemplateException extends InvalidArgumentException implements Mustache_Exception
+{
+ protected $templateName;
+
+ /**
+ * @param string $templateName
+ * @param Exception $previous
+ */
+ public function __construct($templateName, Exception $previous = null)
+ {
+ $this->templateName = $templateName;
+ $message = sprintf('Unknown template: %s', $templateName);
+ if (version_compare(PHP_VERSION, '5.3.0', '>=')) {
+ parent::__construct($message, 0, $previous);
+ } else {
+ parent::__construct($message); // @codeCoverageIgnore
+ }
+ }
+
+ public function getTemplateName()
+ {
+ return $this->templateName;
+ }
+}
diff --git a/Mustache/HelperCollection.php b/Mustache/HelperCollection.php
index 92bcde4..5d8f73c 100644
--- a/Mustache/HelperCollection.php
+++ b/Mustache/HelperCollection.php
@@ -1,168 +1,172 @@
-<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * A collection of helpers for a Mustache instance.
- */
-class Mustache_HelperCollection
-{
- private $helpers = array();
-
- /**
- * Helper Collection constructor.
- *
- * Optionally accepts an array (or Traversable) of `$name => $helper` pairs.
- *
- * @throws InvalidArgumentException if the $helpers argument isn't an array or Traversable
- *
- * @param array|Traversable $helpers (default: null)
- */
- public function __construct($helpers = null)
- {
- if ($helpers !== null) {
- if (!is_array($helpers) && !$helpers instanceof Traversable) {
- throw new InvalidArgumentException('HelperCollection constructor expects an array of helpers');
- }
-
- foreach ($helpers as $name => $helper) {
- $this->add($name, $helper);
- }
- }
- }
-
- /**
- * Magic mutator.
- *
- * @see Mustache_HelperCollection::add
- *
- * @param string $name
- * @param mixed $helper
- */
- public function __set($name, $helper)
- {
- $this->add($name, $helper);
- }
-
- /**
- * Add a helper to this collection.
- *
- * @param string $name
- * @param mixed $helper
- */
- public function add($name, $helper)
- {
- $this->helpers[$name] = $helper;
- }
-
- /**
- * Magic accessor.
- *
- * @see Mustache_HelperCollection::get
- *
- * @param string $name
- *
- * @return mixed Helper
- */
- public function __get($name)
- {
- return $this->get($name);
- }
-
- /**
- * Get a helper by name.
- *
- * @param string $name
- *
- * @return mixed Helper
- */
- public function get($name)
- {
- if (!$this->has($name)) {
- throw new InvalidArgumentException('Unknown helper: '.$name);
- }
-
- return $this->helpers[$name];
- }
-
- /**
- * Magic isset().
- *
- * @see Mustache_HelperCollection::has
- *
- * @param string $name
- *
- * @return boolean True if helper is present
- */
- public function __isset($name)
- {
- return $this->has($name);
- }
-
- /**
- * Check whether a given helper is present in the collection.
- *
- * @param string $name
- *
- * @return boolean True if helper is present
- */
- public function has($name)
- {
- return array_key_exists($name, $this->helpers);
- }
-
- /**
- * Magic unset().
- *
- * @see Mustache_HelperCollection::remove
- *
- * @param string $name
- */
- public function __unset($name)
- {
- $this->remove($name);
- }
-
- /**
- * Check whether a given helper is present in the collection.
- *
- * @throws InvalidArgumentException if the requested helper is not present.
- *
- * @param string $name
- */
- public function remove($name)
- {
- if (!$this->has($name)) {
- throw new InvalidArgumentException('Unknown helper: '.$name);
- }
-
- unset($this->helpers[$name]);
- }
-
- /**
- * Clear the helper collection.
- *
- * Removes all helpers from this collection
- */
- public function clear()
- {
- $this->helpers = array();
- }
-
- /**
- * Check whether the helper collection is empty.
- *
- * @return boolean True if the collection is empty
- */
- public function isEmpty()
- {
- return empty($this->helpers);
- }
-}
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A collection of helpers for a Mustache instance.
+ */
+class Mustache_HelperCollection
+{
+ private $helpers = array();
+
+ /**
+ * Helper Collection constructor.
+ *
+ * Optionally accepts an array (or Traversable) of `$name => $helper` pairs.
+ *
+ * @throws Mustache_Exception_InvalidArgumentException if the $helpers argument isn't an array or Traversable
+ *
+ * @param array|Traversable $helpers (default: null)
+ */
+ public function __construct($helpers = null)
+ {
+ if ($helpers === null) {
+ return;
+ }
+
+ if (!is_array($helpers) && !$helpers instanceof Traversable) {
+ throw new Mustache_Exception_InvalidArgumentException('HelperCollection constructor expects an array of helpers');
+ }
+
+ foreach ($helpers as $name => $helper) {
+ $this->add($name, $helper);
+ }
+ }
+
+ /**
+ * Magic mutator.
+ *
+ * @see Mustache_HelperCollection::add
+ *
+ * @param string $name
+ * @param mixed $helper
+ */
+ public function __set($name, $helper)
+ {
+ $this->add($name, $helper);
+ }
+
+ /**
+ * Add a helper to this collection.
+ *
+ * @param string $name
+ * @param mixed $helper
+ */
+ public function add($name, $helper)
+ {
+ $this->helpers[$name] = $helper;
+ }
+
+ /**
+ * Magic accessor.
+ *
+ * @see Mustache_HelperCollection::get
+ *
+ * @param string $name
+ *
+ * @return mixed Helper
+ */
+ public function __get($name)
+ {
+ return $this->get($name);
+ }
+
+ /**
+ * Get a helper by name.
+ *
+ * @throws Mustache_Exception_UnknownHelperException If helper does not exist
+ *
+ * @param string $name
+ *
+ * @return mixed Helper
+ */
+ public function get($name)
+ {
+ if (!$this->has($name)) {
+ throw new Mustache_Exception_UnknownHelperException($name);
+ }
+
+ return $this->helpers[$name];
+ }
+
+ /**
+ * Magic isset().
+ *
+ * @see Mustache_HelperCollection::has
+ *
+ * @param string $name
+ *
+ * @return bool True if helper is present
+ */
+ public function __isset($name)
+ {
+ return $this->has($name);
+ }
+
+ /**
+ * Check whether a given helper is present in the collection.
+ *
+ * @param string $name
+ *
+ * @return bool True if helper is present
+ */
+ public function has($name)
+ {
+ return array_key_exists($name, $this->helpers);
+ }
+
+ /**
+ * Magic unset().
+ *
+ * @see Mustache_HelperCollection::remove
+ *
+ * @param string $name
+ */
+ public function __unset($name)
+ {
+ $this->remove($name);
+ }
+
+ /**
+ * Check whether a given helper is present in the collection.
+ *
+ * @throws Mustache_Exception_UnknownHelperException if the requested helper is not present
+ *
+ * @param string $name
+ */
+ public function remove($name)
+ {
+ if (!$this->has($name)) {
+ throw new Mustache_Exception_UnknownHelperException($name);
+ }
+
+ unset($this->helpers[$name]);
+ }
+
+ /**
+ * Clear the helper collection.
+ *
+ * Removes all helpers from this collection
+ */
+ public function clear()
+ {
+ $this->helpers = array();
+ }
+
+ /**
+ * Check whether the helper collection is empty.
+ *
+ * @return bool True if the collection is empty
+ */
+ public function isEmpty()
+ {
+ return empty($this->helpers);
+ }
+}
diff --git a/Mustache/LICENSE b/Mustache/LICENSE
index 63c95ac..e0aecc9 100644
--- a/Mustache/LICENSE
+++ b/Mustache/LICENSE
@@ -1,22 +1,21 @@
-Copyright (c) 2010 Justin Hileman
+The MIT License (MIT)
-Permission is hereby granted, free of charge, to any person
-obtaining a copy of this software and associated documentation
-files (the "Software"), to deal in the Software without
-restriction, including without limitation the rights to use,
-copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the
-Software is furnished to do so, subject to the following
-conditions:
+Copyright (c) 2010-2015 Justin Hileman
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
-OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-OTHER DEALINGS IN THE SOFTWARE.
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
+OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Mustache/LambdaHelper.php b/Mustache/LambdaHelper.php
new file mode 100644
index 0000000..e93dbfa
--- /dev/null
+++ b/Mustache/LambdaHelper.php
@@ -0,0 +1,76 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Lambda Helper.
+ *
+ * Passed as the second argument to section lambdas (higher order sections),
+ * giving them access to a `render` method for rendering a string with the
+ * current context.
+ */
+class Mustache_LambdaHelper
+{
+ private $mustache;
+ private $context;
+ private $delims;
+
+ /**
+ * Mustache Lambda Helper constructor.
+ *
+ * @param Mustache_Engine $mustache Mustache engine instance
+ * @param Mustache_Context $context Rendering context
+ * @param string $delims Optional custom delimiters, in the format `{{= <% %> =}}`. (default: null)
+ */
+ public function __construct(Mustache_Engine $mustache, Mustache_Context $context, $delims = null)
+ {
+ $this->mustache = $mustache;
+ $this->context = $context;
+ $this->delims = $delims;
+ }
+
+ /**
+ * Render a string as a Mustache template with the current rendering context.
+ *
+ * @param string $string
+ *
+ * @return string Rendered template
+ */
+ public function render($string)
+ {
+ return $this->mustache
+ ->loadLambda((string) $string, $this->delims)
+ ->renderInternal($this->context);
+ }
+
+ /**
+ * Render a string as a Mustache template with the current rendering context.
+ *
+ * @param string $string
+ *
+ * @return string Rendered template
+ */
+ public function __invoke($string)
+ {
+ return $this->render($string);
+ }
+
+ /**
+ * Get a Lambda Helper with custom delimiters.
+ *
+ * @param string $delims Custom delimiters, in the format `{{= <% %> =}}`
+ *
+ * @return Mustache_LambdaHelper
+ */
+ public function withDelimiters($delims)
+ {
+ return new self($this->mustache, $this->context, $delims);
+ }
+}
diff --git a/Mustache/Loader.php b/Mustache/Loader.php
index f082cf5..23adba1 100644
--- a/Mustache/Loader.php
+++ b/Mustache/Loader.php
@@ -1,26 +1,27 @@
-<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Mustache Template Loader interface.
- */
-interface Mustache_Loader
-{
-
- /**
- * Load a Template by name.
- *
- * @param string $name
- *
- * @return string Mustache Template source
- */
- public function load($name);
-}
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template Loader interface.
+ */
+interface Mustache_Loader
+{
+ /**
+ * Load a Template by name.
+ *
+ * @throws Mustache_Exception_UnknownTemplateException If a template file is not found
+ *
+ * @param string $name
+ *
+ * @return string|Mustache_Source Mustache Template source
+ */
+ public function load($name);
+}
diff --git a/Mustache/Loader/ArrayLoader.php b/Mustache/Loader/ArrayLoader.php
index 0a9ceef..4276493 100644
--- a/Mustache/Loader/ArrayLoader.php
+++ b/Mustache/Loader/ArrayLoader.php
@@ -1,79 +1,79 @@
-<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Mustache Template array Loader implementation.
- *
- * An ArrayLoader instance loads Mustache Template source by name from an initial array:
- *
- * $loader = new ArrayLoader(
- * 'foo' => '{{ bar }}',
- * 'baz' => 'Hey {{ qux }}!'
- * );
- *
- * $tpl = $loader->load('foo'); // '{{ bar }}'
- *
- * The ArrayLoader is used internally as a partials loader by Mustache_Engine instance when an array of partials
- * is set. It can also be used as a quick-and-dirty Template loader.
- *
- * @implements Loader
- * @implements MutableLoader
- */
-class Mustache_Loader_ArrayLoader implements Mustache_Loader, Mustache_Loader_MutableLoader
-{
-
- /**
- * ArrayLoader constructor.
- *
- * @param array $templates Associative array of Template source (default: array())
- */
- public function __construct(array $templates = array())
- {
- $this->templates = $templates;
- }
-
- /**
- * Load a Template.
- *
- * @param string $name
- *
- * @return string Mustache Template source
- */
- public function load($name)
- {
- if (!isset($this->templates[$name])) {
- throw new InvalidArgumentException('Template '.$name.' not found.');
- }
-
- return $this->templates[$name];
- }
-
- /**
- * Set an associative array of Template sources for this loader.
- *
- * @param array $templates
- */
- public function setTemplates(array $templates)
- {
- $this->templates = $templates;
- }
-
- /**
- * Set a Template source by name.
- *
- * @param string $name
- * @param string $template Mustache Template source
- */
- public function setTemplate($name, $template)
- {
- $this->templates[$name] = $template;
- }
-}
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template array Loader implementation.
+ *
+ * An ArrayLoader instance loads Mustache Template source by name from an initial array:
+ *
+ * $loader = new ArrayLoader(
+ * 'foo' => '{{ bar }}',
+ * 'baz' => 'Hey {{ qux }}!'
+ * );
+ *
+ * $tpl = $loader->load('foo'); // '{{ bar }}'
+ *
+ * The ArrayLoader is used internally as a partials loader by Mustache_Engine instance when an array of partials
+ * is set. It can also be used as a quick-and-dirty Template loader.
+ */
+class Mustache_Loader_ArrayLoader implements Mustache_Loader, Mustache_Loader_MutableLoader
+{
+ private $templates;
+
+ /**
+ * ArrayLoader constructor.
+ *
+ * @param array $templates Associative array of Template source (default: array())
+ */
+ public function __construct(array $templates = array())
+ {
+ $this->templates = $templates;
+ }
+
+ /**
+ * Load a Template.
+ *
+ * @throws Mustache_Exception_UnknownTemplateException If a template file is not found
+ *
+ * @param string $name
+ *
+ * @return string Mustache Template source
+ */
+ public function load($name)
+ {
+ if (!isset($this->templates[$name])) {
+ throw new Mustache_Exception_UnknownTemplateException($name);
+ }
+
+ return $this->templates[$name];
+ }
+
+ /**
+ * Set an associative array of Template sources for this loader.
+ *
+ * @param array $templates
+ */
+ public function setTemplates(array $templates)
+ {
+ $this->templates = $templates;
+ }
+
+ /**
+ * Set a Template source by name.
+ *
+ * @param string $name
+ * @param string $template Mustache Template source
+ */
+ public function setTemplate($name, $template)
+ {
+ $this->templates[$name] = $template;
+ }
+}
diff --git a/Mustache/Loader/CascadingLoader.php b/Mustache/Loader/CascadingLoader.php
new file mode 100644
index 0000000..3fb6353
--- /dev/null
+++ b/Mustache/Loader/CascadingLoader.php
@@ -0,0 +1,69 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A Mustache Template cascading loader implementation, which delegates to other
+ * Loader instances.
+ */
+class Mustache_Loader_CascadingLoader implements Mustache_Loader
+{
+ private $loaders;
+
+ /**
+ * Construct a CascadingLoader with an array of loaders.
+ *
+ * $loader = new Mustache_Loader_CascadingLoader(array(
+ * new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__),
+ * new Mustache_Loader_FilesystemLoader(__DIR__.'/templates')
+ * ));
+ *
+ * @param Mustache_Loader[] $loaders
+ */
+ public function __construct(array $loaders = array())
+ {
+ $this->loaders = array();
+ foreach ($loaders as $loader) {
+ $this->addLoader($loader);
+ }
+ }
+
+ /**
+ * Add a Loader instance.
+ *
+ * @param Mustache_Loader $loader
+ */
+ public function addLoader(Mustache_Loader $loader)
+ {
+ $this->loaders[] = $loader;
+ }
+
+ /**
+ * Load a Template by name.
+ *
+ * @throws Mustache_Exception_UnknownTemplateException If a template file is not found
+ *
+ * @param string $name
+ *
+ * @return string Mustache Template source
+ */
+ public function load($name)
+ {
+ foreach ($this->loaders as $loader) {
+ try {
+ return $loader->load($name);
+ } catch (Mustache_Exception_UnknownTemplateException $e) {
+ // do nothing, check the next loader.
+ }
+ }
+
+ throw new Mustache_Exception_UnknownTemplateException($name);
+ }
+}
diff --git a/Mustache/Loader/FilesystemLoader.php b/Mustache/Loader/FilesystemLoader.php
index 34a9ecf..e366df7 100644
--- a/Mustache/Loader/FilesystemLoader.php
+++ b/Mustache/Loader/FilesystemLoader.php
@@ -1,118 +1,135 @@
-<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Mustache Template filesystem Loader implementation.
- *
- * An ArrayLoader instance loads Mustache Template source from the filesystem by name:
- *
- * $loader = new FilesystemLoader(dirname(__FILE__).'/views');
- * $tpl = $loader->load('foo'); // equivalent to `file_get_contents(dirname(__FILE__).'/views/foo.mustache');
- *
- * This is probably the most useful Mustache Loader implementation. It can be used for partials and normal Templates:
- *
- * $m = new Mustache(array(
- * 'loader' => new FilesystemLoader(dirname(__FILE__).'/views'),
- * 'partials_loader' => new FilesystemLoader(dirname(__FILE__).'/views/partials'),
- * ));
- *
- * @implements Loader
- */
-class Mustache_Loader_FilesystemLoader implements Mustache_Loader
-{
- private $baseDir;
- private $extension = '.mustache';
- private $templates = array();
-
- /**
- * Mustache filesystem Loader constructor.
- *
- * Passing an $options array allows overriding certain Loader options during instantiation:
- *
- * $options = array(
- * // The filename extension used for Mustache templates. Defaults to '.mustache'
- * 'extension' => '.ms',
- * );
- *
- * @throws RuntimeException if $baseDir does not exist.
- *
- * @param string $baseDir Base directory containing Mustache template files.
- * @param array $options Array of Loader options (default: array())
- */
- public function __construct($baseDir, array $options = array())
- {
- $this->baseDir = rtrim(realpath($baseDir), '/');
-
- if (!is_dir($this->baseDir)) {
- throw new RuntimeException('FilesystemLoader baseDir must be a directory: '.$baseDir);
- }
-
- if (isset($options['extension'])) {
- $this->extension = '.' . ltrim($options['extension'], '.');
- }
- }
-
- /**
- * Load a Template by name.
- *
- * $loader = new FilesystemLoader(dirname(__FILE__).'/views');
- * $loader->load('admin/dashboard'); // loads "./views/admin/dashboard.mustache";
- *
- * @param string $name
- *
- * @return string Mustache Template source
- */
- public function load($name)
- {
- if (!isset($this->templates[$name])) {
- $this->templates[$name] = $this->loadFile($name);
- }
-
- return $this->templates[$name];
- }
-
- /**
- * Helper function for loading a Mustache file by name.
- *
- * @throws InvalidArgumentException if a template file is not found.
- *
- * @param string $name
- *
- * @return string Mustache Template source
- */
- protected function loadFile($name)
- {
- $fileName = $this->getFileName($name);
-
- if (!file_exists($fileName)) {
- throw new InvalidArgumentException('Template '.$name.' not found.');
- }
-
- return file_get_contents($fileName);
- }
-
- /**
- * Helper function for getting a Mustache template file name.
- *
- * @param string $name
- *
- * @return string Template file name
- */
- protected function getFileName($name)
- {
- $fileName = $this->baseDir . '/' . $name;
- if (substr($fileName, 0 - strlen($this->extension)) !== $this->extension) {
- $fileName .= $this->extension;
- }
-
- return $fileName;
- }
-}
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template filesystem Loader implementation.
+ *
+ * A FilesystemLoader instance loads Mustache Template source from the filesystem by name:
+ *
+ * $loader = new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views');
+ * $tpl = $loader->load('foo'); // equivalent to `file_get_contents(dirname(__FILE__).'/views/foo.mustache');
+ *
+ * This is probably the most useful Mustache Loader implementation. It can be used for partials and normal Templates:
+ *
+ * $m = new Mustache(array(
+ * 'loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'),
+ * 'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'),
+ * ));
+ */
+class Mustache_Loader_FilesystemLoader implements Mustache_Loader
+{
+ private $baseDir;
+ private $extension = '.mustache';
+ private $templates = array();
+
+ /**
+ * Mustache filesystem Loader constructor.
+ *
+ * Passing an $options array allows overriding certain Loader options during instantiation:
+ *
+ * $options = array(
+ * // The filename extension used for Mustache templates. Defaults to '.mustache'
+ * 'extension' => '.ms',
+ * );
+ *
+ * @throws Mustache_Exception_RuntimeException if $baseDir does not exist
+ *
+ * @param string $baseDir Base directory containing Mustache template files
+ * @param array $options Array of Loader options (default: array())
+ */
+ public function __construct($baseDir, array $options = array())
+ {
+ $this->baseDir = $baseDir;
+
+ if (strpos($this->baseDir, '://') === false) {
+ $this->baseDir = realpath($this->baseDir);
+ }
+
+ if ($this->shouldCheckPath() && !is_dir($this->baseDir)) {
+ throw new Mustache_Exception_RuntimeException(sprintf('FilesystemLoader baseDir must be a directory: %s', $baseDir));
+ }
+
+ if (array_key_exists('extension', $options)) {
+ if (empty($options['extension'])) {
+ $this->extension = '';
+ } else {
+ $this->extension = '.' . ltrim($options['extension'], '.');
+ }
+ }
+ }
+
+ /**
+ * Load a Template by name.
+ *
+ * $loader = new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views');
+ * $loader->load('admin/dashboard'); // loads "./views/admin/dashboard.mustache";
+ *
+ * @param string $name
+ *
+ * @return string Mustache Template source
+ */
+ public function load($name)
+ {
+ if (!isset($this->templates[$name])) {
+ $this->templates[$name] = $this->loadFile($name);
+ }
+
+ return $this->templates[$name];
+ }
+
+ /**
+ * Helper function for loading a Mustache file by name.
+ *
+ * @throws Mustache_Exception_UnknownTemplateException If a template file is not found
+ *
+ * @param string $name
+ *
+ * @return string Mustache Template source
+ */
+ protected function loadFile($name)
+ {
+ $fileName = $this->getFileName($name);
+
+ if ($this->shouldCheckPath() && !file_exists($fileName)) {
+ throw new Mustache_Exception_UnknownTemplateException($name);
+ }
+
+ return file_get_contents($fileName);
+ }
+
+ /**
+ * Helper function for getting a Mustache template file name.
+ *
+ * @param string $name
+ *
+ * @return string Template file name
+ */
+ protected function getFileName($name)
+ {
+ $fileName = $this->baseDir . '/' . $name;
+ if (substr($fileName, 0 - strlen($this->extension)) !== $this->extension) {
+ $fileName .= $this->extension;
+ }
+
+ return $fileName;
+ }
+
+ /**
+ * Only check if baseDir is a directory and requested templates are files if
+ * baseDir is using the filesystem stream wrapper.
+ *
+ * @return bool Whether to check `is_dir` and `file_exists`
+ */
+ protected function shouldCheckPath()
+ {
+ return strpos($this->baseDir, '://') === false || strpos($this->baseDir, 'file://') === 0;
+ }
+}
diff --git a/Mustache/Loader/InlineLoader.php b/Mustache/Loader/InlineLoader.php
new file mode 100644
index 0000000..ae297fe
--- /dev/null
+++ b/Mustache/Loader/InlineLoader.php
@@ -0,0 +1,123 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A Mustache Template loader for inline templates.
+ *
+ * With the InlineLoader, templates can be defined at the end of any PHP source
+ * file:
+ *
+ * $loader = new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__);
+ * $hello = $loader->load('hello');
+ * $goodbye = $loader->load('goodbye');
+ *
+ * __halt_compiler();
+ *
+ * @@ hello
+ * Hello, {{ planet }}!
+ *
+ * @@ goodbye
+ * Goodbye, cruel {{ planet }}
+ *
+ * Templates are deliniated by lines containing only `@@ name`.
+ *
+ * The InlineLoader is well-suited to micro-frameworks such as Silex:
+ *
+ * $app->register(new MustacheServiceProvider, array(
+ * 'mustache.loader' => new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__)
+ * ));
+ *
+ * $app->get('/{name}', function ($name) use ($app) {
+ * return $app['mustache']->render('hello', compact('name'));
+ * })
+ * ->value('name', 'world');
+ *
+ * // ...
+ *
+ * __halt_compiler();
+ *
+ * @@ hello
+ * Hello, {{ name }}!
+ */
+class Mustache_Loader_InlineLoader implements Mustache_Loader
+{
+ protected $fileName;
+ protected $offset;
+ protected $templates;
+
+ /**
+ * The InlineLoader requires a filename and offset to process templates.
+ *
+ * The magic constants `__FILE__` and `__COMPILER_HALT_OFFSET__` are usually
+ * perfectly suited to the job:
+ *
+ * $loader = new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__);
+ *
+ * Note that this only works if the loader is instantiated inside the same
+ * file as the inline templates. If the templates are located in another
+ * file, it would be necessary to manually specify the filename and offset.
+ *
+ * @param string $fileName The file to parse for inline templates
+ * @param int $offset A string offset for the start of the templates.
+ * This usually coincides with the `__halt_compiler`
+ * call, and the `__COMPILER_HALT_OFFSET__`
+ */
+ public function __construct($fileName, $offset)
+ {
+ if (!is_file($fileName)) {
+ throw new Mustache_Exception_InvalidArgumentException('InlineLoader expects a valid filename.');
+ }
+
+ if (!is_int($offset) || $offset < 0) {
+ throw new Mustache_Exception_InvalidArgumentException('InlineLoader expects a valid file offset.');
+ }
+
+ $this->fileName = $fileName;
+ $this->offset = $offset;
+ }
+
+ /**
+ * Load a Template by name.
+ *
+ * @throws Mustache_Exception_UnknownTemplateException If a template file is not found
+ *
+ * @param string $name
+ *
+ * @return string Mustache Template source
+ */
+ public function load($name)
+ {
+ $this->loadTemplates();
+
+ if (!array_key_exists($name, $this->templates)) {
+ throw new Mustache_Exception_UnknownTemplateException($name);
+ }
+
+ return $this->templates[$name];
+ }
+
+ /**
+ * Parse and load templates from the end of a source file.
+ */
+ protected function loadTemplates()
+ {
+ if ($this->templates === null) {
+ $this->templates = array();
+ $data = file_get_contents($this->fileName, false, null, $this->offset);
+ foreach (preg_split("/^@@(?= [\w\d\.]+$)/m", $data, -1) as $chunk) {
+ if (trim($chunk)) {
+ list($name, $content) = explode("\n", $chunk, 2);
+ $this->templates[trim($name)] = trim($content);
+ }
+ }
+ }
+ }
+}
diff --git a/Mustache/Loader/MutableLoader.php b/Mustache/Loader/MutableLoader.php
index 952db2f..57fe5be 100644
--- a/Mustache/Loader/MutableLoader.php
+++ b/Mustache/Loader/MutableLoader.php
@@ -1,32 +1,31 @@
-<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Mustache Template mutable Loader interface.
- */
-interface Mustache_Loader_MutableLoader
-{
-
- /**
- * Set an associative array of Template sources for this loader.
- *
- * @param array $templates
- */
- public function setTemplates(array $templates);
-
- /**
- * Set a Template source by name.
- *
- * @param string $name
- * @param string $template Mustache Template source
- */
- public function setTemplate($name, $template);
-}
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template mutable Loader interface.
+ */
+interface Mustache_Loader_MutableLoader
+{
+ /**
+ * Set an associative array of Template sources for this loader.
+ *
+ * @param array $templates
+ */
+ public function setTemplates(array $templates);
+
+ /**
+ * Set a Template source by name.
+ *
+ * @param string $name
+ * @param string $template Mustache Template source
+ */
+ public function setTemplate($name, $template);
+}
diff --git a/Mustache/Loader/ProductionFilesystemLoader.php b/Mustache/Loader/ProductionFilesystemLoader.php
new file mode 100644
index 0000000..e735332
--- /dev/null
+++ b/Mustache/Loader/ProductionFilesystemLoader.php
@@ -0,0 +1,86 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template production filesystem Loader implementation.
+ *
+ * A production-ready FilesystemLoader, which doesn't require reading a file if it already exists in the template cache.
+ *
+ * {@inheritdoc}
+ */
+class Mustache_Loader_ProductionFilesystemLoader extends Mustache_Loader_FilesystemLoader
+{
+ private $statProps;
+
+ /**
+ * Mustache production filesystem Loader constructor.
+ *
+ * Passing an $options array allows overriding certain Loader options during instantiation:
+ *
+ * $options = array(
+ * // The filename extension used for Mustache templates. Defaults to '.mustache'
+ * 'extension' => '.ms',
+ * 'stat_props' => array('size', 'mtime'),
+ * );
+ *
+ * Specifying 'stat_props' overrides the stat properties used to invalidate the template cache. By default, this
+ * uses 'mtime' and 'size', but this can be set to any of the properties supported by stat():
+ *
+ * http://php.net/manual/en/function.stat.php
+ *
+ * You can also disable filesystem stat entirely:
+ *
+ * $options = array('stat_props' => null);
+ *
+ * But with great power comes great responsibility. Namely, if you disable stat-based cache invalidation,
+ * YOU MUST CLEAR THE TEMPLATE CACHE YOURSELF when your templates change. Make it part of your build or deploy
+ * process so you don't forget!
+ *
+ * @throws Mustache_Exception_RuntimeException if $baseDir does not exist.
+ *
+ * @param string $baseDir Base directory containing Mustache template files.
+ * @param array $options Array of Loader options (default: array())
+ */
+ public function __construct($baseDir, array $options = array())
+ {
+ parent::__construct($baseDir, $options);
+
+ if (array_key_exists('stat_props', $options)) {
+ if (empty($options['stat_props'])) {
+ $this->statProps = array();
+ } else {
+ $this->statProps = $options['stat_props'];
+ }
+ } else {
+ $this->statProps = array('size', 'mtime');
+ }
+ }
+
+ /**
+ * Helper function for loading a Mustache file by name.
+ *
+ * @throws Mustache_Exception_UnknownTemplateException If a template file is not found.
+ *
+ * @param string $name
+ *
+ * @return Mustache_Source Mustache Template source
+ */
+ protected function loadFile($name)
+ {
+ $fileName = $this->getFileName($name);
+
+ if (!file_exists($fileName)) {
+ throw new Mustache_Exception_UnknownTemplateException($name);
+ }
+
+ return new Mustache_Source_FilesystemSource($fileName, $this->statProps);
+ }
+}
diff --git a/Mustache/Loader/StringLoader.php b/Mustache/Loader/StringLoader.php
index 8f18062..7012c03 100644
--- a/Mustache/Loader/StringLoader.php
+++ b/Mustache/Loader/StringLoader.php
@@ -1,42 +1,39 @@
-<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Mustache Template string Loader implementation.
- *
- * A StringLoader instance is essentially a noop. It simply passes the 'name' argument straight through:
- *
- * $loader = new StringLoader;
- * $tpl = $loader->load('{{ foo }}'); // '{{ foo }}'
- *
- * This is the default Template Loader instance used by Mustache:
- *
- * $m = new Mustache;
- * $tpl = $m->loadTemplate('{{ foo }}');
- * echo $tpl->render(array('foo' => 'bar')); // "bar"
- *
- * @implements Loader
- */
-class Mustache_Loader_StringLoader implements Mustache_Loader
-{
-
- /**
- * Load a Template by source.
- *
- * @param string $name Mustache Template source
- *
- * @return string Mustache Template source
- */
- public function load($name)
- {
- return $name;
- }
-}
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template string Loader implementation.
+ *
+ * A StringLoader instance is essentially a noop. It simply passes the 'name' argument straight through:
+ *
+ * $loader = new StringLoader;
+ * $tpl = $loader->load('{{ foo }}'); // '{{ foo }}'
+ *
+ * This is the default Template Loader instance used by Mustache:
+ *
+ * $m = new Mustache;
+ * $tpl = $m->loadTemplate('{{ foo }}');
+ * echo $tpl->render(array('foo' => 'bar')); // "bar"
+ */
+class Mustache_Loader_StringLoader implements Mustache_Loader
+{
+ /**
+ * Load a Template by source.
+ *
+ * @param string $name Mustache Template source
+ *
+ * @return string Mustache Template source
+ */
+ public function load($name)
+ {
+ return $name;
+ }
+}
diff --git a/Mustache/Logger.php b/Mustache/Logger.php
new file mode 100644
index 0000000..cb4037a
--- /dev/null
+++ b/Mustache/Logger.php
@@ -0,0 +1,126 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Describes a Mustache logger instance.
+ *
+ * This is identical to the Psr\Log\LoggerInterface.
+ *
+ * The message MUST be a string or object implementing __toString().
+ *
+ * The message MAY contain placeholders in the form: {foo} where foo
+ * will be replaced by the context data in key "foo".
+ *
+ * The context array can contain arbitrary data, the only assumption that
+ * can be made by implementors is that if an Exception instance is given
+ * to produce a stack trace, it MUST be in a key named "exception".
+ *
+ * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
+ * for the full interface specification.
+ */
+interface Mustache_Logger
+{
+ /**
+ * Psr\Log compatible log levels.
+ */
+ const EMERGENCY = 'emergency';
+ const ALERT = 'alert';
+ const CRITICAL = 'critical';
+ const ERROR = 'error';
+ const WARNING = 'warning';
+ const NOTICE = 'notice';
+ const INFO = 'info';
+ const DEBUG = 'debug';
+
+ /**
+ * System is unusable.
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function emergency($message, array $context = array());
+
+ /**
+ * Action must be taken immediately.
+ *
+ * Example: Entire website down, database unavailable, etc. This should
+ * trigger the SMS alerts and wake you up.
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function alert($message, array $context = array());
+
+ /**
+ * Critical conditions.
+ *
+ * Example: Application component unavailable, unexpected exception.
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function critical($message, array $context = array());
+
+ /**
+ * Runtime errors that do not require immediate action but should typically
+ * be logged and monitored.
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function error($message, array $context = array());
+
+ /**
+ * Exceptional occurrences that are not errors.
+ *
+ * Example: Use of deprecated APIs, poor use of an API, undesirable things
+ * that are not necessarily wrong.
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function warning($message, array $context = array());
+
+ /**
+ * Normal but significant events.
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function notice($message, array $context = array());
+
+ /**
+ * Interesting events.
+ *
+ * Example: User logs in, SQL logs.
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function info($message, array $context = array());
+
+ /**
+ * Detailed debug information.
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function debug($message, array $context = array());
+
+ /**
+ * Logs with an arbitrary level.
+ *
+ * @param mixed $level
+ * @param string $message
+ * @param array $context
+ */
+ public function log($level, $message, array $context = array());
+}
diff --git a/Mustache/Logger/AbstractLogger.php b/Mustache/Logger/AbstractLogger.php
new file mode 100644
index 0000000..a169f9c
--- /dev/null
+++ b/Mustache/Logger/AbstractLogger.php
@@ -0,0 +1,121 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * This is a simple Logger implementation that other Loggers can inherit from.
+ *
+ * This is identical to the Psr\Log\AbstractLogger.
+ *
+ * It simply delegates all log-level-specific methods to the `log` method to
+ * reduce boilerplate code that a simple Logger that does the same thing with
+ * messages regardless of the error level has to implement.
+ */
+abstract class Mustache_Logger_AbstractLogger implements Mustache_Logger
+{
+ /**
+ * System is unusable.
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function emergency($message, array $context = array())
+ {
+ $this->log(Mustache_Logger::EMERGENCY, $message, $context);
+ }
+
+ /**
+ * Action must be taken immediately.
+ *
+ * Example: Entire website down, database unavailable, etc. This should
+ * trigger the SMS alerts and wake you up.
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function alert($message, array $context = array())
+ {
+ $this->log(Mustache_Logger::ALERT, $message, $context);
+ }
+
+ /**
+ * Critical conditions.
+ *
+ * Example: Application component unavailable, unexpected exception.
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function critical($message, array $context = array())
+ {
+ $this->log(Mustache_Logger::CRITICAL, $message, $context);
+ }
+
+ /**
+ * Runtime errors that do not require immediate action but should typically
+ * be logged and monitored.
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function error($message, array $context = array())
+ {
+ $this->log(Mustache_Logger::ERROR, $message, $context);
+ }
+
+ /**
+ * Exceptional occurrences that are not errors.
+ *
+ * Example: Use of deprecated APIs, poor use of an API, undesirable things
+ * that are not necessarily wrong.
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function warning($message, array $context = array())
+ {
+ $this->log(Mustache_Logger::WARNING, $message, $context);
+ }
+
+ /**
+ * Normal but significant events.
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function notice($message, array $context = array())
+ {
+ $this->log(Mustache_Logger::NOTICE, $message, $context);
+ }
+
+ /**
+ * Interesting events.
+ *
+ * Example: User logs in, SQL logs.
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function info($message, array $context = array())
+ {
+ $this->log(Mustache_Logger::INFO, $message, $context);
+ }
+
+ /**
+ * Detailed debug information.
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function debug($message, array $context = array())
+ {
+ $this->log(Mustache_Logger::DEBUG, $message, $context);
+ }
+}
diff --git a/Mustache/Logger/StreamLogger.php b/Mustache/Logger/StreamLogger.php
new file mode 100644
index 0000000..402a148
--- /dev/null
+++ b/Mustache/Logger/StreamLogger.php
@@ -0,0 +1,194 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A Mustache Stream Logger.
+ *
+ * The Stream Logger wraps a file resource instance (such as a stream) or a
+ * stream URL. All log messages over the threshold level will be appended to
+ * this stream.
+ *
+ * Hint: Try `php://stderr` for your stream URL.
+ */
+class Mustache_Logger_StreamLogger extends Mustache_Logger_AbstractLogger
+{
+ protected static $levels = array(
+ self::DEBUG => 100,
+ self::INFO => 200,
+ self::NOTICE => 250,
+ self::WARNING => 300,
+ self::ERROR => 400,
+ self::CRITICAL => 500,
+ self::ALERT => 550,
+ self::EMERGENCY => 600,
+ );
+
+ protected $level;
+ protected $stream = null;
+ protected $url = null;
+
+ /**
+ * @throws InvalidArgumentException if the logging level is unknown
+ *
+ * @param resource|string $stream Resource instance or URL
+ * @param int $level The minimum logging level at which this handler will be triggered
+ */
+ public function __construct($stream, $level = Mustache_Logger::ERROR)
+ {
+ $this->setLevel($level);
+
+ if (is_resource($stream)) {
+ $this->stream = $stream;
+ } else {
+ $this->url = $stream;
+ }
+ }
+
+ /**
+ * Close stream resources.
+ */
+ public function __destruct()
+ {
+ if (is_resource($this->stream)) {
+ fclose($this->stream);
+ }
+ }
+
+ /**
+ * Set the minimum logging level.
+ *
+ * @throws Mustache_Exception_InvalidArgumentException if the logging level is unknown
+ *
+ * @param int $level The minimum logging level which will be written
+ */
+ public function setLevel($level)
+ {
+ if (!array_key_exists($level, self::$levels)) {
+ throw new Mustache_Exception_InvalidArgumentException(sprintf('Unexpected logging level: %s', $level));
+ }
+
+ $this->level = $level;
+ }
+
+ /**
+ * Get the current minimum logging level.
+ *
+ * @return int
+ */
+ public function getLevel()
+ {
+ return $this->level;
+ }
+
+ /**
+ * Logs with an arbitrary level.
+ *
+ * @throws Mustache_Exception_InvalidArgumentException if the logging level is unknown
+ *
+ * @param mixed $level
+ * @param string $message
+ * @param array $context
+ */
+ public function log($level, $message, array $context = array())
+ {
+ if (!array_key_exists($level, self::$levels)) {
+ throw new Mustache_Exception_InvalidArgumentException(sprintf('Unexpected logging level: %s', $level));
+ }
+
+ if (self::$levels[$level] >= self::$levels[$this->level]) {
+ $this->writeLog($level, $message, $context);
+ }
+ }
+
+ /**
+ * Write a record to the log.
+ *
+ * @throws Mustache_Exception_LogicException If neither a stream resource nor url is present
+ * @throws Mustache_Exception_RuntimeException If the stream url cannot be opened
+ *
+ * @param int $level The logging level
+ * @param string $message The log message
+ * @param array $context The log context
+ */
+ protected function writeLog($level, $message, array $context = array())
+ {
+ if (!is_resource($this->stream)) {
+ if (!isset($this->url)) {
+ throw new Mustache_Exception_LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().');
+ }
+
+ $this->stream = fopen($this->url, 'a');
+ if (!is_resource($this->stream)) {
+ // @codeCoverageIgnoreStart
+ throw new Mustache_Exception_RuntimeException(sprintf('The stream or file "%s" could not be opened.', $this->url));
+ // @codeCoverageIgnoreEnd
+ }
+ }
+
+ fwrite($this->stream, self::formatLine($level, $message, $context));
+ }
+
+ /**
+ * Gets the name of the logging level.
+ *
+ * @throws InvalidArgumentException if the logging level is unknown
+ *
+ * @param int $level
+ *
+ * @return string
+ */
+ protected static function getLevelName($level)
+ {
+ return strtoupper($level);
+ }
+
+ /**
+ * Format a log line for output.
+ *
+ * @param int $level The logging level
+ * @param string $message The log message
+ * @param array $context The log context
+ *
+ * @return string
+ */
+ protected static function formatLine($level, $message, array $context = array())
+ {
+ return sprintf(
+ "%s: %s\n",
+ self::getLevelName($level),
+ self::interpolateMessage($message, $context)
+ );
+ }
+
+ /**
+ * Interpolate context values into the message placeholders.
+ *
+ * @param string $message
+ * @param array $context
+ *
+ * @return string
+ */
+ protected static function interpolateMessage($message, array $context = array())
+ {
+ if (strpos($message, '{') === false) {
+ return $message;
+ }
+
+ // build a replacement array with braces around the context keys
+ $replace = array();
+ foreach ($context as $key => $val) {
+ $replace['{' . $key . '}'] = $val;
+ }
+
+ // interpolate replacement values into the the message and return
+ return strtr($message, $replace);
+ }
+}
diff --git a/Mustache/Parser.php b/Mustache/Parser.php
index 39911d6..5db5824 100644
--- a/Mustache/Parser.php
+++ b/Mustache/Parser.php
@@ -1,88 +1,385 @@
-<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Mustache Parser class.
- *
- * This class is responsible for turning a set of Mustache tokens into a parse tree.
- */
-class Mustache_Parser
-{
-
- /**
- * Process an array of Mustache tokens and convert them into a parse tree.
- *
- * @param array $tokens Set of Mustache tokens
- *
- * @return array Mustache token parse tree
- */
- public function parse(array $tokens = array())
- {
- return $this->buildTree(new ArrayIterator($tokens));
- }
-
- /**
- * Helper method for recursively building a parse tree.
- *
- * @param ArrayIterator $tokens Stream of Mustache tokens
- * @param array $parent Parent token (default: null)
- *
- * @return array Mustache Token parse tree
- *
- * @throws LogicException when nesting errors or mismatched section tags are encountered.
- */
- private function buildTree(ArrayIterator $tokens, array $parent = null)
- {
- $nodes = array();
-
- do {
- $token = $tokens->current();
- $tokens->next();
-
- if ($token === null) {
- continue;
- } else {
- switch ($token[Mustache_Tokenizer::TYPE]) {
- case Mustache_Tokenizer::T_SECTION:
- case Mustache_Tokenizer::T_INVERTED:
- $nodes[] = $this->buildTree($tokens, $token);
- break;
-
- case Mustache_Tokenizer::T_END_SECTION:
- if (!isset($parent)) {
- throw new LogicException('Unexpected closing tag: /'. $token[Mustache_Tokenizer::NAME]);
- }
-
- if ($token[Mustache_Tokenizer::NAME] !== $parent[Mustache_Tokenizer::NAME]) {
- throw new LogicException('Nesting error: ' . $parent[Mustache_Tokenizer::NAME] . ' vs. ' . $token[Mustache_Tokenizer::NAME]);
- }
-
- $parent[Mustache_Tokenizer::END] = $token[Mustache_Tokenizer::INDEX];
- $parent[Mustache_Tokenizer::NODES] = $nodes;
-
- return $parent;
- break;
-
- default:
- $nodes[] = $token;
- break;
- }
- }
-
- } while ($tokens->valid());
-
- if (isset($parent)) {
- throw new LogicException('Missing closing tag: ' . $parent[Mustache_Tokenizer::NAME]);
- }
-
- return $nodes;
- }
-}
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Parser class.
+ *
+ * This class is responsible for turning a set of Mustache tokens into a parse tree.
+ */
+class Mustache_Parser
+{
+ private $lineNum;
+ private $lineTokens;
+ private $pragmas;
+ private $defaultPragmas = array();
+
+ private $pragmaFilters;
+ private $pragmaBlocks;
+ private $pragmaDynamicNames;
+
+ /**
+ * Process an array of Mustache tokens and convert them into a parse tree.
+ *
+ * @param array $tokens Set of Mustache tokens
+ *
+ * @return array Mustache token parse tree
+ */
+ public function parse(array $tokens = array())
+ {
+ $this->lineNum = -1;
+ $this->lineTokens = 0;
+ $this->pragmas = $this->defaultPragmas;
+
+ $this->pragmaFilters = isset($this->pragmas[Mustache_Engine::PRAGMA_FILTERS]);
+ $this->pragmaBlocks = isset($this->pragmas[Mustache_Engine::PRAGMA_BLOCKS]);
+ $this->pragmaDynamicNames = isset($this->pragmas[Mustache_Engine::PRAGMA_DYNAMIC_NAMES]);
+
+ return $this->buildTree($tokens);
+ }
+
+ /**
+ * Enable pragmas across all templates, regardless of the presence of pragma
+ * tags in the individual templates.
+ *
+ * @internal Users should set global pragmas in Mustache_Engine, not here :)
+ *
+ * @param string[] $pragmas
+ */
+ public function setPragmas(array $pragmas)
+ {
+ $this->pragmas = array();
+ foreach ($pragmas as $pragma) {
+ $this->enablePragma($pragma);
+ }
+ $this->defaultPragmas = $this->pragmas;
+ }
+
+ /**
+ * Helper method for recursively building a parse tree.
+ *
+ * @throws Mustache_Exception_SyntaxException when nesting errors or mismatched section tags are encountered
+ *
+ * @param array &$tokens Set of Mustache tokens
+ * @param array $parent Parent token (default: null)
+ *
+ * @return array Mustache Token parse tree
+ */
+ private function buildTree(array &$tokens, array $parent = null)
+ {
+ $nodes = array();
+
+ while (!empty($tokens)) {
+ $token = array_shift($tokens);
+
+ if ($token[Mustache_Tokenizer::LINE] === $this->lineNum) {
+ $this->lineTokens++;
+ } else {
+ $this->lineNum = $token[Mustache_Tokenizer::LINE];
+ $this->lineTokens = 0;
+ }
+
+ if ($token[Mustache_Tokenizer::TYPE] !== Mustache_Tokenizer::T_COMMENT) {
+ if ($this->pragmaDynamicNames && isset($token[Mustache_Tokenizer::NAME])) {
+ list($name, $isDynamic) = $this->getDynamicName($token);
+ if ($isDynamic) {
+ $token[Mustache_Tokenizer::NAME] = $name;
+ $token[Mustache_Tokenizer::DYNAMIC] = true;
+ }
+ }
+
+ if ($this->pragmaFilters && isset($token[Mustache_Tokenizer::NAME])) {
+ list($name, $filters) = $this->getNameAndFilters($token[Mustache_Tokenizer::NAME]);
+ if (!empty($filters)) {
+ $token[Mustache_Tokenizer::NAME] = $name;
+ $token[Mustache_Tokenizer::FILTERS] = $filters;
+ }
+ }
+ }
+
+ switch ($token[Mustache_Tokenizer::TYPE]) {
+ case Mustache_Tokenizer::T_DELIM_CHANGE:
+ $this->checkIfTokenIsAllowedInParent($parent, $token);
+ $this->clearStandaloneLines($nodes, $tokens);
+ break;
+
+ case Mustache_Tokenizer::T_SECTION:
+ case Mustache_Tokenizer::T_INVERTED:
+ $this->checkIfTokenIsAllowedInParent($parent, $token);
+ $this->clearStandaloneLines($nodes, $tokens);
+ $nodes[] = $this->buildTree($tokens, $token);
+ break;
+
+ case Mustache_Tokenizer::T_END_SECTION:
+ if (!isset($parent)) {
+ $msg = sprintf(
+ 'Unexpected closing tag: /%s on line %d',
+ $token[Mustache_Tokenizer::NAME],
+ $token[Mustache_Tokenizer::LINE]
+ );
+ throw new Mustache_Exception_SyntaxException($msg, $token);
+ }
+
+ $sameName = $token[Mustache_Tokenizer::NAME] !== $parent[Mustache_Tokenizer::NAME];
+ $tokenDynamic = isset($token[Mustache_Tokenizer::DYNAMIC]) && $token[Mustache_Tokenizer::DYNAMIC];
+ $parentDynamic = isset($parent[Mustache_Tokenizer::DYNAMIC]) && $parent[Mustache_Tokenizer::DYNAMIC];
+
+ if ($sameName || ($tokenDynamic !== $parentDynamic)) {
+ $msg = sprintf(
+ 'Nesting error: %s%s (on line %d) vs. %s%s (on line %d)',
+ $parentDynamic ? '*' : '',
+ $parent[Mustache_Tokenizer::NAME],
+ $parent[Mustache_Tokenizer::LINE],
+ $tokenDynamic ? '*' : '',
+ $token[Mustache_Tokenizer::NAME],
+ $token[Mustache_Tokenizer::LINE]
+ );
+ throw new Mustache_Exception_SyntaxException($msg, $token);
+ }
+
+ $this->clearStandaloneLines($nodes, $tokens);
+ $parent[Mustache_Tokenizer::END] = $token[Mustache_Tokenizer::INDEX];
+ $parent[Mustache_Tokenizer::NODES] = $nodes;
+
+ return $parent;
+
+ case Mustache_Tokenizer::T_PARTIAL:
+ $this->checkIfTokenIsAllowedInParent($parent, $token);
+ //store the whitespace prefix for laters!
+ if ($indent = $this->clearStandaloneLines($nodes, $tokens)) {
+ $token[Mustache_Tokenizer::INDENT] = $indent[Mustache_Tokenizer::VALUE];
+ }
+ $nodes[] = $token;
+ break;
+
+ case Mustache_Tokenizer::T_PARENT:
+ $this->checkIfTokenIsAllowedInParent($parent, $token);
+ $nodes[] = $this->buildTree($tokens, $token);
+ break;
+
+ case Mustache_Tokenizer::T_BLOCK_VAR:
+ if ($this->pragmaBlocks) {
+ // BLOCKS pragma is enabled, let's do this!
+ if (isset($parent) && $parent[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_PARENT) {
+ $token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_BLOCK_ARG;
+ }
+ $this->clearStandaloneLines($nodes, $tokens);
+ $nodes[] = $this->buildTree($tokens, $token);
+ } else {
+ // pretend this was just a normal "escaped" token...
+ $token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_ESCAPED;
+ // TODO: figure out how to figure out if there was a space after this dollar:
+ $token[Mustache_Tokenizer::NAME] = '$' . $token[Mustache_Tokenizer::NAME];
+ $nodes[] = $token;
+ }
+ break;
+
+ case Mustache_Tokenizer::T_PRAGMA:
+ $this->enablePragma($token[Mustache_Tokenizer::NAME]);
+ // no break
+
+ case Mustache_Tokenizer::T_COMMENT:
+ $this->clearStandaloneLines($nodes, $tokens);
+ $nodes[] = $token;
+ break;
+
+ default:
+ $nodes[] = $token;
+ break;
+ }
+ }
+
+ if (isset($parent)) {
+ $msg = sprintf(
+ 'Missing closing tag: %s opened on line %d',
+ $parent[Mustache_Tokenizer::NAME],
+ $parent[Mustache_Tokenizer::LINE]
+ );
+ throw new Mustache_Exception_SyntaxException($msg, $parent);
+ }
+
+ return $nodes;
+ }
+
+ /**
+ * Clear standalone line tokens.
+ *
+ * Returns a whitespace token for indenting partials, if applicable.
+ *
+ * @param array $nodes Parsed nodes
+ * @param array $tokens Tokens to be parsed
+ *
+ * @return array|null Resulting indent token, if any
+ */
+ private function clearStandaloneLines(array &$nodes, array &$tokens)
+ {
+ if ($this->lineTokens > 1) {
+ // this is the third or later node on this line, so it can't be standalone
+ return;
+ }
+
+ $prev = null;
+ if ($this->lineTokens === 1) {
+ // this is the second node on this line, so it can't be standalone
+ // unless the previous node is whitespace.
+ if ($prev = end($nodes)) {
+ if (!$this->tokenIsWhitespace($prev)) {
+ return;
+ }
+ }
+ }
+
+ if ($next = reset($tokens)) {
+ // If we're on a new line, bail.
+ if ($next[Mustache_Tokenizer::LINE] !== $this->lineNum) {
+ return;
+ }
+
+ // If the next token isn't whitespace, bail.
+ if (!$this->tokenIsWhitespace($next)) {
+ return;
+ }
+
+ if (count($tokens) !== 1) {
+ // Unless it's the last token in the template, the next token
+ // must end in newline for this to be standalone.
+ if (substr($next[Mustache_Tokenizer::VALUE], -1) !== "\n") {
+ return;
+ }
+ }
+
+ // Discard the whitespace suffix
+ array_shift($tokens);
+ }
+
+ if ($prev) {
+ // Return the whitespace prefix, if any
+ return array_pop($nodes);
+ }
+ }
+
+ /**
+ * Check whether token is a whitespace token.
+ *
+ * True if token type is T_TEXT and value is all whitespace characters.
+ *
+ * @param array $token
+ *
+ * @return bool True if token is a whitespace token
+ */
+ private function tokenIsWhitespace(array $token)
+ {
+ if ($token[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_TEXT) {
+ return preg_match('/^\s*$/', $token[Mustache_Tokenizer::VALUE]);
+ }
+
+ return false;
+ }
+
+ /**
+ * Check whether a token is allowed inside a parent tag.
+ *
+ * @throws Mustache_Exception_SyntaxException if an invalid token is found inside a parent tag
+ *
+ * @param array|null $parent
+ * @param array $token
+ */
+ private function checkIfTokenIsAllowedInParent($parent, array $token)
+ {
+ if (isset($parent) && $parent[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_PARENT) {
+ throw new Mustache_Exception_SyntaxException('Illegal content in < parent tag', $token);
+ }
+ }
+
+ /**
+ * Parse dynamic names.
+ *
+ * @throws Mustache_Exception_SyntaxException when a tag does not allow *
+ * @throws Mustache_Exception_SyntaxException on multiple *s, or dots or filters with *
+ */
+ private function getDynamicName(array $token)
+ {
+ $name = $token[Mustache_Tokenizer::NAME];
+ $isDynamic = false;
+
+ if (preg_match('/^\s*\*\s*/', $name)) {
+ $this->ensureTagAllowsDynamicNames($token);
+ $name = preg_replace('/^\s*\*\s*/', '', $name);
+ $isDynamic = true;
+ }
+
+ return array($name, $isDynamic);
+ }
+
+ /**
+ * Check whether the given token supports dynamic tag names.
+ *
+ * @throws Mustache_Exception_SyntaxException when a tag does not allow *
+ *
+ * @param array $token
+ */
+ private function ensureTagAllowsDynamicNames(array $token)
+ {
+ switch ($token[Mustache_Tokenizer::TYPE]) {
+ case Mustache_Tokenizer::T_PARTIAL:
+ case Mustache_Tokenizer::T_PARENT:
+ case Mustache_Tokenizer::T_END_SECTION:
+ return;
+ }
+
+ $msg = sprintf(
+ 'Invalid dynamic name: %s in %s tag',
+ $token[Mustache_Tokenizer::NAME],
+ Mustache_Tokenizer::getTagName($token[Mustache_Tokenizer::TYPE])
+ );
+
+ throw new Mustache_Exception_SyntaxException($msg, $token);
+ }
+
+
+ /**
+ * Split a tag name into name and filters.
+ *
+ * @param string $name
+ *
+ * @return array [Tag name, Array of filters]
+ */
+ private function getNameAndFilters($name)
+ {
+ $filters = array_map('trim', explode('|', $name));
+ $name = array_shift($filters);
+
+ return array($name, $filters);
+ }
+
+ /**
+ * Enable a pragma.
+ *
+ * @param string $name
+ */
+ private function enablePragma($name)
+ {
+ $this->pragmas[$name] = true;
+
+ switch ($name) {
+ case Mustache_Engine::PRAGMA_BLOCKS:
+ $this->pragmaBlocks = true;
+ break;
+
+ case Mustache_Engine::PRAGMA_FILTERS:
+ $this->pragmaFilters = true;
+ break;
+
+ case Mustache_Engine::PRAGMA_DYNAMIC_NAMES:
+ $this->pragmaDynamicNames = true;
+ break;
+ }
+ }
+}
diff --git a/Mustache/Source.php b/Mustache/Source.php
new file mode 100644
index 0000000..278c2cb
--- /dev/null
+++ b/Mustache/Source.php
@@ -0,0 +1,40 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache template Source interface.
+ */
+interface Mustache_Source
+{
+ /**
+ * Get the Source key (used to generate the compiled class name).
+ *
+ * This must return a distinct key for each template source. For example, an
+ * MD5 hash of the template contents would probably do the trick. The
+ * ProductionFilesystemLoader uses mtime and file path. If your production
+ * source directory is under version control, you could use the current Git
+ * rev and the file path...
+ *
+ * @throws RuntimeException when a source file cannot be read
+ *
+ * @return string
+ */
+ public function getKey();
+
+ /**
+ * Get the template Source.
+ *
+ * @throws RuntimeException when a source file cannot be read
+ *
+ * @return string
+ */
+ public function getSource();
+}
diff --git a/Mustache/Source/FilesystemSource.php b/Mustache/Source/FilesystemSource.php
new file mode 100644
index 0000000..270f584
--- /dev/null
+++ b/Mustache/Source/FilesystemSource.php
@@ -0,0 +1,77 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache template Filesystem Source.
+ *
+ * This template Source uses stat() to generate the Source key, so that using
+ * pre-compiled templates doesn't require hitting the disk to read the source.
+ * It is more suitable for production use, and is used by default in the
+ * ProductionFilesystemLoader.
+ */
+class Mustache_Source_FilesystemSource implements Mustache_Source
+{
+ private $fileName;
+ private $statProps;
+ private $stat;
+
+ /**
+ * Filesystem Source constructor.
+ *
+ * @param string $fileName
+ * @param array $statProps
+ */
+ public function __construct($fileName, array $statProps)
+ {
+ $this->fileName = $fileName;
+ $this->statProps = $statProps;
+ }
+
+ /**
+ * Get the Source key (used to generate the compiled class name).
+ *
+ * @throws Mustache_Exception_RuntimeException when a source file cannot be read
+ *
+ * @return string
+ */
+ public function getKey()
+ {
+ $chunks = array(
+ 'fileName' => $this->fileName,
+ );
+
+ if (!empty($this->statProps)) {
+ if (!isset($this->stat)) {
+ $this->stat = @stat($this->fileName);
+ }
+
+ if ($this->stat === false) {
+ throw new Mustache_Exception_RuntimeException(sprintf('Failed to read source file "%s".', $this->fileName));
+ }
+
+ foreach ($this->statProps as $prop) {
+ $chunks[$prop] = $this->stat[$prop];
+ }
+ }
+
+ return json_encode($chunks);
+ }
+
+ /**
+ * Get the template Source.
+ *
+ * @return string
+ */
+ public function getSource()
+ {
+ return file_get_contents($this->fileName);
+ }
+}
diff --git a/Mustache/Template.php b/Mustache/Template.php
index ebb9df8..4de8239 100644
--- a/Mustache/Template.php
+++ b/Mustache/Template.php
@@ -1,149 +1,180 @@
-<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Abstract Mustache Template class.
- *
- * @abstract
- */
-abstract class Mustache_Template
-{
-
- /**
- * @var Mustache_Engine
- */
- protected $mustache;
-
- /**
- * Mustache Template constructor.
- *
- * @param Mustache_Engine $mustache
- */
- public function __construct(Mustache_Engine $mustache)
- {
- $this->mustache = $mustache;
- }
-
- /**
- * Mustache Template instances can be treated as a function and rendered by simply calling them:
- *
- * $m = new Mustache_Engine;
- * $tpl = $m->loadTemplate('Hello, {{ name }}!');
- * echo $tpl(array('name' => 'World')); // "Hello, World!"
- *
- * @see Mustache_Template::render
- *
- * @param mixed $context Array or object rendering context (default: array())
- *
- * @return string Rendered template
- */
- public function __invoke($context = array())
- {
- return $this->render($context);
- }
-
- /**
- * Render this template given the rendering context.
- *
- * @param mixed $context Array or object rendering context (default: array())
- *
- * @return string Rendered template
- */
- public function render($context = array())
- {
- return $this->renderInternal($this->prepareContextStack($context));
- }
-
- /**
- * Internal rendering method implemented by Mustache Template concrete subclasses.
- *
- * This is where the magic happens :)
- *
- * @param Mustache_Context $context
- * @param string $indent (default: '')
- * @param bool $escape (default: false)
- *
- * @return string Rendered template
- */
- abstract public function renderInternal(Mustache_Context $context, $indent = '', $escape = false);
-
- /**
- * Tests whether a value should be iterated over (e.g. in a section context).
- *
- * In most languages there are two distinct array types: list and hash (or whatever you want to call them). Lists
- * should be iterated, hashes should be treated as objects. Mustache follows this paradigm for Ruby, Javascript,
- * Java, Python, etc.
- *
- * PHP, however, treats lists and hashes as one primitive type: array. So Mustache.php needs a way to distinguish
- * between between a list of things (numeric, normalized array) and a set of variables to be used as section context
- * (associative array). In other words, this will be iterated over:
- *
- * $items = array(
- * array('name' => 'foo'),
- * array('name' => 'bar'),
- * array('name' => 'baz'),
- * );
- *
- * ... but this will be used as a section context block:
- *
- * $items = array(
- * 1 => array('name' => 'foo'),
- * 'banana' => array('name' => 'bar'),
- * 42 => array('name' => 'baz'),
- * );
- *
- * @param mixed $value
- *
- * @return boolean True if the value is 'iterable'
- */
- protected function isIterable($value)
- {
- if (is_object($value)) {
- return $value instanceof Traversable;
- } elseif (is_array($value)) {
- $i = 0;
- foreach ($value as $k => $v) {
- if ($k !== $i++) {
- return false;
- }
- }
-
- return true;
- } else {
- return false;
- }
- }
-
- /**
- * Helper method to prepare the Context stack.
- *
- * Adds the Mustache HelperCollection to the stack's top context frame if helpers are present.
- *
- * @param mixed $context Optional first context frame (default: null)
- *
- * @return Mustache_Context
- */
- protected function prepareContextStack($context = null)
- {
- $stack = new Mustache_Context;
-
- $helpers = $this->mustache->getHelpers();
- if (!$helpers->isEmpty()) {
- $stack->push($helpers);
- }
-
- if (!empty($context)) {
- $stack->push($context);
- }
-
- return $stack;
- }
-}
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Abstract Mustache Template class.
+ *
+ * @abstract
+ */
+abstract class Mustache_Template
+{
+ /**
+ * @var Mustache_Engine
+ */
+ protected $mustache;
+
+ /**
+ * @var bool
+ */
+ protected $strictCallables = false;
+
+ /**
+ * Mustache Template constructor.
+ *
+ * @param Mustache_Engine $mustache
+ */
+ public function __construct(Mustache_Engine $mustache)
+ {
+ $this->mustache = $mustache;
+ }
+
+ /**
+ * Mustache Template instances can be treated as a function and rendered by simply calling them.
+ *
+ * $m = new Mustache_Engine;
+ * $tpl = $m->loadTemplate('Hello, {{ name }}!');
+ * echo $tpl(array('name' => 'World')); // "Hello, World!"
+ *
+ * @see Mustache_Template::render
+ *
+ * @param mixed $context Array or object rendering context (default: array())
+ *
+ * @return string Rendered template
+ */
+ public function __invoke($context = array())
+ {
+ return $this->render($context);
+ }
+
+ /**
+ * Render this template given the rendering context.
+ *
+ * @param mixed $context Array or object rendering context (default: array())
+ *
+ * @return string Rendered template
+ */
+ public function render($context = array())
+ {
+ return $this->renderInternal(
+ $this->prepareContextStack($context)
+ );
+ }
+
+ /**
+ * Internal rendering method implemented by Mustache Template concrete subclasses.
+ *
+ * This is where the magic happens :)
+ *
+ * NOTE: This method is not part of the Mustache.php public API.
+ *
+ * @param Mustache_Context $context
+ * @param string $indent (default: '')
+ *
+ * @return string Rendered template
+ */
+ abstract public function renderInternal(Mustache_Context $context, $indent = '');
+
+ /**
+ * Tests whether a value should be iterated over (e.g. in a section context).
+ *
+ * In most languages there are two distinct array types: list and hash (or whatever you want to call them). Lists
+ * should be iterated, hashes should be treated as objects. Mustache follows this paradigm for Ruby, Javascript,
+ * Java, Python, etc.
+ *
+ * PHP, however, treats lists and hashes as one primitive type: array. So Mustache.php needs a way to distinguish
+ * between between a list of things (numeric, normalized array) and a set of variables to be used as section context
+ * (associative array). In other words, this will be iterated over:
+ *
+ * $items = array(
+ * array('name' => 'foo'),
+ * array('name' => 'bar'),
+ * array('name' => 'baz'),
+ * );
+ *
+ * ... but this will be used as a section context block:
+ *
+ * $items = array(
+ * 1 => array('name' => 'foo'),
+ * 'banana' => array('name' => 'bar'),
+ * 42 => array('name' => 'baz'),
+ * );
+ *
+ * @param mixed $value
+ *
+ * @return bool True if the value is 'iterable'
+ */
+ protected function isIterable($value)
+ {
+ switch (gettype($value)) {
+ case 'object':
+ return $value instanceof Traversable;
+
+ case 'array':
+ $i = 0;
+ foreach ($value as $k => $v) {
+ if ($k !== $i++) {
+ return false;
+ }
+ }
+
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Helper method to prepare the Context stack.
+ *
+ * Adds the Mustache HelperCollection to the stack's top context frame if helpers are present.
+ *
+ * @param mixed $context Optional first context frame (default: null)
+ *
+ * @return Mustache_Context
+ */
+ protected function prepareContextStack($context = null)
+ {
+ $stack = new Mustache_Context();
+
+ $helpers = $this->mustache->getHelpers();
+ if (!$helpers->isEmpty()) {
+ $stack->push($helpers);
+ }
+
+ if (!empty($context)) {
+ $stack->push($context);
+ }
+
+ return $stack;
+ }
+
+ /**
+ * Resolve a context value.
+ *
+ * Invoke the value if it is callable, otherwise return the value.
+ *
+ * @param mixed $value
+ * @param Mustache_Context $context
+ *
+ * @return string
+ */
+ protected function resolveValue($value, Mustache_Context $context)
+ {
+ if (($this->strictCallables ? is_object($value) : !is_string($value)) && is_callable($value)) {
+ return $this->mustache
+ ->loadLambda((string) call_user_func($value))
+ ->renderInternal($context);
+ }
+
+ return $value;
+ }
+}
diff --git a/Mustache/Tokenizer.php b/Mustache/Tokenizer.php
index fd866e3..d96f129 100644
--- a/Mustache/Tokenizer.php
+++ b/Mustache/Tokenizer.php
@@ -1,286 +1,408 @@
-<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Mustache Tokenizer class.
- *
- * This class is responsible for turning raw template source into a set of Mustache tokens.
- */
-class Mustache_Tokenizer
-{
-
- // Finite state machine states
- const IN_TEXT = 0;
- const IN_TAG_TYPE = 1;
- const IN_TAG = 2;
-
- // Token types
- const T_SECTION = '#';
- const T_INVERTED = '^';
- const T_END_SECTION = '/';
- const T_COMMENT = '!';
- const T_PARTIAL = '>';
- const T_PARTIAL_2 = '<';
- const T_DELIM_CHANGE = '=';
- const T_ESCAPED = '_v';
- const T_UNESCAPED = '{';
- const T_UNESCAPED_2 = '&';
- const T_TEXT = '_t';
-
- // Valid token types
- private static $tagTypes = array(
- self::T_SECTION => true,
- self::T_INVERTED => true,
- self::T_END_SECTION => true,
- self::T_COMMENT => true,
- self::T_PARTIAL => true,
- self::T_PARTIAL_2 => true,
- self::T_DELIM_CHANGE => true,
- self::T_ESCAPED => true,
- self::T_UNESCAPED => true,
- self::T_UNESCAPED_2 => true,
- );
-
- // Interpolated tags
- private static $interpolatedTags = array(
- self::T_ESCAPED => true,
- self::T_UNESCAPED => true,
- self::T_UNESCAPED_2 => true,
- );
-
- // Token properties
- const TYPE = 'type';
- const NAME = 'name';
- const OTAG = 'otag';
- const CTAG = 'ctag';
- const INDEX = 'index';
- const END = 'end';
- const INDENT = 'indent';
- const NODES = 'nodes';
- const VALUE = 'value';
-
- private $state;
- private $tagType;
- private $tag;
- private $buffer;
- private $tokens;
- private $seenTag;
- private $lineStart;
- private $otag;
- private $ctag;
-
- /**
- * Scan and tokenize template source.
- *
- * @param string $text Mustache template source to tokenize
- * @param string $delimiters Optionally, pass initial opening and closing delimiters (default: null)
- *
- * @return array Set of Mustache tokens
- */
- public function scan($text, $delimiters = null)
- {
- $this->reset();
-
- if ($delimiters = trim($delimiters)) {
- list($otag, $ctag) = explode(' ', $delimiters);
- $this->otag = $otag;
- $this->ctag = $ctag;
- }
-
- $len = strlen($text);
- for ($i = 0; $i < $len; $i++) {
- switch ($this->state) {
- case self::IN_TEXT:
- if ($this->tagChange($this->otag, $text, $i)) {
- $i--;
- $this->flushBuffer();
- $this->state = self::IN_TAG_TYPE;
- } else {
- if ($text[$i] == "\n") {
- $this->filterLine();
- } else {
- $this->buffer .= $text[$i];
- }
- }
- break;
-
- case self::IN_TAG_TYPE:
-
- $i += strlen($this->otag) - 1;
- if (isset(self::$tagTypes[$text[$i + 1]])) {
- $tag = $text[$i + 1];
- $this->tagType = $tag;
- } else {
- $tag = null;
- $this->tagType = self::T_ESCAPED;
- }
-
- if ($this->tagType === self::T_DELIM_CHANGE) {
- $i = $this->changeDelimiters($text, $i);
- $this->state = self::IN_TEXT;
- } else {
- if ($tag !== null) {
- $i++;
- }
- $this->state = self::IN_TAG;
- }
- $this->seenTag = $i;
- break;
-
- default:
- if ($this->tagChange($this->ctag, $text, $i)) {
- $this->tokens[] = array(
- self::TYPE => $this->tagType,
- self::NAME => trim($this->buffer),
- self::OTAG => $this->otag,
- self::CTAG => $this->ctag,
- self::INDEX => ($this->tagType == self::T_END_SECTION) ? $this->seenTag - strlen($this->otag) : $i + strlen($this->ctag)
- );
-
- $this->buffer = '';
- $i += strlen($this->ctag) - 1;
- $this->state = self::IN_TEXT;
- if ($this->tagType == self::T_UNESCAPED) {
- if ($this->ctag == '}}') {
- $i++;
- } else {
- // Clean up `{{{ tripleStache }}}` style tokens.
- $lastName = $this->tokens[count($this->tokens) - 1][self::NAME];
- if (substr($lastName, -1) === '}') {
- $this->tokens[count($this->tokens) - 1][self::NAME] = trim(substr($lastName, 0, -1));
- }
- }
- }
- } else {
- $this->buffer .= $text[$i];
- }
- break;
- }
- }
-
- $this->filterLine(true);
-
- return $this->tokens;
- }
-
- /**
- * Helper function to reset tokenizer internal state.
- */
- private function reset()
- {
- $this->state = self::IN_TEXT;
- $this->tagType = null;
- $this->tag = null;
- $this->buffer = '';
- $this->tokens = array();
- $this->seenTag = false;
- $this->lineStart = 0;
- $this->otag = '{{';
- $this->ctag = '}}';
- }
-
- /**
- * Flush the current buffer to a token.
- */
- private function flushBuffer()
- {
- if (!empty($this->buffer)) {
- $this->tokens[] = array(self::TYPE => self::T_TEXT, self::VALUE => $this->buffer);
- $this->buffer = '';
- }
- }
-
- /**
- * Test whether the current line is entirely made up of whitespace.
- *
- * @return boolean True if the current line is all whitespace
- */
- private function lineIsWhitespace()
- {
- $tokensCount = count($this->tokens);
- for ($j = $this->lineStart; $j < $tokensCount; $j++) {
- $token = $this->tokens[$j];
- if (isset(self::$tagTypes[$token[self::TYPE]])) {
- if (isset(self::$interpolatedTags[$token[self::TYPE]])) {
- return false;
- }
- } elseif ($token[self::TYPE] == self::T_TEXT) {
- if (preg_match('/\S/', $token[self::VALUE])) {
- return false;
- }
- }
- }
-
- return true;
- }
-
- /**
- * Filter out whitespace-only lines and store indent levels for partials.
- *
- * @param bool $noNewLine Suppress the newline? (default: false)
- */
- private function filterLine($noNewLine = false)
- {
- $this->flushBuffer();
- if ($this->seenTag && $this->lineIsWhitespace()) {
- $tokensCount = count($this->tokens);
- for ($j = $this->lineStart; $j < $tokensCount; $j++) {
- if ($this->tokens[$j][self::TYPE] == self::T_TEXT) {
- if (isset($this->tokens[$j+1]) && $this->tokens[$j+1][self::TYPE] == self::T_PARTIAL) {
- $this->tokens[$j+1][self::INDENT] = $this->tokens[$j][self::VALUE];
- }
-
- $this->tokens[$j] = null;
- }
- }
- } elseif (!$noNewLine) {
- $this->tokens[] = array(self::TYPE => self::T_TEXT, self::VALUE => "\n");
- }
-
- $this->seenTag = false;
- $this->lineStart = count($this->tokens);
- }
-
- /**
- * Change the current Mustache delimiters. Set new `otag` and `ctag` values.
- *
- * @param string $text Mustache template source
- * @param int $index Current tokenizer index
- *
- * @return int New index value
- */
- private function changeDelimiters($text, $index)
- {
- $startIndex = strpos($text, '=', $index) + 1;
- $close = '='.$this->ctag;
- $closeIndex = strpos($text, $close, $index);
-
- list($otag, $ctag) = explode(' ', trim(substr($text, $startIndex, $closeIndex - $startIndex)));
- $this->otag = $otag;
- $this->ctag = $ctag;
-
- return $closeIndex + strlen($close) - 1;
- }
-
- /**
- * Test whether it's time to change tags.
- *
- * @param string $tag Current tag name
- * @param string $text Mustache template source
- * @param int $index Current tokenizer index
- *
- * @return boolean True if this is a closing section tag
- */
- private function tagChange($tag, $text, $index)
- {
- return substr($text, $index, strlen($tag)) === $tag;
- }
-}
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Tokenizer class.
+ *
+ * This class is responsible for turning raw template source into a set of Mustache tokens.
+ */
+class Mustache_Tokenizer
+{
+ // Finite state machine states
+ const IN_TEXT = 0;
+ const IN_TAG_TYPE = 1;
+ const IN_TAG = 2;
+
+ // Token types
+ const T_SECTION = '#';
+ const T_INVERTED = '^';
+ const T_END_SECTION = '/';
+ const T_COMMENT = '!';
+ const T_PARTIAL = '>';
+ const T_PARENT = '<';
+ const T_DELIM_CHANGE = '=';
+ const T_ESCAPED = '_v';
+ const T_UNESCAPED = '{';
+ const T_UNESCAPED_2 = '&';
+ const T_TEXT = '_t';
+ const T_PRAGMA = '%';
+ const T_BLOCK_VAR = '$';
+ const T_BLOCK_ARG = '$arg';
+
+ // Valid token types
+ private static $tagTypes = array(
+ self::T_SECTION => true,
+ self::T_INVERTED => true,
+ self::T_END_SECTION => true,
+ self::T_COMMENT => true,
+ self::T_PARTIAL => true,
+ self::T_PARENT => true,
+ self::T_DELIM_CHANGE => true,
+ self::T_ESCAPED => true,
+ self::T_UNESCAPED => true,
+ self::T_UNESCAPED_2 => true,
+ self::T_PRAGMA => true,
+ self::T_BLOCK_VAR => true,
+ );
+
+ private static $tagNames = array(
+ self::T_SECTION => 'section',
+ self::T_INVERTED => 'inverted section',
+ self::T_END_SECTION => 'section end',
+ self::T_COMMENT => 'comment',
+ self::T_PARTIAL => 'partial',
+ self::T_PARENT => 'parent',
+ self::T_DELIM_CHANGE => 'set delimiter',
+ self::T_ESCAPED => 'variable',
+ self::T_UNESCAPED => 'unescaped variable',
+ self::T_UNESCAPED_2 => 'unescaped variable',
+ self::T_PRAGMA => 'pragma',
+ self::T_BLOCK_VAR => 'block variable',
+ self::T_BLOCK_ARG => 'block variable',
+ );
+
+ // Token properties
+ const TYPE = 'type';
+ const NAME = 'name';
+ const DYNAMIC = 'dynamic';
+ const OTAG = 'otag';
+ const CTAG = 'ctag';
+ const LINE = 'line';
+ const INDEX = 'index';
+ const END = 'end';
+ const INDENT = 'indent';
+ const NODES = 'nodes';
+ const VALUE = 'value';
+ const FILTERS = 'filters';
+
+ private $state;
+ private $tagType;
+ private $buffer;
+ private $tokens;
+ private $seenTag;
+ private $line;
+
+ private $otag;
+ private $otagChar;
+ private $otagLen;
+
+ private $ctag;
+ private $ctagChar;
+ private $ctagLen;
+
+ /**
+ * Scan and tokenize template source.
+ *
+ * @throws Mustache_Exception_SyntaxException when mismatched section tags are encountered
+ * @throws Mustache_Exception_InvalidArgumentException when $delimiters string is invalid
+ *
+ * @param string $text Mustache template source to tokenize
+ * @param string $delimiters Optionally, pass initial opening and closing delimiters (default: empty string)
+ *
+ * @return array Set of Mustache tokens
+ */
+ public function scan($text, $delimiters = '')
+ {
+ // Setting mbstring.func_overload makes things *really* slow.
+ // Let's do everyone a favor and scan this string as ASCII instead.
+ //
+ // The INI directive was removed in PHP 8.0 so we don't need to check there (and can drop it
+ // when we remove support for older versions of PHP).
+ //
+ // @codeCoverageIgnoreStart
+ $encoding = null;
+ if (version_compare(PHP_VERSION, '8.0.0', '<')) {
+ if (function_exists('mb_internal_encoding') && ini_get('mbstring.func_overload') & 2) {
+ $encoding = mb_internal_encoding();
+ mb_internal_encoding('ASCII');
+ }
+ }
+ // @codeCoverageIgnoreEnd
+
+ $this->reset();
+
+ if (is_string($delimiters) && $delimiters = trim($delimiters)) {
+ $this->setDelimiters($delimiters);
+ }
+
+ $len = strlen($text);
+ for ($i = 0; $i < $len; $i++) {
+ switch ($this->state) {
+ case self::IN_TEXT:
+ $char = $text[$i];
+ // Test whether it's time to change tags.
+ if ($char === $this->otagChar && substr($text, $i, $this->otagLen) === $this->otag) {
+ $i--;
+ $this->flushBuffer();
+ $this->state = self::IN_TAG_TYPE;
+ } else {
+ $this->buffer .= $char;
+ if ($char === "\n") {
+ $this->flushBuffer();
+ $this->line++;
+ }
+ }
+ break;
+
+ case self::IN_TAG_TYPE:
+ $i += $this->otagLen - 1;
+ $char = $text[$i + 1];
+ if (isset(self::$tagTypes[$char])) {
+ $tag = $char;
+ $this->tagType = $tag;
+ } else {
+ $tag = null;
+ $this->tagType = self::T_ESCAPED;
+ }
+
+ if ($this->tagType === self::T_DELIM_CHANGE) {
+ $i = $this->changeDelimiters($text, $i);
+ $this->state = self::IN_TEXT;
+ } elseif ($this->tagType === self::T_PRAGMA) {
+ $i = $this->addPragma($text, $i);
+ $this->state = self::IN_TEXT;
+ } else {
+ if ($tag !== null) {
+ $i++;
+ }
+ $this->state = self::IN_TAG;
+ }
+ $this->seenTag = $i;
+ break;
+
+ default:
+ $char = $text[$i];
+ // Test whether it's time to change tags.
+ if ($char === $this->ctagChar && substr($text, $i, $this->ctagLen) === $this->ctag) {
+ $token = array(
+ self::TYPE => $this->tagType,
+ self::NAME => trim($this->buffer),
+ self::OTAG => $this->otag,
+ self::CTAG => $this->ctag,
+ self::LINE => $this->line,
+ self::INDEX => ($this->tagType === self::T_END_SECTION) ? $this->seenTag - $this->otagLen : $i + $this->ctagLen,
+ );
+
+ if ($this->tagType === self::T_UNESCAPED) {
+ // Clean up `{{{ tripleStache }}}` style tokens.
+ if ($this->ctag === '}}') {
+ if (($i + 2 < $len) && $text[$i + 2] === '}') {
+ $i++;
+ } else {
+ $msg = sprintf(
+ 'Mismatched tag delimiters: %s on line %d',
+ $token[self::NAME],
+ $token[self::LINE]
+ );
+
+ throw new Mustache_Exception_SyntaxException($msg, $token);
+ }
+ } else {
+ $lastName = $token[self::NAME];
+ if (substr($lastName, -1) === '}') {
+ $token[self::NAME] = trim(substr($lastName, 0, -1));
+ } else {
+ $msg = sprintf(
+ 'Mismatched tag delimiters: %s on line %d',
+ $token[self::NAME],
+ $token[self::LINE]
+ );
+
+ throw new Mustache_Exception_SyntaxException($msg, $token);
+ }
+ }
+ }
+
+ $this->buffer = '';
+ $i += $this->ctagLen - 1;
+ $this->state = self::IN_TEXT;
+ $this->tokens[] = $token;
+ } else {
+ $this->buffer .= $char;
+ }
+ break;
+ }
+ }
+
+ if ($this->state !== self::IN_TEXT) {
+ $this->throwUnclosedTagException();
+ }
+
+ $this->flushBuffer();
+
+ // Restore the user's encoding...
+ // @codeCoverageIgnoreStart
+ if ($encoding) {
+ mb_internal_encoding($encoding);
+ }
+ // @codeCoverageIgnoreEnd
+
+ return $this->tokens;
+ }
+
+ /**
+ * Helper function to reset tokenizer internal state.
+ */
+ private function reset()
+ {
+ $this->state = self::IN_TEXT;
+ $this->tagType = null;
+ $this->buffer = '';
+ $this->tokens = array();
+ $this->seenTag = false;
+ $this->line = 0;
+
+ $this->otag = '{{';
+ $this->otagChar = '{';
+ $this->otagLen = 2;
+
+ $this->ctag = '}}';
+ $this->ctagChar = '}';
+ $this->ctagLen = 2;
+ }
+
+ /**
+ * Flush the current buffer to a token.
+ */
+ private function flushBuffer()
+ {
+ if (strlen($this->buffer) > 0) {
+ $this->tokens[] = array(
+ self::TYPE => self::T_TEXT,
+ self::LINE => $this->line,
+ self::VALUE => $this->buffer,
+ );
+ $this->buffer = '';
+ }
+ }
+
+ /**
+ * Change the current Mustache delimiters. Set new `otag` and `ctag` values.
+ *
+ * @throws Mustache_Exception_SyntaxException when delimiter string is invalid
+ *
+ * @param string $text Mustache template source
+ * @param int $index Current tokenizer index
+ *
+ * @return int New index value
+ */
+ private function changeDelimiters($text, $index)
+ {
+ $startIndex = strpos($text, '=', $index) + 1;
+ $close = '=' . $this->ctag;
+ $closeIndex = strpos($text, $close, $index);
+
+ if ($closeIndex === false) {
+ $this->throwUnclosedTagException();
+ }
+
+ $token = array(
+ self::TYPE => self::T_DELIM_CHANGE,
+ self::LINE => $this->line,
+ );
+
+ try {
+ $this->setDelimiters(trim(substr($text, $startIndex, $closeIndex - $startIndex)));
+ } catch (Mustache_Exception_InvalidArgumentException $e) {
+ throw new Mustache_Exception_SyntaxException($e->getMessage(), $token);
+ }
+
+ $this->tokens[] = $token;
+
+ return $closeIndex + strlen($close) - 1;
+ }
+
+ /**
+ * Set the current Mustache `otag` and `ctag` delimiters.
+ *
+ * @throws Mustache_Exception_InvalidArgumentException when delimiter string is invalid
+ *
+ * @param string $delimiters
+ */
+ private function setDelimiters($delimiters)
+ {
+ if (!preg_match('/^\s*(\S+)\s+(\S+)\s*$/', $delimiters, $matches)) {
+ throw new Mustache_Exception_InvalidArgumentException(sprintf('Invalid delimiters: %s', $delimiters));
+ }
+
+ list($_, $otag, $ctag) = $matches;
+
+ $this->otag = $otag;
+ $this->otagChar = $otag[0];
+ $this->otagLen = strlen($otag);
+
+ $this->ctag = $ctag;
+ $this->ctagChar = $ctag[0];
+ $this->ctagLen = strlen($ctag);
+ }
+
+ /**
+ * Add pragma token.
+ *
+ * Pragmas are hoisted to the front of the template, so all pragma tokens
+ * will appear at the front of the token list.
+ *
+ * @param string $text
+ * @param int $index
+ *
+ * @return int New index value
+ */
+ private function addPragma($text, $index)
+ {
+ $end = strpos($text, $this->ctag, $index);
+ if ($end === false) {
+ $this->throwUnclosedTagException();
+ }
+
+ $pragma = trim(substr($text, $index + 2, $end - $index - 2));
+
+ // Pragmas are hoisted to the front of the template.
+ array_unshift($this->tokens, array(
+ self::TYPE => self::T_PRAGMA,
+ self::NAME => $pragma,
+ self::LINE => 0,
+ ));
+
+ return $end + $this->ctagLen - 1;
+ }
+
+
+ private function throwUnclosedTagException()
+ {
+ $name = trim($this->buffer);
+ if ($name !== '') {
+ $msg = sprintf('Unclosed tag: %s on line %d', $name, $this->line);
+ } else {
+ $msg = sprintf('Unclosed tag on line %d', $this->line);
+ }
+
+ throw new Mustache_Exception_SyntaxException($msg, array(
+ self::TYPE => $this->tagType,
+ self::NAME => $name,
+ self::OTAG => $this->otag,
+ self::CTAG => $this->ctag,
+ self::LINE => $this->line,
+ self::INDEX => $this->seenTag - $this->otagLen,
+ ));
+ }
+
+ /**
+ * Get the human readable name for a tag type.
+ *
+ * @param string $tagType One of the tokenizer T_* constants
+ *
+ * @return string
+ */
+ static function getTagName($tagType)
+ {
+ return isset(self::$tagNames[$tagType]) ? self::$tagNames[$tagType] : 'unknown';
+ }
+}
diff --git a/config.php.example b/config.php.example
index 083c555..7714153 100644
--- a/config.php.example
+++ b/config.php.example
@@ -15,15 +15,22 @@ define('CONFIG_PROVIDER', 'Universität Freiburg');
define('CONFIG_ADMINS', serialize(array('5fb22037697816a70a847d15245c9f88', '94e48d34587ab9963a2013ddc97e1e45', 'fb91f270a95a5b006be916f2b2da305c')));
-define('CONFIG_IDM_LINK_SN', 'https://www.bwidm.de/attribute/#surname');
-define('CONFIG_IDM_LINK_GIVENNAME', 'https://www.bwidm.de/attribute/#givenName');
-define('CONFIG_IDM_LINK_MAIL', 'https://www.bwidm.de/attribute/#mail');
-define('CONFIG_IDM_LINK_PID', 'https://www.bwidm.de/attribute/#IdPPersistentNameIdentifier');
-define('CONFIG_IDM_LINK_EPSA', 'https://www.bwidm.de/attribute/#eduPersonScopedAffiliation');
+define('CONFIG_IDM_LINK_SN', 'https://www.bwidm.de/attribute.php#Nachname');
+define('CONFIG_IDM_LINK_GIVENNAME', 'https://www.bwidm.de/attribute.php#Vorname');
+define('CONFIG_IDM_LINK_MAIL', 'https://www.bwidm.de/attribute.php#E-Mail-Adresse');
+define('CONFIG_IDM_LINK_PID', 'https://www.bwidm.de/attribute.php#Persistant%20ID');
+define('CONFIG_IDM_LINK_EPSA', 'https://www.bwidm.de/attribute.php#Zugeh%C3%B6rigkeit');
define('CONFIG_SURNAME', 'sn');
define('CONFIG_EPPN', 'eppn');
define('CONFIG_SCOPED_AFFILIATION', 'affiliation');
+// If enabled, when a new user registers, check if there is an existing user with
+// same organizationid, email, first and last name. If so, allow user to merge account
+// with existing one. This should be safe if you trust all the IdPs in your federation,
+// which should be assumed to be true anyways for a million other reasons.
+// If this is false, only offer merge if the existing account is a "test account", local
+// to the masterserver.
+define('CONFIG_ALLOW_SHIB_MERGE', true);
// Have a properties file or set variables here manually.
// Make sure properties file is not in webroot
diff --git a/inc/crypto.inc.php b/inc/crypto.inc.php
index 56f5073..75a0a01 100644
--- a/inc/crypto.inc.php
+++ b/inc/crypto.inc.php
@@ -8,7 +8,7 @@ class Crypto
* which translates to ~130 bit salt
* and 5000 rounds of hashing with SHA-512.
*/
- public static function hash6($password)
+ public static function hash6(string $password): string
{
$salt = substr(str_replace('+', '.', base64_encode(pack('N4', mt_rand(), mt_rand(), mt_rand(), mt_rand()))), 0, 16);
$hash = crypt($password, '$6$' . $salt);
@@ -17,10 +17,10 @@ class Crypto
}
/**
- * Check if the given password matches the given cryp hash.
+ * Check if the given password matches the given crypt hash.
* Useful for checking a hashed password.
*/
- public static function verify($password, $hash)
+ public static function verify(string $password, string $hash): bool
{
return crypt($password, $hash) === $hash;
}
diff --git a/inc/database.inc.php b/inc/database.inc.php
index f76c9e7..6518631 100644
--- a/inc/database.inc.php
+++ b/inc/database.inc.php
@@ -8,28 +8,43 @@ class Database
{
/**
- *
* @var \PDO Database handle
*/
- private static $dbh = false;
- private static $statements = array();
-
-
- /**
+ private static ?PDO $dbh = null;
+
+ private static bool $returnErrors;
+ private static ?string $lastError = null;
+ private static array $explainList = array();
+ private static int $queryCount = 0;
+ private static float $queryTime = 0;
+
+ /**
* Connect to the DB if not already connected.
*/
- private static function init()
+ public static function init(bool $returnErrors = false): bool
{
- if (self::$dbh !== false)
- return;
+ if (self::$dbh !== null)
+ return true;
+ self::$returnErrors = $returnErrors;
try {
- if (CONFIG_SQL_FORCE_UTF8)
- self::$dbh = new PDO(CONFIG_SQL_DSN, CONFIG_SQL_USER, CONFIG_SQL_PASS, array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8"));
- else
- self::$dbh = new PDO(CONFIG_SQL_DSN, CONFIG_SQL_USER, CONFIG_SQL_PASS);
+ self::$dbh = new PDO(CONFIG_SQL_DSN, CONFIG_SQL_USER, CONFIG_SQL_PASS, [
+ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+ PDO::ATTR_EMULATE_PREPARES => true,
+ PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4', // Somehow needed, even if charset=utf8mb4 is in DSN?
+ ]);
} catch (PDOException $e) {
+ if (self::$returnErrors)
+ return false;
Util::traceError('Connecting to the local database failed: ' . $e->getMessage());
}
+ if (CONFIG_DEBUG) {
+ Database::exec("SET SESSION sql_mode='STRICT_ALL_TABLES,NO_ENGINE_SUBSTITUTION,ERROR_FOR_DIVISION_BY_ZERO'");
+ Database::exec("SET SESSION innodb_strict_mode=ON");
+ register_shutdown_function(function() {
+ self::examineLoggedQueries();
+ });
+ }
+ return true;
}
/**
@@ -37,25 +52,100 @@ class Database
*
* @return array|boolean Associative array representing row, or false if no row matches the query
*/
- public static function queryFirst($query, $args = array(), $ignoreError = false)
+ public static function queryFirst(string $query, array $args = [], bool $ignoreError = null)
+ {
+ $res = self::simpleQuery($query, $args, $ignoreError);
+ if ($res === false)
+ return false;
+ return $res->fetch();
+ }
+
+ /**
+ * If you need all rows for a query as plain array you can use this.
+ * Don't use this if you want to do further processing of the data, to save some
+ * memory.
+ *
+ * @return array|bool List of associative arrays representing rows, or false on error
+ */
+ public static function queryAll(string $query, array $args = [], bool $ignoreError = null)
+ {
+ $res = self::simpleQuery($query, $args, $ignoreError);
+ if ($res === false)
+ return false;
+ return $res->fetchAll();
+ }
+
+ /**
+ * Fetch the first column of the query as a plain list-of-values array.
+ *
+ * @return array|bool List of values representing first column of query
+ */
+ public static function queryColumnArray(string $query, array $args = [], bool $ignoreError = null)
+ {
+ $res = self::simpleQuery($query, $args, $ignoreError);
+ if ($res === false)
+ return false;
+ return $res->fetchAll(PDO::FETCH_COLUMN, 0);
+ }
+
+ /**
+ * Fetch two columns as key => value list.
+ *
+ * @return array|bool Associative array, first column is key, second column is value
+ */
+ public static function queryKeyValueList(string $query, array $args = [], bool $ignoreError = null)
{
$res = self::simpleQuery($query, $args, $ignoreError);
if ($res === false)
return false;
- return $res->fetch(PDO::FETCH_ASSOC);
+ return $res->fetchAll(PDO::FETCH_KEY_PAIR);
}
+ /**
+ * Fetch and group by first column. First column is key, value is a list of rows with remaining columns.
+ * [
+ * col1 => [
+ * [col2, col3],
+ * [col2, col3],
+ * ],
+ * ...,
+ * ]
+ *
+ * @return array|bool Associative array, first column is key, remaining columns are array values
+ */
+ public static function queryGroupList(string $query, array $args = [], bool $ignoreError = null)
+ {
+ $res = self::simpleQuery($query, $args, $ignoreError);
+ if ($res === false)
+ return false;
+ return $res->fetchAll(PDO::FETCH_GROUP);
+ }
+
+ /**
+ * Fetch and use first column as key of returned array.
+ * This is like queryGroup list, but it is assumed that the first column is unique, so
+ * the remaining columns won't be wrapped in another array.
+ *
+ * @return array|bool Associative array, first column is key, remaining columns are array values
+ */
+ public static function queryIndexedList(string $query, array $args = [], bool $ignoreError = null)
+ {
+ $res = self::simpleQuery($query, $args, $ignoreError);
+ if ($res === false)
+ return false;
+ return $res->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_UNIQUE);
+ }
/**
* Execute the given query and return the number of rows affected.
* Mostly useful for UPDATEs or INSERTs
- *
+ *
* @param string $query Query to run
* @param array $args Arguments to query
- * @param boolean $ignoreError Ignore query errors and just return false
+ * @param ?bool $ignoreError Ignore query errors and just return false
* @return int|boolean Number of rows affected, or false on error
*/
- public static function exec($query, $args = array(), $ignoreError = false)
+ public static function exec(string $query, array $args = [], bool $ignoreError = null)
{
$res = self::simpleQuery($query, $args, $ignoreError);
if ($res === false)
@@ -64,41 +154,194 @@ class Database
}
/**
- * Get id (promary key) of last row inserted.
- *
+ * Get id (primary key) of last row inserted.
+ *
* @return int the id
*/
- public static function lastInsertId()
+ public static function lastInsertId(): int
{
return self::$dbh->lastInsertId();
}
/**
+ * @return ?string return last error returned by query
+ */
+ public static function lastError(): ?string
+ {
+ return self::$lastError;
+ }
+
+ /**
* Execute the given query and return the corresponding PDOStatement object
* Note that this will re-use PDOStatements, so if you run the same
* query again with different params, do not rely on the first PDOStatement
* still being valid. If you need to do something fancy, use Database::prepare
- * @return \PDOStatement The query result object
+ *
+ * @return \PDOStatement|false The query result object
*/
- public static function simpleQuery($query, $args = array(), $ignoreError = false)
+ public static function simpleQuery(string $query, array $args = [], bool $ignoreError = null)
{
self::init();
- try {
- if (!isset(self::$statements[$query])) {
- self::$statements[$query] = self::$dbh->prepare($query);
- } else {
- self::$statements[$query]->closeCursor();
+ if (CONFIG_DEBUG && !isset(self::$explainList[$query]) && preg_match('/^\s*SELECT/is', $query)) {
+ self::$explainList[$query] = [$args];
+ }
+ // Support passing nested arrays for IN statements, automagically refactor
+ $oquery = $query;
+ self::handleArrayArgument($query, $args);
+ // Now turn any bools into 0 or 1, since PDO unfortunately only does (string)<bool>, which
+ // results in an empty string for false
+ foreach ($args as &$arg) {
+ if ($arg === false) {
+ $arg = '0';
+ } elseif ($arg === true) {
+ $arg = '1';
}
- if (self::$statements[$query]->execute($args) === false) {
- if ($ignoreError)
+ }
+ try {
+ $stmt = self::$dbh->prepare($query);
+ $start = microtime(true);
+ if ($stmt->execute($args) === false) {
+ self::$lastError = implode("\n", $stmt->errorInfo());
+ if ($ignoreError === true || ($ignoreError === null && self::$returnErrors))
return false;
- Util::traceError("Database Error: \n" . implode("\n", self::$statements[$query]->errorInfo()));
+ Util::traceError("Database Error: \n" . self::$lastError);
}
- return self::$statements[$query];
+ if (CONFIG_DEBUG) {
+ $duration = microtime(true) - $start;
+ self::$queryTime += $duration;
+ $duration = round($duration, 3);
+ if (isset(self::$explainList[$oquery])) {
+ self::$explainList[$oquery][] = $duration;
+ } elseif ($duration > 0.1) {
+ error_log('SLOW ****** ' . $duration . "s *********\n" . $query);
+ }
+ self::$queryCount += 1;
+ }
+ return $stmt;
} catch (Exception $e) {
- if ($ignoreError)
- return false;
- Util::traceError("Database Error: \n" . $e->getMessage());
+ self::$lastError = '(' . $e->getCode() . ') ' . $e->getMessage();
+ if ($ignoreError === true || ($ignoreError === null && self::$returnErrors))
+ return false;
+ Util::traceError("Database Error: \n" . self::$lastError);
+ }
+ return false;
+ }
+
+ public static function examineLoggedQueries()
+ {
+ foreach (self::$explainList as $q => $a) {
+ self::explainQuery($q, $a);
+ }
+ }
+
+ private static function explainQuery(string $query, array $data)
+ {
+ $args = array_shift($data);
+ $slow = false;
+ $veryslow = false;
+ foreach ($data as &$ts) {
+ if ($ts > 0.004) {
+ $slow = true;
+ if ($ts > 0.015) {
+ $ts = "[$ts]";
+ $veryslow = true;
+ }
+ }
+ }
+ if (!$slow)
+ return;
+ unset($ts);
+ $res = self::simpleQuery('EXPLAIN ' . $query, $args, true);
+ if ($res === false)
+ return;
+ $rows = $res->fetchAll();
+ if (empty($rows))
+ return;
+ $log = $veryslow;
+ $lens = array();
+ foreach (array_keys($rows[0]) as $key) {
+ $lens[$key] = strlen($key);
+ }
+ foreach ($rows as $row) {
+ if (!$log && $row['rows'] > 20 && preg_match('/filesort|temporary/i', $row['Extra'])) {
+ $log = true;
+ }
+ foreach ($row as $key => $col) {
+ $l = strlen($col);
+ if ($l > $lens[$key]) {
+ $lens[$key] = $l;
+ }
+ }
+ }
+ if (!$log)
+ return;
+ error_log('Possible slow query: ' . $query);
+ error_log('Times: ' . implode(', ', $data));
+ $border = $head = '';
+ foreach ($lens as $key => $len) {
+ $border .= '+' . str_repeat('-', $len + 2);
+ $head .= '| ' . str_pad($key, $len) . ' ';
+ }
+ $border .= '+';
+ $head .= '|';
+ error_log("\n" . $border . "\n" . $head . "\n" . $border);
+ foreach ($rows as $row) {
+ $line = '';
+ foreach ($lens as $key => $len) {
+ $line .= '| '. str_pad($row[$key], $len) . ' ';
+ }
+ error_log($line . "|");
+ }
+ error_log($border);
+ }
+
+ /**
+ * Convert nested array argument to multiple arguments.
+ * If you have:
+ * $query = 'SELECT * FROM tbl WHERE bcol = :bool AND col IN (:list)
+ * $args = ( 'bool' => 1, 'list' => ('foo', 'bar') )
+ * it results in:
+ * $query = '...WHERE bcol = :bool AND col IN (:list_0, :list_1)
+ * $args = ( 'bool' => 1, 'list_0' => 'foo', 'list_1' => 'bar' )
+ *
+ * @param string $query sql query string
+ * @param array $args query arguments
+ * @return void
+ */
+ private static function handleArrayArgument(string &$query, array &$args)
+ {
+ $again = false;
+ foreach (array_keys($args) as $key) {
+ if (is_numeric($key) || $key === '?')
+ continue;
+ if (is_array($args[$key])) {
+ if (empty($args[$key])) {
+ // Empty list - what to do? We try to generate a query string that will not yield any result
+ $args[$key] = 'asdf' . mt_rand(0,PHP_INT_MAX) . mt_rand(0,PHP_INT_MAX)
+ . mt_rand(0,PHP_INT_MAX) . '@' . microtime(true);
+ continue;
+ }
+ $newkey = $key;
+ if ($newkey[0] !== ':') {
+ $newkey = ":$newkey";
+ }
+ $new = array();
+ foreach ($args[$key] as $subIndex => $sub) {
+ if (is_array($sub)) {
+ $new[] = '(' . $newkey . '_' . $subIndex . ')';
+ $again = true;
+ } else {
+ $new[] = $newkey . '_' . $subIndex;
+ }
+ $args[$newkey . '_' . $subIndex] = $sub;
+ }
+ unset($args[$key]);
+ $new = implode(',', $new);
+ $query = preg_replace('/' . $newkey . '\b/', $new, $query);
+ }
+ }
+ if ($again) {
+ self::handleArrayArgument($query, $args);
}
}
@@ -106,10 +349,115 @@ class Database
* Simply calls PDO::prepare and returns the PDOStatement.
* You must call PDOStatement::execute manually on it.
*/
- public static function prepare($query)
+ public static function prepare(string $query)
{
- self:init();
+ self::init();
+ self::$queryCount += 1; // Cannot know actual count
return self::$dbh->prepare($query);
}
+ /**
+ * Insert row into table, returning the generated key.
+ * This requires the table to have an AUTO_INCREMENT column and
+ * usually requires the given $uniqueValues to span across a UNIQUE index.
+ * The code first tries to SELECT the key for the given values without
+ * inserting first. This means this function is best used for cases
+ * where you expect that the entry already exists in the table, so
+ * only one SELECT will run. For all the entries that do not exist,
+ * an INSERT or INSERT IGNORE is run, depending on whether $additionalValues
+ * is empty or not. Another reason we don't run the INSERT (IGNORE) first
+ * is that it will increase the AUTO_INCREMENT value on InnoDB, even when
+ * no INSERT took place. So if you expect a lot of collisions you might
+ * use this function to prevent your A_I value from counting up too
+ * quickly.
+ * Other than that, this is just a dumb version of running INSERT and then
+ * getting the LAST_INSERT_ID(), or doing a query for the existing ID in
+ * case of a key collision.
+ *
+ * @param string $table table to insert into
+ * @param string $aiKey name of the AUTO_INCREMENT column
+ * @param array $uniqueValues assoc array containing columnName => value mapping
+ * @param ?array $additionalValues assoc array containing columnName => value mapping
+ * @return int AUTO_INCREMENT value matching the given unique values entry
+ */
+ public static function insertIgnore(string $table, string $aiKey, array $uniqueValues, array $additionalValues = null): int
+ {
+ // Sanity checks
+ if (array_key_exists($aiKey, $uniqueValues)) {
+ Util::traceError("$aiKey must not be in \$uniqueValues");
+ }
+ if (is_array($additionalValues) && array_key_exists($aiKey, $additionalValues)) {
+ Util::traceError("$aiKey must not be in \$additionalValues");
+ }
+ // Simple SELECT first
+ $selectSql = 'SELECT ' . $aiKey . ' FROM ' . $table . ' WHERE 1';
+ foreach ($uniqueValues as $key => $value) {
+ $selectSql .= ' AND ' . $key . ' = :' . $key;
+ }
+ $selectSql .= ' LIMIT 1';
+ $res = self::queryFirst($selectSql, $uniqueValues);
+ if ($res !== false) {
+ // Exists
+ if (!empty($additionalValues)) {
+ // Simulate ON DUPLICATE KEY UPDATE ...
+ $updateSql = 'UPDATE ' . $table . ' SET ';
+ $first = true;
+ foreach ($additionalValues as $key => $value) {
+ if ($first) {
+ $first = false;
+ } else {
+ $updateSql .= ', ';
+ }
+ $updateSql .= $key . ' = :' . $key;
+ }
+ $updateSql .= ' WHERE ' . $aiKey . ' = :' . $aiKey;
+ $additionalValues[$aiKey] = $res[$aiKey];
+ Database::exec($updateSql, $additionalValues);
+ }
+ return $res[$aiKey];
+ }
+ // Does not exist:
+ if (empty($additionalValues)) {
+ $combined =& $uniqueValues;
+ } else {
+ $combined = $uniqueValues + $additionalValues;
+ }
+ // Aight, try INSERT or INSERT IGNORE
+ $insertSql = 'INTO ' . $table . ' (' . implode(', ', array_keys($combined))
+ . ') VALUES (:' . implode(', :', array_keys($combined)) . ')';
+ if (empty($additionalValues)) {
+ // Simple INSERT IGNORE
+ $insertSql = 'INSERT IGNORE ' . $insertSql;
+ } else {
+ // INSERT ... ON DUPLICATE (in case we have a race)
+ $insertSql = 'INSERT ' . $insertSql . ' ON DUPLICATE KEY UPDATE ';
+ $first = true;
+ foreach ($additionalValues as $key => $value) {
+ if ($first) {
+ $first = false;
+ } else {
+ $insertSql .= ', ';
+ }
+ $insertSql .= $key . ' = VALUES(' . $key . ')';
+ }
+ }
+ self::exec($insertSql, $combined);
+ // Insert done, retrieve key again
+ $res = self::queryFirst($selectSql, $uniqueValues);
+ if ($res === false) {
+ Util::traceError('Could not find value in table ' . $table . ' that was just inserted');
+ }
+ return $res[$aiKey];
+ }
+
+ public static function getQueryCount(): int
+ {
+ return self::$queryCount;
+ }
+
+ public static function getQueryTime(): int
+ {
+ return self::$queryTime;
+ }
+
}
diff --git a/inc/image.inc.php b/inc/image.inc.php
index 1bad04f..03af811 100644
--- a/inc/image.inc.php
+++ b/inc/image.inc.php
@@ -3,21 +3,22 @@
class Image
{
- public static function deleteOwnedBy($userid)
+ public static function deleteOwnedBy(string $userid): bool
{
if ($userid === false || !is_numeric($userid))
return false;
//return Database::exec('DELETE FROM image WHERE ownerid = :userid', array('userid' => $userid));
// TODO
+ return false;
}
- public static function getImageCount($login)
+ public static function getImageCount(string $login): int
{
$ret = Database::queryFirst('SELECT Count(*) AS cnt FROM imagebase '
. ' WHERE imagebase.ownerid = :userid', array('userid' => $login));
if ($ret === false)
return 0;
- return $ret['cnt'];
+ return (int)$ret['cnt'];
}
}
diff --git a/inc/message.inc.php b/inc/message.inc.php
index 7decc12..67f7b08 100644
--- a/inc/message.inc.php
+++ b/inc/message.inc.php
@@ -2,31 +2,31 @@
class Message
{
- private static $list = array();
- private static $alreadyDisplayed = array();
- private static $flushed = false;
+ private static array $list = array();
+ private static array $alreadyDisplayed = array();
+ private static bool $flushed = false;
/**
* Add error message to page. If messages have not been flushed
* yet, it will be added to the queue, otherwise it will be added
* in place during rendering.
*/
- public static function addError($id)
+ public static function addError(string $id): void
{
self::add('error', $id, func_get_args());
}
- public static function addWarning($id)
+ public static function addWarning(string $id): void
{
self::add('warning', $id, func_get_args());
}
- public static function addInfo($id)
+ public static function addInfo(string $id): void
{
self::add('info', $id, func_get_args());
}
- public static function addSuccess($id)
+ public static function addSuccess(string $id): void
{
self::add('success', $id, func_get_args());
}
@@ -35,7 +35,7 @@ class Message
* Internal function that adds a message. Used by
* addError/Success/Info/... above.
*/
- private static function add($type, $id, $params)
+ private static function add(string $type, string $id, array $params): void
{
self::$list[] = array(
'type' => $type,
@@ -50,7 +50,7 @@ class Message
* After calling this, any further calls to add* will be rendered in
* place in the current page output.
*/
- public static function renderList()
+ public static function renderList(): void
{
// Non-Ajax
foreach (self::$list as $item) {
@@ -69,7 +69,7 @@ class Message
* Get all queued messages, flushing the queue.
* Useful in api/ajax mode.
*/
- public static function asString()
+ public static function asString(): string
{
$return = '';
foreach (self::$list as $item) {
@@ -88,7 +88,7 @@ class Message
* Deserialize any messages from the current HTTP request and
* place them in the message queue.
*/
- public static function fromRequest()
+ public static function fromRequest(): void
{
$messages = is_array($_REQUEST['message']) ? $_REQUEST['message'] : array($_REQUEST['message']);
foreach ($messages as $message) {
@@ -102,7 +102,7 @@ class Message
* Turn the current message queue into a serialized version,
* suitable for appending to a GET or POST request
*/
- public static function toRequest()
+ public static function toRequest(): string
{
$parts = array();
foreach (array_merge(self::$list, self::$alreadyDisplayed) as $item) {
diff --git a/inc/render.inc.php b/inc/render.inc.php
index 0147709..6a22d3f 100644
--- a/inc/render.inc.php
+++ b/inc/render.inc.php
@@ -14,7 +14,7 @@ Render::init();
class Render
{
- private static $mustache = false;
+ private static ?Mustache_Engine $mustache = null;
private static $body = '';
private static $header = '';
private static $footer = '';
@@ -24,7 +24,7 @@ class Render
public static function init()
{
- if (self::$mustache !== false)
+ if (self::$mustache !== null)
Util::traceError('Called Render::init() twice!');
self::$mustache = new Mustache_Engine;
}
@@ -32,15 +32,13 @@ class Render
/**
* Output the buffered, generated page
*/
- public static function output()
+ public static function output(): void
{
Header('Content-Type: text/html; charset=utf-8');
- $zip = isset($_SERVER['HTTP_ACCEPT_ENCODING']) && (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false);
- if ($zip)
- ob_start();
+ ob_start('ob_gzhandler');
echo
'<!DOCTYPE html>
- <html>
+ <html lang="de">
<head>
<title>', RENDER_DEFAULT_TITLE, self::$title, '</title>
<meta charset="utf-8">
@@ -69,14 +67,7 @@ class Render
'</body>
</html>'
;
- if ($zip) {
- Header('Content-Encoding: gzip');
- ob_implicit_flush(false);
- $gzip_contents = ob_get_contents();
- ob_end_clean();
- echo "\x1f\x8b\x08\x00\x00\x00\x00\x00";
- echo substr(gzcompress($gzip_contents, 5), 0, -4);
- }
+ ob_end_flush();
}
/**
diff --git a/inc/session.inc.php b/inc/session.inc.php
index 17e3184..f89d87e 100644
--- a/inc/session.inc.php
+++ b/inc/session.inc.php
@@ -3,13 +3,13 @@
class Session
{
- private static $sid = false;
- private static $data = false;
- private static $needUpdate = true;
+ private static ?string $sid = null;
+ private static ?array $data = null;
+ private static bool $needUpdate = true;
- private static function generateSessionId()
+ private static function generateSessionId(): void
{
- if (self::$sid !== false)
+ if (self::$sid !== null)
Util::traceError('Error: Asked to generate session id when already set.');
self::$sid = sha1(
mt_rand(0, 65535)
@@ -24,13 +24,13 @@ class Session
);
}
- public static function create()
+ public static function create(): void
{
self::generateSessionId();
self::$data = array();
}
- public static function load()
+ public static function load(): bool
{
// Try to load session id from cookie
if (!self::loadSessionId()) return false;
@@ -46,7 +46,7 @@ class Session
return self::get('uid');
}
- public static function setUid($value)
+ public static function setUid($value): void
{
if (strlen($value) < 5)
Util::traceError('Invalid user id: ' . $value);
@@ -60,7 +60,7 @@ class Session
return false;
}
- public static function set($key, $value)
+ public static function set(string $key, $value): void
{
if (!is_array(self::$data))
Util::traceError('Tried to set session data with no active session');
@@ -70,9 +70,9 @@ class Session
self::$needUpdate = true;
}
- private static function loadSessionId()
+ private static function loadSessionId(): bool
{
- if (self::$sid !== false)
+ if (self::$sid !== null)
Util::traceError('Error: Asked to load session id when already set.');
if (empty($_COOKIE['sid']))
return false;
@@ -83,18 +83,18 @@ class Session
return true;
}
- public static function delete()
+ public static function delete(): void
{
- if (self::$sid === false) return;
+ if (self::$sid === null) return;
Database::exec('DELETE FROM websession WHERE sid = :sid', array('sid' => self::$sid));
setcookie('sid', '', time() - CONFIG_SESSION_TIMEOUT, null, null, !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off', true);
- self::$sid = false;
- self::$data = false;
+ self::$sid = null;
+ self::$data = null;
}
- public static function save()
+ public static function save(): void
{
- if (self::$sid === false || self::$data === false || !self::$needUpdate)
+ if (self::$sid === null || self::$data === null || !self::$needUpdate)
return;
$data = json_encode(self::$data);
$ret = Database::exec('INSERT INTO websession (sid, dateline, data) '
@@ -108,9 +108,9 @@ class Session
Util::traceError('Error: Could not set Cookie for Client (headers already sent)');
}
- public static function readSessionData()
+ public static function readSessionData(): bool
{
- if (self::$sid === false || self::$data !== false)
+ if (self::$sid === null || self::$data !== null)
Util::traceError('Tried to readSessionData on an active session!');
$data = Database::queryFirst('SELECT dateline, data FROM websession WHERE sid = :sid LIMIT 1', array('sid' => self::$sid));
if ($data === false) {
@@ -121,9 +121,8 @@ class Session
return false;
}
self::$needUpdate = ($data['dateline'] + 3600 < time());
- self::$data = @json_decode($data['data'], true);
- if (!is_array(self::$data))
- self::$data = array();
+ $data = @json_decode($data['data'], true);
+ self::$data = is_array($data) ? $data : [];
return true;
}
diff --git a/inc/shibauth.inc.php b/inc/shibauth.inc.php
new file mode 100644
index 0000000..6ae3a89
--- /dev/null
+++ b/inc/shibauth.inc.php
@@ -0,0 +1,202 @@
+<?php
+
+class ShibAuth
+{
+
+ /**
+ * Log user into master-server using the data provided by the current shibboleth session
+ * @param ?string $accessCode optional one-time access code to retreive session data via thrift
+ * @return array{status: string, firstName: string, lastName: string, mail: string, token: string, sessionId: string, userId: string, organizationId: string, url: string, error: string}
+ */
+ private static function loginInternal(?string $accessCode = null): array
+ {
+ if ($accessCode !== null) {
+ $entrop = strlen(count_chars($accessCode, 3));
+ if (strlen($accessCode) < 32 || strlen($accessCode) > 64 || $entrop < 10) {
+ return ['status' => 'error', 'error' => 'Malformed accessCode'];
+ }
+ }
+ // Handle
+ if (empty($_SERVER['persistent-id'])) {
+ // No persistent id given, should not happen!
+ file_put_contents('/tmp/shib-nopid-' . time() . '-' . $_SERVER['REMOTE_ADDR'] . '.txt', print_r($_SERVER, true));
+ return ['status' => 'error', 'error' => 'Shibboleth metadata missing!'];
+ }
+ // Query database for user
+ // First, use persistent-id as-is
+ $shibId = [ md5($_SERVER['persistent-id']) ];
+ // ... but we might have multiple persistent IDs, split
+ if (strpos($_SERVER['persistent-id'], ';') !== false) {
+ foreach (explode(';', $_SERVER['persistent-id']) as $s) {
+ if (empty($s))
+ continue;
+ $shibId[] = md5($s);
+ }
+ }
+ // Figure out role
+ if (strpos(";{$_SERVER['entitlement']};", CONFIG_ENTITLEMENT) !== false) {
+ $role = 'TUTOR';
+ } else if (strpos(";{$_SERVER[CONFIG_SCOPED_AFFILIATION]};", ';employee@') !== false
+ || strpos(";{$_SERVER[CONFIG_SCOPED_AFFILIATION]};", ';staff@') !== false
+ || strpos(";{$_SERVER[CONFIG_SCOPED_AFFILIATION]};", ';faculty@') !== false) {
+ $role = 'TUTOR';
+ } else {
+ file_put_contents('/tmp/shib-student-' . time() . '-' . $_SERVER['REMOTE_ADDR'] . '.txt', print_r($_SERVER, true));
+ $role = 'STUDENT';
+ // NEW: Ignore students for now
+ return [
+ 'status' => 'error',
+ 'error' => "Sie wurden als Student eingestuft und können sich daher nicht an der " . CONFIG_SUITE . "-Suite anmelden."
+ . "\nFalls Ihr Nutzerkonto kein Studentenkonto ist stellen Sie sicher, dass Ihr IdP für berechtigte"
+ . "\nAccounts entweder das " . CONFIG_SUITE . "-Entitlement ausliefert, oder das Attribut " . CONFIG_SCOPED_AFFILIATION
+ . "\nausgeliefert wird, und es entweder 'employee@..', 'staff@..' oder 'faculty@..' enthält."
+ . "\n\nMehr Informationen finden Sie unter " . CONFIG_HELPURL
+ ];
+ // end IGNORE STUDENTS
+ }
+ // Now we have an array of all persistent-ids, plus the raw string; see if any of those match
+ $user = Database::queryFirst("SELECT user.userid, user.organizationid, user.firstname, user.lastname, user.email "
+ . " FROM user "
+ . " INNER JOIN organization USING (organizationid) "
+ . " WHERE user.shibid IN (:shibid) LIMIT 1", array('shibid' => $shibId));
+ if ($user === false) {
+ // Not found, so we don't know which satellite to use
+ if ($role === 'STUDENT') {
+ $response['status'] = 'ok';
+ $response['firstName'] = $_SERVER['givenName'] ?? 'Anonymous';
+ $response['lastName'] = $_SERVER[CONFIG_SURNAME] ?? 'Student';
+ $response['mail'] = $_SERVER['mail'] ?? 'void@none.invalid';
+ $response['userId'] = $shibId;
+ // Try to figure out orgId
+ if (!isset($response['organizationId']) && isset($_SERVER[CONFIG_EPPN])) {
+ if (preg_match('/@(.+)$/', $_SERVER[CONFIG_EPPN], $out)) {
+ $out = Database::queryFirst("SELECT organizationid FROM organization_suffix WHERE suffix = :suffix", [
+ 'suffix' => $out[1]
+ ]);
+ if ($out !== false) {
+ $response['organizationId'] = $out['organizationid'];
+ }
+ }
+ }
+ if (!isset($response['organizationId']) && isset($_SERVER[CONFIG_SCOPED_AFFILIATION])) {
+ if (preg_match('/(^|;)[^@]+@([^;]+)/', $_SERVER[CONFIG_SCOPED_AFFILIATION], $out)) {
+ $out = Database::queryFirst("SELECT organizationid FROM organization_suffix WHERE suffix = :suffix", [
+ 'suffix' => $out[2]
+ ]);
+ if ($out !== false) {
+ $response['organizationId'] = $out['organizationid'];
+ }
+ }
+ }
+ // This one we send to the running master server handler
+ $rpc = $response;
+ $rpc['role'] = $role;
+ if (isset($response['organizationId']) && $accessCode === null) {
+ $response['satellites2'] = self::getSatelliteList($response['organizationId']);
+ }
+ } else {
+ $response['status'] = 'unregistered';
+ $response['error'] = 'Sie müssen sich erst für die Nutzung von ' . CONFIG_SUITE . ' registrieren';
+ }
+ $response['id'] = $shibId;
+ $response['url'] = CONFIG_MASTERWEBIF;
+ file_put_contents('/tmp/shib-unreg-' . time() . '-' . $_SERVER['REMOTE_ADDR'] . '.txt', print_r($_SERVER, true));
+ } else {
+ /*
+ if (in_array($shibId, unserialize(CONFIG_ADMINS), true) || $shibId === '2fa5c3e020a5aca0cbf9a562268d5173-') {
+ $role = 'STUDENT';
+ }
+ */
+ // Found, see if we got personal information, either temporarily through metadata, or from database
+ $firstName = $user['firstname'];
+ $lastName = $user['lastname'];
+ $mail = $user['email'];
+ if (empty($firstName) && isset($_SERVER['givenName']))
+ $firstName = trim($_SERVER['givenName']);
+ if (empty($lastName) && isset($_SERVER[CONFIG_SURNAME]))
+ $lastName = trim($_SERVER[CONFIG_SURNAME]);
+ if (empty($mail) && isset($_SERVER['mail']))
+ $mail = trim($_SERVER['mail']);
+ //
+ $login = (empty($user['userid']) ? $shibId : $user['userid'] );
+ if (empty($firstName) || empty($lastName) || empty($login)) {
+ // This means the user did not provide personal information on signup, nor does the IdP send them
+ $response['status'] = 'anonymous';
+ $response['error'] = "Ihr IdP liefert nicht die erforderlichen Attribute\n"
+ . CONFIG_SURNAME . ', ' . 'givenName' . ', ' . 'email';
+ } else {
+ // Seems ok!
+ //
+ $response['status'] = 'ok';
+ $response['firstName'] = $firstName;
+ $response['lastName'] = $lastName;
+ $response['mail'] = $mail;
+ $response['userId'] = $user['userid'];
+ $response['organizationId'] = $user['organizationid'];
+ // This one we send to the running master server handler
+ $rpc = $response;
+ $rpc['userId'] = $login;
+ $rpc['role'] = $role;
+ // This one we only send to the user
+ $response['satellites2'] = self::getSatelliteList($user['organizationid']);
+ }
+ }
+
+ if (isset($rpc)) {
+ if ($accessCode !== null) {
+ $rpc['accessCode'] = $accessCode;
+ }
+ $reply = RPC::submit($rpc);
+ if (preg_match('/^TOKEN:(\w+) SESSIONID:(\w+)$/', $reply, $out)) {
+ // For talking to the sat server, also referred to as userToken in Java
+ $response['token'] = $out[1];
+ // For talking to the master server - not known by satellite server, referred to as masterToken in Java
+ $response['sessionId'] = $out[2];
+ } else {
+ if (empty($rpc['mail'])) {
+ $reply .= ' (No email given)';
+ }
+ if (empty($rpc['firstName'])) {
+ $reply .= ' (No first name given)';
+ }
+ if (empty($rpc['lastName'])) {
+ $reply .= ' (No last name given)';
+ }
+ if (empty($rpc['organizationId'])) {
+ $reply .= ' (No organization id found)';
+ }
+ $response['error'] = $reply;
+ $response['status'] = 'error';
+ }
+ }
+ return $response;
+ }
+
+ public static function login(?string $accessCode = null): array
+ {
+ $res = self::loginInternal($accessCode);
+ if ($res['status'] !== 'ok' && isset($res['error']) && $accessCode !== null) {
+ RPC::submit(['status' => 'error', 'error' => $res['error'], 'accessCode' => $accessCode]);
+ }
+ return $res;
+ }
+
+ private static function getSatelliteList($orgId)
+ {
+ // Determine satellite(s)
+ $res = Database::simpleQuery("SELECT satellitename, addresses, certsha256 FROM satellite"
+ . " WHERE organizationid = :organizationid AND userid IS NULL", array('organizationid' => $orgId));
+ $sat2 = array();
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $addrs = json_decode($row['addresses'], true);
+ if (!is_array($addrs) || empty($addrs))
+ continue;
+ $sat2[$row['satellitename']] = array(
+ 'addresses' => $addrs,
+ 'certHash' => $row['certsha256']
+ );
+ }
+ return $sat2;
+ }
+
+} \ No newline at end of file
diff --git a/inc/user.inc.php b/inc/user.inc.php
index a5a8e3c..bc07f5d 100644
--- a/inc/user.inc.php
+++ b/inc/user.inc.php
@@ -3,15 +3,15 @@
class User
{
- private static $user = false;
- private static $organization = NULL;
- private static $isShib = false;
- private static $isInDb = false;
- private static $isAnonymous = false;
+ private static ?array $user = null;
+ private static ?array $organization = NULL;
+ private static bool $isShib = false;
+ private static bool $isInDb = false;
+ private static bool $isAnonymous = false;
- public static function isLoggedIn()
+ public static function isLoggedIn(): bool
{
- return self::$user !== false;
+ return self::$user !== null;
}
public static function isShibbolethAuth()
@@ -26,7 +26,7 @@ class User
public static function isLocalOnly()
{
- return self::$user !== false && self::$isShib === false;
+ return self::$user !== null && self::$isShib === false;
}
public static function isAnonymous()
@@ -39,44 +39,44 @@ class User
return self::$user;
}
- public static function getId()
+ public static function getId(): ?string
{
if (!isset(self::$user['userid']))
- return false;
+ return null;
return self::$user['userid'];
}
- public static function getMail()
+ public static function getMail(): ?string
{
if (!isset(self::$user['email']))
- return false;
+ return null;
return self::$user['email'];
}
- public static function getName()
+ public static function getName(): ?string
{
if (!self::isLoggedIn())
- return false;
+ return null;
return self::$user['firstname'] . ' ' . self::$user['lastname'];
}
- public static function getFirstName()
+ public static function getFirstName(): ?string
{
if (!self::isLoggedIn())
- return false;
+ return null;
return self::$user['firstname'];
}
- public static function getLastName()
+ public static function getLastName(): ?string
{
if (!self::isLoggedIn())
- return false;
+ return null;
return self::$user['lastname'];
}
- public static function hasFullName()
+ public static function hasFullName(): bool
{
- return self::$user !== false && !empty(self::$user['firstname']) && !empty(self::$user['lastname']);
+ return self::$user !== null && !empty(self::$user['firstname']) && !empty(self::$user['lastname']);
}
public static function isTutor()
@@ -84,7 +84,7 @@ class User
return isset(self::$user['role']) && self::$user['role'] === 'TUTOR';
}
- public static function isAdmin()
+ public static function isAdmin(): bool
{
// TODO: per Institution...
return in_array(self::getShibId(), unserialize(CONFIG_ADMINS), true);
@@ -95,19 +95,19 @@ class User
*
* @return string
*/
- public static function getOrganizationId()
+ public static function getOrganizationId(): ?string
{
$org = self::getOrganization();
if (!isset($org['organizationid']))
- return false;
+ return null;
return $org['organizationid'];
}
- public static function getOrganizationName()
+ public static function getOrganizationName(): ?string
{
$org = self::getOrganization();
if (!isset($org['name']))
- return false;
+ return null;
return $org['name'];
}
@@ -116,21 +116,26 @@ class User
*
* @return string
*/
- public static function getRemoteOrganizationId()
+ public static function getRemoteOrganizationId(): ?string
{
if (empty(self::$user['organization']))
- return false;
+ return null;
return self::$user['organization'];
}
- public static function getOrganization()
+ /**
+ * Return user's organization, or null if not known in our DB.
+ * @return ?array{organizationid: string, name: string}
+ */
+ public static function getOrganization(): ?array
{
if (!self::isLoggedIn())
- return false;
+ return null;
if (is_null(self::$organization)) {
- self::$organization = Database::queryFirst('SELECT organizationid, name FROM organization_suffix '
+ $org = Database::queryFirst('SELECT organizationid, name FROM organization_suffix '
. ' INNER JOIN organization USING (organizationid) '
. ' WHERE suffix = :org LIMIT 1', array('org' => self::$user['organization']));
+ self::$organization = $org !== false ? $org : null;
}
return self::$organization;
}
@@ -159,8 +164,10 @@ class User
return false;
}
// Try user from local DB
- self::$user = Database::queryFirst('SELECT userid, shibid, organizationid AS organization, firstname, lastname, email FROM user WHERE userid = :uid LIMIT 1', array('uid' => Session::getUid()));
- self::$isInDb = self::$user !== false;
+ $usr = Database::queryFirst('SELECT userid, shibid, organizationid AS organization, firstname, lastname, email
+ FROM user WHERE userid = :uid LIMIT 1', ['uid' => Session::getUid()]);
+ self::$user = $usr !== false ? $usr : null;
+ self::$isInDb = self::$user !== null;
if (!self::$isInDb) {
Session::delete();
}
@@ -187,10 +194,16 @@ class User
$_SERVER['givenName'] = '';
if (!isset($_SERVER['mail']))
$_SERVER['mail'] = '';
- $shibId = md5($_SERVER['persistent-id']);
+ $shibId = [];
+ if (strpos($_SERVER['persistent-id'], ';') !== false) {
+ foreach (explode(';', $_SERVER['persistent-id']) as $s) {
+ $shibId[] = md5($s);
+ }
+ }
+ $shibId[] = md5($_SERVER['persistent-id']);
self::$user = array(
'userid' => NULL,
- 'shibid' => $shibId,
+ 'shibid' => $shibId[0],
'firstname' => $_SERVER['givenName'],
'lastname' => $_SERVER[CONFIG_SURNAME],
'email' => $_SERVER['mail'],
@@ -205,14 +218,15 @@ class User
else
self::$user['role'] = 'STUDENT';
// Try to figure out organization
- if (isset($_SERVER[CONFIG_EPPN]) && preg_match('/@([0-9a-zA-Z\-\._]+)$/', $_SERVER[CONFIG_EPPN], $out)) {
+ if (isset($_SERVER[CONFIG_EPPN]) && preg_match('/@([0-9a-zA-Z\-._]+)$/', $_SERVER[CONFIG_EPPN], $out)) {
self::$user['organization'] = $out[1];
}
- if (!isset(self::$user['organization']) && isset($_SERVER[CONFIG_SCOPED_AFFILIATION]) && preg_match('/@([0-9a-zA-Z\-\._]+)(;|$)/', $_SERVER[CONFIG_SCOPED_AFFILIATION], $out)) {
+ if (!isset(self::$user['organization']) && isset($_SERVER[CONFIG_SCOPED_AFFILIATION]) && preg_match('/@([0-9a-zA-Z\-._]+)(;|$)/', $_SERVER[CONFIG_SCOPED_AFFILIATION], $out)) {
self::$user['organization'] = $out[1];
}
// Get matching db entry if any
- $user = Database::queryFirst('SELECT userid, firstname, lastname, email, fixedname FROM user WHERE shibid = :shibid LIMIT 1', array('shibid' => $shibId));
+ $user = Database::queryFirst('SELECT userid, firstname, lastname, email, fixedname FROM user
+ WHERE shibid IN (:shibid) LIMIT 1', ['shibid' => $shibId]);
if ($user === false) {
// No match in database, user is not signed up
return true;
@@ -232,11 +246,16 @@ class User
return true;
}
- public static function deploy($anonymous, $existingLogin = false)
+ public static function deploy(bool $anonymous, $existingLogin = false): bool
{
if (empty(self::$user['shibid']))
Util::traceError('NO SHIBID');
+ if (self::getOrganizationId() === null) {
+ Message::addError('Your home organization ID {{0}} is not known to this server', self::getRemoteOrganizationId());
+ Util::redirect('?do=Main');
+ }
+
// Merging with test-account:
if (!empty($existingLogin)) {
if ($anonymous) {
@@ -300,7 +319,7 @@ class User
'mail' => $mail,
'user' => self::getId()
));
- return $ret == 1 || $mail === self::get('email');
+ return $ret == 1 || $mail === self::$user['email'];
}
public static function login($user, $pass)
diff --git a/inc/util.inc.php b/inc/util.inc.php
index aaf46c6..0e06e6a 100644
--- a/inc/util.inc.php
+++ b/inc/util.inc.php
@@ -59,7 +59,7 @@ SADFACE;
public static function redirect($location = false)
{
if ($location === false) {
- $location = preg_replace('/(&|\?)message\[\]\=[^&]*/', '\1', $_SERVER['REQUEST_URI']);
+ $location = preg_replace('/([&?])message\[]=[^&]*/', '\1', $_SERVER['REQUEST_URI']);
}
Session::save();
$messages = Message::toRequest();
@@ -113,9 +113,9 @@ SADFACE;
public static function markup($string)
{
$string = htmlspecialchars($string);
- $string = preg_replace('#(^|[\n \-_/\.])\*(.+?)\*($|[ \-_/\.\!\?,:])#is', '$1<b>$2</b>$3', $string);
- $string = preg_replace('#(^|[\n \-\*/\.])_(.+?)_($|[ \-\*/\.\!\?,:])#is', '$1<u>$2</u>$3', $string);
- $string = preg_replace('#(^|[\n \-_\*\.])/(.+?)/($|[ \-_\*\.\!\?,:])#is', '$1<i>$2</i>$3', $string);
+ $string = preg_replace('#(^|[\n \-_/.])\*(.+?)\*($|[ \-_/.!?,:])#is', '$1<b>$2</b>$3', $string);
+ $string = preg_replace('#(^|[\n \-*/.])_(.+?)_($|[ \-*/.!?,:])#is', '$1<u>$2</u>$3', $string);
+ $string = preg_replace('#(^|[\n \-_*.])/(.+?)/($|[ \-_*.!?,:])#is', '$1<i>$2</i>$3', $string);
return nl2br($string);
}
@@ -123,11 +123,11 @@ SADFACE;
* Convert given number to human readable file size string.
* Will append Bytes, KiB, etc. depending on magnitude of number.
*
- * @param type $bytes numeric value of the filesize to make readable
- * @param type $decimals number of decimals to show, -1 for automatic
- * @return type human readable string representing the given filesize
+ * @param int $bytes numeric value of the filesize to make readable
+ * @param int $decimals number of decimals to show, -1 for automatic
+ * @return string human-readable string representing the given filesize
*/
- public static function readableFileSize($bytes, $decimals = -1)
+ public static function readableFileSize(int $bytes, int $decimals = -1): string
{
static $sz = array('Byte', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB');
$factor = floor((strlen($bytes) - 1) / 3);
@@ -139,20 +139,20 @@ SADFACE;
return sprintf("%.{$decimals}f ", $bytes / pow(1024, $factor)) . $sz[$factor];
}
- public static function sanitizeFilename($name)
+ public static function sanitizeFilename(string $name): string
{
return preg_replace('/[^a-zA-Z0-9_\-]+/', '_', $name);
}
- public static function safePath($path, $prefix = '')
+ public static function safePath(string $path, string $prefix = ''): ?string
{
if (empty($path))
- return false;
+ return null;
$path = trim($path);
- if ($path{0} == '/' || preg_match('/[\x00-\x19\?\*]/', $path))
- return false;
+ if ($path[0] == '/' || preg_match('/[\x00-\x19?*]/', $path))
+ return null;
if (strpos($path, '..') !== false)
- return false;
+ return null;
if (substr($path, 0, 2) !== './')
$path = "./$path";
if (empty($prefix))
@@ -160,7 +160,7 @@ SADFACE;
if (substr($prefix, 0, 2) !== './')
$prefix = "./$prefix";
if (substr($path, 0, strlen($prefix)) !== $prefix)
- return false;
+ return null;
return $path;
}
@@ -249,14 +249,14 @@ SADFACE;
/**
* Send a file to user for download.
*
- * @param type $file path of local file
- * @param type $name name of file to send to user agent
- * @param type $delete delete the file when done?
- * @return boolean false: file could not be opened.
+ * @param string $file path of local file
+ * @param string $name name of file to send to user agent
+ * @param bool $delete delete the file when done?
+ * @return bool false: file could not be opened.
* true: error while reading the file
* - on success, the function does not return
*/
- public static function sendFile($file, $name, $delete = false)
+ public static function sendFile(string $file, string $name, bool $delete = false): bool
{
while ((@ob_get_level()) > 0)
@ob_end_clean();
diff --git a/index.php b/index.php
index d94322e..6b7c3d8 100644
--- a/index.php
+++ b/index.php
@@ -128,6 +128,7 @@ if (defined('CONFIG_DEBUG') && CONFIG_DEBUG) {
if(User::isAdmin()) {
Message::addWarning(User::getShibId());
Message::addWarning('<pre>'.print_r($_SERVER, true).'</pre>');
+ Message::addWarning('<pre>'.print_r($_COOKIE, true).'</pre>');
}
}
diff --git a/modules/adduser.inc.php b/modules/adduser.inc.php
index c725e27..207858b 100644
--- a/modules/adduser.inc.php
+++ b/modules/adduser.inc.php
@@ -25,10 +25,10 @@ class Page_AddUser extends Page
Message::addError('Keine Einrichtung gewählt.');
} else if (empty($firstname) || empty($lastname)
|| empty($login) || empty($password)) {
- Message:addError('Ein Feld wurde nicht ausgefüllt.');
+ Message::addError('Ein Feld wurde nicht ausgefüllt.');
} else {
// Validate login
- if (preg_match('/^[a-z0-9_\.\-]+@([a-z0-9_\.\-]+)$/i', $login, $out)) {
+ if (preg_match('/^[a-z0-9_.\-]+@([a-z0-9_.\-]+)$/i', $login, $out)) {
// Complete login
$suffix = $out[1];
} else if (strpos($login, '@') !== false) {
@@ -47,8 +47,9 @@ class Page_AddUser extends Page
if ($ok === false) {
Message::addError('Login-Suffix @{{0}} ist ungültig.', $suffix);
} else {
- Database::exec('INSERT INTO user (userid, password, organizationid, firstname, lastname, email) '
- . ' VALUES (:userid, :password, :organization, :firstname, :lastname, :email)', array(
+ Database::exec('INSERT INTO user (userid, password, organizationid, firstname, lastname, email)
+ VALUES (:userid, :password, :organization, :firstname, :lastname, :email)
+ ON DUPLICATE KEY UPDATE password = VALUES(password)', array(
'userid' => $login,
'password' => Crypto::hash6($password),
'organization' => $organizationid,
@@ -78,4 +79,4 @@ class Page_AddUser extends Page
Render::addTemplate('adduser/_page', array('orgs' => $orgs));
}
-} \ No newline at end of file
+}
diff --git a/modules/agb.inc.php b/modules/agb.inc.php
index 7d38482..8728612 100644
--- a/modules/agb.inc.php
+++ b/modules/agb.inc.php
@@ -13,6 +13,7 @@ class Page_Agb extends Page
$data['linkidmmail'] = CONFIG_IDM_LINK_MAIL;
$data['linkidmepsa'] = CONFIG_IDM_LINK_EPSA;
$data['linkidmpid'] = CONFIG_IDM_LINK_PID;
+ $data['helpmail'] = CONFIG_HELPMAIL;
Render::addTemplate('agb/_page', $data);
}
diff --git a/modules/images.inc.php b/modules/images.inc.php
new file mode 100644
index 0000000..f962c07
--- /dev/null
+++ b/modules/images.inc.php
@@ -0,0 +1,51 @@
+<?php
+
+class Page_Images extends Page
+{
+
+ protected function doPreprocess()
+ {
+ User::load();
+ if (!User::isShibbolethAuth()) {
+ Message::addError('Not {{0}}', CONFIG_IDM);
+ Util::redirect('?do=Main');
+ }
+ if (!User::isAdmin()) {
+ Message::addError('Not admin!');
+ Util::redirect('?do=Main');
+ }
+ if (Request::post('action') === 'delete') {
+ $image = Request::post('image');
+ $row = Database::queryFirst('SELECT filepath FROM imageversion WHERE imageversionid = :version',
+ ['version' => $image]);
+ if ($row === false) {
+ Message::addError('Image {{0}} nicht gefunden', $image);
+ } else {
+ // PHP process doesn't have write permissions to VM store, plus we don't have the absolute path
+ // for now this has to do, until someone comes along and adds an RPC method in the java app.
+ Message::addInfo('Vergessen Sie nicht, {{0}} vom Storage zu löschen', $row['filepath']);
+ Database::exec("DELETE FROM imageversion WHERE imageversionid = :version",
+ ['version' => $image]);
+ }
+ Util::redirect('?do=images');
+ }
+ }
+
+ protected function doRender()
+ {
+ $res = Database::simpleQuery('SELECT b.displayname, b.description,
+ v.imageversionid, v.createtime, v.expiretime, v.filesize, v.filepath
+ FROM imagebase b
+ INNER JOIN imageversion v USING (imagebaseid)
+ ORDER BY b.imagebaseid ASC, v.createtime ASC');
+ $rows = [];
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $row['createtime_s'] = date('d.m.Y', $row['createtime']);
+ $row['expiretime_s'] = date('d.m.Y', $row['expiretime']);
+ $row['filesize_s'] = Util::readableFileSize($row['filesize']);
+ $rows[] = $row;
+ }
+ Render::addTemplate('image-list', ['list' => $rows]);
+ }
+
+}
diff --git a/modules/main.inc.php b/modules/main.inc.php
index 6119814..3b605a3 100644
--- a/modules/main.inc.php
+++ b/modules/main.inc.php
@@ -35,16 +35,18 @@ class Page_Main extends Page
return;
}
if (!User::isTutor()) {
+ Message::addError('Sie sind kein Mitarbeiter der Einrichtung "' . User::getOrganizationName()
+ . '" und können daher die ' . CONFIG_SUITE . '-Suite nicht nutzen.');
return;
}
// User is not in DB, so he might want so sign up for the service - see if conditions are met
- if (User::getOrganization() !== false) {
+ if (User::getOrganization() !== null) {
// Organization is known, show signup form
$this->renderShibbolethUnregistered();
return;
}
// Nothing we can do here, show error message :-(
- if (User::getRemoteOrganizationId() !== false) {
+ if (User::getRemoteOrganizationId() !== null) {
// Organization is not known, see if we at least have an idea
Message::addWarning('Ihre Hochschule/Einrichtung {{0}} ist leider nicht bekannt. Bitte kontaktieren Sie den Support.', User::getRemoteOrganizationId());
} else {
@@ -63,22 +65,25 @@ class Page_Main extends Page
$data = User::getData();
$data['organization'] = User::getOrganizationName();
// Show testacc merge form if organization has test accounts
- $res = Database::queryFirst('SELECT Count(*) as cnt FROM user WHERE organizationid = :oid AND Length(password) <> 0', array(
- 'oid' => User::getOrganizationId()
- ));
$mail = trim(User::getMail());
- if (!empty($mail)) {
+ $fn = User::getFirstName();
+ $ln = User::getLastName();
+ if (!empty($mail) && (!empty($fn) || !empty($ln))) {
+ $extra = '';
+ if (!CONFIG_ALLOW_SHIB_MERGE) {
+ $extra = ' AND password IS NOT NULL AND Length(password) <> 0 ';
+ }
$existing = Database::queryFirst('SELECT userid FROM user
- WHERE email = :email AND lastname = :ln AND firstname = :fn LIMIT 1', array(
+ WHERE email = :email AND lastname = :ln AND firstname = :fn AND organizationid = :org ' . $extra . ' LIMIT 1', array(
'email' => $mail,
- 'fn' => User::getFirstName(),
- 'ln' => User::getLastName(),
+ 'fn' => $fn,
+ 'ln' => $ln,
+ 'org' => User::getOrganizationId(),
));
if ($existing !== false) {
$data['testlogin'] = $existing['userid'];
}
}
- $data['testacc'] = ($res !== false && $res['cnt'] > 0) || !empty($existing);
$data['suite'] = CONFIG_SUITE;
$data['idm'] = CONFIG_IDM;
Render::addTemplate('main/deploy', $data);
diff --git a/modules/register.inc.php b/modules/register.inc.php
index aa2b94c..f55e900 100644
--- a/modules/register.inc.php
+++ b/modules/register.inc.php
@@ -30,7 +30,7 @@ class Page_Register extends Page
}
if ($testLogin !== false) {
// Check if one of firstname, lastname or email matches
- $user = Database::queryFirst('SELECT firstname, lastname, email, organizationid FROM user WHERE userid = :login LIMIT 1',
+ $user = Database::queryFirst('SELECT firstname, lastname, email, password, organizationid FROM user WHERE userid = :login LIMIT 1',
array('login' => $testLogin));
if ($user === false || User::getOrganizationId() !== $user['organizationid']) {
// Invalid Login
@@ -38,9 +38,13 @@ class Page_Register extends Page
. ' Bitte wenden Sie sich an den {{1}}-Support, wenn dieser Test-Account Ihnen gehört.', $testLogin, CONFIG_SUITE);
Util::redirect('?do=Main');
}
- if (User::getLastName() !== $user['lastname']
- || User::getFirstName() !== $user['firstname']
- || User::getMail() !== $user['email']) {
+ if (empty($user['password']) && !CONFIG_ALLOW_SHIB_MERGE) {
+ Message::addError('Verknüpfung mit altem Shibboleth-basiertem Account nicht erlaubt');
+ Util::redirect('?do=Main');
+ }
+ if (strcasecmp(User::getLastName(), $user['lastname']) !== 0
+ || strcasecmp(User::getFirstName(), $user['firstname']) !== 0
+ || strcasecmp(User::getMail(), $user['email']) !== 0) {
// No match by personal information
Message::addError('Ihre Metadaten stimmen nicht mit dem Test-Account {{0}} überein. '
. ' Bitte wenden Sie sich an den {{1}}-Support, wenn dieser Test-Account Ihnen gehört.', $testLogin, CONFIG_SUITE);
diff --git a/modules/suitelogin.inc.php b/modules/suitelogin.inc.php
new file mode 100644
index 0000000..df3b8f0
--- /dev/null
+++ b/modules/suitelogin.inc.php
@@ -0,0 +1,31 @@
+<?php
+
+class Page_SuiteLogin extends Page
+{
+
+ protected function doPreprocess()
+ {
+ if (empty($_SERVER['persistent-id']))
+ Util::redirect(CONFIG_PREFIX . 'shib/?do=SuiteLogin');
+
+ if (!Request::any('msg')) {
+ $at = Request::any('accessToken');
+ if ($at === false || strlen($at) < 20) {
+ Message::addError('Missing access token');
+ } else {
+ $response = ShibAuth::login($at);
+
+ if ($response['status'] === 'ok') {
+ Message::addSuccess("Login erfolgreich, Sie können dieses Fenster jetzt schließen");
+ } else {
+ Message::addError("Login fehlgeschlagen: {{0}}", $response['error']);
+ if ($response['status'] === 'unregistered') {
+ Util::redirect('?do=Register');
+ }
+ }
+ }
+ Util::redirect('?do=SuiteLogin&msg=1');
+ }
+ }
+
+}
diff --git a/pam.php b/pam.php
index c5cb8fb..20c5a85 100644
--- a/pam.php
+++ b/pam.php
@@ -1,17 +1,40 @@
<?php
// Autoload classes from ./inc which adhere to naming scheme <lowercasename>.inc.php
-function slxAutoloader($class)
-{
- $file = 'inc/' . preg_replace('/[^a-z0-9]/', '', mb_strtolower($class)) . '.inc.php';
+spl_autoload_register(function ($class) {
+ $file = 'inc/' . preg_replace('/[^a-z0-9]/', '', strtolower($class)) . '.inc.php';
if (!file_exists($file))
return;
require_once $file;
-}
-spl_autoload_register('slxAutoloader');
+});
require_once 'config.php';
+$action = Request::any('action');
+
+//
+// New version - browser based
+//
+if ($action === 'browser') {
+ // Browser requesting a token
+ Header('Location: shib/client_auth.php?token=' . Request::any('token'));
+ exit;
+}
+
+if ($action === 'verify') {
+ // pam stack on client trying to verify
+ $row = Database::queryFirst("SELECT username FROM client_token WHERE token = :token AND dateline > UNIX_TIMESTAMP() - 300",
+ ['token' => (string)Request::any('token')]);
+ Header('Content-Type: text/plain; charset=utf-8');
+ if ($row === false) {
+ die("ERROR=Invalid token");
+ }
+ die("USER={$row['username']}");
+}
+
+//
+// Old way, ECP
+//
Header('Content-Type: text/plain; charset=utf-8');
$res = Database::simpleQuery("SELECT suffix, authmethod FROM organization INNER JOIN organization_suffix USING(organizationid)");
diff --git a/shib/api.php b/shib/api.php
index 533ae78..eec1e3d 100644
--- a/shib/api.php
+++ b/shib/api.php
@@ -11,175 +11,15 @@ die( json_encode($_SERVER, JSON_PRETTY_PRINT) );
// */
// Autoload classes from ./inc which adhere to naming scheme <lowercasename>.inc.php
-function slxAutoloader($class)
+spl_autoload_register(function ($class)
{
$file = 'inc/' . preg_replace('/[^a-z0-9]/', '', mb_strtolower($class)) . '.inc.php';
if (!file_exists($file))
return;
require_once $file;
-}
-spl_autoload_register('slxAutoloader');
+});
-$response = array();
-
-if (empty($_SERVER['persistent-id'])) {
- // No persistent id given, should not happen!
- $response['status'] = 'error';
- $response['error'] = 'Shibboleth meta data missing!';
- file_put_contents('/tmp/shib-nopid-' . time() . '-' . $_SERVER['REMOTE_ADDR'] . '.txt', print_r($_SERVER, true));
-} else {
- // Query database for user
- $shibId = md5($_SERVER['persistent-id']);
- $user = Database::queryFirst("SELECT user.userid, user.organizationid, user.firstname, user.lastname, user.email "
- . " FROM user "
- . " INNER JOIN organization USING (organizationid) "
- . " WHERE user.shibid = :shibid LIMIT 1", array('shibid' => $shibId));
- // Figure out role
- if (strpos(";{$_SERVER['entitlement']};", CONFIG_ENTITLEMENT) !== false) {
- $role = 'TUTOR';
- } else if (strpos(";{$_SERVER[CONFIG_SCOPED_AFFILIATION]};", ';employee@') !== false
- || strpos(";{$_SERVER[CONFIG_SCOPED_AFFILIATION]};", ';staff@') !== false
- || strpos(";{$_SERVER[CONFIG_SCOPED_AFFILIATION]};", ';faculty@') !== false) {
- $role = 'TUTOR';
- } else {
- file_put_contents('/tmp/shib-student-' . time() . '-' . $_SERVER['REMOTE_ADDR'] . '.txt', print_r($_SERVER, true));
- $role = 'STUDENT';
- // NEW: Ignore students for now
- $response = array(
- 'status' => 'error',
- 'error' => "Sie wurden als Student eingestuft und können sich daher nicht an der " . CONFIG_SUITE . "-Suite anmelden."
- . "\nFalls Ihr Nutzerkonto kein Studentenkonto ist stellen Sie sicher, dass Ihr IdP für berechtigte"
- . "\nAccounts entweder das " . CONFIG_SUITE . "-Entitlement ausliefert, oder das Attribut CONFIG_SCOPED_AFFILIATION"
- . "\nausgeliefert wird, und es entweder 'employee@..', 'staff@..' oder 'faculty@..' enthält."
- . "\n\nMehr Informationen finden Sie unter " . CONFIG_HELPURL
- );
- Header('Content-Type: text/plain; charset=utf-8');
- die(json_encode($response, JSON_PRETTY_PRINT));
- // end IGNORE STUDENTS
- }
- if ($user === false) {
- // Not found, so we don't know which satellite to use
- if ($role === 'STUDENT') {
- $response['status'] = 'ok';
- if (isset($_SERVER['givenName'])) {
- $response['firstName'] = $_SERVER['givenName'];
- }
- if (isset($_SERVER[CONFIG_SURNAME])) {
- $response['lastName'] = $_SERVER[CONFIG_SURNAME];
- }
- if (isset($_SERVER['mail'])) {
- $response['mail'] = $_SERVER['mail'];
- }
- $response['userId'] = $shibId;
- // Try to figure out orgId
- if (!isset($response['organizationId']) && isset($_SERVER[CONFIG_EPPN])) {
- if (preg_match('/@(.+)$/', $_SERVER[CONFIG_EPPN], $out)) {
- $out = Database::queryFirst("SELECT organizationid FROM organization_suffix WHERE suffix = :suffix", array(
- 'suffix' => $out[1]
- ));
- if ($out !== false) {
- $response['organizationId'] = $out['organizationid'];
- }
- }
- }
- if (!isset($response['organizationId']) && isset($_SERVER[CONFIG_SCOPED_AFFILIATION])) {
- if (preg_match('/(^|;)[^@]+@([^;]+)/', $_SERVER[CONFIG_SCOPED_AFFILIATION], $out)) {
- $out = Database::queryFirst("SELECT organizationid FROM organization_suffix WHERE suffix = :suffix", array(
- 'suffix' => $out[2]
- ));
- if ($out !== false) {
- $response['organizationId'] = $out['organizationid'];
- }
- }
- }
- // This one we send to the running master server handler
- $rpc = $response;
- $rpc['role'] = $role;
- // This one we only send to the user
- // TODO
- /*
- $response['satellites'] = $sat1;
- $response['satellites2'] = $sat2;
- */
- } else {
- $response['status'] = 'unregistered';
- }
- $response['id'] = $shibId;
- $response['url'] = CONFIG_MASTERWEBIF;
- file_put_contents('/tmp/shib-unreg-' . time() . '-' . $_SERVER['REMOTE_ADDR'] . '.txt', print_r($_SERVER, true));
- } else {
- // Found, see if we got personal information, either temporarily through metadata, or from database
- $firstName = $user['firstname'];
- $lastName = $user['lastname'];
- $mail = $user['email'];
- if (empty($firstName) && isset($_SERVER['givenName']))
- $firstName = trim($_SERVER['givenName']);
- if (empty($lastName) && isset($_SERVER[CONFIG_SURNAME]))
- $lastName = trim($_SERVER[CONFIG_SURNAME]);
- if (empty($mail) && isset($_SERVER['mail']))
- $mail = trim($_SERVER['mail']);
- //
- $login = (empty($user['userid']) ? $shibId : $user['userid'] );
- if (empty($firstName) || empty($lastName) || empty($login)) {
- // This means the user did not provide personal information on signup, nor does the IdP send them
- $response['status'] = 'anonymous';
- } else {
- // Seems ok!
- // Determine satellite(s)
- $res = Database::simpleQuery("SELECT satellitename, addresses, certsha256 FROM satellite"
- . " WHERE organizationid = :organizationid AND userid IS NULL", array('organizationid' => $user['organizationid']));
- $sat1 = array(); // Legacy
- $sat2 = array();
- while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
- $addrs = json_decode($row['addresses'], true);
- if (!is_array($addrs) || empty($addrs))
- continue;
- $sat1[$row['satellitename']] = $addrs[0];
- $sat2[$row['satellitename']] = array(
- 'addresses' => $addrs,
- 'certHash' => $row['certsha256']
- );
- }
- //
- $response['status'] = 'ok';
- $response['firstName'] = $firstName;
- $response['lastName'] = $lastName;
- $response['mail'] = $mail;
- $response['userId'] = $user['userid'];
- $response['organizationId'] = $user['organizationid'];
- // This one we send to the running master server handler
- $rpc = $response;
- $rpc['userId'] = $login;
- $rpc['role'] = $role;
- // This one we only send to the user
- $response['satellites'] = $sat1;
- $response['satellites2'] = $sat2;
- }
- }
-}
-
-if (isset($rpc)) {
- $reply = RPC::submit($rpc);
- if (preg_match('/^TOKEN:(\w+) SESSIONID:(\w+)$/', $reply, $out)) {
- $response['token'] = $out[1];
- $response['sessionId'] = $out[2];
- } else {
- if (empty($rpc['mail'])) {
- $reply .= ' (No email given)';
- }
- if (empty($rpc['firstName'])) {
- $reply .= ' (No first name given)';
- }
- if (empty($rpc['lastName'])) {
- $reply .= ' (No last name given)';
- }
- if (empty($rpc['organizationId'])) {
- $reply .= ' (No organization id found)';
- }
- $response['error'] = $reply;
- $response['status'] = 'error';
- }
-}
+$response = ShibAuth::login();
Header('Content-Type: text/plain; charset=utf-8');
echo json_encode($response, JSON_PRETTY_PRINT);
diff --git a/templates/agb/_page.html b/templates/agb/_page.html
index 6ce581e..b1eb8d7 100644
--- a/templates/agb/_page.html
+++ b/templates/agb/_page.html
@@ -1,35 +1,192 @@
-<h2>Datenschutz</h2>
-<p>
- Bei der Registrierung für den {{suite}}-Dienst werden die folgenden
- Nutzerinformationen von der Heimateinrichtung an den zentralen
- {{suite}}-Server des Dienstbetreibers ({{provider}})
- verschlüsselt übermittelt:
-</p>
-<ul>
- <li>Vor- und Nachname (<a href="{{linkidmsn}}">sn</a>, <a href="{{linkidmgivenname}}">givenName</a>) (*)</li>
- <li>E-Mailadresse (<a href="{{linkidmmail}}">mail</a>) (*)</li>
- <li>Eindeutige, anonyme Nutzerkennung (<a href="{{linkidmpid}}">IdPPersistentNameIdentifier</a>)</li>
- <li>Heimateinrichtung (<a href="{{linkidmepsa}}">eduPersonScopedAffiliation</a>)</li>
- <li>Status des Nutzers (Dozent/Mitarbeiter, Student, ..., <a href="{{linkidmepsa}}">eduPersonScopedAffiliation</a>)</li>
-</ul>
-<p>
- Die mit (*) gekennzeichneten Daten werden nicht zentral gespeichert
- sondern nur an Ihre Einrichtung weitergeleitet, es sei denn, Sie nehmen
- am zentralen VM-Austausch teil.
- <br>
- Wenn Sie nicht am {{suite}}-Dienst
- teilnehmen, werden keine Daten übertragen. Die Vorschriften des
- Landesdatenschutzgesetzes (LDSG) und bereichsspezifische
- Datenschutzvorschriften (insbesondere TKG, TMG) in den jeweils geltenden
- Fassungen werden beachtet.
-</p>
-
-<h2>Pflichten</h2>
-<p>
- Jeder Nutzer, der virtuelle Lehrumgebungen hochlädt oder modifiziert, ist dafür
- verantwortlich, dass alle an der eigenen Institution gültigen Lizenzbestimmungen
- eingehalten werden. Gleichzeitig ist er auch dazu verpflichtet, dass er gemäß der
- Empfehlungen des {{suite}}-Teams nach bestem Wissen und Gewissen dafür sorgt,
- dass sich keine Schadsoftware (Viren, Trojaner, …) in den virtuellen
- Lehrumgebungen befindet.
-</p> \ No newline at end of file
+<div class="row">
+ <div class="col-md-6">
+ <h2>Privacy Policy</h2>
+
+ <p><b>Name of service</b>: {{suite}}</p>
+
+ <h4>Service description</h4>
+
+ <p>{{suite}} enables the operation of computer pools with non-persistent virtual machines, which are tailored by
+ lecturers to their courses. Those VMs can optionally be shared across universities.</p>
+ </div>
+ <div class="col-md-6">
+ <h2>Datenschutz</h2>
+
+ <p><b>Name der Dienstleistung</b>: {{suite}}</p>
+
+ <h4>Beschreibung des Dienstes</h4>
+
+ <p>{{suite}} ermöglicht den Betrieb von Rechnerpools mit nicht-persistenten virtuellen Maschinen,
+ die von Dozierenden auf ihre Lehrveranstaltungen zugeschnitten werden. Diese VMs können optional
+ auch hochschulübergreifend genutzt werden.</p>
+ </div>
+</div>
+<div class="row">
+ <div class="col-md-6">
+ <h2>Obligations</h2>
+
+ <p>Every user who uploads or modifies virtual teaching environments is responsible for ensuring that all license
+ provisions applicable at their own institution are complied with. At the same time, they are also obliged to
+ ensure to the best of their knowledge and belief that there is no malware (viruses, trojans, etc.) in the
+ virtual teaching environments in accordance with the recommendations of the {{suite}} team.</p>
+ </div>
+ <div class="col-md-6">
+ <h2>Verpflichtungen</h2>
+
+ <p>Jeder Nutzende, der virtuelle Lehrumgebungen hochlädt oder verändert, ist dafür verantwortlich,
+ dass alle an der eigenen Hochschule geltenden Lizenzbestimmungen eingehalten werden.
+ Gleichzeitig ist er verpflichtet, nach bestem Wissen und Gewissen dafür zu sorgen, dass sich in
+ den virtuellen Lehrumgebungen keine Schadsoftware (Viren, Trojaner etc.) gemäß den Empfehlungen
+ des {{suite}}-Teams befindet.</p>
+ </div>
+</div>
+<div class="row">
+ <div class="col-md-6">
+ <h2>Data controller and contact information</h2>
+
+ <p>University of Freiburg, IT Services: <a href="mailto:{{helpmail}}">{{helpmail}}</a></p>
+
+ <p><b>Jurisdiction</b>: DE-BW Germany Baden-Württemberg</p>
+ </div>
+ <div class="col-md-6">
+ <h2>Datenverantwortlicher und Kontaktinformationen</h2>
+
+ <p>Albert-Ludwigs-Universität Freiburg, Rechenzentrum: <a href="mailto:{{helpmail}}">{{helpmail}}</a></p>
+
+ <b>Zuständigkeitsbereich:</b> DE-BW Deutschland Baden-Württemberg
+ </div>
+</div>
+<div class="row">
+ <div class="col-md-6">
+ <h2>Personal data processed</h2>
+
+ <p>The following data is requested from your Home Organisation</p>
+
+ <ul>
+ <li>your unique user identifier (<a href="{{linkidmpid}}">IdPPersistentNameIdentifier</a>)</li>
+ <li>your role in your Home Organisation (<a href="{{linkidmepsa}}">eduPersonScopedAffiliation</a>)</li>
+ <li>your permission to use this service independent of your role (eduPersonEntitlement)</li>
+ <li>your first and last name (<a href="{{linkidmsn}}">sn</a>, <a href="{{linkidmgivenname}}">givenName</a>) (*)</li>
+ <li>your email address (<a href="{{linkidmmail}}">mail</a>) (*)</li>
+ </ul>
+
+ <p>The data marked with (*) is not stored on our servers but only forwarded to your institution, unless you
+ participate in the central VM exchange.</p>
+ </div>
+ <div class="col-md-6">
+ <h2>Verarbeitete personenbezogene Daten</h2>
+
+ <p>Die folgenden Daten werden von Ihrer Heimatorganisation angefordert</p>
+
+ <ul>
+ <li>Eindeutige, anonyme Nutzerkennung (<a href="{{linkidmpid}}">IdPPersistentNameIdentifier</a>)</li>
+ <li>Status des Nutzers (Dozent/Mitarbeiter, Student, ..., <a
+ href="{{linkidmepsa}}">eduPersonScopedAffiliation</a>)
+ </li>
+ <li>Ihre Erlaubnis, diesen Dienst unabhängig von Ihrer Rolle zu nutzen (eduPersonEntitlement)</li>
+ <li>Vor- und Nachname (<a href="{{linkidmsn}}">sn</a>, <a href="{{linkidmgivenname}}">givenName</a>) (*)</li>
+ <li>E-Mailadresse (<a href="{{linkidmmail}}">mail</a>) (*)</li>
+ </ul>
+
+ <p>
+ Die mit (*) gekennzeichneten Daten werden nicht auf unseren Servern gespeichert,
+ sondern nur an Ihre Einrichtung weitergeleitet, sofern Sie nicht am zentralen
+ VM-Austausch teilnehmen.
+ </p>
+ </div>
+</div>
+<div class="row">
+ <div class="col-md-6">
+ <h4>Purpose of the processing of personal data</h4>
+
+ <p>The data is used</p>
+
+ <ul>
+ <li>to authorise your access to and use of the resources we provide</li>
+ <li>to relate shared public VMs to the owning/uploading person</li>
+ </ul>
+ </div>
+ <div class="col-md-6">
+ <h4>Zweck der Verarbeitung der personenbezogenen Daten</h2>
+
+ <p>Die Daten werden verwendet,</p>
+
+ <ul>
+ <li>um Ihren Zugang zu den von uns bereitgestellten Ressourcen und deren Nutzung zu autorisieren</li>
+ <li>um gemeinsam genutzte öffentliche VMs der besitzenden/hochladenden Person zuzuordnen</li>
+ </ul>
+ </div>
+</div>
+<div class="row">
+ <div class="col-md-6">
+ <h4>Third parties to whom personal data is disclosed</h4>
+
+ <p>We may share your personal data with third parties (or otherwise allow them access to it) in the following
+ cases:</p>
+
+ <ul>
+ <li>(a) to satisfy any applicable law, regulation, legal process, subpoena or governmental request</li>
+ <li>(b) to enforce this Privacy Notice, including investigation of potential violations thereof</li>
+ <li>(c) to relate shared VMs with the owner/uploading person e.g. when the VM is downloaded to a {{suite}}
+ satellite server of another institution using the service
+ </li>
+ </ul>
+
+ <p>Your personal data may also be accessible by others users of the service if you shared a public VM.</p>
+ </div>
+ <div class="col-md-6">
+ <h4>Dritte, an die personenbezogene Daten weitergegeben werden</h4>
+
+ <p>In den folgenden Fällen können wir Ihre personenbezogenen Daten an Dritte weitergeben
+ (oder ihnen anderweitig Zugang dazu gewähren):</p>
+
+ <ul>
+ <li>(a) zur Erfüllung geltender Gesetze, Vorschriften, rechtlicher Verfahren, Vorladungen oder behördlicher Anfragen</li>
+ <li>(b) zur Durchsetzung dieser Datenschutzerklärung, einschließlich der Untersuchung möglicher Verstöße dagegen</li>
+ <li>(c) um freigegebene VMs mit dem Eigentümer/der hochladenden Person in Verbindung zu bringen, z. B.
+ wenn die VM auf einen {{suite}}-Satellitenserver einer anderen Einrichtung heruntergeladen wird, die den Dienst nutzt</li>
+ </ul>
+
+ <p>Ihre personenbezogenen Daten können auch für andere Nutzer des Dienstes zugänglich sein, wenn Sie eine
+ öffentliche VM freigegeben haben.</p>
+ </div>
+</div>
+<div class="row">
+ <div class="col-md-6">
+ <h4>How to access, rectify and delete the personal data</h4>
+
+ <p>Contact the data controller above.
+ To rectify the data released by your Home Organisation, contact your Home Organisation's IT helpdesk.</p>
+
+ <p><b>Data retention</b>: Personal data is deleted on request by the user or if the user hasn't used the service
+ for two years.</p>
+
+ </div>
+ <div class="col-md-6">
+ <h4>Zugang, Berichtigung und Löschung der personenbezogenen Daten</h4>
+
+ <p>Wenden Sie sich an den oben genannten Datenverantwortlichen. Um die von Ihrer Heimateinrichtung freigegebenen Daten
+ zu berichtigen, wenden Sie sich an den IT-Helpdesk Ihrer Heimateinrichtung.</p>
+
+ <p><b>Aufbewahrung von Daten</b>: Personenbezogene Daten werden auf Antrag des Nutzers oder wenn der Nutzer den
+ Dienst zwei Jahre lang nicht genutzt hat, gelöscht.</p>
+
+ </div>
+</div>
+<div class="row">
+ <div class="col-md-6">
+ <h4>Data Protection Code of Conduct</h4>
+
+ <p>Your personal data will be protected according to the <a
+ href="http://www.geant.net/uri/dataprotection-code-of-conduct/v1"
+ rel="nofollow">Code of Conduct for Service Providers</a>, a common standard for the research and higher
+ education sector to protect your privacy.</p>
+ </div>
+ <div class="col-md-6">
+ <h4>Verhaltenskodex</h4>
+
+ <p>Ihre persönlichen Daten werden gemäß dem <a href="http://www.geant.net/uri/dataprotection-code-of-conduct/v1"
+ rel="nofollow">Code of Conduct for Service Providers</a> geschützt, einem gemeinsamen Standard für den
+ Forschungs- und Hochschulsektor zum Schutz Ihrer Privatsphäre.</p>
+ </div>
+</div>
diff --git a/templates/image-list.html b/templates/image-list.html
new file mode 100644
index 0000000..3c4561e
--- /dev/null
+++ b/templates/image-list.html
@@ -0,0 +1,51 @@
+<h1>Images</h1>
+
+<div class="alert alert-warning">
+ Die Löschfunktion entfernt lediglich den Datenbankeintrag. Bitte <b>löschen Sie die zugehörige Datei</b>
+ aus dem Storage-Verzeichnis (Spalte Pfad)
+</div>
+
+<form method="post" action="?do=images">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="delete">
+ <table class="table">
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Größe</th>
+ <th>Erstellt</th>
+ <th>Läuft ab</th>
+ <th>Pfad</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ {{#list}}
+ <tr>
+ <td title="{{description}}">
+ {{displayname}}
+ <div class="small">{{imageversionid}}</div>
+ </td>
+ <td class="slx-nowrap">
+ {{filesize_s}}
+ </td>
+ <td class="slx-nowrap">
+ {{createtime_s}}
+ </td>
+ <td class="slx-nowrap">
+ {{expiretime_s}}
+ </td>
+ <td class="small">
+ {{filepath}}
+ </td>
+ <td>
+ <button type="submit" name="image" value="{{imageversionid}}" class="btn btn-xs btn-danger"
+ onclick="return confirm('Wirklich?')">
+ <span class="glyphicon glyphicon-remove"></span>
+ </button>
+ </td>
+ </tr>
+ {{/list}}
+ </tbody>
+ </table>
+</form> \ No newline at end of file
diff --git a/templates/main-menu.html b/templates/main-menu.html
index e4583cc..ea32b1d 100644
--- a/templates/main-menu.html
+++ b/templates/main-menu.html
@@ -13,6 +13,7 @@
<ul class="nav navbar-nav">
{{#admin}}
<li><a href="?do=AddUser">AddUser</a></li>
+ <li><a href="?do=images">Manage Images</a></li>
{{/admin}}
</ul>
<ul class="nav navbar-nav navbar-right">
diff --git a/templates/main/deploy.html b/templates/main/deploy.html
index 4181a0b..e06edb0 100644
--- a/templates/main/deploy.html
+++ b/templates/main/deploy.html
@@ -19,6 +19,7 @@
Ihr Name und Ihre e-Mail-Adresse zentral gespeichert und für Dozenten anderer
Hochschulen auffindbar gemacht. Sie können diese Einstellung später jederzeit ändern.
</p>
+ <br><br>
<div class="input-group">
<span class="input-group-addon">
@@ -30,9 +31,11 @@
<p>
Sofern Sie am landesweiten VM-Austausch teilnehmen, werden Sie für andere Dozenten
über diese Daten auffindbar sein. Andernfalls werden diese Daten lediglich auf den
- {{suite}}-Satelliten-Server Ihrer eigenen Einrichtung übertragen. Sollten Sie auch
+ {{suite}}-Satellitenserver Ihrer eigenen Einrichtung übertragen. Sollten Sie auch
damit nicht einverstanden sein, schicken Sie die Registrierung bitte nicht ab.
</p>
+ <br><br>
+
<div class="group-group">
<div class="input-group">
<span class="input-group-addon slx-ga">
@@ -60,20 +63,21 @@
</div>
</div>
- {{#testacc}}
- <p>
- Haben Sie bisher einen lokalen Account (Test-Account) benutzt? Falls ja können Sie diesen
- jetzt mit Ihrem {{idm}}-Account zusammenführen, um Ihre bisherigen Veranstaltungen und Virtuelle
- Maschinen zu übernehmen. Ansonsten lassen Sie das Feld leer.
- </p>
+ {{#testlogin}}
+ <div class="alert alert-info">
+ Haben Sie bisher einen lokalen Account (Test-Account) benutzt, oder wurde Ihre <b>persistent-id</b> geändert?
+ Wenn gewünscht können Sie diesen alten Account jetzt mit Ihrem neuen {{idm}}-Account zusammenführen,
+ um Ihre bisherigen Veranstaltungen und Virtuelle Maschinen zu übernehmen. <b>Ansonsten leeren Sie dieses
+ Feld bitte, um eine neue Identität zu erhalten.</b>
+ </div>
<div class="input-group">
<span class="input-group-addon">
- Test-Login
+ Alte ID / Test-Login
</span>
<input class="form-control" name="testlogin" type="text" value="{{testlogin}}" placeholder="login@einrichtung.de">
</div>
- {{/testacc}}
+ {{/testlogin}}
<div class="pull-right">
<button type="submit" class="btn btn-primary">Registrieren</button>
diff --git a/templates/main/logged-in.html b/templates/main/logged-in.html
index c841016..43dbef4 100644
--- a/templates/main/logged-in.html
+++ b/templates/main/logged-in.html
@@ -18,7 +18,7 @@
<div>
Wenn Sie Ihre Teilname am {{suite}}-Service zurückziehen möchten, werden Ihre personenbezogenen
Daten vom Zentral-Server gelöscht (falls dort vorhanden). Alle von Ihnen öffentlich zugänglich gemachten
- Virtuellen Maschinen werden ebenfalls gelöscht. Kopien solcher VMs auf den Satelliten-Servern
+ Virtuellen Maschinen werden ebenfalls gelöscht. Kopien solcher VMs auf den Satellitenservern
anderer Hochschulen können jedoch nicht gelöscht werden. Hier wird lediglich die Verknüpfung
mit Ihren Meta-Daten entfernt.
</div>
diff --git a/templates/sharemode/deploy.html b/templates/sharemode/deploy.html
index ce0750c..09f7892 100644
--- a/templates/sharemode/deploy.html
+++ b/templates/sharemode/deploy.html
@@ -8,7 +8,7 @@
<p>
Sofern Sie am landesweiten VM-Austausch teilnehmen wollen, werden Sie für andere Dozenten
über diese Daten auffindbar sein. Andernfalls werden diese Daten lediglich auf den
- {{suite}}-Satelliten-Server Ihrer eigenen Einrichtung übertragen.
+ {{suite}}-Satellitenserver Ihrer eigenen Einrichtung übertragen.
</p>
<div class="group-group">
<div class="input-group">