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