Merge branch '2.0'
[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-2014 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 $includedTemplate) {
432 $includedTemplateFilename = $this->getSourceFilename($includedTemplate, $application);
433 $includedMTime = @filemtime($includedTemplateFilename);
434
435 if ($includedMTime >= $compileMTime) {
436 return false;
437 }
438 }
439 }
440
441 return true;
442 }
443 }
444 }
445
446 /**
447 * Compiles a template.
448 *
449 * @param string $templateName
450 * @param string $sourceFilename
451 * @param string $compiledFilename
452 * @param array $metaData
453 */
454 protected function compileTemplate($templateName, $sourceFilename, $compiledFilename, array $metaData) {
455 // get source
456 $sourceContent = $this->getSourceContent($sourceFilename);
457
458 // compile template
459 $this->getCompiler()->compile($templateName, $sourceContent, $compiledFilename, $metaData);
460 }
461
462 /**
463 * Returns the template compiler.
464 *
465 * @return \wcf\system\template\TemplateCompiler
466 */
467 public function getCompiler() {
468 if ($this->compilerObj === null) {
469 $this->compilerObj = new TemplateCompiler($this);
470 }
471
472 return $this->compilerObj;
473 }
474
475 /**
476 * Reads the content of a template file.
477 *
478 * @param string $sourceFilename
479 * @return string $sourceContent
480 */
481 public function getSourceContent($sourceFilename) {
482 $sourceContent = '';
483 if (!file_exists($sourceFilename) || (($sourceContent = @file_get_contents($sourceFilename)) === false)) {
484 throw new SystemException("Could not open template '$sourceFilename' for reading");
485 }
486 else {
487 return $sourceContent;
488 }
489 }
490
491 /**
492 * Returns the class name of a plugin.
493 *
494 * @param string $type
495 * @param string $tag
496 * @return string
497 */
498 public function getPluginClassName($type, $tag) {
499 return $this->pluginNamespace.StringUtil::firstCharToUpperCase($tag).StringUtil::firstCharToUpperCase(mb_strtolower($type)).'TemplatePlugin';
500 }
501
502 /**
503 * Enables execution in sandbox.
504 */
505 public function enableSandbox() {
506 $index = count($this->sandboxVars);
507 $this->sandboxVars[$index] = $this->v;
508 }
509
510 /**
511 * Disables execution in sandbox.
512 */
513 public function disableSandbox() {
514 if (empty($this->sandboxVars)) {
515 throw new SystemException('TemplateEngine is currently not running in a sandbox.');
516 }
517
518 $this->v = array_pop($this->sandboxVars);
519 }
520
521 /**
522 * Returns the output of a template.
523 *
524 * @param string $templateName
525 * @param string $application
526 * @param array $variables
527 * @param boolean $sandbox enables execution in sandbox
528 * @return string
529 */
530 public function fetch($templateName, $application = 'wcf', array $variables = array(), $sandbox = false) {
531 // enable sandbox
532 if ($sandbox) {
533 $this->enableSandbox();
534 }
535
536 // add new template variables
537 if (!empty($variables)) {
538 $this->v = array_merge($this->v, $variables);
539 }
540
541 // get output
542 ob_start();
543 $this->display($templateName, $application, false);
544 $output = ob_get_contents();
545 ob_end_clean();
546
547 // disable sandbox
548 if ($sandbox) {
549 $this->disableSandbox();
550 }
551
552 return $output;
553 }
554
555 /**
556 * Executes a compiled template scripting source and returns the result.
557 *
558 * @param string $compiledSource
559 * @param array $variables
560 * @param boolean $sandbox enables execution in sandbox
561 * @return string
562 */
563 public function fetchString($compiledSource, array $variables = array(), $sandbox = true) {
564 // enable sandbox
565 if ($sandbox) {
566 $this->enableSandbox();
567 }
568
569 // add new template variables
570 if (!empty($variables)) {
571 $this->v = array_merge($this->v, $variables);
572 }
573
574 // get output
575 ob_start();
576 eval('?>'.$compiledSource);
577 $output = ob_get_contents();
578 ob_end_clean();
579
580 // disable sandbox
581 if ($sandbox) {
582 $this->disableSandbox();
583 }
584
585 return $output;
586 }
587
588 /**
589 * Deletes all compiled templates.
590 *
591 * @param string $compileDir
592 */
593 public static function deleteCompiledTemplates($compileDir = '') {
594 if (empty($compileDir)) $compileDir = WCF_DIR.'templates/compiled/';
595
596 // delete compiled templates
597 DirectoryUtil::getInstance($compileDir)->removePattern(new Regex('.*_.*\.php$'));
598 }
599
600 /**
601 * Returns an array with all prefilters.
602 *
603 * @return array<string>
604 */
605 public function getPrefilters() {
606 return $this->prefilters;
607 }
608
609 /**
610 * Returns the active template group id.
611 *
612 * @return integer
613 */
614 public function getTemplateGroupID() {
615 return $this->templateGroupID;
616 }
617
618 /**
619 * Sets the active template group id.
620 *
621 * @param integer $templateGroupID
622 */
623 public function setTemplateGroupID($templateGroupID) {
624 if ($templateGroupID && !isset($this->templateGroupCache[$templateGroupID])) {
625 $templateGroupID = 0;
626 }
627
628 $this->templateGroupID = $templateGroupID;
629 }
630
631 /**
632 * Loads cached template group information.
633 */
634 protected function loadTemplateGroupCache() {
635 $this->templateGroupCache = TemplateGroupCacheBuilder::getInstance()->getData();
636 }
637
638 /**
639 * Registers prefilters.
640 *
641 * @param array<string> $prefilters
642 */
643 public function registerPrefilter(array $prefilters) {
644 foreach ($prefilters as $name) {
645 $this->prefilters[$name] = $name;
646 }
647 }
648
649 /**
650 * Sets the dir for the compiled templates.
651 *
652 * @param string $compileDir
653 */
654 public function setCompileDir($compileDir) {
655 if (!is_dir($compileDir)) {
656 throw new SystemException("'".$compileDir."' is not a valid dir");
657 }
658
659 $this->compileDir = $compileDir;
660 }
661
662 /**
663 * Includes a template.
664 *
665 * @param string $templateName
666 * @param string $application
667 * @param array $variables
668 * @param boolean $sandbox enables execution in sandbox
669 */
670 protected function includeTemplate($templateName, $application, array $variables = array(), $sandbox = true) {
671 // enable sandbox
672 if ($sandbox) {
673 $this->enableSandbox();
674 }
675
676 // add new template variables
677 if (!empty($variables)) {
678 $this->v = array_merge($this->v, $variables);
679 }
680
681 // display template
682 $this->display($templateName, $application, false);
683
684 // disable sandbox
685 if ($sandbox) {
686 $this->disableSandbox();
687 }
688 }
689
690 /**
691 * Returns the value of a template variable.
692 *
693 * @param string $varname
694 * @return mixed
695 */
696 public function get($varname) {
697 if (isset($this->v[$varname])) {
698 return $this->v[$varname];
699 }
700
701 return null;
702 }
703
704 /**
705 * Loads all available template listeners.
706 *
707 * @deprecated since 2.1
708 */
709 protected function loadTemplateListeners() {
710 // does nothing
711 }
712
713 /**
714 * Returns true if requested template has assigned template listeners.
715 *
716 * @param string $templateName
717 * @param string $application
718 * @return boolean
719 */
720 public function hasTemplateListeners($templateName, $application = 'wcf') {
721 if (isset($this->templateListeners[$application]) && isset($this->templateListeners[$application][$templateName])) {
722 return true;
723 }
724
725 return false;
726 }
727
728 /**
729 * Loads template listener code.
730 */
731 protected function loadTemplateListenerCode() {
732 if (!$this->templateListenersLoaded) {
733 $this->templateListeners = TemplateListenerCodeCacheBuilder::getInstance()->getData(array('environment' => $this->environment));
734 $this->templateListenersLoaded = true;
735 }
736 }
737
738 /**
739 * Returns template listener's code.
740 *
741 * @param string $templateName
742 * @param string $eventName
743 * @return string
744 */
745 public function getTemplateListenerCode($templateName, $eventName) {
746 $this->loadTemplateListenerCode();
747
748 if (isset($this->templateListeners[$templateName][$eventName])) {
749 return implode("\n", $this->templateListeners[$templateName][$eventName]);
750 }
751
752 return '';
753 }
754
755 /**
756 * Reads meta data from file.
757 *
758 * @param string $templateName
759 * @param string $filename
760 * @return array
761 */
762 protected function getMetaData($templateName, $filename) {
763 if (!file_exists($filename) || !is_readable($filename)) {
764 return null;
765 }
766
767 // get file contents
768 $contents = file_get_contents($filename);
769
770 // find first newline
771 $position = strpos($contents, "\n");
772 if ($position === false) {
773 return null;
774 }
775
776 // cut contents
777 $contents = substr($contents, $position + 1);
778
779 // read serializes data
780 $data = @unserialize($contents);
781 if ($data === false || !is_array($data)) {
782 return null;
783 }
784
785 return $data;
786 }
787 }