Add explicit `return null;` statements
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / template / TemplateEngine.class.php
index bf1d554835a541d43edad38331ae1a850390e2bf..cbf84b053771d61bd78c460a820b48b50227bc3e 100755 (executable)
@@ -1,5 +1,7 @@
 <?php
+
 namespace wcf\system\template;
+
 use wcf\data\template\Template;
 use wcf\system\cache\builder\TemplateGroupCacheBuilder;
 use wcf\system\cache\builder\TemplateListenerCodeCacheBuilder;
@@ -13,783 +15,818 @@ use wcf\util\StringUtil;
 
 /**
  * Loads and displays template.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2019 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @package    WoltLabSuite\Core\System\Template
+ *
+ * @author  Alexander Ebert
+ * @copyright   2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\System\Template
  */
-class TemplateEngine extends SingletonFactory {
-       /**
-        * directory used to cache previously compiled templates
-        * @var string
-        */
-       public $compileDir = '';
-       
-       /**
-        * active language id used to identify specific language versions of compiled templates
-        * @var int
-        */
-       public $languageID = 0;
-       
-       /**
-        * directories used as template source
-        * @var string[]
-        */
-       public $templatePaths = [];
-       
-       /**
-        * namespace containing template modifiers and plugins
-        * @var string
-        */
-       public $pluginNamespace = '';
-       
-       /**
-        * active template compiler
-        * @var TemplateCompiler
-        */
-       protected $compilerObj;
-       
-       /**
-        * forces the template engine to recompile all included templates
-        * @var bool
-        */
-       protected $forceCompile = false;
-       
-       /**
-        * list of registered prefilters
-        * @var string[]
-        */
-       protected $prefilters = [];
-       
-       /**
-        * cached list of known template groups
-        * @var array
-        */
-       protected $templateGroupCache = [];
-       
-       /**
-        * active template group id
-        * @var int
-        */
-       protected $templateGroupID = 0;
-       
-       /**
-        * all available template variables and those assigned during runtime
-        * @var mixed[][]
-        */
-       protected $v = [];
-       
-       /**
-        * sandboxed values of currently active foreach loops' `item` and `key` variables
-        * 
-        * for each currently active `foreach` loop, an array is added:
-        *      $foreachHash => [
-        *              (optional) 'item' => sandboxed value of an existing variable with the same name,
-        *              (optional) 'key' => (optional) sandboxed value of an existing variable with the same name
-        *      ]
-        * 
-        * @var mixed[][][]
-        */
-       protected $foreachVars = [];
-       
-       /**
-        * all cached variables for usage after execution in sandbox
-        * @var mixed[][]
-        */
-       protected $sandboxVars = [];
-       
-       /**
-        * contains all templates with assigned template listeners.
-        * @var string[][][]
-        */
-       protected $templateListeners = [];
-       
-       /**
-        * true, if template listener code was already loaded
-        * @var bool
-        */
-       protected $templateListenersLoaded = false;
-       
-       /**
-        * current environment
-        * @var string
-        */
-       protected $environment = 'user';
-       
-       /**
-        * @inheritDoc
-        */
-       protected function init() {
-               $this->templatePaths = ['wcf' => WCF_DIR.'templates/'];
-               $this->pluginNamespace = 'wcf\system\template\plugin\\';
-               $this->compileDir = WCF_DIR.'templates/compiled/';
-               
-               $this->loadTemplateGroupCache();
-               $this->assignSystemVariables();
-       }
-       
-       /**
-        * Adds a new application.
-        * 
-        * @param       string          $abbreviation
-        * @param       string          $templatePath
-        */
-       public function addApplication($abbreviation, $templatePath) {
-               $this->templatePaths[$abbreviation] = $templatePath;
-       }
-       
-       /**
-        * Sets active language id.
-        * 
-        * @param       int             $languageID
-        */
-       public function setLanguageID($languageID) {
-               $this->languageID = $languageID;
-       }
-       
-       /**
-        * Assigns some system variables.
-        */
-       protected function assignSystemVariables() {
-               $this->v['tpl'] = [];
-               
-               // assign super globals
-               $this->v['tpl']['get'] =& $_GET;
-               $this->v['tpl']['post'] =& $_POST;
-               $this->v['tpl']['cookie'] =& $_COOKIE;
-               $this->v['tpl']['server'] =& $_SERVER;
-               $this->v['tpl']['env'] =& $_ENV;
-               
-               // system info
-               $this->v['tpl']['now'] = TIME_NOW;
-               $this->v['tpl']['template'] = '';
-               $this->v['tpl']['includedTemplates'] = [];
-               
-               // section / foreach / capture arrays
-               $this->v['tpl']['section'] = $this->v['tpl']['foreach'] = $this->v['tpl']['capture'] = [];
-       }
-       
-       /**
-        * Assigns a template variable.
-        * 
-        * @param       mixed           $variable
-        * @param       mixed           $value
-        */
-       public function assign($variable, $value = '') {
-               if (is_array($variable)) {
-                       foreach ($variable as $key => $value) {
-                               if (empty($key)) continue;
-                               
-                               $this->assign($key, $value);
-                       }
-               }
-               else {
-                       $this->v[$variable] = $value;
-               }
-       }
-       
-       /**
-        * Appends content to an existing template variable.
-        * 
-        * @param       mixed           $variable
-        * @param       mixed           $value
-        */
-       public function append($variable, $value = '') {
-               if (is_array($variable)) {
-                       foreach ($variable as $key => $val) {
-                               if ($key != '') {
-                                       $this->append($key, $val);
-                               }
-                       }
-               }
-               else {
-                       if (!empty($variable)) {
-                               if (isset($this->v[$variable])) {
-                                       if (is_array($this->v[$variable]) && is_array($value)) {
-                                               $keys = array_keys($value);
-                                               foreach ($keys as $key) {
-                                                       if (isset($this->v[$variable][$key])) {
-                                                               $this->v[$variable][$key] .= $value[$key];
-                                                       }
-                                                       else {
-                                                               $this->v[$variable][$key] = $value[$key];
-                                                       }
-                                               }
-                                       }
-                                       else {
-                                               $this->v[$variable] .= $value;
-                                       }
-                               }
-                               else {
-                                       $this->v[$variable] = $value;
-                               }
-                       }
-               }
-       }
-       
-       /**
-        * Prepends content to an existing template variable.
-        * 
-        * @param       mixed           $variable
-        * @param       mixed           $value
-        */
-       public function prepend($variable, $value = '') {
-               if (is_array($variable)) {
-                       foreach ($variable as $key => $val) {
-                               if ($key != '') {
-                                       $this->prepend($key, $val);
-                               }
-                       }
-               }
-               else {
-                       if (!empty($variable)) {
-                               if (isset($this->v[$variable])) {
-                                       if (is_array($this->v[$variable]) && is_array($value)) {
-                                               $keys = array_keys($value);
-                                               foreach ($keys as $key) {
-                                                       if (isset($this->v[$variable][$key])) {
-                                                               $this->v[$variable][$key] = $value[$key] . $this->v[$variable][$key];
-                                                       }
-                                                       else {
-                                                               $this->v[$variable][$key] = $value[$key];
-                                                       }
-                                               }
-                                       }
-                                       else {
-                                               $this->v[$variable] = $value . $this->v[$variable];
-                                       }
-                               }
-                               else {
-                                       $this->v[$variable] = $value;
-                               }
-                       }
-               }
-       }
-       
-       /**
-        * Assigns a template variable by reference.
-        * 
-        * @param       string          $variable
-        * @param       mixed           $value
-        */
-       public function assignByRef($variable, &$value) {
-               if (!empty($variable)) {
-                       $this->v[$variable] = &$value;
-               }
-       }
-       
-       /**
-        * Clears an assignment of template variables.
-        * 
-        * @param       mixed           $variables
-        */
-       public function clearAssign(array $variables) {
-               foreach ($variables as $key) {
-                       unset($this->v[$key]);
-               }
-       }
-       
-       /**
-        * Clears assignment of all template variables. This should not be called
-        * during runtime as it could leed to an unexpected behaviour.
-        */
-       public function clearAllAssign() {
-               $this->v = [];
-       }
-       
-       /**
-        * Outputs a template.
-        * 
-        * @param       string          $templateName
-        * @param       string          $application
-        * @param       bool            $sendHeaders
-        */
-       public function display($templateName, $application = 'wcf', $sendHeaders = true) {
-               if ($sendHeaders) {
-                       HeaderUtil::sendHeaders();
-                       
-                       // call beforeDisplay event
-                       if (!defined('NO_IMPORTS')) EventHandler::getInstance()->fireAction($this, 'beforeDisplay');
-               }
-               
-               $sourceFilename = $this->getSourceFilename($templateName, $application);
-               $compiledFilename = $this->getCompiledFilename($templateName, $application);
-               $metaDataFilename = $this->getMetaDataFilename($templateName);
-               $metaData = $this->getMetaData($templateName, $metaDataFilename);
-               
-               // check if compilation is necessary
-               if (($metaData === null) || !$this->isCompiled($templateName, $sourceFilename, $compiledFilename, $application, $metaData)) {
-                       // compile
-                       $this->compileTemplate($templateName, $sourceFilename, $compiledFilename, [
-                               'application' => $application,
-                               'data' => $metaData,
-                               'filename' => $metaDataFilename
-                       ]);
-               }
-               
-               // assign current package id
-               $this->assign('__APPLICATION', $application);
-               
-               include($compiledFilename);
-               
-               if ($sendHeaders) {
-                       // call afterDisplay event
-                       if (!defined('NO_IMPORTS')) EventHandler::getInstance()->fireAction($this, 'afterDisplay');
-               }
-       }
-       
-       /**
-        * Returns the absolute filename of a template source.
-        * 
-        * @param       string          $templateName
-        * @param       string          $application
-        * @return      string          $path
-        * @throws      SystemException
-        */
-       public function getSourceFilename($templateName, $application) {
-               $sourceFilename = $this->getPath($this->templatePaths[$application], $templateName);
-               if (!empty($sourceFilename)) {
-                       return $sourceFilename;
-               }
-               
-               // try to find template within WCF if not already searching WCF
-               if ($application != 'wcf') {
-                       $sourceFilename = $this->getSourceFilename($templateName, 'wcf');
-                       if (!empty($sourceFilename)) {
-                               return $sourceFilename;
-                       }
-               }
-               
-               throw new SystemException("Unable to find template '".$templateName."'");
-       }
-       
-       /**
-        * Returns path if template was found.
-        * 
-        * @param       string          $templatePath
-        * @param       string          $templateName
-        * @return      string
-        */
-       protected function getPath($templatePath, $templateName) {
-               if (!Template::isSystemCritical($templateName)) {
-                       $templateGroupID = $this->getTemplateGroupID();
-                       
-                       while ($templateGroupID != 0) {
-                               $templateGroup = $this->templateGroupCache[$templateGroupID];
-                               
-                               $path = $templatePath . $templateGroup->templateGroupFolderName . $templateName . '.tpl';
-                               if (file_exists($path)) {
-                                       return $path;
-                               }
-                               
-                               $templateGroupID = $templateGroup->parentTemplateGroupID;
-                       }
-               }
-               
-               // use default template
-               $path = $templatePath.$templateName.'.tpl';
-               
-               if (file_exists($path)) {
-                       return $path;
-               }
-               
-               return '';
-       }
-       
-       /**
-        * Returns the absolute filename of a compiled template.
-        * 
-        * @param       string          $templateName
-        * @param       string          $application
-        * @return      string
-        */
-       public function getCompiledFilename($templateName, $application) {
-               return $this->compileDir.$this->getTemplateGroupID().'_'.$application.'_'.$this->languageID.'_'.$templateName.'.php';
-       }
-       
-       /**
-        * Returns the absolute filename for template's meta data.
-        * 
-        * @param       string          $templateName
-        * @return      string
-        */
-       public function getMetaDataFilename($templateName) {
-               return $this->compileDir.$this->getTemplateGroupID().'_'.$templateName.'.meta.php';
-       }
-       
-       /**
-        * Returns true if the template with the given data is already compiled.
-        * 
-        * @param       string          $templateName
-        * @param       string          $sourceFilename
-        * @param       string          $compiledFilename
-        * @param       string          $application
-        * @param       array           $metaData
-        * @return      bool
-        */
-       protected function isCompiled($templateName, $sourceFilename, $compiledFilename, $application, array $metaData) {
-               if ($this->forceCompile || !file_exists($compiledFilename)) {
-                       return false;
-               }
-               else {
-                       $sourceMTime = @filemtime($sourceFilename);
-                       $compileMTime = @filemtime($compiledFilename);
-                       
-                       if ($sourceMTime >= $compileMTime) {
-                               return false;
-                       }
-                       else {
-                               // check for meta data
-                               if (!empty($metaData['include'])) {
-                                       foreach ($metaData['include'] as $application => $includedTemplates) {
-                                               foreach ($includedTemplates as $includedTemplate) {
-                                                       $includedTemplateFilename = $this->getSourceFilename($includedTemplate, $application);
-                                                       $includedMTime = @filemtime($includedTemplateFilename);
-                                                       
-                                                       if ($includedMTime >= $compileMTime) {
-                                                               return false;
-                                                       }
-                                               }
-                                       }
-                               }
-                               
-                               return true;
-                       }
-               }
-       }
-       
-       /**
-        * Compiles a template.
-        * 
-        * @param       string          $templateName
-        * @param       string          $sourceFilename
-        * @param       string          $compiledFilename
-        * @param       array           $metaData
-        */
-       protected function compileTemplate($templateName, $sourceFilename, $compiledFilename, array $metaData) {
-               // get source
-               $sourceContent = $this->getSourceContent($sourceFilename);
-               
-               // compile template
-               $this->getCompiler()->compile($templateName, $sourceContent, $compiledFilename, $metaData);
-       }
-       
-       /**
-        * Returns the template compiler.
-        * 
-        * @return      TemplateCompiler
-        */
-       public function getCompiler() {
-               if ($this->compilerObj === null) {
-                       $this->compilerObj = new TemplateCompiler($this);
-               }
-               
-               return $this->compilerObj;
-       }
-       
-       /**
-        * Reads the content of a template file.
-        * 
-        * @param       string          $sourceFilename
-        * @return      string
-        * @throws      SystemException
-        */
-       public function getSourceContent($sourceFilename) {
-               /** @noinspection PhpUnusedLocalVariableInspection */
-               $sourceContent = '';
-               if (!file_exists($sourceFilename) || (($sourceContent = @file_get_contents($sourceFilename)) === false)) {
-                       throw new SystemException("Could not open template '$sourceFilename' for reading");
-               }
-               else {
-                       return $sourceContent;
-               }
-       }
-       
-       /**
-        * Returns the class name of a plugin.
-        * 
-        * @param       string          $type
-        * @param       string          $tag
-        * @return      string
-        */
-       public function getPluginClassName($type, $tag) {
-               return $this->pluginNamespace.StringUtil::firstCharToUpperCase($tag).StringUtil::firstCharToUpperCase(mb_strtolower($type)).'TemplatePlugin';
-       }
-       
-       /**
-        * Enables execution in sandbox.
-        */
-       public function enableSandbox() {
-               $index = count($this->sandboxVars);
-               $this->sandboxVars[$index] = $this->v;
-       }
-       
-       /**
-        * Disables execution in sandbox.
-        */
-       public function disableSandbox() {
-               if (empty($this->sandboxVars)) {
-                       throw new SystemException('TemplateEngine is currently not running in a sandbox.');
-               }
-               
-               $this->v = array_pop($this->sandboxVars);
-       }
-       
-       /**
-        * Returns the output of a template.
-        * 
-        * @param       string          $templateName
-        * @param       string          $application
-        * @param       array           $variables
-        * @param       bool            $sandbox        enables execution in sandbox
-        * @return      string
-        */
-       public function fetch($templateName, $application = 'wcf', array $variables = [], $sandbox = false) {
-               // enable sandbox
-               if ($sandbox) {
-                       $this->enableSandbox();
-               }
-               
-               // add new template variables
-               if (!empty($variables)) {
-                       $this->v = array_merge($this->v, $variables);
-               }
-               
-               // get output
-               try {
-                       ob_start();
-                       $this->display($templateName, $application, false);
-                       $output = ob_get_contents();
-               }
-               finally {
-                       ob_end_clean();
-               }
-               
-               // disable sandbox
-               if ($sandbox) {
-                       $this->disableSandbox();
-               }
-               
-               return $output;
-       }
-       
-       /**
-        * Executes a compiled template scripting source and returns the result.
-        * 
-        * @param       string          $compiledSource
-        * @param       array           $variables
-        * @param       bool            $sandbox        enables execution in sandbox
-        * @return      string
-        */
-       public function fetchString($compiledSource, array $variables = [], $sandbox = true) {
-               // enable sandbox
-               if ($sandbox) {
-                       $this->enableSandbox();
-               }
-               
-               // add new template variables
-               if (!empty($variables)) {
-                       $this->v = array_merge($this->v, $variables);
-               }
-               
-               // get output
-               ob_start();
-               eval('?>'.$compiledSource);
-               $output = ob_get_contents();
-               ob_end_clean();
-               
-               // disable sandbox
-               if ($sandbox) {
-                       $this->disableSandbox();
-               }
-               
-               return $output;
-       }
-       
-       /**
-        * Deletes all compiled templates.
-        * 
-        * @param       string          $compileDir
-        */
-       public static function deleteCompiledTemplates($compileDir = '') {
-               if (empty($compileDir)) $compileDir = WCF_DIR.'templates/compiled/';
-               
-               // delete compiled templates
-               DirectoryUtil::getInstance($compileDir)->removePattern(new Regex('.*_.*\.php$'));
-       }
-       
-       /**
-        * Returns an array with all prefilters.
-        * 
-        * @return      string[]
-        */
-       public function getPrefilters() {
-               return $this->prefilters;
-       }
-       
-       /**
-        * Returns the active template group id.
-        * 
-        * @return      int
-        */
-       public function getTemplateGroupID() {
-               return $this->templateGroupID;
-       }
-       
-       /**
-        * Sets the active template group id.
-        * 
-        * @param       int             $templateGroupID
-        */
-       public function setTemplateGroupID($templateGroupID) {
-               if ($templateGroupID && !isset($this->templateGroupCache[$templateGroupID])) {
-                       $templateGroupID = 0;
-               }
-               
-               $this->templateGroupID = $templateGroupID;
-       }
-       
-       /**
-        * Loads cached template group information.
-        */
-       protected function loadTemplateGroupCache() {
-               $this->templateGroupCache = TemplateGroupCacheBuilder::getInstance()->getData();
-       }
-       
-       /**
-        * Registers prefilters.
-        * 
-        * @param       string[]                $prefilters
-        */
-       public function registerPrefilter(array $prefilters) {
-               foreach ($prefilters as $name) {
-                       $this->prefilters[$name] = $name;
-               }
-       }
-       
-       /**
-        * Removes a prefilter by its internal name.
-        * 
-        * @param       string          $name   internal prefilter identifier
-        */
-       public function removePrefilter($name) {
-               unset($this->prefilters[$name]);
-       }
-       
-       /**
-        * Sets the dir for the compiled templates.
-        * 
-        * @param       string          $compileDir
-        * @throws      SystemException
-        */
-       public function setCompileDir($compileDir) {
-               if (!is_dir($compileDir)) {
-                       throw new SystemException("'".$compileDir."' is not a valid dir");
-               }
-               
-               $this->compileDir = $compileDir;
-       }
-       
-       /**
-        * Includes a template.
-        * 
-        * @param       string          $templateName
-        * @param       string          $application
-        * @param       array           $variables
-        * @param       bool            $sandbox        enables execution in sandbox
-        */
-       protected function includeTemplate($templateName, $application, array $variables = [], $sandbox = true) {
-               // enable sandbox
-               if ($sandbox) {
-                       $this->enableSandbox();
-               }
-               
-               // add new template variables
-               if (!empty($variables)) {
-                       $this->v = array_merge($this->v, $variables);
-               }
-               
-               // display template
-               $this->display($templateName, $application, false);
-               
-               // disable sandbox
-               if ($sandbox) {
-                       $this->disableSandbox();
-               }
-       }
-       
-       /**
-        * Returns the value of a template variable.
-        * 
-        * @param       string          $varname
-        * @return      mixed
-        */
-       public function get($varname) {
-               if (isset($this->v[$varname])) {
-                       return $this->v[$varname];
-               }
-               
-               return null;
-       }
-       
-       /**
-        * Loads template listener code.
-        */
-       protected function loadTemplateListenerCode() {
-               if (!$this->templateListenersLoaded) {
-                       $this->templateListeners = TemplateListenerCodeCacheBuilder::getInstance()->getData(['environment' => $this->environment]);
-                       $this->templateListenersLoaded = true;
-               }
-       }
-       
-       /**
-        * Returns template listener's code.
-        * 
-        * @param       string          $templateName
-        * @param       string          $eventName
-        * @return      string
-        */
-       public function getTemplateListenerCode($templateName, $eventName) {
-               $this->loadTemplateListenerCode();
-               
-               if (isset($this->templateListeners[$templateName][$eventName])) {
-                       return implode("\n", $this->templateListeners[$templateName][$eventName]);
-               }
-               
-               return '';
-       }
-       
-       /**
-        * Reads meta data from file.
-        * 
-        * @param       string          $templateName
-        * @param       string          $filename
-        * @return      array
-        */
-       protected function getMetaData($templateName, $filename) {
-               if (!file_exists($filename) || !is_readable($filename)) {
-                       return null;
-               }
-               
-               // get file contents
-               $contents = file_get_contents($filename);
-               
-               // find first newline
-               $position = strpos($contents, "\n");
-               if ($position === false) {
-                       return null;
-               }
-               
-               // cut contents
-               $contents = substr($contents, $position + 1);
-               
-               // read serializes data
-               $data = @unserialize($contents);
-               if ($data === false || !is_array($data)) {
-                       return null;
-               }
-               
-               return $data;
-       }
+class TemplateEngine extends SingletonFactory
+{
+    /**
+     * directory used to cache previously compiled templates
+     * @var string
+     */
+    public $compileDir = '';
+
+    /**
+     * active language id used to identify specific language versions of compiled templates
+     * @var int
+     */
+    public $languageID = 0;
+
+    /**
+     * directories used as template source
+     * @var string[]
+     */
+    public $templatePaths = [];
+
+    /**
+     * namespace containing template modifiers and plugins
+     * @var string
+     */
+    public $pluginNamespace = '';
+
+    /**
+     * active template compiler
+     * @var TemplateCompiler
+     */
+    protected $compilerObj;
+
+    /**
+     * forces the template engine to recompile all included templates
+     * @var bool
+     */
+    protected $forceCompile = false;
+
+    /**
+     * list of registered prefilters
+     * @var string[]
+     */
+    protected $prefilters = [];
+
+    /**
+     * cached list of known template groups
+     * @var array
+     */
+    protected $templateGroupCache = [];
+
+    /**
+     * active template group id
+     * @var int
+     */
+    protected $templateGroupID = 0;
+
+    /**
+     * all available template variables and those assigned during runtime
+     * @var mixed[][]
+     */
+    protected $v = [];
+
+    /**
+     * sandboxed values of currently active foreach loops' `item` and `key` variables
+     *
+     * for each currently active `foreach` loop, an array is added:
+     *  $foreachHash => [
+     *      (optional) 'item' => sandboxed value of an existing variable with the same name,
+     *      (optional) 'key' => (optional) sandboxed value of an existing variable with the same name
+     *  ]
+     *
+     * @var mixed[][][]
+     */
+    protected $foreachVars = [];
+
+    /**
+     * all cached variables for usage after execution in sandbox
+     * @var mixed[][]
+     */
+    protected $sandboxVars = [];
+
+    /**
+     * contains all templates with assigned template listeners.
+     * @var string[][][]
+     */
+    protected $templateListeners = [];
+
+    /**
+     * true, if template listener code was already loaded
+     * @var bool
+     */
+    protected $templateListenersLoaded = false;
+
+    /**
+     * current environment
+     * @var string
+     */
+    protected $environment = 'user';
+
+    /**
+     * @inheritDoc
+     */
+    protected function init()
+    {
+        $this->templatePaths = ['wcf' => WCF_DIR . 'templates/'];
+        $this->pluginNamespace = 'wcf\system\template\plugin\\';
+        $this->compileDir = WCF_DIR . 'templates/compiled/';
+
+        $this->loadTemplateGroupCache();
+        $this->assignSystemVariables();
+    }
+
+    /**
+     * Adds a new application.
+     *
+     * @param string $abbreviation
+     * @param string $templatePath
+     */
+    public function addApplication($abbreviation, $templatePath)
+    {
+        $this->templatePaths[$abbreviation] = $templatePath;
+    }
+
+    /**
+     * Sets active language id.
+     *
+     * @param int $languageID
+     */
+    public function setLanguageID($languageID)
+    {
+        $this->languageID = $languageID;
+    }
+
+    /**
+     * Assigns some system variables.
+     */
+    protected function assignSystemVariables()
+    {
+        $this->v['tpl'] = [];
+
+        // assign super globals
+        $this->v['tpl']['get'] = &$_GET;
+        $this->v['tpl']['post'] = &$_POST;
+        $this->v['tpl']['cookie'] = &$_COOKIE;
+        $this->v['tpl']['server'] = &$_SERVER;
+        $this->v['tpl']['env'] = &$_ENV;
+
+        // system info
+        $this->v['tpl']['now'] = TIME_NOW;
+        $this->v['tpl']['template'] = '';
+        $this->v['tpl']['includedTemplates'] = [];
+
+        // section / foreach / capture arrays
+        $this->v['tpl']['section'] = $this->v['tpl']['foreach'] = $this->v['tpl']['capture'] = [];
+    }
+
+    /**
+     * Assigns a template variable.
+     *
+     * @param mixed $variable
+     * @param mixed $value
+     */
+    public function assign($variable, $value = '')
+    {
+        if (\is_array($variable)) {
+            foreach ($variable as $key => $value) {
+                if (empty($key)) {
+                    continue;
+                }
+
+                $this->assign($key, $value);
+            }
+        } else {
+            $this->v[$variable] = $value;
+        }
+    }
+
+    /**
+     * Appends content to an existing template variable.
+     *
+     * @param mixed $variable
+     * @param mixed $value
+     */
+    public function append($variable, $value = '')
+    {
+        if (\is_array($variable)) {
+            foreach ($variable as $key => $val) {
+                if ($key != '') {
+                    $this->append($key, $val);
+                }
+            }
+        } else {
+            if (!empty($variable)) {
+                if (isset($this->v[$variable])) {
+                    if (\is_array($this->v[$variable]) && \is_array($value)) {
+                        $keys = \array_keys($value);
+                        foreach ($keys as $key) {
+                            if (isset($this->v[$variable][$key])) {
+                                $this->v[$variable][$key] .= $value[$key];
+                            } else {
+                                $this->v[$variable][$key] = $value[$key];
+                            }
+                        }
+                    } else {
+                        $this->v[$variable] .= $value;
+                    }
+                } else {
+                    $this->v[$variable] = $value;
+                }
+            }
+        }
+    }
+
+    /**
+     * Prepends content to an existing template variable.
+     *
+     * @param mixed $variable
+     * @param mixed $value
+     */
+    public function prepend($variable, $value = '')
+    {
+        if (\is_array($variable)) {
+            foreach ($variable as $key => $val) {
+                if ($key != '') {
+                    $this->prepend($key, $val);
+                }
+            }
+        } else {
+            if (!empty($variable)) {
+                if (isset($this->v[$variable])) {
+                    if (\is_array($this->v[$variable]) && \is_array($value)) {
+                        $keys = \array_keys($value);
+                        foreach ($keys as $key) {
+                            if (isset($this->v[$variable][$key])) {
+                                $this->v[$variable][$key] = $value[$key] . $this->v[$variable][$key];
+                            } else {
+                                $this->v[$variable][$key] = $value[$key];
+                            }
+                        }
+                    } else {
+                        $this->v[$variable] = $value . $this->v[$variable];
+                    }
+                } else {
+                    $this->v[$variable] = $value;
+                }
+            }
+        }
+    }
+
+    /**
+     * Assigns a template variable by reference.
+     *
+     * @param string $variable
+     * @param mixed $value
+     */
+    public function assignByRef($variable, &$value)
+    {
+        if (!empty($variable)) {
+            $this->v[$variable] = &$value;
+        }
+    }
+
+    /**
+     * Clears an assignment of template variables.
+     *
+     * @param mixed $variables
+     */
+    public function clearAssign(array $variables)
+    {
+        foreach ($variables as $key) {
+            unset($this->v[$key]);
+        }
+    }
+
+    /**
+     * Clears assignment of all template variables. This should not be called
+     * during runtime as it could leed to an unexpected behaviour.
+     */
+    public function clearAllAssign()
+    {
+        $this->v = [];
+    }
+
+    /**
+     * Outputs a template.
+     *
+     * @param string $templateName
+     * @param string $application
+     * @param bool $sendHeaders
+     */
+    public function display($templateName, $application = 'wcf', $sendHeaders = true)
+    {
+        if ($sendHeaders) {
+            HeaderUtil::sendHeaders();
+
+            // call beforeDisplay event
+            if (!\defined('NO_IMPORTS')) {
+                EventHandler::getInstance()->fireAction($this, 'beforeDisplay');
+            }
+        }
+
+        $sourceFilename = $this->getSourceFilename($templateName, $application);
+        $compiledFilename = $this->getCompiledFilename($templateName, $application);
+        $metaDataFilename = $this->getMetaDataFilename($templateName);
+        $metaData = $this->getMetaData($templateName, $metaDataFilename);
+
+        // check if compilation is necessary
+        if (
+            $metaData === null
+            || !$this->isCompiled($templateName, $sourceFilename, $compiledFilename, $application, $metaData)
+        ) {
+            // compile
+            $this->compileTemplate($templateName, $sourceFilename, $compiledFilename, [
+                'application' => $application,
+                'data' => $metaData,
+                'filename' => $metaDataFilename,
+            ]);
+        }
+
+        // assign current package id
+        $this->assign('__APPLICATION', $application);
+
+        include($compiledFilename);
+
+        if ($sendHeaders) {
+            // call afterDisplay event
+            if (!\defined('NO_IMPORTS')) {
+                EventHandler::getInstance()->fireAction($this, 'afterDisplay');
+            }
+        }
+    }
+
+    /**
+     * Returns the absolute filename of a template source.
+     *
+     * @param string $templateName
+     * @param string $application
+     * @return  string      $path
+     * @throws  SystemException
+     */
+    public function getSourceFilename($templateName, $application)
+    {
+        $sourceFilename = $this->getPath($this->templatePaths[$application], $templateName);
+        if (!empty($sourceFilename)) {
+            return $sourceFilename;
+        }
+
+        // try to find template within WCF if not already searching WCF
+        if ($application != 'wcf') {
+            $sourceFilename = $this->getSourceFilename($templateName, 'wcf');
+            if (!empty($sourceFilename)) {
+                return $sourceFilename;
+            }
+        }
+
+        throw new SystemException("Unable to find template '" . $templateName . "'");
+    }
+
+    /**
+     * Returns path if template was found.
+     *
+     * @param string $templatePath
+     * @param string $templateName
+     * @return  string
+     */
+    protected function getPath($templatePath, $templateName)
+    {
+        if (!Template::isSystemCritical($templateName)) {
+            $templateGroupID = $this->getTemplateGroupID();
+
+            while ($templateGroupID != 0) {
+                $templateGroup = $this->templateGroupCache[$templateGroupID];
+
+                $path = $templatePath . $templateGroup->templateGroupFolderName . $templateName . '.tpl';
+                if (\file_exists($path)) {
+                    return $path;
+                }
+
+                $templateGroupID = $templateGroup->parentTemplateGroupID;
+            }
+        }
+
+        // use default template
+        $path = $templatePath . $templateName . '.tpl';
+
+        if (\file_exists($path)) {
+            return $path;
+        }
+
+        return '';
+    }
+
+    /**
+     * Returns the absolute filename of a compiled template.
+     *
+     * @param string $templateName
+     * @param string $application
+     * @return  string
+     */
+    public function getCompiledFilename($templateName, $application)
+    {
+        return $this->compileDir . $this->getTemplateGroupID() . '_' . $application . '_' . $this->languageID . '_' . $templateName . '.php';
+    }
+
+    /**
+     * Returns the absolute filename for template's meta data.
+     *
+     * @param string $templateName
+     * @return  string
+     */
+    public function getMetaDataFilename($templateName)
+    {
+        return $this->compileDir . $this->getTemplateGroupID() . '_' . $templateName . '.meta.php';
+    }
+
+    /**
+     * Returns true if the template with the given data is already compiled.
+     *
+     * @param string $templateName
+     * @param string $sourceFilename
+     * @param string $compiledFilename
+     * @param string $application
+     * @param array $metaData
+     * @return  bool
+     */
+    protected function isCompiled($templateName, $sourceFilename, $compiledFilename, $application, array $metaData)
+    {
+        if ($this->forceCompile || !\file_exists($compiledFilename)) {
+            return false;
+        } else {
+            $sourceMTime = @\filemtime($sourceFilename);
+            $compileMTime = @\filemtime($compiledFilename);
+
+            if ($sourceMTime >= $compileMTime) {
+                return false;
+            } else {
+                // check for meta data
+                if (!empty($metaData['include'])) {
+                    foreach ($metaData['include'] as $application => $includedTemplates) {
+                        foreach ($includedTemplates as $includedTemplate) {
+                            $includedTemplateFilename = $this->getSourceFilename($includedTemplate, $application);
+                            $includedMTime = @\filemtime($includedTemplateFilename);
+
+                            if ($includedMTime >= $compileMTime) {
+                                return false;
+                            }
+                        }
+                    }
+                }
+
+                return true;
+            }
+        }
+    }
+
+    /**
+     * Compiles a template.
+     *
+     * @param string $templateName
+     * @param string $sourceFilename
+     * @param string $compiledFilename
+     * @param array $metaData
+     */
+    protected function compileTemplate($templateName, $sourceFilename, $compiledFilename, array $metaData)
+    {
+        // get source
+        $sourceContent = $this->getSourceContent($sourceFilename);
+
+        // compile template
+        $this->getCompiler()->compile($templateName, $sourceContent, $compiledFilename, $metaData);
+    }
+
+    /**
+     * Returns the template compiler.
+     *
+     * @return  TemplateCompiler
+     */
+    public function getCompiler()
+    {
+        if ($this->compilerObj === null) {
+            $this->compilerObj = new TemplateCompiler($this);
+        }
+
+        return $this->compilerObj;
+    }
+
+    /**
+     * Reads the content of a template file.
+     *
+     * @param string $sourceFilename
+     * @return  string
+     * @throws  SystemException
+     */
+    public function getSourceContent($sourceFilename)
+    {
+        /** @noinspection PhpUnusedLocalVariableInspection */
+        $sourceContent = '';
+        if (!\file_exists($sourceFilename) || (($sourceContent = @\file_get_contents($sourceFilename)) === false)) {
+            throw new SystemException("Could not open template '{$sourceFilename}' for reading");
+        } else {
+            return $sourceContent;
+        }
+    }
+
+    /**
+     * Returns the class name of a plugin.
+     *
+     * @param string $type
+     * @param string $tag
+     * @return  string
+     */
+    public function getPluginClassName($type, $tag)
+    {
+        return $this->pluginNamespace . StringUtil::firstCharToUpperCase($tag) . StringUtil::firstCharToUpperCase(\mb_strtolower($type)) . 'TemplatePlugin';
+    }
+
+    /**
+     * Enables execution in sandbox.
+     */
+    public function enableSandbox()
+    {
+        $index = \count($this->sandboxVars);
+        $this->sandboxVars[$index] = $this->v;
+    }
+
+    /**
+     * Disables execution in sandbox.
+     */
+    public function disableSandbox()
+    {
+        if (empty($this->sandboxVars)) {
+            throw new SystemException('TemplateEngine is currently not running in a sandbox.');
+        }
+
+        $this->v = \array_pop($this->sandboxVars);
+    }
+
+    /**
+     * Returns the output of a template.
+     *
+     * @param string $templateName
+     * @param string $application
+     * @param array $variables
+     * @param bool $sandbox enables execution in sandbox
+     * @return  string
+     */
+    public function fetch($templateName, $application = 'wcf', array $variables = [], $sandbox = false)
+    {
+        // enable sandbox
+        if ($sandbox) {
+            $this->enableSandbox();
+        }
+
+        // add new template variables
+        if (!empty($variables)) {
+            $this->v = \array_merge($this->v, $variables);
+        }
+
+        // get output
+        try {
+            \ob_start();
+            $this->display($templateName, $application, false);
+            $output = \ob_get_contents();
+        } finally {
+            \ob_end_clean();
+        }
+
+        // disable sandbox
+        if ($sandbox) {
+            $this->disableSandbox();
+        }
+
+        return $output;
+    }
+
+    /**
+     * Executes a compiled template scripting source and returns the result.
+     *
+     * @param string $compiledSource
+     * @param array $variables
+     * @param bool $sandbox enables execution in sandbox
+     * @return  string
+     */
+    public function fetchString($compiledSource, array $variables = [], $sandbox = true)
+    {
+        // enable sandbox
+        if ($sandbox) {
+            $this->enableSandbox();
+        }
+
+        // add new template variables
+        if (!empty($variables)) {
+            $this->v = \array_merge($this->v, $variables);
+        }
+
+        // get output
+        \ob_start();
+        eval('?>' . $compiledSource);
+        $output = \ob_get_contents();
+        \ob_end_clean();
+
+        // disable sandbox
+        if ($sandbox) {
+            $this->disableSandbox();
+        }
+
+        return $output;
+    }
+
+    /**
+     * Deletes all compiled templates.
+     *
+     * @param string $compileDir
+     */
+    public static function deleteCompiledTemplates($compileDir = '')
+    {
+        if (empty($compileDir)) {
+            $compileDir = WCF_DIR . 'templates/compiled/';
+        }
+
+        // delete compiled templates
+        DirectoryUtil::getInstance($compileDir)->removePattern(new Regex('.*_.*\.php$'));
+    }
+
+    /**
+     * Returns an array with all prefilters.
+     *
+     * @return  string[]
+     */
+    public function getPrefilters()
+    {
+        return $this->prefilters;
+    }
+
+    /**
+     * Returns the active template group id.
+     *
+     * @return  int
+     */
+    public function getTemplateGroupID()
+    {
+        return $this->templateGroupID;
+    }
+
+    /**
+     * Sets the active template group id.
+     *
+     * @param int $templateGroupID
+     */
+    public function setTemplateGroupID($templateGroupID)
+    {
+        if ($templateGroupID && !isset($this->templateGroupCache[$templateGroupID])) {
+            $templateGroupID = 0;
+        }
+
+        $this->templateGroupID = $templateGroupID;
+    }
+
+    /**
+     * Loads cached template group information.
+     */
+    protected function loadTemplateGroupCache()
+    {
+        $this->templateGroupCache = TemplateGroupCacheBuilder::getInstance()->getData();
+    }
+
+    /**
+     * Registers prefilters.
+     *
+     * @param string[] $prefilters
+     */
+    public function registerPrefilter(array $prefilters)
+    {
+        foreach ($prefilters as $name) {
+            $this->prefilters[$name] = $name;
+        }
+    }
+
+    /**
+     * Removes a prefilter by its internal name.
+     *
+     * @param string $name internal prefilter identifier
+     */
+    public function removePrefilter($name)
+    {
+        unset($this->prefilters[$name]);
+    }
+
+    /**
+     * Sets the dir for the compiled templates.
+     *
+     * @param string $compileDir
+     * @throws  SystemException
+     */
+    public function setCompileDir($compileDir)
+    {
+        if (!\is_dir($compileDir)) {
+            throw new SystemException("'" . $compileDir . "' is not a valid dir");
+        }
+
+        $this->compileDir = $compileDir;
+    }
+
+    /**
+     * Includes a template.
+     *
+     * @param string $templateName
+     * @param string $application
+     * @param array $variables
+     * @param bool $sandbox enables execution in sandbox
+     */
+    protected function includeTemplate($templateName, $application, array $variables = [], $sandbox = true)
+    {
+        // enable sandbox
+        if ($sandbox) {
+            $this->enableSandbox();
+        }
+
+        // add new template variables
+        if (!empty($variables)) {
+            $this->v = \array_merge($this->v, $variables);
+        }
+
+        // display template
+        $this->display($templateName, $application, false);
+
+        // disable sandbox
+        if ($sandbox) {
+            $this->disableSandbox();
+        }
+    }
+
+    /**
+     * Returns the value of a template variable.
+     *
+     * @param string $varname
+     * @return  mixed
+     */
+    public function get($varname)
+    {
+        if (isset($this->v[$varname])) {
+            return $this->v[$varname];
+        }
+    }
+
+    /**
+     * Loads template listener code.
+     */
+    protected function loadTemplateListenerCode()
+    {
+        if (!$this->templateListenersLoaded) {
+            $this->templateListeners = TemplateListenerCodeCacheBuilder::getInstance()
+                ->getData(['environment' => $this->environment]);
+            $this->templateListenersLoaded = true;
+        }
+    }
+
+    /**
+     * Returns template listener's code.
+     *
+     * @param string $templateName
+     * @param string $eventName
+     * @return  string
+     */
+    public function getTemplateListenerCode($templateName, $eventName)
+    {
+        $this->loadTemplateListenerCode();
+
+        if (isset($this->templateListeners[$templateName][$eventName])) {
+            return \implode("\n", $this->templateListeners[$templateName][$eventName]);
+        }
+
+        return '';
+    }
+
+    /**
+     * Reads meta data from file.
+     *
+     * @param string $templateName
+     * @param string $filename
+     * @return  array|null
+     */
+    protected function getMetaData($templateName, $filename)
+    {
+        if (!\file_exists($filename) || !\is_readable($filename)) {
+            return null;
+        }
+
+        // get file contents
+        $contents = \file_get_contents($filename);
+
+        // find first newline
+        $position = \strpos($contents, "\n");
+        if ($position === false) {
+            return null;
+        }
+
+        // cut contents
+        $contents = \substr($contents, $position + 1);
+
+        // read serializes data
+        $data = @\unserialize($contents);
+        if ($data === false || !\is_array($data)) {
+            return null;
+        }
+
+        return $data;
+    }
 }