contributing.md 0000666 00000002504 13603744006 0007605 0 ustar 00 How to contribute & use the issue tracker ========================================= Nette welcomes your contributions. There are several ways to help out: * Create an issue on GitHub, if you have found a bug * Write test cases for open bug issues * Write fixes for open bug/feature issues, preferably with test cases included * Contribute to the [documentation](https://nette.org/en/writing) Issues ------ Please **do not use the issue tracker to ask questions**. We will be happy to help you on [Nette forum](https://forum.nette.org) or chat with us on [Gitter](https://gitter.im/nette/nette). A good bug report shouldn't leave others needing to chase you up for more information. Please try to be as detailed as possible in your report. **Feature requests** are welcome. But take a moment to find out whether your idea fits with the scope and aims of the project. It's up to *you* to make a strong case to convince the project's developers of the merits of this feature. Contributing ------------ If you'd like to contribute, please take a moment to read [the contributing guide](https://nette.org/en/contributing). The best way to propose a feature is to discuss your ideas on [Nette forum](https://forum.nette.org) before implementing them. Please do not fix whitespace, format code, or make a purely cosmetic patch. Thanks! :heart: src/Latte/IMacro.php 0000666 00000001317 13603744006 0010300 0 ustar 00 1, 'hr' => 1, 'br' => 1, 'input' => 1, 'meta' => 1, 'area' => 1, 'embed' => 1, 'keygen' => 1, 'source' => 1, 'base' => 1, 'col' => 1, 'link' => 1, 'param' => 1, 'basefont' => 1, 'frame' => 1, 'isindex' => 1, 'wbr' => 1, 'command' => 1, 'track' => 1, ]; /** * Checks callback. */ public static function checkCallback($callable): callable { if (!is_callable($callable, false, $text)) { throw new \InvalidArgumentException("Callback '$text' is not callable."); } return $callable; } /** * Finds the best suggestion. */ public static function getSuggestion(array $items, $value): ?string { $best = null; $min = (strlen($value) / 4 + 1) * 10 + .1; foreach (array_unique($items, SORT_REGULAR) as $item) { $item = is_object($item) ? $item->getName() : $item; if (($len = levenshtein($item, $value, 10, 11, 10)) > 0 && $len < $min) { $min = $len; $best = $item; } } return $best; } public static function removeFilter(string &$modifier, string $filter): bool { $modifier = preg_replace('#\|(' . $filter . ')\s?(?=\||$)#Di', '', $modifier, -1, $found); return (bool) $found; } /** * Starts the $haystack string with the prefix $needle? */ public static function startsWith(string $haystack, string $needle): bool { return strncmp($haystack, $needle, strlen($needle)) === 0; } } src/Latte/exceptions.php 0000666 00000003110 13603744006 0011300 0 ustar 00 sourceCode = $code; $this->sourceLine = $line; $this->sourceName = $name; if (@is_file($name)) { // @ - may trigger error $this->message = rtrim($this->message, '.') . ' in ' . str_replace(dirname(dirname($name)), '...', $name) . ($line ? ":$line" : ''); } return $this; } } /** * The exception that indicates error of the last Regexp execution. */ class RegexpException extends \Exception { public const MESSAGES = [ PREG_INTERNAL_ERROR => 'Internal error', PREG_BACKTRACK_LIMIT_ERROR => 'Backtrack limit was exhausted', PREG_RECURSION_LIMIT_ERROR => 'Recursion limit was exhausted', PREG_BAD_UTF8_ERROR => 'Malformed UTF-8 data', PREG_BAD_UTF8_OFFSET_ERROR => 'Offset didn\'t correspond to the begin of a valid UTF-8 code point', 6 => 'Failed due to limited JIT stack space', // PREG_JIT_STACKLIMIT_ERROR ]; public function __construct($message, $code = null) { parent::__construct($message ?: (self::MESSAGES[$code] ?? 'Unknown error'), $code); } } /** * The exception that indicates error during rendering template. */ class RuntimeException extends \Exception { } src/Latte/Loaders/StringLoader.php 0000666 00000002261 13603744006 0013113 0 ustar 00 content] */ private $templates; public function __construct(array $templates = null) { $this->templates = $templates; } /** * Returns template source code. */ public function getContent($name): string { if ($this->templates === null) { return $name; } elseif (isset($this->templates[$name])) { return $this->templates[$name]; } else { throw new \RuntimeException("Missing template '$name'."); } } public function isExpired($name, $time): bool { return false; } /** * Returns referred template name. */ public function getReferredName($name, $referringName): string { if ($this->templates === null) { throw new \LogicException("Missing template '$name'."); } return $name; } /** * Returns unique identifier for caching. */ public function getUniqueId($name): string { return $this->getContent($name); } } src/Latte/Loaders/FileLoader.php 0000666 00000004057 13603744006 0012531 0 ustar 00 baseDir = $baseDir ? $this->normalizePath("$baseDir/") : null; } /** * Returns template source code. */ public function getContent($fileName): string { $file = $this->baseDir . $fileName; if ($this->baseDir && !Latte\Helpers::startsWith($this->normalizePath($file), $this->baseDir)) { throw new \RuntimeException("Template '$file' is not within the allowed path '$this->baseDir'."); } elseif (!is_file($file)) { throw new \RuntimeException("Missing template file '$file'."); } elseif ($this->isExpired($fileName, time())) { if (@touch($file) === false) { trigger_error("File's modification time is in the future. Cannot update it: " . error_get_last()['message'], E_USER_WARNING); } } return file_get_contents($file); } public function isExpired($file, $time): bool { $mtime = @filemtime($this->baseDir . $file); // @ - stat may fail return !$mtime || $mtime > $time; } /** * Returns referred template name. */ public function getReferredName($file, $referringFile): string { if ($this->baseDir || !preg_match('#/|\\\\|[a-z][a-z0-9+.-]*:#iA', $file)) { $file = $this->normalizePath($referringFile . '/../' . $file); } return $file; } /** * Returns unique identifier for caching. */ public function getUniqueId($file): string { return $this->baseDir . strtr($file, '/', DIRECTORY_SEPARATOR); } private static function normalizePath(string $path): string { $res = []; foreach (explode('/', strtr($path, '\\', '/')) as $part) { if ($part === '..' && $res && end($res) !== '..') { array_pop($res); } elseif ($part !== '.') { $res[] = $part; } } return implode(DIRECTORY_SEPARATOR, $res); } } src/Latte/ILoader.php 0000666 00000001073 13603744006 0010444 0 ustar 00 addMacro('include', [$me, 'macroInclude']); $me->addMacro('includeblock', [$me, 'macroIncludeBlock']); // deprecated $me->addMacro('import', [$me, 'macroImport'], null, null, self::ALLOWED_IN_HEAD); $me->addMacro('extends', [$me, 'macroExtends'], null, null, self::ALLOWED_IN_HEAD); $me->addMacro('layout', [$me, 'macroExtends'], null, null, self::ALLOWED_IN_HEAD); $me->addMacro('snippet', [$me, 'macroBlock'], [$me, 'macroBlockEnd']); $me->addMacro('block', [$me, 'macroBlock'], [$me, 'macroBlockEnd'], null, self::AUTO_CLOSE); $me->addMacro('define', [$me, 'macroBlock'], [$me, 'macroBlockEnd']); $me->addMacro('snippetArea', [$me, 'macroBlock'], [$me, 'macroBlockEnd']); $me->addMacro('ifset', [$me, 'macroIfset'], '}'); $me->addMacro('elseifset', [$me, 'macroIfset']); } /** * Initializes before template parsing. * @return void */ public function initialize() { $this->namedBlocks = []; $this->blockTypes = []; $this->extends = null; $this->imports = []; } /** * Finishes template parsing. */ public function finalize() { $compiler = $this->getCompiler(); $functions = []; foreach ($this->namedBlocks as $name => $code) { $compiler->addMethod( $functions[$name] = $this->generateMethodName($name), '?>' . $compiler->expandTokens($code) . 'namedBlocks) { $compiler->addProperty('blocks', $functions); $compiler->addProperty('blockTypes', $this->blockTypes); } return [ ($this->extends === null ? '' : '$this->parentName = ' . $this->extends . ';') . implode($this->imports), ]; } /********************* macros ****************d*g**/ /** * {include block} */ public function macroInclude(MacroNode $node, PhpWriter $writer) { $node->replaced = false; $destination = $node->tokenizer->fetchWord(); // destination [,] [params] if (!preg_match('~#|[\w-]+$~DA', $destination)) { return false; } $destination = ltrim($destination, '#'); $parent = $destination === 'parent'; if ($destination === 'parent' || $destination === 'this') { for ( $item = $node->parentNode; $item && $item->name !== 'block' && !isset($item->data->name); $item = $item->parentNode ); if (!$item) { throw new CompileException("Cannot include $destination block outside of any block."); } $destination = $item->data->name; } $noEscape = Helpers::removeFilter($node->modifiers, 'noescape'); if (!$noEscape && Helpers::removeFilter($node->modifiers, 'escape')) { trigger_error('Macro ' . $node->getNotation() . ' provides auto-escaping, remove |escape.'); } if ($node->modifiers && !$noEscape) { $node->modifiers .= '|escape'; } return $writer->write( '$this->renderBlock' . ($parent ? 'Parent' : '') . '(' . (strpos($destination, '$') === false ? var_export($destination, true) : $destination) . ', %node.array? + ' . (isset($this->namedBlocks[$destination]) || $parent ? 'get_defined_vars()' : '$this->params') . ($node->modifiers ? ', function ($s, $type) { $_fi = new LR\FilterInfo($type); return %modifyContent($s); }' : ($noEscape || $parent ? '' : ', ' . var_export(implode($node->context), true))) . ');' ); } /** * {includeblock "file"} * @deprecated */ public function macroIncludeBlock(MacroNode $node, PhpWriter $writer) { //trigger_error('Macro {includeblock} is deprecated, use similar macro {import}.', E_USER_DEPRECATED); $node->replaced = false; if ($node->modifiers) { throw new CompileException('Modifiers are not allowed in ' . $node->getNotation()); } return $writer->write( 'ob_start(function () {}); $this->createTemplate(%node.word, %node.array? + get_defined_vars(), "includeblock")->renderToContentType(%var); echo rtrim(ob_get_clean());', implode($node->context) ); } /** * {import "file"} */ public function macroImport(MacroNode $node, PhpWriter $writer) { if ($node->modifiers) { throw new CompileException('Modifiers are not allowed in ' . $node->getNotation()); } $destination = $node->tokenizer->fetchWord(); $this->checkExtraArgs($node); $code = $writer->write('$this->createTemplate(%word, $this->params, "import")->render();', $destination); if ($this->getCompiler()->isInHead()) { $this->imports[] = $code; } else { return $code; } } /** * {extends none | $var | "file"} */ public function macroExtends(MacroNode $node, PhpWriter $writer) { $notation = $node->getNotation(); if ($node->modifiers) { throw new CompileException("Modifiers are not allowed in $notation"); } elseif (!$node->args) { throw new CompileException("Missing destination in $notation"); } elseif ($node->parentNode) { throw new CompileException("$notation must be placed outside any macro."); } elseif ($this->extends !== null) { throw new CompileException("Multiple $notation declarations are not allowed."); } elseif ($node->args === 'none') { $this->extends = 'FALSE'; } else { $this->extends = $writer->write('%node.word%node.args'); } if (!$this->getCompiler()->isInHead()) { trigger_error("$notation must be placed in template head.", E_USER_WARNING); } } /** * {block [name]} * {snippet [name]} * {snippetArea [name]} * {define name} */ public function macroBlock(MacroNode $node, PhpWriter $writer) { $name = $node->tokenizer->fetchWord(); if ($node->name === 'block' && $name === null) { // anonymous block return $node->modifiers === '' ? '' : 'ob_start(function () {})'; } elseif ($node->name === 'define' && $node->modifiers) { throw new CompileException('Modifiers are not allowed in ' . $node->getNotation()); } $node->data->name = $name = ltrim((string) $name, '#'); if ($name == null) { if ($node->name === 'define') { throw new CompileException('Missing block name.'); } } elseif (strpos($name, '$') !== false) { // dynamic block/snippet if ($node->name === 'snippet') { if ($node->prefix && isset($node->htmlNode->attrs[$this->snippetAttribute])) { throw new CompileException("Cannot combine HTML attribute $this->snippetAttribute with n:snippet."); } for ( $parent = $node->parentNode; $parent && !($parent->name === 'snippet' || $parent->name === 'snippetArea'); $parent = $parent->parentNode ); if (!$parent) { throw new CompileException('Dynamic snippets are allowed only inside static snippet/snippetArea.'); } $parent->data->dynamic = true; $node->data->leave = true; $node->closingCode = 'global->snippetDriver->leave(); ?>'; $enterCode = '$this->global->snippetDriver->enter(' . $writer->formatWord($name) . ', "' . SnippetDriver::TYPE_DYNAMIC . '");'; if ($node->prefix) { $node->attrCode = $writer->write("snippetAttribute=\"' . htmlSpecialChars(\$this->global->snippetDriver->getHtmlId({$writer->formatWord($name)})) . '\"' ?>"); return $writer->write($enterCode); } $node->closingCode .= "\n"; $this->checkExtraArgs($node); return $writer->write("?>\n
' . $this->context[1] . ')| ## end of HTML attribute (?P' . $this->delimiters[0] . ') ~xsi'); if (!empty($matches['quote'])) { // (attribute end) '" $this->addToken(Token::HTML_ATTRIBUTE_END, $matches[0]); $this->setContext(self::CONTEXT_HTML_TAG); } else { return $this->processMacro($matches); } } /** * Handles CONTEXT_HTML_COMMENT. */ private function contextHtmlComment() { $matches = $this->match('~ (?P ' . $this->context[1] . '>)| ## end of HTML comment (?P ' . $this->delimiters[0] . ') ~xsi'); if (!empty($matches['htmlcomment'])) { // --> $this->addToken(Token::HTML_TAG_END, $matches[0]); $this->setContext(self::CONTEXT_HTML_TEXT); } else { return $this->processMacro($matches); } } /** * Handles CONTEXT_NONE. */ private function contextNone() { $matches = $this->match('~ (?P ' . $this->delimiters[0] . ') ~xsi'); return $this->processMacro($matches); } /** * Handles CONTEXT_MACRO. */ private function contextMacro(): void { $matches = $this->match('~ (?P \*.*?\*' . $this->delimiters[1] . '\n{0,2})| (?P (?> ' . self::RE_STRING . '| \{(?>' . self::RE_STRING . '|[^\'"{}])*+\}| [^\'"{}]+ )++) ' . $this->delimiters[1] . ' (?P [ \t]*(?=\n))? ~xsiA'); if (!empty($matches['macro'])) { $token = $this->addToken(Token::MACRO_TAG, $this->context[1][1] . $matches[0]); [$token->name, $token->value, $token->modifiers, $token->empty, $token->closing] = $this->parseMacroTag($matches['macro']); $this->context = $this->context[1][0]; } elseif (!empty($matches['comment'])) { $this->addToken(Token::COMMENT, $this->context[1][1] . $matches[0]); $this->context = $this->context[1][0]; } else { throw new CompileException('Malformed macro'); } } private function processMacro(array $matches) { if (!empty($matches['macro'])) { // {macro} or {* *} $this->setContext(self::CONTEXT_MACRO, [$this->context, $matches['macro']]); } else { return false; } } /** * Matches next token. */ private function match(string $re): array { if (!preg_match($re, $this->input, $matches, PREG_OFFSET_CAPTURE, $this->offset)) { if (preg_last_error()) { throw new RegexpException(null, preg_last_error()); } return []; } $value = substr($this->input, $this->offset, $matches[0][1] - $this->offset); if ($value !== '') { $this->addToken(Token::TEXT, $value); } $this->offset = $matches[0][1] + strlen($matches[0][0]); foreach ($matches as $k => $v) { $matches[$k] = $v[0]; } return $matches; } /** * @param string $type Parser::CONTENT_HTML, CONTENT_XHTML, CONTENT_XML or CONTENT_TEXT * @return static */ public function setContentType(string $type) { if (in_array($type, [self::CONTENT_HTML, self::CONTENT_XHTML, self::CONTENT_XML], true)) { $this->setContext(self::CONTEXT_HTML_TEXT); $this->xmlMode = $type === self::CONTENT_XML; } else { $this->setContext(self::CONTEXT_NONE); } return $this; } /** * @return static */ public function setContext(string $context, $quote = null) { $this->context = [$context, $quote]; return $this; } /** * Changes macro tag delimiters. * @return static */ public function setSyntax(string $type) { $type = $type ?: $this->defaultSyntax; if (isset($this->syntaxes[$type])) { $this->setDelimiters($this->syntaxes[$type][0], $this->syntaxes[$type][1]); } else { throw new \InvalidArgumentException("Unknown syntax '$type'"); } return $this; } /** * Changes macro tag delimiters (as regular expression). * @return static */ public function setDelimiters(string $left, string $right) { $this->delimiters = [$left, $right]; return $this; } /** * Parses macro tag to name, arguments a modifiers parts. * @param string $tag {name arguments | modifiers} * @internal */ public function parseMacroTag(string $tag): ?array { if (!preg_match('~^ (?P /?) ( (?P \?|[a-z]\w*+(?:[.:]\w+)*+(?!::|\(|\\\\))| ## ?, name, /name, but not function( or class:: or namespace\ (?P [=\~#%^&_]?) ## expression, =expression, ... )(?P (?:' . self::RE_STRING . '|[^\'"])*?) (?P (?(?:' . self::RE_STRING . '|(?:\((?P>modArgs)\))|[^\'"/()]|/(?=.))*+))? (?P /?$) ()$~Disx', $tag, $match)) { if (preg_last_error()) { throw new RegexpException(null, preg_last_error()); } return null; } if ($match['name'] === '') { $match['name'] = $match['shortname'] ?: ($match['closing'] ? '' : '='); } return [$match['name'], trim($match['args']), $match['modifiers'], (bool) $match['empty'], (bool) $match['closing']]; } private function addToken(string $type, string $text): Token { $this->output[] = $token = new Token; $token->type = $type; $token->text = $text; $token->line = $this->getLine() - substr_count(ltrim($text), "\n"); return $token; } public function getLine(): int { return $this->offset ? substr_count(substr($this->input, 0, $this->offset - 1), "\n") + 1 : 1; } /** * Process low-level macros. */ protected function filter(Token $token): void { if ($token->type === Token::MACRO_TAG && $token->name === '/syntax') { $this->setSyntax($this->defaultSyntax); $token->type = Token::COMMENT; } elseif ($token->type === Token::MACRO_TAG && $token->name === 'syntax') { $this->setSyntax($token->value); $token->type = Token::COMMENT; } elseif ($token->type === Token::HTML_ATTRIBUTE_BEGIN && $token->name === 'n:syntax') { $this->setSyntax($token->value); $this->syntaxEndTag = $this->lastHtmlTag; $this->syntaxEndLevel = 1; $token->type = Token::COMMENT; } elseif ($token->type === Token::HTML_TAG_BEGIN && $this->lastHtmlTag === $this->syntaxEndTag) { $this->syntaxEndLevel++; } elseif ($token->type === Token::HTML_TAG_END && $this->lastHtmlTag === ('/' . $this->syntaxEndTag) && --$this->syntaxEndLevel === 0) { $this->setSyntax($this->defaultSyntax); } elseif ($token->type === Token::MACRO_TAG && $token->name === 'contentType') { if (strpos($token->value, 'html') !== false) { $this->setContentType(self::CONTENT_HTML); } elseif (strpos($token->value, 'xml') !== false) { $this->setContentType(self::CONTENT_XML); } else { $this->setContentType(self::CONTENT_TEXT); } } } } src/Latte/Compiler/PhpWriter.php 0000666 00000043122 13603744006 0012624 0 ustar 00 tokenizer, null, $node->context); $me->modifiers = &$node->modifiers; $me->functions = $compiler ? $compiler->getFunctions() : []; return $me; } public function __construct(MacroTokens $tokens, string $modifiers = null, array $context = null) { $this->tokens = $tokens; $this->modifiers = $modifiers; $this->context = $context; } /** * Expands %node.word, %node.array, %node.args, %escape(), %modify(), %var, %raw, %word in code. */ public function write(string $mask, ...$args): string { $mask = preg_replace('#%(node|\d+)\.#', '%$1_', $mask); $mask = preg_replace_callback('#%escape(\(([^()]*+|(?1))+\))#', function ($m) { return $this->escapePass(new MacroTokens(substr($m[1], 1, -1)))->joinAll(); }, $mask); $mask = preg_replace_callback('#%modify(Content)?(\(([^()]*+|(?2))+\))#', function ($m) { return $this->formatModifiers(substr($m[2], 1, -1), (bool) $m[1]); }, $mask); $pos = $this->tokens->position; $word = null; if (strpos($mask, '%node_word') !== false) { $word = $this->tokens->fetchWord(); if ($word === null) { throw new CompileException('Invalid content of macro'); } } $code = preg_replace_callback('#([,+]\s*)?%(node_|\d+_|)(word|var|raw|array|args)(\?)?(\s*\+\s*)?()#', function ($m) use ($word, &$args) { [, $l, $source, $format, $cond, $r] = $m; switch ($source) { case 'node_': $arg = $word; break; case '': $arg = current($args); next($args); break; default: $arg = $args[(int) $source]; break; } switch ($format) { case 'word': $code = $this->formatWord($arg); break; case 'args': $code = $this->formatArgs(); break; case 'array': $code = $this->formatArray(); $code = $cond && $code === '[]' ? '' : $code; break; case 'var': $code = var_export($arg, true); break; case 'raw': $code = (string) $arg; break; } if ($cond && $code === '') { return $r ? $l : $r; } else { return $l . $code . $r; } }, $mask); $this->tokens->position = $pos; return $code; } /** * Formats modifiers calling. */ public function formatModifiers(string $var, bool $isContent = false): string { $tokens = new MacroTokens(ltrim($this->modifiers, '|')); $tokens = $this->preprocess($tokens); $tokens = $this->modifierPass($tokens, $var, $isContent); $tokens = $this->quotingPass($tokens); return $tokens->joinAll(); } /** * Formats macro arguments to PHP code. (It advances tokenizer to the end as a side effect.) */ public function formatArgs(MacroTokens $tokens = null): string { $tokens = $this->preprocess($tokens); $tokens = $this->quotingPass($tokens); return $tokens->joinAll(); } /** * Formats macro arguments to PHP array. (It advances tokenizer to the end as a side effect.) */ public function formatArray(MacroTokens $tokens = null): string { $tokens = $this->preprocess($tokens); $tokens = $this->expandCastPass($tokens); $tokens = $this->quotingPass($tokens); return $tokens->joinAll(); } /** * Formats parameter to PHP string. */ public function formatWord(string $s): string { return (is_numeric($s) || preg_match('#^\$|[\'"]|^(true|TRUE)$|^(false|FALSE)$|^(null|NULL)$|^[\w\\\\]{3,}::[A-Z0-9_]{2,}$#D', $s)) ? $this->formatArgs(new MacroTokens($s)) : '"' . $s . '"'; } /** * Preprocessor for tokens. (It advances tokenizer to the end as a side effect.) */ public function preprocess(MacroTokens $tokens = null): MacroTokens { $tokens = $tokens === null ? $this->tokens : $tokens; $this->validateTokens($tokens); $tokens = $this->removeCommentsPass($tokens); $tokens = $this->replaceFunctionsPass($tokens); $tokens = $this->optionalChainingPass($tokens); $tokens = $this->shortTernaryPass($tokens); $tokens = $this->inlineModifierPass($tokens); $tokens = $this->inOperatorPass($tokens); return $tokens; } /** * @throws CompileException */ public function validateTokens(MacroTokens $tokens): void { $brackets = []; $pos = $tokens->position; while ($tokens->nextToken()) { if ($tokens->isCurrent('?>')) { throw new CompileException('Forbidden ?> inside macro'); } elseif ($tokens->isCurrent('(', '[', '{')) { static $counterpart = ['(' => ')', '[' => ']', '{' => '}']; $brackets[] = $counterpart[$tokens->currentValue()]; } elseif ($tokens->isCurrent(')', ']', '}') && $tokens->currentValue() !== array_pop($brackets)) { throw new CompileException('Unexpected ' . $tokens->currentValue()); } elseif ( $tokens->isCurrent('function', 'class', 'interface', 'trait') && $tokens->isNext($tokens::T_SYMBOL, '&') || $tokens->isCurrent('return', 'yield') && !$brackets ) { throw new CompileException("Forbidden keyword '{$tokens->currentValue()}' inside macro."); } } if ($brackets) { throw new CompileException('Missing ' . array_pop($brackets)); } $tokens->position = $pos; } /** * Removes PHP comments. */ public function removeCommentsPass(MacroTokens $tokens): MacroTokens { $res = new MacroTokens; while ($tokens->nextToken()) { if (!$tokens->isCurrent($tokens::T_COMMENT)) { $res->append($tokens->currentToken()); } } return $res; } /** * Replace global functions with custom ones. */ public function replaceFunctionsPass(MacroTokens $tokens): MacroTokens { $res = new MacroTokens; while ($tokens->nextToken()) { $name = $tokens->currentValue(); if ( $tokens->isCurrent($tokens::T_SYMBOL) && ($orig = $this->functions[strtolower($name)] ?? null) && $tokens->isNext('(') && !$tokens->isPrev('::', '->', '\\') ) { if ($name !== $orig) { trigger_error("Case mismatch on function name '$name', correct name is '$orig'.", E_USER_WARNING); } $res->append('($this->global->_fn' . strtolower($name) . ')'); } else { $res->append($tokens->currentToken()); } } return $res; } /** * Simplified ternary expressions without third part. */ public function shortTernaryPass(MacroTokens $tokens): MacroTokens { $res = new MacroTokens; $inTernary = $tmp = []; $errors = 0; while ($tokens->nextToken()) { if ($tokens->isCurrent('?') && $tokens->isNext() && !$tokens->isNext(':', ',', ')', ']', '|')) { $inTernary[] = $tokens->depth; $tmp[] = $tokens->isNext('['); } elseif ($tokens->isCurrent(':')) { array_pop($inTernary); array_pop($tmp); } elseif ($tokens->isCurrent(',', ')', ']', '|') && end($inTernary) === $tokens->depth + $tokens->isCurrent(')', ']')) { $res->append(' : null'); array_pop($inTernary); $errors += array_pop($tmp); } $res->append($tokens->currentToken()); } if ($inTernary) { $errors += array_pop($tmp); $res->append(' : null'); } if ($errors) { $tokens->reset(); trigger_error('Short ternary operator requires braces around array: ' . $tokens->joinAll(), E_USER_DEPRECATED); } return $res; } /** * Optional Chaining $var?->prop?->elem[1]?->call()?->item */ public function optionalChainingPass(MacroTokens $tokens): MacroTokens { $startDepth = $tokens->depth; $res = new MacroTokens; while ($tokens->depth >= $startDepth && $tokens->nextToken()) { if (!$tokens->isCurrent($tokens::T_VARIABLE)) { $res->append($tokens->currentToken()); continue; } $addBraces = ''; $expr = new MacroTokens([$tokens->currentToken()]); $rescue = null; do { if ($tokens->nextToken('?')) { if ($tokens->isNext() && (!$tokens->isNext($tokens::T_CHAR) || $tokens->isNext('(', '[', '{', ':', '!', '@'))) { // is it ternary operator? $expr->append($addBraces . ' ?'); break; } $rescue = [$res->tokens, $expr->tokens, $tokens->position, $addBraces]; if (!$tokens->isNext('->')) { $expr->prepend('('); $expr->append(' ?? null)' . $addBraces); break; } $expr->prepend('(($_tmp = '); $expr->append(' ?? null) === null ? null : '); $res->tokens = array_merge($res->tokens, $expr->tokens); $expr = new MacroTokens('$_tmp'); $addBraces .= ')'; } elseif ($tokens->nextToken('->')) { $expr->append($tokens->currentToken()); if (!$tokens->nextToken($tokens::T_SYMBOL)) { $expr->append($addBraces); break; } $expr->append($tokens->currentToken()); } elseif ($tokens->nextToken('[', '(')) { $expr->tokens = array_merge($expr->tokens, [$tokens->currentToken()], $this->optionalChainingPass($tokens)->tokens); if ($rescue && $tokens->isNext(':')) { // it was ternary operator [$res->tokens, $expr->tokens, $tokens->position, $addBraces] = $rescue; $expr->append($addBraces . ' ?'); break; } } else { $expr->append($addBraces); break; } } while (true); $res->tokens = array_merge($res->tokens, $expr->tokens); } return $res; } /** * Pseudocast (expand). */ public function expandCastPass(MacroTokens $tokens): MacroTokens { $res = new MacroTokens('['); $expand = null; while ($tokens->nextToken()) { if ($tokens->isCurrent('(expand)') && $tokens->depth === 0) { $expand = true; $res->append('],'); } elseif ($expand && $tokens->isCurrent(',') && !$tokens->depth) { $expand = false; $res->append(', ['); } else { $res->append($tokens->currentToken()); } } if ($expand === null) { $res->append(']'); } else { $res->prepend('array_merge(')->append($expand ? ', [])' : '])'); } return $res; } /** * Quotes symbols to strings. */ public function quotingPass(MacroTokens $tokens): MacroTokens { $res = new MacroTokens; while ($tokens->nextToken()) { $res->append($tokens->isCurrent($tokens::T_SYMBOL) && (!$tokens->isPrev() || $tokens->isPrev(',', '(', '[', '=>', ':', '?', '.', '<', '>', '<=', '>=', '===', '!==', '==', '!=', '<>', '&&', '||', '=', 'and', 'or', 'xor', '??')) && (!$tokens->isNext() || $tokens->isNext(',', ';', ')', ']', '=>', ':', '?', '.', '<', '>', '<=', '>=', '===', '!==', '==', '!=', '<>', '&&', '||', 'and', 'or', 'xor', '??')) && !preg_match('#^[A-Z_][A-Z0-9_]{2,}$#', $tokens->currentValue()) ? "'" . $tokens->currentValue() . "'" : $tokens->currentToken() ); } return $res; } /** * Syntax $entry in [item1, item2]. */ public function inOperatorPass(MacroTokens $tokens): MacroTokens { while ($tokens->nextToken()) { if ($tokens->isCurrent($tokens::T_VARIABLE)) { $start = $tokens->position; $depth = $tokens->depth; $expr = $arr = []; $expr[] = $tokens->currentToken(); while ($tokens->isNext($tokens::T_VARIABLE, $tokens::T_SYMBOL, $tokens::T_NUMBER, $tokens::T_STRING, '[', ']', '(', ')', '->') && !$tokens->isNext('in')) { $expr[] = $tokens->nextToken(); } if ($depth === $tokens->depth && $tokens->nextValue('in') && ($arr[] = $tokens->nextToken('['))) { while ($tokens->isNext()) { $arr[] = $tokens->nextToken(); if ($tokens->isCurrent(']') && $tokens->depth === $depth) { $new = array_merge($tokens->parse('in_array('), $expr, $tokens->parse(', '), $arr, $tokens->parse(', true)')); array_splice($tokens->tokens, $start, $tokens->position - $start + 1, $new); $tokens->position = $start + count($new) - 1; continue 2; } } } $tokens->position = $start; } } return $tokens->reset(); } /** * Process inline filters ($var|filter) */ public function inlineModifierPass(MacroTokens $tokens): MacroTokens { $result = new MacroTokens; while ($tokens->nextToken()) { if ($tokens->isCurrent('(', '[')) { $result->tokens = array_merge($result->tokens, $this->inlineModifierInner($tokens)); } else { $result->append($tokens->currentToken()); } } return $result; } private function inlineModifierInner(MacroTokens $tokens): array { $isFunctionOrArray = $tokens->isPrev($tokens::T_VARIABLE, $tokens::T_SYMBOL) || $tokens->isCurrent('['); $result = new MacroTokens; $args = new MacroTokens; $modifiers = new MacroTokens; $current = $args; $anyModifier = false; $result->append($tokens->currentToken()); while ($tokens->nextToken()) { if ($tokens->isCurrent('(', '[')) { $current->tokens = array_merge($current->tokens, $this->inlineModifierInner($tokens)); } elseif ($current !== $modifiers && $tokens->isCurrent('|')) { $anyModifier = true; $current = $modifiers; } elseif ($tokens->isCurrent(')', ']') || ($isFunctionOrArray && $tokens->isCurrent(','))) { $partTokens = count($modifiers->tokens) ? $this->modifierPass($modifiers, $args->tokens)->tokens : $args->tokens; $result->tokens = array_merge($result->tokens, $partTokens); if ($tokens->isCurrent(',')) { $result->append($tokens->currentToken()); $args = new MacroTokens; $modifiers = new MacroTokens; $current = $args; continue; } elseif ($isFunctionOrArray || !$anyModifier) { $result->append($tokens->currentToken()); } else { array_shift($result->tokens); } return $result->tokens; } else { $current->append($tokens->currentToken()); } } throw new CompileException('Unbalanced brackets.'); } /** * Formats modifiers calling. * @param string|array $var * @throws CompileException */ public function modifierPass(MacroTokens $tokens, $var, bool $isContent = false): MacroTokens { $inside = false; $res = new MacroTokens($var); while ($tokens->nextToken()) { if ($tokens->isCurrent($tokens::T_WHITESPACE)) { $res->append(' '); } elseif ($inside) { if ($tokens->isCurrent(':', ',') && !$tokens->depth) { $res->append(', '); $tokens->nextAll($tokens::T_WHITESPACE); } elseif ($tokens->isCurrent('|') && !$tokens->depth) { $res->append(')'); $inside = false; } else { $res->append($tokens->currentToken()); } } else { if ($tokens->isCurrent($tokens::T_SYMBOL)) { if ($tokens->isCurrent('escape')) { if ($isContent) { $res->prepend('LR\Filters::convertTo($_fi, ' . var_export(implode($this->context), true) . ', ') ->append(')'); } else { $res = $this->escapePass($res); } $tokens->nextToken('|'); } elseif (!strcasecmp($tokens->currentValue(), 'checkurl')) { $res->prepend('LR\Filters::safeUrl('); $inside = true; } else { $name = strtolower($tokens->currentValue()); $res->prepend($isContent ? '$this->filters->filterContent(' . var_export($name, true) . ', $_fi, ' : '($this->filters->' . $name . ')(' ); $inside = true; } } else { throw new CompileException("Modifier name must be alphanumeric string, '{$tokens->currentValue()}' given."); } } } if ($inside) { $res->append(')'); } return $res; } /** * Escapes expression in tokens. */ public function escapePass(MacroTokens $tokens): MacroTokens { $tokens = clone $tokens; [$contentType, $context] = $this->context; switch ($contentType) { case Compiler::CONTENT_XHTML: case Compiler::CONTENT_HTML: switch ($context) { case Compiler::CONTEXT_HTML_TEXT: return $tokens->prepend('LR\Filters::escapeHtmlText(')->append(')'); case Compiler::CONTEXT_HTML_TAG: case Compiler::CONTEXT_HTML_ATTRIBUTE_UNQUOTED_URL: return $tokens->prepend('LR\Filters::escapeHtmlAttrUnquoted(')->append(')'); case Compiler::CONTEXT_HTML_ATTRIBUTE: case Compiler::CONTEXT_HTML_ATTRIBUTE_URL: return $tokens->prepend('LR\Filters::escapeHtmlAttr(')->append(')'); case Compiler::CONTEXT_HTML_ATTRIBUTE_JS: return $tokens->prepend('LR\Filters::escapeHtmlAttr(LR\Filters::escapeJs(')->append('))'); case Compiler::CONTEXT_HTML_ATTRIBUTE_CSS: return $tokens->prepend('LR\Filters::escapeHtmlAttr(LR\Filters::escapeCss(')->append('))'); case Compiler::CONTEXT_HTML_COMMENT: return $tokens->prepend('LR\Filters::escapeHtmlComment(')->append(')'); case Compiler::CONTEXT_HTML_BOGUS_COMMENT: return $tokens->prepend('LR\Filters::escapeHtml(')->append(')'); case Compiler::CONTEXT_HTML_JS: case Compiler::CONTEXT_HTML_CSS: return $tokens->prepend('LR\Filters::escape' . ucfirst($context) . '(')->append(')'); default: throw new CompileException("Unknown context $contentType, $context."); } // break omitted case Compiler::CONTENT_XML: switch ($context) { case Compiler::CONTEXT_XML_TEXT: case Compiler::CONTEXT_XML_ATTRIBUTE: case Compiler::CONTEXT_XML_BOGUS_COMMENT: return $tokens->prepend('LR\Filters::escapeXml(')->append(')'); case Compiler::CONTEXT_XML_COMMENT: return $tokens->prepend('LR\Filters::escapeHtmlComment(')->append(')'); case Compiler::CONTEXT_XML_TAG: return $tokens->prepend('LR\Filters::escapeXmlAttrUnquoted(')->append(')'); default: throw new CompileException("Unknown context $contentType, $context."); } // break omitted case Compiler::CONTENT_JS: case Compiler::CONTENT_CSS: case Compiler::CONTENT_ICAL: return $tokens->prepend('LR\Filters::escape' . ucfirst($contentType) . '(')->append(')'); case Compiler::CONTENT_TEXT: return $tokens; case null: return $tokens->prepend('($this->filters->escape)(')->append(')'); default: throw new CompileException("Unknown context $contentType."); } } } src/Latte/Compiler/MacroTokens.php 0000666 00000006151 13603744006 0013126 0 ustar 00 parse($input)); $this->ignored = [self::T_COMMENT, self::T_WHITESPACE]; } public function parse(string $s): array { self::$tokenizer = self::$tokenizer ?: new Tokenizer([ self::T_WHITESPACE => '\s+', self::T_COMMENT => '(?s)/\*.*?\*/', self::T_STRING => Parser::RE_STRING, self::T_KEYWORD => '(?:true|false|null|TRUE|FALSE|NULL|INF|NAN|and|or|xor|clone|new|instanceof|return|continue|break)(?![\w\pL_])', // keyword self::T_CAST => '\((?:expand|string|array|int|integer|float|bool|boolean|object)\)', // type casting self::T_VARIABLE => '\$[\w\pL_]+', self::T_NUMBER => '[+-]?[0-9]+(?:\.[0-9]+)?(?:e[0-9]+)?', self::T_SYMBOL => '[\w\pL_]+(?:-+[\w\pL_]+)*', self::T_CHAR => '::|=>|->|\+\+|--|<<|>>|<=>|<=|>=|===|!==|==|!=|<>|&&|\|\||\?\?|\?>|\*\*|\.\.\.|[^"\']', // =>, any char except quotes ], 'u'); return self::$tokenizer->tokenize($s); } /** * Appends simple token or string (will be parsed). * @return static */ public function append($val, int $position = null) { if ($val != null) { // intentionally @ array_splice( $this->tokens, $position === null ? count($this->tokens) : $position, 0, is_array($val) ? [$val] : $this->parse($val) ); } return $this; } /** * Prepends simple token or string (will be parsed). * @return static */ public function prepend($val) { if ($val != null) { // intentionally @ array_splice($this->tokens, 0, 0, is_array($val) ? [$val] : $this->parse($val)); } return $this; } /** * Reads single token (optionally delimited by comma) from string. */ public function fetchWord(): ?string { $words = $this->fetchWords(); return $words ? implode(':', $words) : null; } /** * Reads single tokens delimited by colon from string. */ public function fetchWords(): array { do { $words[] = $this->joinUntil(self::T_WHITESPACE, ',', ':'); } while ($this->nextToken(':')); if (count($words) === 1 && ($space = $this->nextValue(self::T_WHITESPACE)) && (($dot = $this->nextValue('.')) || $this->isPrev('.'))) { $words[0] .= $space . $dot . $this->joinUntil(','); } $this->nextToken(','); $this->nextAll(self::T_WHITESPACE, self::T_COMMENT); return $words === [''] ? [] : $words; } public function reset() { $this->depth = 0; return parent::reset(); } protected function next(): void { parent::next(); if ($this->isCurrent('[', '(', '{')) { $this->depth++; } elseif ($this->isCurrent(']', ')', '}')) { $this->depth--; } } } src/Latte/Compiler/Compiler.php 0000666 00000054163 13603744006 0012461 0 ustar 00 IMacro[]] */ private $macros = []; /** @var string[] of orig name */ private $functions = []; /** @var int[] IMacro flags */ private $flags; /** @var HtmlNode|null */ private $htmlNode; /** @var MacroNode|null */ private $macroNode; /** @var string[] */ private $placeholders = []; /** @var string */ private $contentType = self::CONTENT_HTML; /** @var string|null */ private $context; /** @var mixed */ private $lastAttrValue; /** @var int */ private $tagOffset; /** @var bool */ private $inHead; /** @var array of [name => [body, arguments]] */ private $methods = []; /** @var array of [name => serialized value] */ private $properties = []; /** * Adds new macro with IMacro flags. * @return static */ public function addMacro(string $name, IMacro $macro, int $flags = null) { if (!isset($this->flags[$name])) { $this->flags[$name] = $flags ?: IMacro::DEFAULT_FLAGS; } elseif ($flags && $this->flags[$name] !== $flags) { throw new \LogicException("Incompatible flags for macro $name."); } $this->macros[$name][] = $macro; return $this; } /** * Registers run-time function. */ public function addFunction(string $name): string { $lname = strtolower($name); $this->functions[$lname] = $name; return '_fn' . $lname; } /** * Compiles tokens to PHP code. * @param Token[] $tokens */ public function compile(array $tokens, string $className): string { $this->tokens = $tokens; $output = ''; $this->output = &$output; $this->inHead = true; $this->htmlNode = $this->macroNode = $this->context = null; $this->placeholders = $this->properties = []; $this->methods = ['main' => null, 'prepare' => null]; $macroHandlers = new \SplObjectStorage; if ($this->macros) { array_map([$macroHandlers, 'attach'], array_merge(...array_values($this->macros))); } foreach ($macroHandlers as $handler) { $handler->initialize($this); } foreach ($tokens as $this->position => $token) { if ($this->inHead && !($token->type === $token::COMMENT || $token->type === $token::MACRO_TAG && ($this->flags[$token->name] ?? null) & IMacro::ALLOWED_IN_HEAD || $token->type === $token::TEXT && trim($token->text) === '' )) { $this->inHead = false; } $this->{"process$token->type"}($token); } while ($this->htmlNode) { if (!empty($this->htmlNode->macroAttrs)) { throw new CompileException('Missing ' . self::printEndTag($this->htmlNode)); } $this->htmlNode = $this->htmlNode->parentNode; } while ($this->macroNode) { if (~$this->flags[$this->macroNode->name] & IMacro::AUTO_CLOSE) { throw new CompileException('Missing ' . self::printEndTag($this->macroNode)); } $this->closeMacro($this->macroNode->name); } $prepare = $epilogs = ''; foreach ($macroHandlers as $handler) { $res = $handler->finalize(); $prepare .= empty($res[0]) ? '' : ""; $epilogs = (empty($res[1]) ? '' : "") . $epilogs; } $this->addMethod('main', $this->expandTokens("extract(\$this->params);?>\n$output$epilogsaddMethod('prepare', "extract(\$this->params);?>$preparecontentType !== self::CONTENT_HTML) { $this->addProperty('contentType', $this->contentType); } $members = []; foreach ($this->properties as $name => $value) { $members[] = "\tpublic $$name = " . PhpHelpers::dump($value) . ';'; } foreach (array_filter($this->methods) as $name => $method) { $members[] = "\n\tfunction $name($method[arguments])\n\t{\n" . ($method['body'] ? "\t\t$method[body]\n" : '') . "\t}"; } return "contentType = $type; $this->context = null; return $this; } public function getMacroNode(): ?MacroNode { return $this->macroNode; } public function getMacros(): array { return $this->macros; } public function getFunctions(): array { return $this->functions; } /** * Returns current line number. */ public function getLine(): ?int { return isset($this->tokens[$this->position]) ? $this->tokens[$this->position]->line : null; } public function isInHead(): bool { return $this->inHead; } /** * Adds custom method to template. * @internal */ public function addMethod(string $name, string $body, string $arguments = ''): void { $this->methods[$name] = ['body' => trim($body), 'arguments' => $arguments]; } /** * Returns custom methods. * @internal */ public function getMethods(): array { return $this->methods; } /** * Adds custom property to template. * @internal */ public function addProperty(string $name, $value): void { $this->properties[$name] = $value; } /** * Returns custom properites. * @internal */ public function getProperties(): array { return $this->properties; } /** @internal */ public function expandTokens(string $s): string { return strtr($s, $this->placeholders); } private function processText(Token $token): void { if ($this->lastAttrValue === '' && $this->context && Helpers::startsWith($this->context, self::CONTEXT_HTML_ATTRIBUTE)) { $this->lastAttrValue = $token->text; } $this->output .= $this->escape($token->text); } private function processMacroTag(Token $token): void { if ($this->context === self::CONTEXT_HTML_TAG || $this->context && Helpers::startsWith($this->context, self::CONTEXT_HTML_ATTRIBUTE)) { $this->lastAttrValue = true; } $isRightmost = !isset($this->tokens[$this->position + 1]) || substr($this->tokens[$this->position + 1]->text, 0, 1) === "\n"; if ($token->closing) { $this->closeMacro($token->name, $token->value, $token->modifiers, $isRightmost); } else { if (!$token->empty && ($this->flags[$token->name] ?? null) & IMacro::AUTO_EMPTY) { $pos = $this->position; while (($t = $this->tokens[++$pos] ?? null) && ($t->type !== Token::MACRO_TAG || $t->name !== $token->name) && ($t->type !== Token::HTML_ATTRIBUTE_BEGIN || $t->name !== Parser::N_PREFIX . $token->name)); $token->empty = $t ? !$t->closing : true; } $node = $this->openMacro($token->name, $token->value, $token->modifiers, $isRightmost); if ($token->empty) { if ($node->empty) { throw new CompileException("Unexpected /} in tag {$token->text}"); } $this->closeMacro($token->name, null, null, $isRightmost); } } } private function processHtmlTagBegin(Token $token): void { if ($token->closing) { while ($this->htmlNode) { if (strcasecmp($this->htmlNode->name, $token->name) === 0) { break; } if ($this->htmlNode->macroAttrs) { throw new CompileException("Unexpected $token->name>, expecting " . self::printEndTag($this->htmlNode)); } $this->htmlNode = $this->htmlNode->parentNode; } if (!$this->htmlNode) { $this->htmlNode = new HtmlNode($token->name); } $this->htmlNode->closing = true; $this->htmlNode->endLine = $this->getLine(); $this->context = self::CONTEXT_HTML_TEXT; } elseif ($token->text === '