Namespaces

  • Latte
    • Loaders
    • Macros
    • Runtime
  • Nette
    • Application
      • Responses
      • Routers
      • UI
    • Bridges
      • ApplicationDI
      • ApplicationLatte
      • ApplicationTracy
      • CacheDI
      • CacheLatte
      • DatabaseDI
      • DatabaseTracy
      • DITracy
      • FormsDI
      • FormsLatte
      • Framework
      • HttpDI
      • HttpTracy
      • MailDI
      • ReflectionDI
      • SecurityDI
      • SecurityTracy
    • Caching
      • Storages
    • ComponentModel
    • Database
      • Conventions
      • Drivers
      • Table
    • DI
      • Config
        • Adapters
      • Extensions
    • Forms
      • Controls
      • Rendering
    • Http
    • Iterators
    • Loaders
    • Localization
    • Mail
    • Neon
    • PhpGenerator
    • Reflection
    • Security
    • Tokenizer
    • Utils
  • Tracy
    • Bridges
      • Nette
  • none

Classes

  • Compiler
  • Engine
  • HtmlNode
  • MacroNode
  • MacroTokens
  • Parser
  • PhpWriter
  • Token

Interfaces

  • ILoader
  • IMacro

Traits

  • Strict

Exceptions

  • CompileException
  • RegexpException
  • RuntimeException
  • Overview
  • Namespace
  • Class
  • Tree
  • Deprecated
  1: <?php
  2: 
  3: /**
  4:  * This file is part of the Latte (https://latte.nette.org)
  5:  * Copyright (c) 2008 David Grudl (https://davidgrudl.com)
  6:  */
  7: 
  8: namespace Latte;
  9: 
 10: 
 11: /**
 12:  * PHP code generator helpers.
 13:  */
 14: class PhpWriter
 15: {
 16:     use Strict;
 17: 
 18:     /** @var MacroTokens */
 19:     private $tokens;
 20: 
 21:     /** @var string */
 22:     private $modifiers;
 23: 
 24:     /** @var array|null */
 25:     private $context;
 26: 
 27: 
 28:     public static function using(MacroNode $node)
 29:     {
 30:         $me = new static($node->tokenizer, null, $node->context);
 31:         $me->modifiers = &$node->modifiers;
 32:         return $me;
 33:     }
 34: 
 35: 
 36:     public function __construct(MacroTokens $tokens, $modifiers = null, array $context = null)
 37:     {
 38:         $this->tokens = $tokens;
 39:         $this->modifiers = $modifiers;
 40:         $this->context = $context;
 41:     }
 42: 
 43: 
 44:     /**
 45:      * Expands %node.word, %node.array, %node.args, %escape(), %modify(), %var, %raw, %word in code.
 46:      * @param  string
 47:      * @return string
 48:      */
 49:     public function write($mask)
 50:     {
 51:         $mask = preg_replace('#%(node|\d+)\.#', '%$1_', $mask);
 52:         $mask = preg_replace_callback('#%escape(\(([^()]*+|(?1))+\))#', function ($m) {
 53:             return $this->escapePass(new MacroTokens(substr($m[1], 1, -1)))->joinAll();
 54:         }, $mask);
 55:         $mask = preg_replace_callback('#%modify(Content)?(\(([^()]*+|(?2))+\))#', function ($m) {
 56:             return $this->formatModifiers(substr($m[2], 1, -1), (bool) $m[1]);
 57:         }, $mask);
 58: 
 59:         $args = func_get_args();
 60:         $pos = $this->tokens->position;
 61:         $word = strpos($mask, '%node_word') === false ? null : $this->tokens->fetchWord();
 62: 
 63:         $code = preg_replace_callback('#([,+]\s*)?%(node_|\d+_|)(word|var|raw|array|args)(\?)?(\s*\+\s*)?()#',
 64:         function ($m) use ($word, &$args) {
 65:             list(, $l, $source, $format, $cond, $r) = $m;
 66: 
 67:             switch ($source) {
 68:                 case 'node_':
 69:                     $arg = $word; break;
 70:                 case '':
 71:                     $arg = next($args); break;
 72:                 default:
 73:                     $arg = $args[(int) $source + 1]; break;
 74:             }
 75: 
 76:             switch ($format) {
 77:                 case 'word':
 78:                     $code = $this->formatWord($arg); break;
 79:                 case 'args':
 80:                     $code = $this->formatArgs(); break;
 81:                 case 'array':
 82:                     $code = $this->formatArray();
 83:                     $code = $cond && $code === '[]' ? '' : $code; break;
 84:                 case 'var':
 85:                     $code = var_export($arg, true); break;
 86:                 case 'raw':
 87:                     $code = (string) $arg; break;
 88:             }
 89: 
 90:             if ($cond && $code === '') {
 91:                 return $r ? $l : $r;
 92:             } else {
 93:                 return $l . $code . $r;
 94:             }
 95:         }, $mask);
 96: 
 97:         $this->tokens->position = $pos;
 98:         return $code;
 99:     }
100: 
101: 
102:     /**
103:      * Formats modifiers calling.
104:      * @param  string
105:      * @return string
106:      */
107:     public function formatModifiers($var, $isContent = false)
108:     {
109:         $tokens = new MacroTokens(ltrim($this->modifiers, '|'));
110:         $tokens = $this->preprocess($tokens);
111:         $tokens = $this->modifierPass($tokens, $var, $isContent);
112:         $tokens = $this->quotingPass($tokens);
113:         return $tokens->joinAll();
114:     }
115: 
116: 
117:     /**
118:      * Formats macro arguments to PHP code. (It advances tokenizer to the end as a side effect.)
119:      * @return string
120:      */
121:     public function formatArgs(MacroTokens $tokens = null)
122:     {
123:         $tokens = $this->preprocess($tokens);
124:         $tokens = $this->quotingPass($tokens);
125:         return $tokens->joinAll();
126:     }
127: 
128: 
129:     /**
130:      * Formats macro arguments to PHP array. (It advances tokenizer to the end as a side effect.)
131:      * @return string
132:      */
133:     public function formatArray(MacroTokens $tokens = null)
134:     {
135:         $tokens = $this->preprocess($tokens);
136:         $tokens = $this->expandCastPass($tokens);
137:         $tokens = $this->quotingPass($tokens);
138:         return $tokens->joinAll();
139:     }
140: 
141: 
142:     /**
143:      * Formats parameter to PHP string.
144:      * @param  string
145:      * @return string
146:      */
147:     public function formatWord($s)
148:     {
149:         return (is_numeric($s) || preg_match('#^\$|[\'"]|^(true|TRUE)\z|^(false|FALSE)\z|^(null|NULL)\z|^[\w\\\\]{3,}::[A-Z0-9_]{2,}\z#', $s))
150:             ? $this->formatArgs(new MacroTokens($s))
151:             : '"' . $s . '"';
152:     }
153: 
154: 
155:     /**
156:      * Preprocessor for tokens. (It advances tokenizer to the end as a side effect.)
157:      * @return MacroTokens
158:      */
159:     public function preprocess(MacroTokens $tokens = null)
160:     {
161:         $tokens = $tokens === null ? $this->tokens : $tokens;
162:         $this->validateTokens($tokens);
163:         $tokens = $this->removeCommentsPass($tokens);
164:         $tokens = $this->shortTernaryPass($tokens);
165:         $tokens = $this->inlineModifierPass($tokens);
166:         $tokens = $this->inOperatorPass($tokens);
167:         return $tokens;
168:     }
169: 
170: 
171:     /**
172:      * @throws CompileException
173:      * @return void
174:      */
175:     public function validateTokens(MacroTokens $tokens)
176:     {
177:         $deprecatedVars = array_flip(['$template', '$_b', '$_l', '$_g', '$_args', '$_fi', '$_control', '$_presenter', '$_form', '$_input', '$_label', '$_snippetMode']);
178:         $brackets = [];
179:         $pos = $tokens->position;
180:         while ($tokens->nextToken()) {
181:             if ($tokens->isCurrent('?>')) {
182:                 throw new CompileException('Forbidden ?> inside macro');
183: 
184:             } elseif ($tokens->isCurrent($tokens::T_VARIABLE) && isset($deprecatedVars[$tokens->currentValue()])) {
185:                 trigger_error("Variable {$tokens->currentValue()} is deprecated.", E_USER_DEPRECATED);
186: 
187:             } elseif ($tokens->isCurrent($tokens::T_SYMBOL)
188:                 && !$tokens->isPrev('::') && !$tokens->isNext('::') && !$tokens->isPrev('->') && !$tokens->isNext('\\')
189:                 && preg_match('#^[A-Z0-9]{3,}$#', $val = $tokens->currentValue())
190:             ) {
191:                 trigger_error("Replace literal $val with constant('$val')", E_USER_DEPRECATED);
192: 
193:             } elseif ($tokens->isCurrent('(', '[', '{')) {
194:                 static $counterpart = ['(' => ')', '[' => ']', '{' => '}'];
195:                 $brackets[] = $counterpart[$tokens->currentValue()];
196: 
197:             } elseif ($tokens->isCurrent(')', ']', '}') && $tokens->currentValue() !== array_pop($brackets)) {
198:                 throw new CompileException('Unexpected ' . $tokens->currentValue());
199: 
200:             } elseif ($tokens->isCurrent('function', 'class', 'interface', 'trait') && $tokens->isNext($tokens::T_SYMBOL, '&')
201:                 || $tokens->isCurrent('return', 'yield') && !$brackets
202:             ) {
203:                 throw new CompileException("Forbidden keyword '{$tokens->currentValue()}' inside macro.");
204:             }
205:         }
206:         if ($brackets) {
207:             throw new CompileException('Missing ' . array_pop($brackets));
208:         }
209:         $tokens->position = $pos;
210:     }
211: 
212: 
213:     /**
214:      * Removes PHP comments.
215:      * @return MacroTokens
216:      */
217:     public function removeCommentsPass(MacroTokens $tokens)
218:     {
219:         $res = new MacroTokens;
220:         while ($tokens->nextToken()) {
221:             if (!$tokens->isCurrent($tokens::T_COMMENT)) {
222:                 $res->append($tokens->currentToken());
223:             }
224:         }
225:         return $res;
226:     }
227: 
228: 
229:     /**
230:      * Simplified ternary expressions without third part.
231:      * @return MacroTokens
232:      */
233:     public function shortTernaryPass(MacroTokens $tokens)
234:     {
235:         $res = new MacroTokens;
236:         $inTernary = [];
237:         while ($tokens->nextToken()) {
238:             if ($tokens->isCurrent('?')) {
239:                 $inTernary[] = $tokens->depth;
240: 
241:             } elseif ($tokens->isCurrent(':')) {
242:                 array_pop($inTernary);
243: 
244:             } elseif ($tokens->isCurrent(',', ')', ']', '|') && end($inTernary) === $tokens->depth + $tokens->isCurrent(')', ']')) {
245:                 $res->append(' : NULL');
246:                 array_pop($inTernary);
247:             }
248:             $res->append($tokens->currentToken());
249:         }
250: 
251:         if ($inTernary) {
252:             $res->append(' : NULL');
253:         }
254:         return $res;
255:     }
256: 
257: 
258:     /**
259:      * Pseudocast (expand).
260:      * @return MacroTokens
261:      */
262:     public function expandCastPass(MacroTokens $tokens)
263:     {
264:         $res = new MacroTokens('[');
265:         $expand = null;
266:         while ($tokens->nextToken()) {
267:             if ($tokens->isCurrent('(expand)') && $tokens->depth === 0) {
268:                 $expand = true;
269:                 $res->append('],');
270:             } elseif ($expand && $tokens->isCurrent(',') && !$tokens->depth) {
271:                 $expand = false;
272:                 $res->append(', [');
273:             } else {
274:                 $res->append($tokens->currentToken());
275:             }
276:         }
277: 
278:         if ($expand === null) {
279:             $res->append(']');
280:         } else {
281:             $res->prepend('array_merge(')->append($expand ? ', [])' : '])');
282:         }
283:         return $res;
284:     }
285: 
286: 
287:     /**
288:      * Quotes symbols to strings.
289:      * @return MacroTokens
290:      */
291:     public function quotingPass(MacroTokens $tokens)
292:     {
293:         $res = new MacroTokens;
294:         while ($tokens->nextToken()) {
295:             $res->append($tokens->isCurrent($tokens::T_SYMBOL)
296:                 && (!$tokens->isPrev() || $tokens->isPrev(',', '(', '[', '=>', ':', '?', '.', '<', '>', '<=', '>=', '===', '!==', '==', '!=', '<>', '&&', '||', '=', 'and', 'or', 'xor', '??'))
297:                 && (!$tokens->isNext() || $tokens->isNext(',', ';', ')', ']', '=>', ':', '?', '.', '<', '>', '<=', '>=', '===', '!==', '==', '!=', '<>', '&&', '||', 'and', 'or', 'xor', '??'))
298:                 && !preg_match('#^[A-Z_][A-Z0-9_]{2,}$#', $tokens->currentValue())
299:                 ? "'" . $tokens->currentValue() . "'"
300:                 : $tokens->currentToken()
301:             );
302:         }
303:         return $res;
304:     }
305: 
306: 
307:     /**
308:      * Syntax $entry in [item1, item2].
309:      * @return MacroTokens
310:      */
311:     public function inOperatorPass(MacroTokens $tokens)
312:     {
313:         while ($tokens->nextToken()) {
314:             if ($tokens->isCurrent($tokens::T_VARIABLE)) {
315:                 $start = $tokens->position;
316:                 $depth = $tokens->depth;
317:                 $expr = $arr = [];
318: 
319:                 $expr[] = $tokens->currentToken();
320:                 while ($tokens->isNext($tokens::T_VARIABLE, $tokens::T_SYMBOL, $tokens::T_NUMBER, $tokens::T_STRING, '[', ']', '(', ')', '->')
321:                     && !$tokens->isNext('in')) {
322:                     $expr[] = $tokens->nextToken();
323:                 }
324: 
325:                 if ($depth === $tokens->depth && $tokens->nextValue('in') && ($arr[] = $tokens->nextToken('['))) {
326:                     while ($tokens->isNext()) {
327:                         $arr[] = $tokens->nextToken();
328:                         if ($tokens->isCurrent(']') && $tokens->depth === $depth) {
329:                             $new = array_merge($tokens->parse('in_array('), $expr, $tokens->parse(', '), $arr, $tokens->parse(', TRUE)'));
330:                             array_splice($tokens->tokens, $start, $tokens->position - $start + 1, $new);
331:                             $tokens->position = $start + count($new) - 1;
332:                             continue 2;
333:                         }
334:                     }
335:                 }
336:                 $tokens->position = $start;
337:             }
338:         }
339:         return $tokens->reset();
340:     }
341: 
342: 
343:     /**
344:      * Process inline filters ($var|filter)
345:      * @return MacroTokens
346:      */
347:     public function inlineModifierPass(MacroTokens $tokens)
348:     {
349:         $result = new MacroTokens;
350:         while ($tokens->nextToken()) {
351:             if ($tokens->isCurrent('(', '[')) {
352:                 $result->tokens = array_merge($result->tokens, $this->inlineModifierInner($tokens));
353:             } else {
354:                 $result->append($tokens->currentToken());
355:             }
356:         }
357:         return $result;
358:     }
359: 
360: 
361:     private function inlineModifierInner(MacroTokens $tokens)
362:     {
363:         $isFunctionOrArray = $tokens->isPrev($tokens::T_VARIABLE, $tokens::T_SYMBOL) || $tokens->isCurrent('[');
364:         $result = new MacroTokens;
365:         $args = new MacroTokens;
366:         $modifiers = new MacroTokens;
367:         $current = $args;
368:         $anyModifier = false;
369:         $result->append($tokens->currentToken());
370: 
371:         while ($tokens->nextToken()) {
372:             if ($tokens->isCurrent('(', '[')) {
373:                 $current->tokens = array_merge($current->tokens, $this->inlineModifierInner($tokens));
374: 
375:             } elseif ($current !== $modifiers && $tokens->isCurrent('|')) {
376:                 $anyModifier = true;
377:                 $current = $modifiers;
378: 
379:             } elseif ($tokens->isCurrent(')', ']') || ($isFunctionOrArray && $tokens->isCurrent(','))) {
380:                 $partTokens = count($modifiers->tokens)
381:                     ? $this->modifierPass($modifiers, $args->tokens)->tokens
382:                     : $args->tokens;
383:                 $result->tokens = array_merge($result->tokens, $partTokens);
384:                 if ($tokens->isCurrent(',')) {
385:                     $result->append($tokens->currentToken());
386:                     $args = new MacroTokens;
387:                     $modifiers = new MacroTokens;
388:                     $current = $args;
389:                     continue;
390:                 } elseif ($isFunctionOrArray || !$anyModifier) {
391:                     $result->append($tokens->currentToken());
392:                 } else {
393:                     array_shift($result->tokens);
394:                 }
395:                 return $result->tokens;
396: 
397:             } else {
398:                 $current->append($tokens->currentToken());
399:             }
400:         }
401:         throw new CompileException('Unbalanced brackets.');
402:     }
403: 
404: 
405:     /**
406:      * Formats modifiers calling.
407:      * @param  MacroTokens
408:      * @param  string|array
409:      * @throws CompileException
410:      * @return MacroTokens
411:      */
412:     public function modifierPass(MacroTokens $tokens, $var, $isContent = false)
413:     {
414:         $inside = false;
415:         $res = new MacroTokens($var);
416:         while ($tokens->nextToken()) {
417:             if ($tokens->isCurrent($tokens::T_WHITESPACE)) {
418:                 $res->append(' ');
419: 
420:             } elseif ($inside) {
421:                 if ($tokens->isCurrent(':', ',')) {
422:                     $res->append(', ');
423:                     $tokens->nextAll($tokens::T_WHITESPACE);
424: 
425:                 } elseif ($tokens->isCurrent('|')) {
426:                     $res->append(')');
427:                     $inside = false;
428: 
429:                 } else {
430:                     $res->append($tokens->currentToken());
431:                 }
432:             } else {
433:                 if ($tokens->isCurrent($tokens::T_SYMBOL)) {
434:                     if ($tokens->isCurrent('escape')) {
435:                         if ($isContent) {
436:                             $res->prepend('LR\Filters::convertTo($_fi, ' . var_export(implode($this->context), true) . ', ')
437:                                 ->append(')');
438:                         } else {
439:                             $res = $this->escapePass($res);
440:                         }
441:                         $tokens->nextToken('|');
442:                     } elseif (!strcasecmp($tokens->currentValue(), 'checkurl')) {
443:                         $res->prepend('LR\Filters::safeUrl(');
444:                         $inside = true;
445:                     } else {
446:                         $name = strtolower($tokens->currentValue());
447:                         $res->prepend($isContent
448:                             ? '$this->filters->filterContent(' . var_export($name, true) . ', $_fi, '
449:                             : 'call_user_func($this->filters->' . $name . ', '
450:                         );
451:                         $inside = true;
452:                     }
453:                 } else {
454:                     throw new CompileException("Modifier name must be alphanumeric string, '{$tokens->currentValue()}' given.");
455:                 }
456:             }
457:         }
458:         if ($inside) {
459:             $res->append(')');
460:         }
461:         return $res;
462:     }
463: 
464: 
465:     /**
466:      * Escapes expression in tokens.
467:      * @return MacroTokens
468:      */
469:     public function escapePass(MacroTokens $tokens)
470:     {
471:         $tokens = clone $tokens;
472:         list($contentType, $context) = $this->context;
473:         switch ($contentType) {
474:             case Compiler::CONTENT_XHTML:
475:             case Compiler::CONTENT_HTML:
476:                 switch ($context) {
477:                     case Compiler::CONTEXT_HTML_TEXT:
478:                         return $tokens->prepend('LR\Filters::escapeHtmlText(')->append(')');
479:                     case Compiler::CONTEXT_HTML_TAG:
480:                     case Compiler::CONTEXT_HTML_ATTRIBUTE_UNQUOTED_URL:
481:                         return $tokens->prepend('LR\Filters::escapeHtmlAttrUnquoted(')->append(')');
482:                     case Compiler::CONTEXT_HTML_ATTRIBUTE:
483:                     case Compiler::CONTEXT_HTML_ATTRIBUTE_URL:
484:                         return $tokens->prepend('LR\Filters::escapeHtmlAttr(')->append(')');
485:                     case Compiler::CONTEXT_HTML_ATTRIBUTE_JS:
486:                         return $tokens->prepend('LR\Filters::escapeHtmlAttr(LR\Filters::escapeJs(')->append('))');
487:                     case Compiler::CONTEXT_HTML_ATTRIBUTE_CSS:
488:                         return $tokens->prepend('LR\Filters::escapeHtmlAttr(LR\Filters::escapeCss(')->append('))');
489:                     case Compiler::CONTEXT_HTML_COMMENT:
490:                         return $tokens->prepend('LR\Filters::escapeHtmlComment(')->append(')');
491:                     case Compiler::CONTEXT_HTML_BOGUS_COMMENT:
492:                         return $tokens->prepend('LR\Filters::escapeHtml(')->append(')');
493:                     case Compiler::CONTEXT_HTML_JS:
494:                     case Compiler::CONTEXT_HTML_CSS:
495:                         return $tokens->prepend('LR\Filters::escape' . ucfirst($context) . '(')->append(')');
496:                     default:
497:                         throw new CompileException("Unknown context $contentType, $context.");
498:                 }
499:                 // break omitted
500:             case Compiler::CONTENT_XML:
501:                 switch ($context) {
502:                     case Compiler::CONTEXT_XML_TEXT:
503:                     case Compiler::CONTEXT_XML_ATTRIBUTE:
504:                     case Compiler::CONTEXT_XML_BOGUS_COMMENT:
505:                         return $tokens->prepend('LR\Filters::escapeXml(')->append(')');
506:                     case Compiler::CONTEXT_XML_COMMENT:
507:                         return $tokens->prepend('LR\Filters::escapeHtmlComment(')->append(')');
508:                     case Compiler::CONTEXT_XML_TAG:
509:                         return $tokens->prepend('LR\Filters::escapeXmlAttrUnquoted(')->append(')');
510:                     default:
511:                         throw new CompileException("Unknown context $contentType, $context.");
512:                 }
513:                 // break omitted
514:             case Compiler::CONTENT_JS:
515:             case Compiler::CONTENT_CSS:
516:             case Compiler::CONTENT_ICAL:
517:                 return $tokens->prepend('LR\Filters::escape' . ucfirst($contentType) . '(')->append(')');
518:             case Compiler::CONTENT_TEXT:
519:                 return $tokens;
520:             case null:
521:                 return $tokens->prepend('call_user_func($this->filters->escape, ')->append(')');
522:             default:
523:                 throw new CompileException("Unknown context $contentType.");
524:         }
525:     }
526: }
527: 
Nette 2.4-20170829 API API documentation generated by ApiGen 2.8.0