Removed unused line
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / template / TemplateScriptingCompiler.class.php
1 <?php
2 namespace wcf\system\template;
3 use wcf\system\exception\SystemException;
4 use wcf\system\template\plugin\ICompilerTemplatePlugin;
5 use wcf\system\template\plugin\IPrefilterTemplatePlugin;
6 use wcf\system\WCF;
7 use wcf\util\StringStack;
8 use wcf\util\StringUtil;
9
10 /**
11 * Compiles template sources into valid PHP code.
12 *
13 * @author Marcel Werk
14 * @copyright 2001-2012 WoltLab GmbH
15 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
16 * @package com.woltlab.wcf
17 * @subpackage system.template
18 * @category Community Framework
19 */
20 class TemplateScriptingCompiler {
21 /**
22 * template engine object
23 * @var wcf\system\templateTemplateEngine
24 */
25 protected $template;
26
27 /**
28 * PHP functions that can be used in the modifier syntax and are unknown
29 * to PHP's function_exists function
30 * @var array<string>
31 */
32 protected $unknownPHPFunctions = array('isset', 'unset', 'empty');
33
34 /**
35 * PHP functions that can not be used in the modifier syntax
36 * @var array<string>
37 */
38 protected $disabledPHPFunctions = array(
39 'system', 'exec', 'passthru', 'shell_exec', // command line execution
40 'include', 'require', 'include_once', 'require_once', // includes
41 'eval', 'virtual', 'call_user_func_array', 'call_user_func', 'assert' // code execution
42 );
43
44 /**
45 * pattern to match variable operators like -> or .
46 * @var string
47 */
48 protected $variableOperatorPattern;
49
50 /**
51 * pattern to match condition operators like == or <
52 * @var string
53 */
54 protected $conditionOperatorPattern;
55
56 /**
57 * negative lookbehind for a backslash
58 * @var string
59 */
60 protected $escapedPattern;
61
62 /**
63 * pattern to match valid variable names
64 * @var string
65 */
66 protected $validVarnamePattern;
67
68 /**
69 * pattern to match constants like CONSTANT or __CONSTANT
70 * @var string
71 */
72 protected $constantPattern;
73
74 /**
75 * pattern to match double quoted strings like "blah" or "quote: \"blah\""
76 * @var string
77 */
78 protected $doubleQuotePattern;
79
80 /**
81 * pattern to match single quoted strings like 'blah' or 'don\'t'
82 * @var string
83 */
84 protected $singleQuotePattern;
85
86 /**
87 * pattern to match single or double quoted strings
88 * @var string
89 */
90 protected $quotePattern;
91
92 /**
93 * pattern to match numbers, true, false and null
94 * @var string
95 */
96 protected $numericPattern;
97
98 /**
99 * pattern to match simple variables like $foo
100 * @var string
101 */
102 protected $simpleVarPattern;
103
104 /**
105 * pattern to match outputs like @$foo or #CONST
106 * @var string
107 */
108 protected $outputPattern;
109
110 /**
111 * identifier of currently compiled template
112 * @var string
113 */
114 protected $currentIdentifier;
115
116 /**
117 * current line number during template compilation
118 * @var string
119 */
120 protected $currentLineNo;
121
122 /**
123 * list of automatically loaded tenplate plugins
124 * @var array<string>
125 */
126 protected $autoloadPlugins = array();
127
128 /**
129 * stack with template tags data
130 * @var array
131 */
132 protected $tagStack = array();
133
134 /**
135 * list of loaded compiler plugin objects
136 * @var array<wcf\system\template\ICompilerTemplatePlugin>
137 */
138 protected $compilerPlugins = array();
139
140 /**
141 * stack used to compile the capture tag
142 * @var array
143 */
144 protected $captureStack = array();
145
146 /**
147 * left delimiter of template syntax
148 * @var string
149 */
150 protected $leftDelimiter = '{';
151
152 /**
153 * right delimiter of template syntax
154 * @var string
155 */
156 protected $rightDelimiter = '}';
157
158 /**
159 * left delimiter of template syntax used in regular expressions
160 * @var string
161 */
162 protected $ldq;
163
164 /**
165 * right delimiter of template syntax used in regular expressions
166 * @var string
167 */
168 protected $rdq;
169
170 /**
171 * list of static includes per template
172 * @var array<string>
173 */
174 protected $staticIncludes = array();
175
176 /**
177 * Creates a new TemplateScriptingCompiler object.
178 *
179 * @param wcf\system\templateTemplateEngine $template
180 */
181 public function __construct(TemplateEngine $template) {
182 $this->template = $template;
183
184 // quote left and right delimiter for use in regular expressions
185 $this->ldq = preg_quote($this->leftDelimiter, '~').'(?=\S)';
186 $this->rdq = '(?<=\S)'.preg_quote($this->rightDelimiter, '~');
187
188 // build regular expressions
189 $this->buildPattern();
190 }
191
192 /**
193 * Compiles the source of a template.
194 *
195 * @param string $identifier
196 * @param string $sourceContent
197 * @param array $metaData
198 * @param boolean $isolated
199 * @return string
200 */
201 public function compileString($identifier, $sourceContent, array $metaData = array(), $isolated = false) {
202 if ($isolated) {
203 $previousData = array(
204 'autoloadPlugins' => $this->autoloadPlugins,
205 'currentIdentifier' => $this->currentIdentifier,
206 'currentLineNo' => $this->currentLineNo,
207 'literalStack' => $this->literalStack,
208 'stringStack' => $this->stringStack,
209 'tagStack' => $this->tagStack
210 );
211 }
212 else {
213 $this->staticIncludes = array();
214 }
215
216 // reset vars
217 $this->autoloadPlugins = $this->tagStack = $this->stringStack = $this->literalStack = array();
218 $this->currentIdentifier = $identifier;
219 $this->currentLineNo = 1;
220
221 // apply prefilters
222 $sourceContent = $this->applyPrefilters($identifier, $sourceContent);
223
224 // replace all {literal} Tags with unique hash values
225 $sourceContent = $this->replaceLiterals($sourceContent);
226
227 // handle <?php tags
228 $sourceContent = $this->replacePHPTags($sourceContent);
229
230 // remove comments
231 $sourceContent = $this->removeComments($sourceContent);
232
233 // match all template tags
234 $matches = array();
235 preg_match_all("~".$this->ldq."(.*?)".$this->rdq."~s", $sourceContent, $matches);
236 $templateTags = $matches[1];
237
238 // Split content by template tags to obtain non-template content
239 $textBlocks = preg_split("~".$this->ldq.".*?".$this->rdq."~s", $sourceContent);
240
241 // compile the template tags into php-code
242 $compiledTags = array();
243 for ($i = 0, $j = count($templateTags); $i < $j; $i++) {
244 $this->currentLineNo += StringUtil::countSubstring($textBlocks[$i], "\n");
245 $compiledTags[] = $this->compileTag($templateTags[$i], $identifier, $metaData);
246 $this->currentLineNo += StringUtil::countSubstring($templateTags[$i], "\n");
247 }
248
249 // throw error messages for unclosed tags
250 if (count($this->tagStack) > 0) {
251 foreach ($this->tagStack as $tagStack) {
252 throw new SystemException($this->formatSyntaxError('unclosed tag {'.$tagStack[0].'}', $this->currentIdentifier, $tagStack[1]));
253 }
254 return false;
255 }
256
257 $compiledContent = '';
258 // Interleave the compiled contents and text blocks to get the final result.
259 for ($i = 0, $j = count($compiledTags); $i < $j; $i++) {
260 if ($compiledTags[$i] == '') {
261 // tag result empty, remove first newline from following text block
262 $textBlocks[$i + 1] = preg_replace('%^(\r\n|\r|\n)%', '', $textBlocks[$i + 1]);
263 }
264 $compiledContent .= $textBlocks[$i].$compiledTags[$i];
265 }
266 $compiledContent .= $textBlocks[$i];
267 $compiledContent = chop($compiledContent);
268
269 // @todo: INSERT POSTFILTERS HERE!?
270
271 // reinsert {literal} Tags
272 $compiledContent = $this->reinsertLiterals($compiledContent);
273
274 // include Plugins
275 $compiledAutoloadPlugins = '';
276 if (count($this->autoloadPlugins) > 0) {
277 $compiledAutoloadPlugins = "<?php\n";
278 foreach ($this->autoloadPlugins as $className) {
279 $compiledAutoloadPlugins .= "if (!isset(\$this->pluginObjects['$className'])) {\n";
280 $compiledAutoloadPlugins .= "\$this->pluginObjects['$className'] = new $className;\n";
281 $compiledAutoloadPlugins .= "}\n";
282 }
283 $compiledAutoloadPlugins .= "?>";
284 }
285
286 // restore data
287 if ($isolated) {
288 $this->autoloadPlugins = $previousData['autoloadPlugins'];
289 $this->currentIdentifier = $previousData['currentIdentifier'];
290 $this->currentLineNo = $previousData['currentLineNo'];
291 $this->literalStack = $previousData['literalStack'];
292 $this->stringStack = $previousData['stringStack'];
293 $this->tagStack = $previousData['tagStack'];
294 }
295
296 return array(
297 'meta' => array(
298 'include' => $this->staticIncludes
299 ),
300 'template' => $compiledAutoloadPlugins.$compiledContent
301 );
302 }
303
304 /**
305 * Compiles a template tag.
306 *
307 * @param string $tag
308 * @param string $identifier
309 * @param array $metaData
310 */
311 protected function compileTag($tag, $identifier, array &$metaData) {
312 if (preg_match('~^'.$this->outputPattern.'~s', $tag)) {
313 // variable output
314 return $this->compileOutputTag($tag);
315 }
316
317 $match = array();
318 // replace 'else if' with 'elseif'
319 $tag = preg_replace('~^else\s+if(?=\s)~i', 'elseif', $tag);
320
321 if (preg_match('~^(/?\w+)~', $tag, $match)) {
322 // build in function or plugin
323 $tagCommand = $match[1];
324 $tagArgs = StringUtil::substring($tag, StringUtil::length($tagCommand));
325
326 switch ($tagCommand) {
327 case 'if':
328 $this->pushTag('if');
329 return $this->compileIfTag($tagArgs);
330
331 case 'elseif':
332 list($openTag) = end($this->tagStack);
333 if ($openTag != 'if' && $openTag != 'elseif') {
334 throw new SystemException($this->formatSyntaxError('unxepected {elseif}', $this->currentIdentifier, $this->currentLineNo));
335 }
336 else if ($openTag == 'if') {
337 $this->pushTag('elseif');
338 }
339 return $this->compileIfTag($tagArgs, true);
340
341 case 'else':
342 list($openTag) = end($this->tagStack);
343 if ($openTag != 'if' && $openTag != 'elseif') {
344 throw new SystemException($this->formatSyntaxError('unexpected {else}', $this->currentIdentifier, $this->currentLineNo));
345 }
346 $this->pushTag('else');
347 return '<?php } else { ?>';
348
349 case '/if':
350 list($openTag) = end($this->tagStack);
351 if ($openTag != 'if' && $openTag != 'elseif' && $openTag != 'else') {
352 throw new SystemException($this->formatSyntaxError('unexpected {/if}', $this->currentIdentifier, $this->currentLineNo));
353 }
354 $this->popTag('if');
355 return '<?php } ?>';
356
357 case 'include':
358 return $this->compileIncludeTag($tagArgs, $identifier, $metaData);
359
360 case 'foreach':
361 $this->pushTag('foreach');
362 return $this->compileForeachTag($tagArgs);
363
364 case 'foreachelse':
365 list($openTag) = end($this->tagStack);
366 if ($openTag != 'foreach') {
367 throw new SystemException($this->formatSyntaxError('unexpected {foreachelse}', $this->currentIdentifier, $this->currentLineNo));
368 }
369 $this->pushTag('foreachelse');
370 return '<?php } } else { { ?>';
371
372 case '/foreach':
373 list($openTag) = end($this->tagStack);
374 if ($openTag != 'foreach' && $openTag != 'foreachelse') {
375 throw new SystemException($this->formatSyntaxError('unexpected {/foreach}', $this->currentIdentifier, $this->currentLineNo));
376 }
377 $this->popTag('foreach');
378 return "<?php } } ?>";
379
380 case 'section':
381 $this->pushTag('section');
382 return $this->compileSectionTag($tagArgs);
383
384 case 'sectionelse':
385 list($openTag) = end($this->tagStack);
386 if ($openTag != 'section') {
387 throw new SystemException($this->formatSyntaxError('unexpected {sectionelse}', $this->currentIdentifier, $this->currentLineNo));
388 }
389 $this->pushTag('sectionelse');
390 return '<?php } } else { { ?>';
391
392 case '/section':
393 list($openTag) = end($this->tagStack);
394 if ($openTag != 'section' && $openTag != 'sectionelse') {
395 throw new SystemException($this->formatSyntaxError('unexpected {/section}', $this->currentIdentifier, $this->currentLineNo));
396 }
397 $this->popTag('section');
398 return "<?php } } ?>";
399
400 case 'capture':
401 $this->pushTag('capture');
402 return $this->compileCaptureTag(true, $tagArgs);
403
404 case '/capture':
405 list($openTag) = end($this->tagStack);
406 if ($openTag != 'capture') {
407 throw new SystemException($this->formatSyntaxError('unexpected {/capture}', $this->currentIdentifier, $this->currentLineNo));
408 }
409 $this->popTag('capture');
410 return $this->compileCaptureTag(false);
411
412 case 'ldelim':
413 return $this->leftDelimiter;
414
415 case 'rdelim':
416 return $this->rightDelimiter;
417
418 default:
419 // 1) compiler functions first
420 if ($phpCode = $this->compileCompilerPlugin($tagCommand, $tagArgs)) {
421 return $phpCode;
422 }
423 // 2) block functions
424 if ($phpCode = $this->compileBlockPlugin($tagCommand, $tagArgs)) {
425 return $phpCode;
426 }
427 // 3) functions
428 if ($phpCode = $this->compileFunctionPlugin($tagCommand, $tagArgs)) {
429 return $phpCode;
430 }
431 }
432 }
433
434 throw new SystemException($this->formatSyntaxError('unknown tag {'.$tag.'}', $this->currentIdentifier, $this->currentLineNo));
435 }
436
437 /**
438 * Compiles a function plugin and returns the output of the plugin or false
439 * if the plugin doesn't exist.
440 *
441 * @param string $tagCommand
442 * @param string $tagArgs
443 * @return mixed
444 */
445 protected function compileFunctionPlugin($tagCommand, $tagArgs) {
446 $className = $this->template->getPluginClassName('function', $tagCommand);
447 if (!class_exists($className)) {
448 return false;
449 }
450 $this->autoloadPlugins[$className] = $className;
451
452 $tagArgs = $this->makeArgString($this->parseTagArgs($tagArgs, $tagCommand));
453
454 return "<?php echo \$this->pluginObjects['".$className."']->execute(array(".$tagArgs."), \$this); ?>";
455 }
456
457 /**
458 * Compiles a block plugin and returns the output of the plugin or false
459 * if the plugin doesn't exist.
460 *
461 * @param string $tagCommand
462 * @param string $tagArgs
463 * @return mixed
464 */
465 protected function compileBlockPlugin($tagCommand, $tagArgs) {
466 // check wheater this is the start ({block}) or the
467 // end tag ({/block})
468 if (substr($tagCommand, 0, 1) == '/') {
469 $tagCommand = substr($tagCommand, 1);
470 $startTag = false;
471 }
472 else {
473 $startTag = true;
474 }
475
476 $className = $this->template->getPluginClassName('block', $tagCommand);
477 if (!class_exists($className)) {
478 return false;
479 }
480 $this->autoloadPlugins[$className] = $className;
481
482 if ($startTag) {
483 $this->pushTag($tagCommand);
484
485 $tagArgs = $this->makeArgString($this->parseTagArgs($tagArgs, $tagCommand));
486
487 $phpCode = "<?php \$this->tagStack[] = array('".$tagCommand."', array(".$tagArgs."));\n";
488 $phpCode .= "\$this->pluginObjects['".$className."']->init(\$this->tagStack[count(\$this->tagStack) - 1][1], \$this);\n";
489 $phpCode .= "while (\$this->pluginObjects['".$className."']->next(\$this)) { ob_start(); ?>";
490 }
491 else {
492 list($openTag) = end($this->tagStack);
493 if ($openTag != $tagCommand) {
494 throw new SystemException($this->formatSyntaxError('unexpected {/'.$tagCommand.'}', $this->currentIdentifier, $this->currentLineNo));
495 }
496 $this->popTag($tagCommand);
497 $phpCode = "<?php echo \$this->pluginObjects['".$className."']->execute(\$this->tagStack[count(\$this->tagStack) - 1][1], ob_get_clean(), \$this); }\n";
498 $phpCode .= "array_pop(\$this->tagStack);\n";
499 }
500
501 return $phpCode;
502 }
503
504 /**
505 * Compiles a compiler function/block and returns the output of the plugin
506 * or false if the plugin doesn't exist.
507 *
508 * @param string $tagCommand
509 * @param string $tagArgs
510 * @return mixed
511 */
512 protected function compileCompilerPlugin($tagCommand, $tagArgs) {
513 // check wheater this is the start ({block}) or the
514 // end tag ({/block})
515 if (substr($tagCommand, 0, 1) == '/') {
516 $tagCommand = substr($tagCommand, 1);
517 $startTag = false;
518 }
519 else {
520 $startTag = true;
521 }
522
523 $className = $this->template->getPluginClassName('compiler', $tagCommand);
524 // if necessary load plugin from plugin-dir
525 if (!isset($this->compilerPlugins[$className])) {
526 if (!class_exists($className)) {
527 return false;
528 }
529
530 $this->compilerPlugins[$className] = new $className();
531
532 if (!($this->compilerPlugins[$className] instanceof ICompilerTemplatePlugin)) {
533 throw new SystemException($this->formatSyntaxError("Compiler plugin '".$tagCommand."' does not implement the interface 'ICompilerTemplatePlugin'", $this->currentIdentifier));
534 }
535 }
536
537 // execute plugin
538 if ($startTag) {
539 $tagArgs = $this->parseTagArgs($tagArgs, $tagCommand);
540 $phpCode = $this->compilerPlugins[$className]->executeStart($tagArgs, $this);
541 }
542 else {
543 $phpCode = $this->compilerPlugins[$className]->executeEnd($this);
544 }
545
546 return $phpCode;
547 }
548
549 /**
550 * Compiles a capture tag and returns the compiled PHP code.
551 *
552 * @param boolean $startTag
553 * @param string $captureTag
554 * @return string
555 */
556 protected function compileCaptureTag($startTag, $captureTag = null) {
557 if ($startTag) {
558 $append = false;
559 $args = $this->parseTagArgs($captureTag, 'capture');
560
561 if (!isset($args['name'])) {
562 $args['name'] = "'default'";
563 }
564
565 if (!isset($args['assign'])) {
566 if (isset($args['append'])) {
567 $args['assign'] = $args['append'];
568 $append = true;
569 }
570 else {
571 $args['assign'] = '';
572 }
573 }
574
575 $this->captureStack[] = array('name' => $args['name'], 'variable' => $args['assign'], 'append' => $append);
576 return '<?php ob_start(); ?>';
577 }
578 else {
579 $capture = array_pop($this->captureStack);
580 $phpCode = "<?php\n";
581 $phpCode .= "\$this->v['tpl']['capture'][".$capture['name']."] = ob_get_clean();\n";
582 if (!empty($capture['variable'])) $phpCode .= "\$this->".($capture['append'] ? 'append' : 'assign')."(".$capture['variable'].", \$this->v['tpl']['capture'][".$capture['name']."]);\n";
583 $phpCode .= "?>";
584 return $phpCode;
585 }
586 }
587
588 /**
589 * Compiles a section tag and returns the compiled PHP code.
590 *
591 * @param string $sectionTag
592 * @return string
593 */
594 protected function compileSectionTag($sectionTag) {
595 $args = $this->parseTagArgs($sectionTag, 'section');
596
597 // check arguments
598 if (!isset($args['loop'])) {
599 throw new SystemException($this->formatSyntaxError("missing 'loop' attribute in section tag", $this->currentIdentifier, $this->currentLineNo));
600 }
601 if (!isset($args['name'])) {
602 throw new SystemException($this->formatSyntaxError("missing 'name' attribute in section tag", $this->currentIdentifier, $this->currentLineNo));
603 }
604 if (!isset($args['show'])) {
605 $args['show'] = true;
606 }
607
608 $sectionProp = "\$this->v['tpl']['section'][".$args['name']."]";
609
610 $phpCode = "<?php\n";
611 $phpCode .= "if (".$args['loop'].") {\n";
612 $phpCode .= $sectionProp." = array();\n";
613 $phpCode .= $sectionProp."['loop'] = (is_array(".$args['loop'].") ? count(".$args['loop'].") : max(0, (int)".$args['loop']."));\n";
614 $phpCode .= $sectionProp."['show'] = ".$args['show'].";\n";
615 if (!isset($args['step'])) {
616 $phpCode .= $sectionProp."['step'] = 1;\n";
617 }
618 else {
619 $phpCode .= $sectionProp."['step'] = ".$args['step'].";\n";
620 }
621 if (!isset($args['max'])) {
622 $phpCode .= $sectionProp."['max'] = ".$sectionProp."['loop'];\n";
623 }
624 else {
625 $phpCode .= $sectionProp."['max'] = (".$args['max']." < 0 ? ".$sectionProp."['loop'] : ".$args['max'].");\n";
626 }
627 if (!isset($args['start'])) {
628 $phpCode .= $sectionProp."['start'] = (".$sectionProp."['step'] > 0 ? 0 : ".$sectionProp."['loop'] - 1);\n";
629 }
630 else {
631 $phpCode .= $sectionProp."['start'] = ".$args['start'].";\n";
632 $phpCode .= "if (".$sectionProp."['start'] < 0) {\n";
633 $phpCode .= $sectionProp."['start'] = max(".$sectionProp."['step'] > 0 ? 0 : -1, ".$sectionProp."['loop'] + ".$sectionProp."['start']);\n}\n";
634 $phpCode .= "else {\n";
635 $phpCode .= $sectionProp."['start'] = min(".$sectionProp."['start'], ".$sectionProp."['step'] > 0 ? ".$sectionProp."['loop'] : ".$sectionProp."['loop'] - 1);\n}\n";
636 }
637
638 if (!isset($args['start']) && !isset($args['step']) && !isset($args['max'])) {
639 $phpCode .= $sectionProp."['total'] = ".$sectionProp."['loop'];\n";
640 } else {
641 $phpCode .= $sectionProp."['total'] = min(ceil((".$sectionProp."['step'] > 0 ? ".$sectionProp."['loop'] - ".$sectionProp."['start'] : ".$sectionProp."['start'] + 1) / abs(".$sectionProp."['step'])), ".$sectionProp."['max']);\n";
642 }
643 $phpCode .= "if (".$sectionProp."['total'] == 0) ".$sectionProp."['show'] = false;\n";
644 $phpCode .= "} else {\n";
645 $phpCode .= "".$sectionProp."['total'] = 0;\n";
646 $phpCode .= "".$sectionProp."['show'] = false;}\n";
647
648 $phpCode .= "if (".$sectionProp."['show']) {\n";
649 $phpCode .= "for (".$sectionProp."['index'] = ".$sectionProp."['start'], ".$sectionProp."['rowNumber'] = 1;\n";
650 $phpCode .= $sectionProp."['rowNumber'] <= ".$sectionProp."['total'];\n";
651 $phpCode .= $sectionProp."['index'] += ".$sectionProp."['step'], ".$sectionProp."['rowNumber']++) {\n";
652 $phpCode .= "\$this->v[".$args['name']."] = ".$sectionProp."['index'];\n";
653 $phpCode .= $sectionProp."['previousIndex'] = ".$sectionProp."['index'] - ".$sectionProp."['step'];\n";
654 $phpCode .= $sectionProp."['nextIndex'] = ".$sectionProp."['index'] + ".$sectionProp."['step'];\n";
655 $phpCode .= $sectionProp."['first'] = (".$sectionProp."['rowNumber'] == 1);\n";
656 $phpCode .= $sectionProp."['last'] = (".$sectionProp."['rowNumber'] == ".$sectionProp."['total']);\n";
657 $phpCode .= "?>";
658
659 return $phpCode;
660 }
661
662 /**
663 * Compiles a foreach tag and returns the compiled PHP code.
664 *
665 * @param string $foreachTag
666 * @return string
667 */
668 protected function compileForeachTag($foreachTag) {
669 $args = $this->parseTagArgs($foreachTag, 'foreach');
670
671 // check arguments
672 if (!isset($args['from'])) {
673 throw new SystemException($this->formatSyntaxError("missing 'from' attribute in foreach tag", $this->currentIdentifier, $this->currentLineNo));
674 }
675 if (!isset($args['item'])) {
676 throw new SystemException($this->formatSyntaxError("missing 'item' attribute in foreach tag", $this->currentIdentifier, $this->currentLineNo));
677 }
678
679 $foreachProp = '';
680 if (isset($args['name'])) {
681 $foreachProp = "\$this->v['tpl']['foreach'][".$args['name']."]";
682 }
683
684 $phpCode = "<?php\n";
685 if (!empty($foreachProp)) {
686 $phpCode .= $foreachProp."['total'] = count(".$args['from'].");\n";
687 $phpCode .= $foreachProp."['show'] = (".$foreachProp."['total'] > 0 ? true : false);\n";
688 $phpCode .= $foreachProp."['iteration'] = 0;\n";
689 }
690 $phpCode .= "if (count(".$args['from'].") > 0) {\n";
691
692 if (isset($args['key'])) {
693 $phpCode .= "foreach (".$args['from']." as ".(StringUtil::substring($args['key'], 0, 1) != '$' ? "\$this->v[".$args['key']."]" : $args['key'])." => ".(StringUtil::substring($args['item'], 0, 1) != '$' ? "\$this->v[".$args['item']."]" : $args['item']).") {\n";
694 }
695 else {
696 $phpCode .= "foreach (".$args['from']." as ".(StringUtil::substring($args['item'], 0, 1) != '$' ? "\$this->v[".$args['item']."]" : $args['item']).") {\n";
697 }
698
699 if (!empty($foreachProp)) {
700 $phpCode .= $foreachProp."['first'] = (".$foreachProp."['iteration'] == 0 ? true : false);\n";
701 $phpCode .= $foreachProp."['last'] = ((".$foreachProp."['iteration'] == ".$foreachProp."['total'] - 1) ? true : false);\n";
702 $phpCode .= $foreachProp."['iteration']++;\n";
703 }
704
705 $phpCode .= "?>";
706 return $phpCode;
707 }
708
709 /**
710 * Compiles an include tag and returns the compiled PHP code.
711 *
712 * @param string $includeTag
713 * @param string $identifier
714 * @param array $metaData
715 * @return string
716 */
717 protected function compileIncludeTag($includeTag, $identifier, array &$metaData) {
718 $args = $this->parseTagArgs($includeTag, 'include');
719 $append = false;
720
721 // check arguments
722 if (!isset($args['file'])) {
723 throw new SystemException($this->formatSyntaxError("missing 'file' attribute in include tag", $this->currentIdentifier, $this->currentLineNo));
724 }
725
726 // get filename
727 $file = $args['file'];
728 unset($args['file']);
729
730 // special parameters
731 $assignVar = false;
732 if (isset($args['assign'])) {
733 $assignVar = $args['assign'];
734 unset($args['assign']);
735 }
736
737 if (isset($args['append'])) {
738 $assignVar = $args['append'];
739 $append = true;
740 unset($args['append']);
741 }
742
743 $sandbox = 0;
744 if (isset($args['sandbox'])) {
745 $sandbox = $args['sandbox'];
746 unset($args['sandbox']);
747 }
748
749 $once = false;
750 if (isset($args['once'])) {
751 $once = $args['once'];
752 unset($args['once']);
753 }
754
755 $application = '$this->v[\'__APPLICATION\']';
756 if (isset($args['application'])) {
757 $application = $args['application'];
758 unset($args['application']);
759 }
760
761 $templateName = substr($file, 1, -1);
762 // check for static includes
763 if ($sandbox === 'false' && $assignVar === false && $once === false) {
764 $phpCode = '';
765 if (!in_array($templateName, $this->staticIncludes)) {
766 $this->staticIncludes[] = $templateName;
767 }
768
769 // pass remaining tag args as variables
770 $variables = array();
771 if (!empty($args)) {
772 foreach ($args as $variable => $value) {
773 if (substr($value, 0, 1) == "'") {
774 // string values
775 $phpCode .= "\$this->v['".$variable."'] = ".$value.";\n";
776 }
777 else {
778 if (preg_match('~^\$this->v\[\'(.*)\'\]$~U', $value, $matches)) {
779 // value is a variable itself
780 $phpCode .= "\$this->v['".$variable."'] = ".$value.";\n";
781 }
782 else {
783 // value is boolean, an integer or anything else
784 $phpCode .= "\$this->v['".$variable."'] = ".$value.";\n";
785 }
786 }
787 }
788 }
789 if (!empty($phpCode)) $phpCode = "<?php\n".$phpCode."\n?>";
790
791 $tplPackageID = WCF::getTPL()->getPackageID($templateName, $metaData['application']);
792 $sourceFilename = WCF::getTPL()->getSourceFilename($templateName, $tplPackageID);
793 $metaDataFilename = WCF::getTPL()->getMetaDataFilename($templateName);
794
795 $data = $this->compileString($templateName, file_get_contents($sourceFilename), array(
796 'application' => $metaData['application'],
797 'data' => null,
798 'filename' => ''
799 ), true);
800
801 return $phpCode . $data['template'];
802 }
803
804 // make argument string
805 $argString = $this->makeArgString($args);
806
807 // build phpCode
808 $phpCode = "<?php\n";
809 if ($once) $phpCode .= "if (!isset(\$this->v['tpl']['includedTemplates'][".$file."])) {\n";
810 $hash = StringUtil::getRandomID();
811 $phpCode .= "\$outerTemplateName".$hash." = \$this->v['tpl']['template'];\n";
812
813 if ($assignVar !== false) {
814 $phpCode .= "ob_start();\n";
815 }
816
817 $phpCode .= '$this->includeTemplate('.$file.', '.$application.', array('.$argString.'), ('.$sandbox.' ? 1 : 0));'."\n";
818
819 if ($assignVar !== false) {
820 $phpCode .= '$this->'.($append ? 'append' : 'assign').'('.$assignVar.', ob_get_clean());'."\n";
821 }
822
823 $phpCode .= "\$this->v['tpl']['template'] = \$outerTemplateName".$hash.";\n";
824 $phpCode .= "\$this->v['tpl']['includedTemplates'][".$file."] = 1;\n";
825 if ($once) $phpCode .= "}\n";
826 $phpCode .= '?>';
827
828 return $phpCode;
829 }
830
831 /**
832 * Parses an argument list and returns the keys and values in an associative
833 * array.
834 *
835 * @param string $tagArgs
836 * @param string $tag
837 * @return array
838 */
839 public function parseTagArgs($tagArgs, $tag) {
840 // replace strings
841 $tagArgs = $this->replaceQuotes($tagArgs);
842
843 // validate tag arguments
844 if (!preg_match('~^(?:\s+\w+\s*=\s*[^=]*(?=\s|$))*$~s', $tagArgs)) {
845 throw new SystemException($this->formatSyntaxError('syntax error in tag {'.$tag.'}', $this->currentIdentifier, $this->currentLineNo));
846 }
847
848 // parse tag arguments
849 $matches = array();
850 // find all variables
851 preg_match_all('~\s+(\w+)\s*=\s*([^=]*)(?=\s|$)~s', $tagArgs, $matches);
852 $args = array();
853 for ($i = 0, $j = count($matches[1]); $i < $j; $i++) {
854 $name = $matches[1][$i];
855 $string = $this->compileVariableTag($matches[2][$i], false);
856
857 // reinserts strings
858 foreach (StringStack::getStack('singleQuote') as $hash => $value) {
859 if (StringUtil::indexOf($string, $hash) !== false) {
860 $string = StringUtil::replace($hash, $value, $string);
861 }
862 }
863 foreach (StringStack::getStack('doubleQuote') as $hash => $value) {
864 if (StringUtil::indexOf($string, $hash) !== false) {
865 $string = StringUtil::replace($hash, $value, $string);
866 }
867 }
868
869 $args[$name] = $string;
870 }
871
872 // clear stack
873 $this->reinsertQuotes('');
874
875 return $args;
876 }
877
878 /**
879 * Takes an array created by TemplateCompiler::parseTagArgs() and creates
880 * a string.
881 *
882 * @param array $args
883 * @return string $args
884 */
885 public static function makeArgString($args) {
886 $argString = '';
887 foreach ($args as $key => $val) {
888 if ($argString != '') {
889 $argString .= ', ';
890 }
891 $argString .= "'$key' => $val";
892 }
893 return $argString;
894 }
895
896 /**
897 * Returns a formatted syntax error message.
898 *
899 * @param string $errorMsg
900 * @param string $file
901 * @param integer $line
902 * @return string
903 */
904 public static function formatSyntaxError($errorMsg, $file = null, $line = null) {
905 $errorMsg = 'Template compilation failed: '.$errorMsg;
906 if ($file && $line) {
907 $errorMsg .= " in template '$file' on line $line";
908 }
909 elseif ($file && !$line) {
910 $errorMsg .= " in template '$file'";
911 }
912 return $errorMsg;
913 }
914
915 /**
916 * Compiles an {if} tag and returns the compiled PHP code.
917 *
918 * @param string $tagArgs
919 * @param boolean $elseif true, if this tag is an else tag
920 * @return string
921 */
922 protected function compileIfTag($tagArgs, $elseif = false) {
923 $tagArgs = $this->replaceQuotes($tagArgs);
924 $tagArgs = str_replace(' ', '', $tagArgs);
925
926 // split tags
927 preg_match_all('~('.$this->conditionOperatorPattern.')~', $tagArgs, $matches);
928 $operators = $matches[1];
929 $values = preg_split('~(?:'.$this->conditionOperatorPattern.')~', $tagArgs);
930 $leftParentheses = 0;
931 $result = '';
932
933 for ($i = 0, $j = count($values); $i < $j; $i++) {
934 $operator = (isset($operators[$i]) ? $operators[$i] : null);
935
936 if ($operator !== '!' && $values[$i] == '') {
937 throw new SystemException($this->formatSyntaxError('syntax error in tag {'.($elseif ? 'elseif' : 'if').'}', $this->currentIdentifier, $this->currentLineNo));
938 }
939
940 $leftParenthesis = StringUtil::countSubstring($values[$i], '(');
941 $rightParenthesis = StringUtil::countSubstring($values[$i], ')');
942 if ($leftParenthesis > $rightParenthesis) {
943 $leftParentheses += $leftParenthesis - $rightParenthesis;
944 $value = StringUtil::substring($values[$i], $leftParenthesis - $rightParenthesis);
945 $result .= str_repeat('(', $leftParenthesis - $rightParenthesis);
946
947 if (str_replace('(', '', StringUtil::substring($values[$i], 0, $leftParenthesis - $rightParenthesis)) != '') {
948 throw new SystemException($this->formatSyntaxError('syntax error in tag {'.($elseif ? 'elseif' : 'if').'}', $this->currentIdentifier, $this->currentLineNo));
949 }
950 }
951 else if ($leftParenthesis < $rightParenthesis) {
952 $leftParentheses += $leftParenthesis - $rightParenthesis;
953 $value = StringUtil::substring($values[$i], 0, $leftParenthesis - $rightParenthesis);
954
955 if ($leftParentheses < 0 || str_replace(')', '', StringUtil::substring($values[$i], $leftParenthesis - $rightParenthesis)) != '') {
956 throw new SystemException($this->formatSyntaxError('syntax error in tag {'.($elseif ? 'elseif' : 'if').'}', $this->currentIdentifier, $this->currentLineNo));
957 }
958 }
959 else $value = $values[$i];
960
961 try {
962 $result .= $this->compileVariableTag($value, false);
963 }
964 catch (SystemException $e) {
965 throw new SystemException($this->formatSyntaxError('syntax error in tag {'.($elseif ? 'elseif' : 'if').'}', $this->currentIdentifier, $this->currentLineNo), 0, nl2br($e));
966 }
967
968 if ($leftParenthesis < $rightParenthesis) {
969 $result .= str_repeat(')', $rightParenthesis - $leftParenthesis);
970 }
971
972 if ($operator) $result .= ' '.$operator.' ';
973 }
974
975 return '<?php '.($elseif ? '} elseif' : 'if').' ('.$result.') { ?>';
976 }
977
978 /**
979 * Adds a tag to the tag stack.
980 *
981 * @param string $tag
982 */
983 public function pushTag($tag) {
984 $this->tagStack[] = array($tag, $this->currentLineNo);
985 }
986
987 /**
988 * Deletes a tag from the tag stack.
989 *
990 * @param string $tag
991 * @return string $tag
992 */
993 public function popTag($tag) {
994 list($openTag, $lineNo) = array_pop($this->tagStack);
995 if ($tag == $openTag) {
996 return $openTag;
997 }
998 if ($tag == 'if' && ($openTag == 'else' || $openTag == 'elseif')) {
999 return $this->popTag($tag);
1000 }
1001 if ($tag == 'foreach' && $openTag == 'foreachelse') {
1002 return $this->popTag($tag);
1003 }
1004 if ($tag == 'section' && $openTag == 'sectionelse') {
1005 return $this->popTag($tag);
1006 }
1007 }
1008
1009 /**
1010 * Compiles an output tag and returns the compiled PHP code.
1011 *
1012 * @param string $tag
1013 * @return string
1014 */
1015 protected function compileOutputTag($tag) {
1016 $encodeHTML = false;
1017 $formatNumeric = false;
1018 if ($tag[0] == '@') {
1019 $tag = StringUtil::substring($tag, 1);
1020 }
1021 else if ($tag[0] == '#') {
1022 $tag = StringUtil::substring($tag, 1);
1023 $formatNumeric = true;
1024 }
1025 else {
1026 $encodeHTML = true;
1027 }
1028
1029 $parsedTag = $this->compileVariableTag($tag);
1030
1031 // the @ operator at the beginning of an output avoids
1032 // the default call of StringUtil::encodeHTML()
1033 if ($encodeHTML) {
1034 $parsedTag = 'wcf\util\StringUtil::encodeHTML('.$parsedTag.')';
1035 }
1036 // the # operator at the beginning of an output instructs
1037 // the complier to call the StringUtil::formatNumeric() method
1038 else if ($formatNumeric) {
1039 $parsedTag = 'wcf\util\StringUtil::formatNumeric('.$parsedTag.')';
1040 }
1041
1042 return '<?php echo '.$parsedTag.'; ?>';
1043 }
1044
1045 /**
1046 * Compiles a variable tag and returns the compiled PHP code.
1047 *
1048 * @param string $variable
1049 * @param string $type
1050 * @param boolean $allowConstants
1051 * @return string
1052 */
1053 protected function compileSimpleVariable($variable, $type = '', $allowConstants = true) {
1054 if ($type == '') $type = $this->getVariableType($variable);
1055
1056 if ($type == 'variable') return '$this->v[\''.substr($variable, 1).'\']';
1057 else if ($type == 'string') return $variable;
1058 else if ($allowConstants && ($variable == 'true' || $variable == 'false' || $variable == 'null' || preg_match('/^[A-Z0-9_]*$/', $variable))) return $variable;
1059 else return "'".$variable."'";
1060 }
1061
1062 /**
1063 * Compiles a modifier tag and returns the compiled PHP code.
1064 *
1065 * @param array $data
1066 * @return string
1067 */
1068 protected function compileModifier($data) {
1069 if (isset($data['className'])) {
1070 return "\$this->pluginObjects['".$data['className']."']->execute(array(".implode(',', $data['parameter'])."), \$this)";
1071 }
1072 else {
1073 return $data['name'].'('.implode(',', $data['parameter']).')';
1074 }
1075 }
1076
1077 /**
1078 * Returns type of the given variable.
1079 *
1080 * @param string $variable
1081 * @return string
1082 */
1083 protected function getVariableType($variable) {
1084 if (substr($variable, 0, 1) == '$') return 'variable';
1085 else if (substr($variable, 0, 2) == '@@') return 'string';
1086 else return 'constant';
1087 }
1088
1089 /**
1090 * Compiles a variable tag and returns the compiled PHP code.
1091 *
1092 * @param string $tag
1093 * @return string
1094 */
1095 public function compileVariableTag($tag, $replaceQuotes = true) {
1096 // replace all quotes with unique hash values
1097 $compiledTag = $tag;
1098 if ($replaceQuotes) $compiledTag = $this->replaceQuotes($compiledTag);
1099 // replace numbers and special constants
1100 $compiledTag = $this->replaceConstants($compiledTag);
1101
1102 // split tags
1103 preg_match_all('~('.$this->variableOperatorPattern.')~', $compiledTag, $matches);
1104 $operators = $matches[1];
1105 $values = preg_split('~(?:'.$this->variableOperatorPattern.')~', $compiledTag);
1106
1107 // parse tags
1108 $statusStack = array(0 => 'start');
1109 $result = '';
1110 $modifierData = null;
1111 for ($i = 0, $j = count($values); $i < $j; $i++) {
1112 // check value
1113 $status = end($statusStack);
1114 $operator = (isset($operators[$i]) ? $operators[$i] : null);
1115 $values[$i] = trim($values[$i]);
1116
1117 if ($values[$i] !== '') {
1118 $variableType = $this->getVariableType($values[$i]);
1119
1120 switch ($status) {
1121 case 'start':
1122 $result .= $this->compileSimpleVariable($values[$i], $variableType);
1123 $statusStack[0] = $status = $variableType;
1124 break;
1125
1126 case 'object access':
1127 if (/*strpos($values[$i], '$') !== false || */strpos($values[$i], '@@') !== false) {
1128 throw new SystemException($this->formatSyntaxError("unexpected '->".$values[$i]."' in tag '".$tag."'", $this->currentIdentifier, $this->currentLineNo));
1129 }
1130 if (strpos($values[$i], '$') !== false) $result .= '{'.$this->compileSimpleVariable($values[$i], $variableType).'}';
1131 else $result .= $values[$i];
1132 $statusStack[count($statusStack) - 1] = $status = 'object';
1133 break;
1134
1135 case 'object method start':
1136 $statusStack[count($statusStack) - 1] = 'object method';
1137 $result .= $this->compileSimpleVariable($values[$i], $variableType);
1138 $statusStack[] = $status = $variableType;
1139 break;
1140
1141 case 'object method parameter separator':
1142 array_pop($statusStack);
1143 $result .= $this->compileSimpleVariable($values[$i], $variableType);
1144 $statusStack[] = $status = $variableType;
1145 break;
1146
1147 case 'dot access':
1148 $result .= $this->compileSimpleVariable($values[$i], $variableType, false);
1149 $result .= ']';
1150 $statusStack[count($statusStack) - 1] = $status = 'variable';
1151 break;
1152
1153 case 'object method':
1154 case 'left parenthesis':
1155 $result .= $this->compileSimpleVariable($values[$i], $variableType);
1156 $statusStack[] = $status = $variableType;
1157 break;
1158
1159 case 'bracket open':
1160 $result .= $this->compileSimpleVariable($values[$i], $variableType, false);
1161 $statusStack[] = $status = $variableType;
1162 break;
1163
1164 case 'math':
1165 $result .= $this->compileSimpleVariable($values[$i], $variableType);
1166 $statusStack[count($statusStack) - 1] = $status = $variableType;
1167 break;
1168
1169 case 'modifier end':
1170 $result .= $this->compileSimpleVariable($values[$i], $variableType);
1171 $statusStack[] = $status = $variableType;
1172 break;
1173
1174 case 'modifier':
1175 if (strpos($values[$i], '$') !== false || strpos($values[$i], '@@') !== false) {
1176 throw new SystemException($this->formatSyntaxError("unknown modifier '".$values[$i]."'", $this->currentIdentifier, $this->currentLineNo));
1177 }
1178
1179 // handle modifier name
1180 $modifierData['name'] = $values[$i];
1181 $className = $this->template->getPluginClassName('modifier', $modifierData['name']);
1182 if (class_exists($className)) {
1183 $modifierData['className'] = $className;
1184 $this->autoloadPlugins[$modifierData['className']] = $modifierData['className'];
1185 }
1186 else if ((!function_exists($modifierData['name']) && !in_array($modifierData['name'], $this->unknownPHPFunctions)) || in_array($modifierData['name'], $this->disabledPHPFunctions)) {
1187 throw new SystemException($this->formatSyntaxError("unknown modifier '".$values[$i]."'", $this->currentIdentifier, $this->currentLineNo));
1188 }
1189
1190 $statusStack[count($statusStack) - 1] = $status = 'modifier end';
1191 break;
1192
1193 case 'object':
1194 case 'constant':
1195 case 'variable':
1196 case 'string':
1197 throw new SystemException($this->formatSyntaxError('unknown tag {'.$tag.'}', $this->currentIdentifier, $this->currentLineNo));
1198 break;
1199 }
1200 }
1201
1202 // check operator
1203 if ($operator !== null) {
1204 switch ($operator) {
1205 case '.':
1206 if ($status == 'variable' || $status == 'object') {
1207 if ($status == 'object') $statusStack[count($statusStack) - 1] = 'variable';
1208 $result .= '[';
1209 $statusStack[] = 'dot access';
1210 break;
1211 }
1212
1213 throw new SystemException($this->formatSyntaxError("unexpected '.' in tag '".$tag."'", $this->currentIdentifier, $this->currentLineNo));
1214 break;
1215
1216 // object access
1217 case '->':
1218 if ($status == 'variable' || $status == 'object') {
1219 $result .= $operator;
1220 $statusStack[count($statusStack) - 1] = 'object access';
1221 break;
1222 }
1223
1224 throw new SystemException($this->formatSyntaxError("unexpected '->' in tag '".$tag."'", $this->currentIdentifier, $this->currentLineNo));
1225 break;
1226
1227 // left parenthesis
1228 case '(':
1229 if ($status == 'object') {
1230 $statusStack[count($statusStack) - 1] = 'variable';
1231 $statusStack[] = 'object method start';
1232 $result .= $operator;
1233 break;
1234 }
1235 else if ($status == 'math' || $status == 'start' || $status == 'left parenthesis' || $status == 'bracket open' || $status == 'modifier end') {
1236 if ($status == 'start') $statusStack[count($statusStack) - 1] = 'constant';
1237 $statusStack[] = 'left parenthesis';
1238 $result .= $operator;
1239 break;
1240 }
1241
1242 throw new SystemException($this->formatSyntaxError("unexpected '(' in tag '".$tag."'", $this->currentIdentifier, $this->currentLineNo));
1243 break;
1244
1245 // right parenthesis
1246 case ')':
1247 while ($oldStatus = array_pop($statusStack)) {
1248 if ($oldStatus != 'variable' && $oldStatus != 'object' && $oldStatus != 'constant' && $oldStatus != 'string') {
1249 if ($oldStatus == 'object method start' || $oldStatus == 'object method' || $oldStatus == 'left parenthesis') {
1250 $result .= $operator;
1251 break 2;
1252 }
1253 else break;
1254 }
1255 }
1256
1257 throw new SystemException($this->formatSyntaxError("unexpected ')' in tag '".$tag."'", $this->currentIdentifier, $this->currentLineNo));
1258 break;
1259
1260 // bracket open
1261 case '[':
1262 if ($status == 'variable' || $status == 'object') {
1263 if ($status == 'object') $statusStack[count($statusStack) - 1] = 'variable';
1264 $statusStack[] = 'bracket open';
1265 $result .= $operator;
1266 break;
1267 }
1268
1269 throw new SystemException($this->formatSyntaxError("unexpected '[' in tag '".$tag."'", $this->currentIdentifier, $this->currentLineNo));
1270 break;
1271
1272 // bracket close
1273 case ']':
1274 while ($oldStatus = array_pop($statusStack)) {
1275 if ($oldStatus != 'variable' && $oldStatus != 'object' && $oldStatus != 'constant' && $oldStatus != 'string') {
1276 if ($oldStatus == 'bracket open') {
1277 $result .= $operator;
1278 break 2;
1279 }
1280 else break;
1281 }
1282 }
1283
1284 throw new SystemException($this->formatSyntaxError("unexpected ']' in tag '".$tag."'", $this->currentIdentifier, $this->currentLineNo));
1285 break;
1286
1287 // modifier
1288 case '|':
1289 // handle previous modifier
1290 if ($modifierData !== null) {
1291 if ($result !== '') $modifierData['parameter'][] = $result;
1292 $result = $this->compileModifier($modifierData);
1293 }
1294
1295 // clear status stack
1296 while ($oldStatus = array_pop($statusStack)) {
1297 if ($oldStatus != 'variable' && $oldStatus != 'object' && $oldStatus != 'constant' && $oldStatus != 'string' && $oldStatus != 'modifier end') {
1298 throw new SystemException($this->formatSyntaxError("unexpected '|' in tag '".$tag."'", $this->currentIdentifier, $this->currentLineNo));
1299 }
1300 }
1301
1302 $statusStack = array(0 => 'modifier');
1303 $modifierData = array('name' => '', 'parameter' => array(0 => $result));
1304 $result = '';
1305 break;
1306
1307 // modifier parameter
1308 case ':':
1309 while ($oldStatus = array_pop($statusStack)) {
1310 if ($oldStatus != 'variable' && $oldStatus != 'object' && $oldStatus != 'constant' && $oldStatus != 'string') {
1311 if ($oldStatus == 'modifier end') {
1312 $statusStack[] = 'modifier end';
1313 if ($result !== '') $modifierData['parameter'][] = $result;
1314 $result = '';
1315 break 2;
1316 }
1317 else break;
1318 }
1319 }
1320
1321 throw new SystemException($this->formatSyntaxError("unexpected ':' in tag '".$tag."'", $this->currentIdentifier, $this->currentLineNo));
1322 break;
1323
1324 case ',':
1325 while ($oldStatus = array_pop($statusStack)) {
1326 if ($oldStatus != 'variable' && $oldStatus != 'object' && $oldStatus != 'constant' && $oldStatus != 'string') {
1327 if ($oldStatus == 'object method') {
1328 $result .= $operator;
1329 $statusStack[] = 'object method';
1330 $statusStack[] = 'object method parameter separator';
1331 break 2;
1332 }
1333 else break;
1334 }
1335 }
1336
1337 throw new SystemException($this->formatSyntaxError("unexpected ',' in tag '".$tag."'", $this->currentIdentifier, $this->currentLineNo));
1338 break;
1339
1340 // math operators
1341 case '+':
1342 case '-':
1343 case '*':
1344 case '/':
1345 case '%':
1346 case '^':
1347 if ($status == 'variable' || $status == 'object' || $status == 'constant' || $status == 'string' || $status == 'modifier end') {
1348 $result .= $operator;
1349 $statusStack[count($statusStack) - 1] = 'math';
1350 break;
1351 }
1352
1353 throw new SystemException($this->formatSyntaxError("unexpected '".$operator."' in tag '".$tag."'", $this->currentIdentifier, $this->currentLineNo));
1354 break;
1355 }
1356 }
1357 }
1358
1359 // handle open modifier
1360 if ($modifierData !== null) {
1361 if ($result !== '') $modifierData['parameter'][] = $result;
1362 $result = $this->compileModifier($modifierData);
1363 }
1364
1365 // reinserts strings
1366 $result = $this->reinsertQuotes($result);
1367 $result = $this->reinsertConstants($result);
1368
1369 return $result;
1370 }
1371
1372 /**
1373 * Generates the regexp pattern.
1374 */
1375 protected function buildPattern() {
1376 $this->variableOperatorPattern = '\-\>|\.|\(|\)|\[|\]|\||\:|\+|\-|\*|\/|\%|\^|\,';
1377 $this->conditionOperatorPattern = '===|!==|==|!=|<=|<|>=|(?<!-)>|\|\||&&|!|=';
1378 $this->escapedPattern = '(?<!\\\\)';
1379 $this->validVarnamePattern = '(?:[a-zA-Z_][a-zA-Z_0-9]*)';
1380 $this->constantPattern = '(?:[A-Z_][A-Z_0-9]*)';
1381 $this->doubleQuotePattern = '"(?:[^"\\\\]+|\\\\.)*"';
1382 $this->singleQuotePattern = '\'(?:[^\'\\\\]+|\\\\.)*\'';
1383 $this->quotePattern = '(?:' . $this->doubleQuotePattern . '|' . $this->singleQuotePattern . ')';
1384 $this->numericPattern = '(?i)(?:(?:\-?\d+(?:\.\d+)?)|true|false|null)';
1385 $this->simpleVarPattern = '(?:\$('.$this->validVarnamePattern.'))';
1386 $this->outputPattern = '(?:(?:@|#)?(?:'.$this->constantPattern.'|'.$this->quotePattern.'|'.$this->numericPattern.'|'.$this->simpleVarPattern.'|\())';
1387 }
1388
1389 /**
1390 * Returns the instance of the template engine class.
1391 *
1392 * @return wcf\system\templateTemplateEngine
1393 */
1394 public function getTemplate() {
1395 return $this->template;
1396 }
1397
1398 /**
1399 * Returns the left delimiter for template tags.
1400 *
1401 * @return string
1402 */
1403 public function getLeftDelimiter() {
1404 return $this->leftDelimiter;
1405 }
1406
1407 /**
1408 * Returns the right delimiter for template tags.
1409 *
1410 * @return string
1411 */
1412 public function getRightDelimiter() {
1413 return $this->rightDelimiter;
1414 }
1415
1416 /**
1417 * Returns the name of the current template.
1418 *
1419 * @return string
1420 */
1421 public function getCurrentIdentifier() {
1422 return $this->currentIdentifier;
1423 }
1424
1425 /**
1426 * Returns the current line number.
1427 *
1428 * @return integer
1429 */
1430 public function getCurrentLineNo() {
1431 return $this->currentLineNo;
1432 }
1433
1434 /**
1435 * Applies the prefilters to the given string.
1436 *
1437 * @param string $templateName
1438 * @param string $string
1439 * @return string
1440 */
1441 public function applyPrefilters($templateName, $string) {
1442 foreach ($this->template->getPrefilters() as $prefilter) {
1443 if (!is_object($prefilter)) {
1444 $className = $this->template->getPluginClassName('prefilter', $prefilter);
1445 if (!class_exists($className)) {
1446 throw new SystemException($this->formatSyntaxError('unable to find prefilter class '.$className, $this->currentIdentifier));
1447 }
1448 $prefilter = new $className();
1449 }
1450
1451 if ($prefilter instanceof IPrefilterTemplatePlugin) {
1452 $string = $prefilter->execute($templateName, $string, $this);
1453 }
1454 else {
1455 throw new SystemException($this->formatSyntaxError("Prefilter '".(is_object($prefilter) ? get_class($prefilter) : $prefilter)."' does not implement the interface 'IPrefilterTemplatePlugin'", $this->currentIdentifier));
1456 }
1457 }
1458
1459 return $string;
1460 }
1461
1462 /**
1463 * Replaces all {literal} Tags with unique hash values.
1464 *
1465 * @param string $string
1466 * @return string
1467 */
1468 public function replaceLiterals($string) {
1469 return preg_replace_callback("~".$this->ldq."literal".$this->rdq."(.*?)".$this->ldq."/literal".$this->rdq."~s", array($this, 'replaceLiteralsCallback'), $string);
1470 }
1471
1472 /**
1473 * Reinserts the literal tags.
1474 *
1475 * @param string $string
1476 * @return string
1477 */
1478 public function reinsertLiterals($string) {
1479 return StringStack::reinsertStrings($string, 'literal');
1480 }
1481
1482 /**
1483 * Callback function used in replaceLiterals()
1484 */
1485 private function replaceLiteralsCallback($matches) {
1486 return StringStack::pushToStringStack($matches[1], 'literal');
1487 }
1488
1489 /**
1490 * Removes template comments
1491 *
1492 * @param string $string
1493 * @return string
1494 */
1495 public function removeComments($string) {
1496 return preg_replace("~".$this->ldq."\*.*?\*".$this->rdq."~s", '', $string);
1497 }
1498
1499 /**
1500 * Replaces all quotes with unique hash values.
1501 *
1502 * @param string $string
1503 * @return string
1504 */
1505 public function replaceQuotes($string) {
1506 $string = preg_replace_callback('~\'([^\'\\\\]+|\\\\.)*\'~', array($this, 'replaceSingleQuotesCallback'), $string);
1507 $string = preg_replace_callback('~"([^"\\\\]+|\\\\.)*"~', array($this, 'replaceDoubleQuotesCallback'), $string);
1508
1509 return $string;
1510 }
1511
1512 /**
1513 * Callback function used in replaceQuotes()
1514 */
1515 private function replaceSingleQuotesCallback($matches) {
1516 return StringStack::pushToStringStack($matches[0], 'singleQuote');
1517 }
1518
1519 /**
1520 * Callback function used in replaceQuotes()
1521 */
1522 private function replaceDoubleQuotesCallback($matches) {
1523 // parse unescaped simple vars in double quotes
1524 // replace $foo with {$this->v['foo']}
1525 $matches[0] = preg_replace('~'.$this->escapedPattern.$this->simpleVarPattern.'~', '{$this->v[\'\\1\']}', $matches[0]);
1526 return StringStack::pushToStringStack($matches[0], 'doubleQuote');
1527 }
1528
1529 /**
1530 * Reinserts the quotes.
1531 *
1532 * @param string $string
1533 * @return string
1534 */
1535 public function reinsertQuotes($string) {
1536 $string = StringStack::reinsertStrings($string, 'singleQuote');
1537 $string = StringStack::reinsertStrings($string, 'doubleQuote');
1538
1539 return $string;
1540 }
1541
1542 /**
1543 * Replaces all constants with unique hash values.
1544 *
1545 * @param string $string
1546 * @return string
1547 */
1548 public function replaceConstants($string) {
1549 return preg_replace_callback('~(?<=^|'.$this->variableOperatorPattern.')(?i)((?:\-?\d+(?:\.\d+)?)|true|false|null)(?=$|'.$this->variableOperatorPattern.')~', array($this, 'replaceConstantsCallback'), $string);
1550 }
1551
1552 /**
1553 * Callback function used in replaceConstants()
1554 */
1555 private function replaceConstantsCallback($matches) {
1556 return StringStack::pushToStringStack($matches[1], 'constants');
1557 }
1558
1559 /**
1560 * Reinserts the constants.
1561 *
1562 * @param string $string
1563 * @return string
1564 */
1565 public function reinsertConstants($string) {
1566 return StringStack::reinsertStrings($string, 'constants');
1567 }
1568
1569 /**
1570 * Replaces all php tags.
1571 *
1572 * @param string $string
1573 * @return string
1574 */
1575 public function replacePHPTags($string) {
1576 if (StringUtil::indexOf($string, '<?') !== false) {
1577 $string = StringUtil::replace('<?php', '@@PHP_START_TAG@@', $string);
1578 $string = StringUtil::replace('<?', '@@PHP_SHORT_START_TAG@@', $string);
1579 $string = StringUtil::replace('?>', '@@PHP_END_TAG@@', $string);
1580 $string = StringUtil::replace('@@PHP_END_TAG@@', "<?php echo '?>'; ?>\n", $string);
1581 $string = StringUtil::replace('@@PHP_SHORT_START_TAG@@', "<?php echo '<?'; ?>\n", $string);
1582 $string = StringUtil::replace('@@PHP_START_TAG@@', "<?php echo '<?php'; ?>\n", $string);
1583 }
1584
1585 return $string;
1586 }
1587 }