3 namespace wcf\system\template
;
5 use wcf\data\template\Template
;
6 use wcf\system\cache\builder\TemplateGroupCacheBuilder
;
7 use wcf\system\cache\builder\TemplateListenerCodeCacheBuilder
;
8 use wcf\system\event\EventHandler
;
9 use wcf\system\exception\SystemException
;
11 use wcf\system\SingletonFactory
;
12 use wcf\util\DirectoryUtil
;
13 use wcf\util\HeaderUtil
;
14 use wcf\util\StringUtil
;
17 * Loads and displays template.
19 * @author Alexander Ebert
20 * @copyright 2001-2019 WoltLab GmbH
21 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
22 * @package WoltLabSuite\Core\System\Template
24 class TemplateEngine
extends SingletonFactory
27 * directory used to cache previously compiled templates
30 public $compileDir = '';
33 * active language id used to identify specific language versions of compiled templates
36 public $languageID = 0;
39 * directories used as template source
42 public $templatePaths = [];
45 * namespace containing template modifiers and plugins
48 public $pluginNamespace = '';
51 * active template compiler
52 * @var TemplateCompiler
54 protected $compilerObj;
57 * forces the template engine to recompile all included templates
60 protected $forceCompile = false;
63 * list of registered prefilters
66 protected $prefilters = [];
69 * cached list of known template groups
72 protected $templateGroupCache = [];
75 * active template group id
78 protected $templateGroupID = 0;
81 * all available template variables and those assigned during runtime
87 * sandboxed values of currently active foreach loops' `item` and `key` variables
89 * for each currently active `foreach` loop, an array is added:
91 * (optional) 'item' => sandboxed value of an existing variable with the same name,
92 * (optional) 'key' => (optional) sandboxed value of an existing variable with the same name
97 protected $foreachVars = [];
100 * all cached variables for usage after execution in sandbox
103 protected $sandboxVars = [];
106 * contains all templates with assigned template listeners.
109 protected $templateListeners = [];
112 * true, if template listener code was already loaded
115 protected $templateListenersLoaded = false;
118 * current environment
121 protected $environment = 'user';
126 protected function init()
128 $this->templatePaths
= ['wcf' => WCF_DIR
. 'templates/'];
129 $this->pluginNamespace
= 'wcf\system\template\plugin\\';
130 $this->compileDir
= WCF_DIR
. 'templates/compiled/';
132 $this->loadTemplateGroupCache();
133 $this->assignSystemVariables();
137 * Adds a new application.
139 * @param string $abbreviation
140 * @param string $templatePath
142 public function addApplication($abbreviation, $templatePath)
144 $this->templatePaths
[$abbreviation] = $templatePath;
148 * Sets active language id.
150 * @param int $languageID
152 public function setLanguageID($languageID)
154 $this->languageID
= $languageID;
158 * Assigns some system variables.
160 protected function assignSystemVariables()
162 $this->v
['tpl'] = [];
164 // assign super globals
165 $this->v
['tpl']['get'] = &$_GET;
166 $this->v
['tpl']['post'] = &$_POST;
167 $this->v
['tpl']['cookie'] = &$_COOKIE;
168 $this->v
['tpl']['server'] = &$_SERVER;
169 $this->v
['tpl']['env'] = &$_ENV;
172 $this->v
['tpl']['now'] = TIME_NOW
;
173 $this->v
['tpl']['template'] = '';
174 $this->v
['tpl']['includedTemplates'] = [];
176 // section / foreach / capture arrays
177 $this->v
['tpl']['section'] = $this->v
['tpl']['foreach'] = $this->v
['tpl']['capture'] = [];
181 * Assigns a template variable.
183 * @param mixed $variable
184 * @param mixed $value
186 public function assign($variable, $value = '')
188 if (\
is_array($variable)) {
189 foreach ($variable as $key => $value) {
194 $this->assign($key, $value);
197 $this->v
[$variable] = $value;
202 * Appends content to an existing template variable.
204 * @param mixed $variable
205 * @param mixed $value
207 public function append($variable, $value = '')
209 if (\
is_array($variable)) {
210 foreach ($variable as $key => $val) {
212 $this->append($key, $val);
216 if (!empty($variable)) {
217 if (isset($this->v
[$variable])) {
218 if (\
is_array($this->v
[$variable]) && \
is_array($value)) {
219 $keys = \array_keys
($value);
220 foreach ($keys as $key) {
221 if (isset($this->v
[$variable][$key])) {
222 $this->v
[$variable][$key] .= $value[$key];
224 $this->v
[$variable][$key] = $value[$key];
228 $this->v
[$variable] .= $value;
231 $this->v
[$variable] = $value;
238 * Prepends content to an existing template variable.
240 * @param mixed $variable
241 * @param mixed $value
243 public function prepend($variable, $value = '')
245 if (\
is_array($variable)) {
246 foreach ($variable as $key => $val) {
248 $this->prepend($key, $val);
252 if (!empty($variable)) {
253 if (isset($this->v
[$variable])) {
254 if (\
is_array($this->v
[$variable]) && \
is_array($value)) {
255 $keys = \array_keys
($value);
256 foreach ($keys as $key) {
257 if (isset($this->v
[$variable][$key])) {
258 $this->v
[$variable][$key] = $value[$key] . $this->v
[$variable][$key];
260 $this->v
[$variable][$key] = $value[$key];
264 $this->v
[$variable] = $value . $this->v
[$variable];
267 $this->v
[$variable] = $value;
274 * Assigns a template variable by reference.
276 * @param string $variable
277 * @param mixed $value
279 public function assignByRef($variable, &$value)
281 if (!empty($variable)) {
282 $this->v
[$variable] = &$value;
287 * Clears an assignment of template variables.
289 * @param mixed $variables
291 public function clearAssign(array $variables)
293 foreach ($variables as $key) {
294 unset($this->v
[$key]);
299 * Clears assignment of all template variables. This should not be called
300 * during runtime as it could leed to an unexpected behaviour.
302 public function clearAllAssign()
308 * Outputs a template.
310 * @param string $templateName
311 * @param string $application
312 * @param bool $sendHeaders
314 public function display($templateName, $application = 'wcf', $sendHeaders = true)
317 HeaderUtil
::sendHeaders();
319 // call beforeDisplay event
320 if (!\
defined('NO_IMPORTS')) {
321 EventHandler
::getInstance()->fireAction($this, 'beforeDisplay');
325 $sourceFilename = $this->getSourceFilename($templateName, $application);
326 $compiledFilename = $this->getCompiledFilename($templateName, $application);
327 $metaDataFilename = $this->getMetaDataFilename($templateName);
328 $metaData = $this->getMetaData($templateName, $metaDataFilename);
330 // check if compilation is necessary
333 ||
!$this->isCompiled($templateName, $sourceFilename, $compiledFilename, $application, $metaData)
336 $this->compileTemplate($templateName, $sourceFilename, $compiledFilename, [
337 'application' => $application,
339 'filename' => $metaDataFilename,
343 // assign current package id
344 $this->assign('__APPLICATION', $application);
346 include($compiledFilename);
349 // call afterDisplay event
350 if (!\
defined('NO_IMPORTS')) {
351 EventHandler
::getInstance()->fireAction($this, 'afterDisplay');
357 * Returns the absolute filename of a template source.
359 * @param string $templateName
360 * @param string $application
361 * @return string $path
362 * @throws SystemException
364 public function getSourceFilename($templateName, $application)
366 $sourceFilename = $this->getPath($this->templatePaths
[$application], $templateName);
367 if (!empty($sourceFilename)) {
368 return $sourceFilename;
371 // try to find template within WCF if not already searching WCF
372 if ($application != 'wcf') {
373 $sourceFilename = $this->getSourceFilename($templateName, 'wcf');
374 if (!empty($sourceFilename)) {
375 return $sourceFilename;
379 throw new SystemException("Unable to find template '" . $templateName . "'");
383 * Returns path if template was found.
385 * @param string $templatePath
386 * @param string $templateName
389 protected function getPath($templatePath, $templateName)
391 if (!Template
::isSystemCritical($templateName)) {
392 $templateGroupID = $this->getTemplateGroupID();
394 while ($templateGroupID != 0) {
395 $templateGroup = $this->templateGroupCache
[$templateGroupID];
397 $path = $templatePath . $templateGroup->templateGroupFolderName
. $templateName . '.tpl';
398 if (\file_exists
($path)) {
402 $templateGroupID = $templateGroup->parentTemplateGroupID
;
406 // use default template
407 $path = $templatePath . $templateName . '.tpl';
409 if (\file_exists
($path)) {
417 * Returns the absolute filename of a compiled template.
419 * @param string $templateName
420 * @param string $application
423 public function getCompiledFilename($templateName, $application)
425 return $this->compileDir
. $this->getTemplateGroupID() . '_' . $application . '_' . $this->languageID
. '_' . $templateName . '.php';
429 * Returns the absolute filename for template's meta data.
431 * @param string $templateName
434 public function getMetaDataFilename($templateName)
436 return $this->compileDir
. $this->getTemplateGroupID() . '_' . $templateName . '.meta.php';
440 * Returns true if the template with the given data is already compiled.
442 * @param string $templateName
443 * @param string $sourceFilename
444 * @param string $compiledFilename
445 * @param string $application
446 * @param array $metaData
449 protected function isCompiled($templateName, $sourceFilename, $compiledFilename, $application, array $metaData)
451 if ($this->forceCompile ||
!\file_exists
($compiledFilename)) {
454 $sourceMTime = @\filemtime
($sourceFilename);
455 $compileMTime = @\filemtime
($compiledFilename);
457 if ($sourceMTime >= $compileMTime) {
460 // check for meta data
461 if (!empty($metaData['include'])) {
462 foreach ($metaData['include'] as $application => $includedTemplates) {
463 foreach ($includedTemplates as $includedTemplate) {
464 $includedTemplateFilename = $this->getSourceFilename($includedTemplate, $application);
465 $includedMTime = @\filemtime
($includedTemplateFilename);
467 if ($includedMTime >= $compileMTime) {
480 * Compiles a template.
482 * @param string $templateName
483 * @param string $sourceFilename
484 * @param string $compiledFilename
485 * @param array $metaData
487 protected function compileTemplate($templateName, $sourceFilename, $compiledFilename, array $metaData)
490 $sourceContent = $this->getSourceContent($sourceFilename);
493 $this->getCompiler()->compile($templateName, $sourceContent, $compiledFilename, $metaData);
497 * Returns the template compiler.
499 * @return TemplateCompiler
501 public function getCompiler()
503 if ($this->compilerObj
=== null) {
504 $this->compilerObj
= new TemplateCompiler($this);
507 return $this->compilerObj
;
511 * Reads the content of a template file.
513 * @param string $sourceFilename
515 * @throws SystemException
517 public function getSourceContent($sourceFilename)
519 /** @noinspection PhpUnusedLocalVariableInspection */
521 if (!\file_exists
($sourceFilename) ||
(($sourceContent = @\file_get_contents
($sourceFilename)) === false)) {
522 throw new SystemException("Could not open template '{$sourceFilename}' for reading");
524 return $sourceContent;
529 * Returns the class name of a plugin.
531 * @param string $type
535 public function getPluginClassName($type, $tag)
537 return $this->pluginNamespace
. StringUtil
::firstCharToUpperCase($tag) . StringUtil
::firstCharToUpperCase(\
mb_strtolower($type)) . 'TemplatePlugin';
541 * Enables execution in sandbox.
543 public function enableSandbox()
545 $index = \
count($this->sandboxVars
);
546 $this->sandboxVars
[$index] = $this->v
;
550 * Disables execution in sandbox.
552 public function disableSandbox()
554 if (empty($this->sandboxVars
)) {
555 throw new SystemException('TemplateEngine is currently not running in a sandbox.');
558 $this->v
= \array_pop
($this->sandboxVars
);
562 * Returns the output of a template.
564 * @param string $templateName
565 * @param string $application
566 * @param array $variables
567 * @param bool $sandbox enables execution in sandbox
570 public function fetch($templateName, $application = 'wcf', array $variables = [], $sandbox = false)
574 $this->enableSandbox();
577 // add new template variables
578 if (!empty($variables)) {
579 $this->v
= \array_merge
($this->v
, $variables);
585 $this->display($templateName, $application, false);
586 $output = \
ob_get_contents();
593 $this->disableSandbox();
600 * Executes a compiled template scripting source and returns the result.
602 * @param string $compiledSource
603 * @param array $variables
604 * @param bool $sandbox enables execution in sandbox
607 public function fetchString($compiledSource, array $variables = [], $sandbox = true)
611 $this->enableSandbox();
614 // add new template variables
615 if (!empty($variables)) {
616 $this->v
= \array_merge
($this->v
, $variables);
621 eval('?>' . $compiledSource);
622 $output = \
ob_get_contents();
627 $this->disableSandbox();
634 * Deletes all compiled templates.
636 * @param string $compileDir
638 public static function deleteCompiledTemplates($compileDir = '')
640 if (empty($compileDir)) {
641 $compileDir = WCF_DIR
. 'templates/compiled/';
644 // delete compiled templates
645 DirectoryUtil
::getInstance($compileDir)->removePattern(new Regex('.*_.*\.php$'));
649 * Returns an array with all prefilters.
653 public function getPrefilters()
655 return $this->prefilters
;
659 * Returns the active template group id.
663 public function getTemplateGroupID()
665 return $this->templateGroupID
;
669 * Sets the active template group id.
671 * @param int $templateGroupID
673 public function setTemplateGroupID($templateGroupID)
675 if ($templateGroupID && !isset($this->templateGroupCache
[$templateGroupID])) {
676 $templateGroupID = 0;
679 $this->templateGroupID
= $templateGroupID;
683 * Loads cached template group information.
685 protected function loadTemplateGroupCache()
687 $this->templateGroupCache
= TemplateGroupCacheBuilder
::getInstance()->getData();
691 * Registers prefilters.
693 * @param string[] $prefilters
695 public function registerPrefilter(array $prefilters)
697 foreach ($prefilters as $name) {
698 $this->prefilters
[$name] = $name;
703 * Removes a prefilter by its internal name.
705 * @param string $name internal prefilter identifier
707 public function removePrefilter($name)
709 unset($this->prefilters
[$name]);
713 * Sets the dir for the compiled templates.
715 * @param string $compileDir
716 * @throws SystemException
718 public function setCompileDir($compileDir)
720 if (!\
is_dir($compileDir)) {
721 throw new SystemException("'" . $compileDir . "' is not a valid dir");
724 $this->compileDir
= $compileDir;
728 * Includes a template.
730 * @param string $templateName
731 * @param string $application
732 * @param array $variables
733 * @param bool $sandbox enables execution in sandbox
735 protected function includeTemplate($templateName, $application, array $variables = [], $sandbox = true)
739 $this->enableSandbox();
742 // add new template variables
743 if (!empty($variables)) {
744 $this->v
= \array_merge
($this->v
, $variables);
748 $this->display($templateName, $application, false);
752 $this->disableSandbox();
757 * Returns the value of a template variable.
759 * @param string $varname
762 public function get($varname)
764 if (isset($this->v
[$varname])) {
765 return $this->v
[$varname];
770 * Loads template listener code.
772 protected function loadTemplateListenerCode()
774 if (!$this->templateListenersLoaded
) {
775 $this->templateListeners
= TemplateListenerCodeCacheBuilder
::getInstance()
776 ->getData(['environment' => $this->environment
]);
777 $this->templateListenersLoaded
= true;
782 * Returns template listener's code.
784 * @param string $templateName
785 * @param string $eventName
788 public function getTemplateListenerCode($templateName, $eventName)
790 $this->loadTemplateListenerCode();
792 if (isset($this->templateListeners
[$templateName][$eventName])) {
793 return \
implode("\n", $this->templateListeners
[$templateName][$eventName]);
800 * Reads meta data from file.
802 * @param string $templateName
803 * @param string $filename
806 protected function getMetaData($templateName, $filename)
808 if (!\file_exists
($filename) ||
!\
is_readable($filename)) {
813 $contents = \file_get_contents
($filename);
815 // find first newline
816 $position = \
strpos($contents, "\n");
817 if ($position === false) {
822 $contents = \
substr($contents, $position +
1);
824 // read serializes data
825 $data = @\
unserialize($contents);
826 if ($data === false ||
!\
is_array($data)) {