a910ef33e7835f4840dcabd5219309044b1ea868
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / template / TemplateEngine.class.php
1 <?php
2
3 namespace wcf\system\template;
4
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;
10 use wcf\system\Regex;
11 use wcf\system\SingletonFactory;
12 use wcf\util\DirectoryUtil;
13 use wcf\util\HeaderUtil;
14 use wcf\util\StringUtil;
15
16 /**
17 * Loads and displays template.
18 *
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
23 */
24 class TemplateEngine extends SingletonFactory
25 {
26 /**
27 * directory used to cache previously compiled templates
28 * @var string
29 */
30 public $compileDir = '';
31
32 /**
33 * active language id used to identify specific language versions of compiled templates
34 * @var int
35 */
36 public $languageID = 0;
37
38 /**
39 * directories used as template source
40 * @var string[]
41 */
42 public $templatePaths = [];
43
44 /**
45 * namespace containing template modifiers and plugins
46 * @var string
47 */
48 public $pluginNamespace = '';
49
50 /**
51 * active template compiler
52 * @var TemplateCompiler
53 */
54 protected $compilerObj;
55
56 /**
57 * forces the template engine to recompile all included templates
58 * @var bool
59 */
60 protected $forceCompile = false;
61
62 /**
63 * list of registered prefilters
64 * @var string[]
65 */
66 protected $prefilters = [];
67
68 /**
69 * cached list of known template groups
70 * @var array
71 */
72 protected $templateGroupCache = [];
73
74 /**
75 * active template group id
76 * @var int
77 */
78 protected $templateGroupID = 0;
79
80 /**
81 * all available template variables and those assigned during runtime
82 * @var mixed[][]
83 */
84 protected $v = [];
85
86 /**
87 * sandboxed values of currently active foreach loops' `item` and `key` variables
88 *
89 * for each currently active `foreach` loop, an array is added:
90 * $foreachHash => [
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
93 * ]
94 *
95 * @var mixed[][][]
96 */
97 protected $foreachVars = [];
98
99 /**
100 * all cached variables for usage after execution in sandbox
101 * @var mixed[][]
102 */
103 protected $sandboxVars = [];
104
105 /**
106 * contains all templates with assigned template listeners.
107 * @var string[][][]
108 */
109 protected $templateListeners = [];
110
111 /**
112 * true, if template listener code was already loaded
113 * @var bool
114 */
115 protected $templateListenersLoaded = false;
116
117 /**
118 * current environment
119 * @var string
120 */
121 protected $environment = 'user';
122
123 /**
124 * @inheritDoc
125 */
126 protected function init()
127 {
128 $this->templatePaths = ['wcf' => WCF_DIR . 'templates/'];
129 $this->pluginNamespace = 'wcf\system\template\plugin\\';
130 $this->compileDir = WCF_DIR . 'templates/compiled/';
131
132 $this->loadTemplateGroupCache();
133 $this->assignSystemVariables();
134 }
135
136 /**
137 * Adds a new application.
138 *
139 * @param string $abbreviation
140 * @param string $templatePath
141 */
142 public function addApplication($abbreviation, $templatePath)
143 {
144 $this->templatePaths[$abbreviation] = $templatePath;
145 }
146
147 /**
148 * Sets active language id.
149 *
150 * @param int $languageID
151 */
152 public function setLanguageID($languageID)
153 {
154 $this->languageID = $languageID;
155 }
156
157 /**
158 * Assigns some system variables.
159 */
160 protected function assignSystemVariables()
161 {
162 $this->v['tpl'] = [];
163
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;
170
171 // system info
172 $this->v['tpl']['now'] = TIME_NOW;
173 $this->v['tpl']['template'] = '';
174 $this->v['tpl']['includedTemplates'] = [];
175
176 // section / foreach / capture arrays
177 $this->v['tpl']['section'] = $this->v['tpl']['foreach'] = $this->v['tpl']['capture'] = [];
178 }
179
180 /**
181 * Assigns a template variable.
182 *
183 * @param mixed $variable
184 * @param mixed $value
185 */
186 public function assign($variable, $value = '')
187 {
188 if (\is_array($variable)) {
189 foreach ($variable as $key => $value) {
190 if (empty($key)) {
191 continue;
192 }
193
194 $this->assign($key, $value);
195 }
196 } else {
197 $this->v[$variable] = $value;
198 }
199 }
200
201 /**
202 * Appends content to an existing template variable.
203 *
204 * @param mixed $variable
205 * @param mixed $value
206 */
207 public function append($variable, $value = '')
208 {
209 if (\is_array($variable)) {
210 foreach ($variable as $key => $val) {
211 if ($key != '') {
212 $this->append($key, $val);
213 }
214 }
215 } else {
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];
223 } else {
224 $this->v[$variable][$key] = $value[$key];
225 }
226 }
227 } else {
228 $this->v[$variable] .= $value;
229 }
230 } else {
231 $this->v[$variable] = $value;
232 }
233 }
234 }
235 }
236
237 /**
238 * Prepends content to an existing template variable.
239 *
240 * @param mixed $variable
241 * @param mixed $value
242 */
243 public function prepend($variable, $value = '')
244 {
245 if (\is_array($variable)) {
246 foreach ($variable as $key => $val) {
247 if ($key != '') {
248 $this->prepend($key, $val);
249 }
250 }
251 } else {
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];
259 } else {
260 $this->v[$variable][$key] = $value[$key];
261 }
262 }
263 } else {
264 $this->v[$variable] = $value . $this->v[$variable];
265 }
266 } else {
267 $this->v[$variable] = $value;
268 }
269 }
270 }
271 }
272
273 /**
274 * Assigns a template variable by reference.
275 *
276 * @param string $variable
277 * @param mixed $value
278 */
279 public function assignByRef($variable, &$value)
280 {
281 if (!empty($variable)) {
282 $this->v[$variable] = &$value;
283 }
284 }
285
286 /**
287 * Clears an assignment of template variables.
288 *
289 * @param mixed $variables
290 */
291 public function clearAssign(array $variables)
292 {
293 foreach ($variables as $key) {
294 unset($this->v[$key]);
295 }
296 }
297
298 /**
299 * Clears assignment of all template variables. This should not be called
300 * during runtime as it could leed to an unexpected behaviour.
301 */
302 public function clearAllAssign()
303 {
304 $this->v = [];
305 }
306
307 /**
308 * Outputs a template.
309 *
310 * @param string $templateName
311 * @param string $application
312 * @param bool $sendHeaders
313 */
314 public function display($templateName, $application = 'wcf', $sendHeaders = true)
315 {
316 if ($sendHeaders) {
317 HeaderUtil::sendHeaders();
318
319 // call beforeDisplay event
320 if (!\defined('NO_IMPORTS')) {
321 EventHandler::getInstance()->fireAction($this, 'beforeDisplay');
322 }
323 }
324
325 $sourceFilename = $this->getSourceFilename($templateName, $application);
326 $compiledFilename = $this->getCompiledFilename($templateName, $application);
327 $metaDataFilename = $this->getMetaDataFilename($templateName);
328 $metaData = $this->getMetaData($templateName, $metaDataFilename);
329
330 // check if compilation is necessary
331 if (
332 $metaData === null
333 || !$this->isCompiled($templateName, $sourceFilename, $compiledFilename, $application, $metaData)
334 ) {
335 // compile
336 $this->compileTemplate($templateName, $sourceFilename, $compiledFilename, [
337 'application' => $application,
338 'data' => $metaData,
339 'filename' => $metaDataFilename,
340 ]);
341 }
342
343 // assign current package id
344 $this->assign('__APPLICATION', $application);
345
346 include($compiledFilename);
347
348 if ($sendHeaders) {
349 // call afterDisplay event
350 if (!\defined('NO_IMPORTS')) {
351 EventHandler::getInstance()->fireAction($this, 'afterDisplay');
352 }
353 }
354 }
355
356 /**
357 * Returns the absolute filename of a template source.
358 *
359 * @param string $templateName
360 * @param string $application
361 * @return string $path
362 * @throws SystemException
363 */
364 public function getSourceFilename($templateName, $application)
365 {
366 $sourceFilename = $this->getPath($this->templatePaths[$application], $templateName);
367 if (!empty($sourceFilename)) {
368 return $sourceFilename;
369 }
370
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;
376 }
377 }
378
379 throw new SystemException("Unable to find template '" . $templateName . "'");
380 }
381
382 /**
383 * Returns path if template was found.
384 *
385 * @param string $templatePath
386 * @param string $templateName
387 * @return string
388 */
389 protected function getPath($templatePath, $templateName)
390 {
391 if (!Template::isSystemCritical($templateName)) {
392 $templateGroupID = $this->getTemplateGroupID();
393
394 while ($templateGroupID != 0) {
395 $templateGroup = $this->templateGroupCache[$templateGroupID];
396
397 $path = $templatePath . $templateGroup->templateGroupFolderName . $templateName . '.tpl';
398 if (\file_exists($path)) {
399 return $path;
400 }
401
402 $templateGroupID = $templateGroup->parentTemplateGroupID;
403 }
404 }
405
406 // use default template
407 $path = $templatePath . $templateName . '.tpl';
408
409 if (\file_exists($path)) {
410 return $path;
411 }
412
413 return '';
414 }
415
416 /**
417 * Returns the absolute filename of a compiled template.
418 *
419 * @param string $templateName
420 * @param string $application
421 * @return string
422 */
423 public function getCompiledFilename($templateName, $application)
424 {
425 return $this->compileDir . $this->getTemplateGroupID() . '_' . $application . '_' . $this->languageID . '_' . $templateName . '.php';
426 }
427
428 /**
429 * Returns the absolute filename for template's meta data.
430 *
431 * @param string $templateName
432 * @return string
433 */
434 public function getMetaDataFilename($templateName)
435 {
436 return $this->compileDir . $this->getTemplateGroupID() . '_' . $templateName . '.meta.php';
437 }
438
439 /**
440 * Returns true if the template with the given data is already compiled.
441 *
442 * @param string $templateName
443 * @param string $sourceFilename
444 * @param string $compiledFilename
445 * @param string $application
446 * @param array $metaData
447 * @return bool
448 */
449 protected function isCompiled($templateName, $sourceFilename, $compiledFilename, $application, array $metaData)
450 {
451 if ($this->forceCompile || !\file_exists($compiledFilename)) {
452 return false;
453 } else {
454 $sourceMTime = @\filemtime($sourceFilename);
455 $compileMTime = @\filemtime($compiledFilename);
456
457 if ($sourceMTime >= $compileMTime) {
458 return false;
459 } else {
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);
466
467 if ($includedMTime >= $compileMTime) {
468 return false;
469 }
470 }
471 }
472 }
473
474 return true;
475 }
476 }
477 }
478
479 /**
480 * Compiles a template.
481 *
482 * @param string $templateName
483 * @param string $sourceFilename
484 * @param string $compiledFilename
485 * @param array $metaData
486 */
487 protected function compileTemplate($templateName, $sourceFilename, $compiledFilename, array $metaData)
488 {
489 // get source
490 $sourceContent = $this->getSourceContent($sourceFilename);
491
492 // compile template
493 $this->getCompiler()->compile($templateName, $sourceContent, $compiledFilename, $metaData);
494 }
495
496 /**
497 * Returns the template compiler.
498 *
499 * @return TemplateCompiler
500 */
501 public function getCompiler()
502 {
503 if ($this->compilerObj === null) {
504 $this->compilerObj = new TemplateCompiler($this);
505 }
506
507 return $this->compilerObj;
508 }
509
510 /**
511 * Reads the content of a template file.
512 *
513 * @param string $sourceFilename
514 * @return string
515 * @throws SystemException
516 */
517 public function getSourceContent($sourceFilename)
518 {
519 /** @noinspection PhpUnusedLocalVariableInspection */
520 $sourceContent = '';
521 if (!\file_exists($sourceFilename) || (($sourceContent = @\file_get_contents($sourceFilename)) === false)) {
522 throw new SystemException("Could not open template '{$sourceFilename}' for reading");
523 } else {
524 return $sourceContent;
525 }
526 }
527
528 /**
529 * Returns the class name of a plugin.
530 *
531 * @param string $type
532 * @param string $tag
533 * @return string
534 */
535 public function getPluginClassName($type, $tag)
536 {
537 return $this->pluginNamespace . StringUtil::firstCharToUpperCase($tag) . StringUtil::firstCharToUpperCase(\mb_strtolower($type)) . 'TemplatePlugin';
538 }
539
540 /**
541 * Enables execution in sandbox.
542 */
543 public function enableSandbox()
544 {
545 $index = \count($this->sandboxVars);
546 $this->sandboxVars[$index] = $this->v;
547 }
548
549 /**
550 * Disables execution in sandbox.
551 */
552 public function disableSandbox()
553 {
554 if (empty($this->sandboxVars)) {
555 throw new SystemException('TemplateEngine is currently not running in a sandbox.');
556 }
557
558 $this->v = \array_pop($this->sandboxVars);
559 }
560
561 /**
562 * Returns the output of a template.
563 *
564 * @param string $templateName
565 * @param string $application
566 * @param array $variables
567 * @param bool $sandbox enables execution in sandbox
568 * @return string
569 */
570 public function fetch($templateName, $application = 'wcf', array $variables = [], $sandbox = false)
571 {
572 // enable sandbox
573 if ($sandbox) {
574 $this->enableSandbox();
575 }
576
577 // add new template variables
578 if (!empty($variables)) {
579 $this->v = \array_merge($this->v, $variables);
580 }
581
582 // get output
583 try {
584 \ob_start();
585 $this->display($templateName, $application, false);
586 $output = \ob_get_contents();
587 } finally {
588 \ob_end_clean();
589 }
590
591 // disable sandbox
592 if ($sandbox) {
593 $this->disableSandbox();
594 }
595
596 return $output;
597 }
598
599 /**
600 * Executes a compiled template scripting source and returns the result.
601 *
602 * @param string $compiledSource
603 * @param array $variables
604 * @param bool $sandbox enables execution in sandbox
605 * @return string
606 */
607 public function fetchString($compiledSource, array $variables = [], $sandbox = true)
608 {
609 // enable sandbox
610 if ($sandbox) {
611 $this->enableSandbox();
612 }
613
614 // add new template variables
615 if (!empty($variables)) {
616 $this->v = \array_merge($this->v, $variables);
617 }
618
619 // get output
620 \ob_start();
621 eval('?>' . $compiledSource);
622 $output = \ob_get_contents();
623 \ob_end_clean();
624
625 // disable sandbox
626 if ($sandbox) {
627 $this->disableSandbox();
628 }
629
630 return $output;
631 }
632
633 /**
634 * Deletes all compiled templates.
635 *
636 * @param string $compileDir
637 */
638 public static function deleteCompiledTemplates($compileDir = '')
639 {
640 if (empty($compileDir)) {
641 $compileDir = WCF_DIR . 'templates/compiled/';
642 }
643
644 // delete compiled templates
645 DirectoryUtil::getInstance($compileDir)->removePattern(new Regex('.*_.*\.php$'));
646 }
647
648 /**
649 * Returns an array with all prefilters.
650 *
651 * @return string[]
652 */
653 public function getPrefilters()
654 {
655 return $this->prefilters;
656 }
657
658 /**
659 * Returns the active template group id.
660 *
661 * @return int
662 */
663 public function getTemplateGroupID()
664 {
665 return $this->templateGroupID;
666 }
667
668 /**
669 * Sets the active template group id.
670 *
671 * @param int $templateGroupID
672 */
673 public function setTemplateGroupID($templateGroupID)
674 {
675 if ($templateGroupID && !isset($this->templateGroupCache[$templateGroupID])) {
676 $templateGroupID = 0;
677 }
678
679 $this->templateGroupID = $templateGroupID;
680 }
681
682 /**
683 * Loads cached template group information.
684 */
685 protected function loadTemplateGroupCache()
686 {
687 $this->templateGroupCache = TemplateGroupCacheBuilder::getInstance()->getData();
688 }
689
690 /**
691 * Registers prefilters.
692 *
693 * @param string[] $prefilters
694 */
695 public function registerPrefilter(array $prefilters)
696 {
697 foreach ($prefilters as $name) {
698 $this->prefilters[$name] = $name;
699 }
700 }
701
702 /**
703 * Removes a prefilter by its internal name.
704 *
705 * @param string $name internal prefilter identifier
706 */
707 public function removePrefilter($name)
708 {
709 unset($this->prefilters[$name]);
710 }
711
712 /**
713 * Sets the dir for the compiled templates.
714 *
715 * @param string $compileDir
716 * @throws SystemException
717 */
718 public function setCompileDir($compileDir)
719 {
720 if (!\is_dir($compileDir)) {
721 throw new SystemException("'" . $compileDir . "' is not a valid dir");
722 }
723
724 $this->compileDir = $compileDir;
725 }
726
727 /**
728 * Includes a template.
729 *
730 * @param string $templateName
731 * @param string $application
732 * @param array $variables
733 * @param bool $sandbox enables execution in sandbox
734 */
735 protected function includeTemplate($templateName, $application, array $variables = [], $sandbox = true)
736 {
737 // enable sandbox
738 if ($sandbox) {
739 $this->enableSandbox();
740 }
741
742 // add new template variables
743 if (!empty($variables)) {
744 $this->v = \array_merge($this->v, $variables);
745 }
746
747 // display template
748 $this->display($templateName, $application, false);
749
750 // disable sandbox
751 if ($sandbox) {
752 $this->disableSandbox();
753 }
754 }
755
756 /**
757 * Returns the value of a template variable.
758 *
759 * @param string $varname
760 * @return mixed
761 */
762 public function get($varname)
763 {
764 if (isset($this->v[$varname])) {
765 return $this->v[$varname];
766 }
767 }
768
769 /**
770 * Loads template listener code.
771 */
772 protected function loadTemplateListenerCode()
773 {
774 if (!$this->templateListenersLoaded) {
775 $this->templateListeners = TemplateListenerCodeCacheBuilder::getInstance()
776 ->getData(['environment' => $this->environment]);
777 $this->templateListenersLoaded = true;
778 }
779 }
780
781 /**
782 * Returns template listener's code.
783 *
784 * @param string $templateName
785 * @param string $eventName
786 * @return string
787 */
788 public function getTemplateListenerCode($templateName, $eventName)
789 {
790 $this->loadTemplateListenerCode();
791
792 if (isset($this->templateListeners[$templateName][$eventName])) {
793 return \implode("\n", $this->templateListeners[$templateName][$eventName]);
794 }
795
796 return '';
797 }
798
799 /**
800 * Reads meta data from file.
801 *
802 * @param string $templateName
803 * @param string $filename
804 * @return array
805 */
806 protected function getMetaData($templateName, $filename)
807 {
808 if (!\file_exists($filename) || !\is_readable($filename)) {
809 return;
810 }
811
812 // get file contents
813 $contents = \file_get_contents($filename);
814
815 // find first newline
816 $position = \strpos($contents, "\n");
817 if ($position === false) {
818 return;
819 }
820
821 // cut contents
822 $contents = \substr($contents, $position + 1);
823
824 // read serializes data
825 $data = @\unserialize($contents);
826 if ($data === false || !\is_array($data)) {
827 return;
828 }
829
830 return $data;
831 }
832 }