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