diff --git a/src/Latte/Compiler/Compiler.php b/src/Latte/Compiler/Compiler.php index cf7b09c8c..4957e8c15 100644 --- a/src/Latte/Compiler/Compiler.php +++ b/src/Latte/Compiler/Compiler.php @@ -110,6 +110,7 @@ public function addMacro(string $name, Macro $macro, int $flags = null) } elseif ($flags && $this->flags[$name] !== $flags) { throw new \LogicException("Incompatible flags for macro $name."); } + $this->macros[$name][] = $macro; return $this; } @@ -159,6 +160,7 @@ public function compile(array $tokens, string $className): string )) { $this->inHead = false; } + $this->{"process$token->type"}($token); } @@ -166,6 +168,7 @@ public function compile(array $tokens, string $className): string if (!empty($this->htmlNode->macroAttrs)) { throw new CompileException('Missing ' . self::printEndTag($this->htmlNode)); } + $this->htmlNode = $this->htmlNode->parentNode; } @@ -173,6 +176,7 @@ public function compile(array $tokens, string $className): string if (~$this->flags[$this->macroNode->name] & Macro::AUTO_CLOSE) { throw new CompileException('Missing ' . self::printEndTag($this->macroNode)); } + $this->closeMacro($this->macroNode->name); } @@ -188,14 +192,17 @@ public function compile(array $tokens, string $className): string if ($prepare) { $this->addMethod('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, true) . ';'; } + foreach (array_filter($this->methods) as $name => $method) { $members[] = "\n\tpublic function $name($method[arguments])" . ($method['returns'] ? ': ' . $method['returns'] : '') @@ -334,6 +341,7 @@ private function processText(Token $token): void ) { $this->lastAttrValue = $token->text; } + $this->output .= $this->escape($token->text); } @@ -361,11 +369,13 @@ private function processMacroTag(Token $token): void && ($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, '', '', $isRightmost); } } @@ -379,14 +389,18 @@ private function processHtmlTagBegin(Token $token): void if (strcasecmp($this->htmlNode->name, $token->name) === 0) { break; } + if ($this->htmlNode->macroAttrs) { throw new CompileException("Unexpected 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; @@ -402,6 +416,7 @@ private function processHtmlTagBegin(Token $token): void $this->htmlNode->startLine = $this->getLine(); $this->context = self::CONTEXT_HTML_TAG; } + $this->tagOffset = strlen($this->output); $this->output .= $this->escape($token->text); } @@ -476,6 +491,7 @@ private function processHtmlAttributeBegin(Token $token): void } elseif ($this->macroNode && $this->macroNode->htmlNode === $this->htmlNode) { throw new CompileException("n:attributes must not appear inside macro; found $token->name inside {{$this->macroNode->name}}."); } + $this->htmlNode->macroAttrs[$name] = $token->value; return; } @@ -563,6 +579,7 @@ public function openMacro( $this->output = &$node->content; $this->output = ''; } + return $node; } @@ -616,6 +633,7 @@ public function closeMacro( if ($node->prefix && $node->prefix !== MacroNode::PREFIX_TAG) { $this->htmlNode->attrCode .= $node->attrCode; } + $this->output = &$node->saved[0]; $this->writeCode((string) $node->openingCode, $node->replaced, $node->saved[1]); $this->output .= $node->content; @@ -632,6 +650,7 @@ private function writeCode(string $code, ?bool $isReplaced, ?bool $isRightmost, if ($isReplaced === null) { $isReplaced = preg_match('#<\?php.*\secho\s#As', $code); } + if ($isLeftmost && !$isReplaced) { $this->output = substr($this->output, 0, $leftOfs); // alone macro without output -> remove indentation if (!$isClosing && substr($code, -2) !== '?>') { @@ -641,6 +660,7 @@ private function writeCode(string $code, ?bool $isReplaced, ?bool $isRightmost, $code .= "\n"; // double newline to avoid newline eating by PHP } } + $this->output .= $code; } @@ -673,6 +693,7 @@ public function writeAttrsMacro(string $html): void } }); } + unset($attrs[$attrName]); } @@ -687,7 +708,6 @@ public function writeAttrsMacro(string $html): void }); } - foreach (array_reverse($this->macros) as $name => $foo) { $attrName = MacroNode::PREFIX_TAG . "-$name"; if (!isset($attrs[$attrName])) { @@ -722,6 +742,7 @@ public function writeAttrsMacro(string $html): void } }); } + unset($attrs[$name]); } } diff --git a/src/Latte/Compiler/MacroTokens.php b/src/Latte/Compiler/MacroTokens.php index 8f68810cd..474a5d333 100644 --- a/src/Latte/Compiler/MacroTokens.php +++ b/src/Latte/Compiler/MacroTokens.php @@ -82,6 +82,7 @@ public function append($val, int $position = null) is_array($val) ? [$val] : $this->parse($val) ); } + return $this; } @@ -96,6 +97,7 @@ public function prepend($val) if ($val != null) { // intentionally @ array_splice($this->tokens, 0, 0, is_array($val) ? [$val] : $this->parse($val)); } + return $this; } @@ -124,6 +126,7 @@ public function fetchWords(): array && (($dot = $this->nextValue('.')) || $this->isPrev('.'))) { $words[0] .= $space . $dot . $this->joinUntil(','); } + $this->nextToken(','); $this->nextAll(self::T_WHITESPACE, self::T_COMMENT); return $words === [''] ? [] : $words; diff --git a/src/Latte/Compiler/Parser.php b/src/Latte/Compiler/Parser.php index 87de676f6..25282a405 100644 --- a/src/Latte/Compiler/Parser.php +++ b/src/Latte/Compiler/Parser.php @@ -115,10 +115,12 @@ public function parse(string $input): array if ($this->{'context' . $this->context[0]}() === false) { break; } + while ($tokenCount < count($this->output)) { $this->filter($this->output[$tokenCount++]); } } + if ($this->context[0] === self::CONTEXT_MACRO) { throw new CompileException('Malformed macro'); } @@ -126,6 +128,7 @@ public function parse(string $input): array if ($this->offset < strlen($input)) { $this->addToken(Token::TEXT, substr($this->input, $this->offset)); } + return $this->output; } @@ -176,6 +179,7 @@ private function contextHtmlCData(): bool if (empty($matches['tag'])) { return $this->processMacro($matches); } + // addToken(Token::HTML_TAG_BEGIN, $matches[0]); $token->name = $this->lastHtmlTag; @@ -219,6 +223,7 @@ private function contextHtmlTag(): bool $this->setContext(self::CONTEXT_HTML_ATTRIBUTE, $matches['value']); } } + return true; } else { @@ -240,6 +245,7 @@ private function contextHtmlAttribute(): bool if (empty($matches['quote'])) { return $this->processMacro($matches); } + // (attribute end) '" $this->addToken(Token::HTML_ATTRIBUTE_END, $matches[0]); $this->setContext(self::CONTEXT_HTML_TAG); @@ -260,6 +266,7 @@ private function contextHtmlComment(): bool if (empty($matches['htmlcomment'])) { return $this->processMacro($matches); } + // --> $this->addToken(Token::HTML_TAG_END, $matches[0]); $this->setContext(self::CONTEXT_HTML_TEXT); @@ -320,6 +327,7 @@ private function processMacro(array $matches): bool if (empty($matches['macro'])) { return false; } + // {macro} or {* *} $this->setContext(self::CONTEXT_MACRO, [$this->context, $matches['macro']]); return true; @@ -336,6 +344,7 @@ private function match(string $re): array if (preg_last_error()) { throw new RegexpException(null, preg_last_error()); } + return []; } @@ -343,10 +352,12 @@ private function match(string $re): array 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; } @@ -363,6 +374,7 @@ public function setContentType(string $type) } else { $this->setContext(self::CONTEXT_NONE); } + return $this; } @@ -425,11 +437,14 @@ public function parseMacroTag(string $tag): ?array 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']]; } diff --git a/src/Latte/Compiler/PhpHelpers.php b/src/Latte/Compiler/PhpHelpers.php index 5e4ab49dd..99effe0bc 100644 --- a/src/Latte/Compiler/PhpHelpers.php +++ b/src/Latte/Compiler/PhpHelpers.php @@ -45,6 +45,7 @@ public static function reformatCode(string $source): string } elseif (substr($next[1], -1) === "\n") { $php .= "\n" . str_repeat("\t", $level); } + $tokens->next(); } else { @@ -56,16 +57,18 @@ public static function reformatCode(string $source): string } else { $php = rtrim($php, "\t"); } + $res .= $php . $token; } + $php = ''; $lastChar = ';'; } - } elseif ($name === T_ELSE || $name === T_ELSEIF) { if ($tokens[$n + 1] === ':' && $lastChar === '}') { $php .= ';'; // semicolon needed in if(): ... if() ... else: } + $lastChar = ''; $php .= $token; @@ -80,6 +83,7 @@ public static function reformatCode(string $source): string } elseif ($prev[0] === T_OPEN_TAG) { $token = ''; } + $php .= $token; } elseif ($name === T_OBJECT_OPERATOR) { @@ -90,6 +94,7 @@ public static function reformatCode(string $source): string if (in_array($name, [T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES], true)) { $level++; } + $lastChar = ''; $php .= $token; } @@ -109,6 +114,7 @@ public static function reformatCode(string $source): string $token .= "\n" . str_repeat("\t", $level); // indent last line } } + $lastChar = $token; $php .= $token; } @@ -117,6 +123,7 @@ public static function reformatCode(string $source): string if ($php) { $res .= " ') . self::dump($v) . ",\n" : ($s === '' ? '' : ', ') . ($indexed ? '' : self::dump($k) . ' => ') . self::dump($v); } + return '[' . $s . ']'; } elseif ($value === null) { return 'null'; diff --git a/src/Latte/Compiler/PhpWriter.php b/src/Latte/Compiler/PhpWriter.php index aa4c0612a..f500bc38b 100644 --- a/src/Latte/Compiler/PhpWriter.php +++ b/src/Latte/Compiler/PhpWriter.php @@ -220,9 +220,11 @@ public function validateTokens(MacroTokens $tokens): void throw new CompileException("Forbidden variable {$tokenValue}."); } } + if ($brackets) { throw new CompileException('Missing ' . array_pop($brackets)); } + $tokens->position = $pos; } @@ -251,6 +253,7 @@ public function validateKeywords(MacroTokens $tokens): void throw new CompileException("Forbidden keyword '{$tokens->currentValue()}' inside macro."); } } + $tokens->position = $pos; } @@ -264,6 +267,7 @@ public function removeCommentsPass(MacroTokens $tokens): MacroTokens while ($tokens->nextToken()) { $res->append($tokens->isCurrent($tokens::T_COMMENT) ? ' ' : $tokens->currentToken()); } + return $res; } @@ -285,11 +289,13 @@ public function replaceFunctionsPass(MacroTokens $tokens): MacroTokens if ($name !== $orig) { trigger_error("Case mismatch on function name '$name', correct name is '$orig'.", E_USER_WARNING); } + $res->append('($this->global->fn->' . $orig . ')'); } else { $res->append($tokens->currentToken()); } } + return $res; } @@ -319,12 +325,14 @@ public function shortTernaryPass(MacroTokens $tokens): MacroTokens $res->append(' : null'); array_pop($inTernary); } + $res->append($tokens->currentToken()); } if ($inTernary) { $res->append(' : null'); } + return $res; } @@ -377,6 +385,7 @@ public function optionalChainingPass(MacroTokens $tokens): MacroTokens $expr->append($addBraces); break; } + $expr->append($tokens->currentToken()); } elseif ($tokens->nextToken('[', '(')) { @@ -419,6 +428,7 @@ public function expandCastPass(MacroTokens $tokens): MacroTokens } else { $res->prepend('array_merge(')->append($expand ? ', [])' : '])'); } + return $res; } @@ -439,6 +449,7 @@ public function quotingPass(MacroTokens $tokens): MacroTokens : $tokens->currentToken() ); } + return $res; } @@ -471,9 +482,11 @@ public function inOperatorPass(MacroTokens $tokens): MacroTokens } } } + $tokens->position = $start; } } + return $tokens->reset(); } @@ -525,12 +538,14 @@ public function sandboxPass(MacroTokens $tokens): MacroTokens if (!$this->policy->isFunctionAllowed($name)) { throw new SecurityViolationException("Function $name() is not allowed."); } + $static = false; $expr->append('('); } else { // any calling $expr->prepend('$this->call('); $expr->append(')('); } + $expr->tokens = array_merge($expr->tokens, $this->sandboxPass($tokens)->tokens); } elseif ($tokens->nextToken('->', '::')) { // property, method or constant @@ -549,6 +564,7 @@ public function sandboxPass(MacroTokens $tokens): MacroTokens $expr->append('::class'); $static = false; } + $expr->append(', '); if ($tokens->nextToken($tokens::T_SYMBOL)) { // $obj->member or $obj::member @@ -562,7 +578,6 @@ public function sandboxPass(MacroTokens $tokens): MacroTokens } else { $expr->append($tokens->currentValue()); } - } elseif ($tokens->nextToken('{')) { // $obj->{...} $member = array_merge([$tokens->currentToken()], $this->sandboxPass($tokens)->tokens); $expr->append('(string) '); @@ -584,7 +599,6 @@ public function sandboxPass(MacroTokens $tokens): MacroTokens $expr->append(')' . $op); $expr->tokens = array_merge($expr->tokens, $member); } - } elseif ($tokens->nextToken('[', '{')) { // array access $static = false; $expr->tokens = array_merge($expr->tokens, [$tokens->currentToken()], $this->sandboxPass($tokens)->tokens); @@ -614,6 +628,7 @@ public function inlineModifierPass(MacroTokens $tokens): MacroTokens $result->append($tokens->currentToken()); } } + return $result; } @@ -655,12 +670,14 @@ private function inlineModifierInner(MacroTokens $tokens): array } else { array_shift($result->tokens); } + return $result->tokens; } else { $current->append($tokens->currentToken()); } } + throw new CompileException('Unbalanced brackets.'); } @@ -698,6 +715,7 @@ public function modifierPass(MacroTokens $tokens, $var, bool $isContent = false) } else { $res = $this->escapePass($res); } + $tokens->nextToken('|'); } elseif (!strcasecmp($tokens->currentValue(), 'checkurl')) { $res->prepend('LR\Filters::safeUrl('); @@ -712,6 +730,7 @@ public function modifierPass(MacroTokens $tokens, $var, bool $isContent = false) if ($this->policy && !$this->policy->isFilterAllowed($name)) { throw new SecurityViolationException("Filter |$name is not allowed."); } + $name = strtolower($name); $res->prepend( $isContent @@ -724,9 +743,11 @@ public function modifierPass(MacroTokens $tokens, $var, bool $isContent = false) throw new CompileException("Modifier name must be alphanumeric string, '{$tokens->currentValue()}' given."); } } + if ($inside) { $res->append(')'); } + return $res; } diff --git a/src/Latte/Compiler/TokenIterator.php b/src/Latte/Compiler/TokenIterator.php index a0a41a3d1..b7ec4638a 100644 --- a/src/Latte/Compiler/TokenIterator.php +++ b/src/Latte/Compiler/TokenIterator.php @@ -128,6 +128,7 @@ public function isCurrent(...$args): bool if (!isset($this->tokens[$this->position])) { return false; } + $token = $this->tokens[$this->position]; return in_array($token[Tokenizer::VALUE], $args, true) || in_array($token[Tokenizer::TYPE], $args, true); @@ -164,10 +165,12 @@ public function consumeValue(...$args): string if ($token = $this->scan($args, true, true)) { // onlyFirst, advance return $token[Tokenizer::VALUE]; } + $pos = $this->position + 1; while (($next = $this->tokens[$pos] ?? null) && in_array($next[Tokenizer::TYPE], $this->ignored, true)) { $pos++; } + throw new CompileException($next ? "Unexpected token '" . $next[Tokenizer::VALUE] . "'." : 'Unexpected end.'); } @@ -228,10 +231,10 @@ protected function scan( } else { $res[] = $token; } - } elseif ($until || !in_array($token[Tokenizer::TYPE], $this->ignored, true)) { return $res; } + $pos += $prev ? -1 : 1; } while (true); } diff --git a/src/Latte/Compiler/Tokenizer.php b/src/Latte/Compiler/Tokenizer.php index 6b05beb97..22c8a5d88 100644 --- a/src/Latte/Compiler/Tokenizer.php +++ b/src/Latte/Compiler/Tokenizer.php @@ -51,6 +51,7 @@ public function tokenize(string $input): array if (preg_last_error()) { throw new RegexpException(null, preg_last_error()); } + $len = 0; $count = count($this->types); foreach ($tokens as &$match) { @@ -63,14 +64,17 @@ public function tokenize(string $input): array break; } } + $match = [self::VALUE => $match[0], self::OFFSET => $len, self::TYPE => $type]; $len += strlen($match[self::VALUE]); } + if ($len !== strlen($input)) { [$line, $col] = $this->getCoordinates($input, $len); $token = str_replace("\n", '\n', substr($input, $len, 10)); throw new CompileException("Unexpected '$token' on line $line, column $col."); } + return $tokens; } diff --git a/src/Latte/Engine.php b/src/Latte/Engine.php index 7a0ecd61b..dde6e62b4 100644 --- a/src/Latte/Engine.php +++ b/src/Latte/Engine.php @@ -109,6 +109,7 @@ public function createTemplate(string $name, array $params = []): Runtime\Templa if (!class_exists($class, false)) { $this->loadTemplate($name); } + $this->providers['fn'] = $this->functions; return new $class($this, $params, $this->filters, $this->providers, $name, $this->sandboxed ? $this->policy : null); } @@ -122,6 +123,7 @@ public function compile(string $name): string foreach ($this->onCompile ?: [] as $cb) { (Helpers::checkCallback($cb))($this); } + $this->onCompile = []; $source = $this->getLoader()->getContent($name); @@ -141,6 +143,7 @@ public function compile(string $name): string if (!$e instanceof CompileException) { $e = new CompileException($e instanceof SecurityViolationException ? $e->getMessage() : "Thrown exception '{$e->getMessage()}'", 0, $e); } + $line = isset($tokens) ? $this->getCompiler()->getLine() : $this->getParser()->getLine(); @@ -183,6 +186,7 @@ private function loadTemplate(string $name): void throw (new CompileException('Error in template: ' . error_get_last()['message'])) ->setSource($code, error_get_last()['line'], "$name (compiled)"); } + return; } @@ -202,6 +206,7 @@ private function loadTemplate(string $name): void if ($lock) { flock($lock, LOCK_UN); // release shared lock so we can get exclusive } + $lock = $this->acquireLock("$file.lock", LOCK_EX); // while waiting for exclusive lock, someone might have already created the cache @@ -211,6 +216,7 @@ private function loadTemplate(string $name): void @unlink("$file.tmp"); // @ - file may not exist throw new \RuntimeException("Unable to create '$file'."); } + if (function_exists('opcache_invalidate')) { @opcache_invalidate($file, true); // @ can be restricted } @@ -238,6 +244,7 @@ private function acquireLock(string $file, int $mode) } elseif (!@flock($handle, $mode)) { // @ is escalated to exception throw new \RuntimeException('Unable to acquire ' . ($mode & LOCK_EX ? 'exclusive' : 'shared') . " lock on file '$file'. " . error_get_last()['message']); } + return $handle; } @@ -332,6 +339,7 @@ public function invokeFunction(string $name, array $args) : '.'; throw new \LogicException("Function '$name' is not defined$hint"); } + return ($this->functions->$name)(...$args); } @@ -428,6 +436,7 @@ public function getParser(): Parser if (!$this->parser) { $this->parser = new Parser; } + return $this->parser; } @@ -439,6 +448,7 @@ public function getCompiler(): Compiler Macros\CoreMacros::install($this->compiler); Macros\BlockMacros::install($this->compiler); } + return $this->compiler; } @@ -456,6 +466,7 @@ public function getLoader(): Loader if (!$this->loader) { $this->loader = new Loaders\FileLoader; } + return $this->loader; } diff --git a/src/Latte/Helpers.php b/src/Latte/Helpers.php index 4af2df441..c2a487a29 100644 --- a/src/Latte/Helpers.php +++ b/src/Latte/Helpers.php @@ -32,6 +32,7 @@ public static function checkCallback($callable): callable if (!is_callable($callable, false, $text)) { throw new \InvalidArgumentException("Callback '$text' is not callable."); } + return $callable; } @@ -50,6 +51,7 @@ public static function getSuggestion(array $items, $value): ?string $best = $item; } } + return $best; } diff --git a/src/Latte/Loaders/FileLoader.php b/src/Latte/Loaders/FileLoader.php index f6bab71e0..4a9f6a274 100644 --- a/src/Latte/Loaders/FileLoader.php +++ b/src/Latte/Loaders/FileLoader.php @@ -46,6 +46,7 @@ public function getContent($fileName): string 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); } @@ -65,6 +66,7 @@ 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; } @@ -88,6 +90,7 @@ protected static function normalizePath(string $path): string $res[] = $part; } } + return implode(DIRECTORY_SEPARATOR, $res); } } diff --git a/src/Latte/Loaders/StringLoader.php b/src/Latte/Loaders/StringLoader.php index 56a383d61..dc96099ac 100644 --- a/src/Latte/Loaders/StringLoader.php +++ b/src/Latte/Loaders/StringLoader.php @@ -61,6 +61,7 @@ public function getReferredName($name, $referringName): string if ($this->templates === null) { throw new \LogicException("Missing template '$name'."); } + return $name; } diff --git a/src/Latte/Macros/BlockMacros.php b/src/Latte/Macros/BlockMacros.php index 076adcb33..59980ddc8 100644 --- a/src/Latte/Macros/BlockMacros.php +++ b/src/Latte/Macros/BlockMacros.php @@ -122,6 +122,7 @@ public function macroInclude(MacroNode $node, PhpWriter $writer) if (!$item) { throw new CompileException("Cannot include $name block outside of any block."); } + $name = $item->data->name; } @@ -204,6 +205,7 @@ public function macroExtends(MacroNode $node, PhpWriter $writer): void } else { $this->extends = $writer->write('%node.word%node.args'); } + if (!$this->getCompiler()->isInHead()) { trigger_error($node->getNotation() . ' must be placed in template head.', E_USER_WARNING); } @@ -346,6 +348,7 @@ public function macroBlock(MacroNode $node, PhpWriter $writer): string $tokens->consumeValue(','); } } + if ($args) { $node->data->args = '[' . implode(', ', $args) . '] = $ʟ_args + [' . str_repeat('null, ', count($args)) . '];'; } @@ -430,12 +433,14 @@ public function macroIfset(MacroNode $node, PhpWriter $writer) if (!preg_match('~#|[\w-]+$~DA', $node->args)) { return false; } + $list = []; while (($name = $node->tokenizer->fetchWord()) !== null) { $list[] = preg_match('~#|[\w-]+$~DA', $name) ? '$this->blockQueue["' . ltrim($name, '#') . '"]' : $writer->formatArgs(new Latte\MacroTokens($name)); } + return ($node->name === 'elseifset' ? '} else' : '') . 'if (isset(' . implode(', ', $list) . ')) {'; } diff --git a/src/Latte/Macros/CoreMacros.php b/src/Latte/Macros/CoreMacros.php index e05b6b6ee..f4dafeec8 100644 --- a/src/Latte/Macros/CoreMacros.php +++ b/src/Latte/Macros/CoreMacros.php @@ -505,6 +505,7 @@ public function macroVar(MacroNode $node, PhpWriter $writer): string $res->append($tokens->currentToken()); } } + if ($var === null) { $res->append($node->name === 'default' ? '=>null' : '=null'); } diff --git a/src/Latte/Macros/MacroSet.php b/src/Latte/Macros/MacroSet.php index 45eaf2f08..cedfd0cc7 100644 --- a/src/Latte/Macros/MacroSet.php +++ b/src/Latte/Macros/MacroSet.php @@ -44,6 +44,7 @@ public function addMacro(string $name, $begin, $end = null, $attr = null, int $f if (!$begin && !$end && !$attr) { throw new \InvalidArgumentException("At least one argument must be specified for macro '$name'."); } + foreach ([$begin, $end, $attr] as $arg) { if ($arg && !is_string($arg)) { Latte\Helpers::checkCallback($arg); @@ -110,6 +111,7 @@ public function nodeOpened(MacroNode $node) } elseif (!$node->attrCode) { $node->attrCode = ""; } + $node->context[1] = Latte\Compiler::CONTEXT_HTML_TEXT; } elseif ($node->empty && $node->prefix) { @@ -122,10 +124,10 @@ public function nodeOpened(MacroNode $node) } elseif (!$node->openingCode && is_string($res) && $res !== '') { $node->openingCode = ""; } - } elseif (!$end) { return false; } + return null; } diff --git a/src/Latte/Runtime/Blueprint.php b/src/Latte/Runtime/Blueprint.php index 5c3688758..181113df6 100644 --- a/src/Latte/Runtime/Blueprint.php +++ b/src/Latte/Runtime/Blueprint.php @@ -60,6 +60,7 @@ public function printVars(array $vars): void if (Latte\Helpers::startsWith($name, 'ʟ_')) { continue; } + $type = Php\Type::getType($value) ?: 'mixed'; $res .= "{varType $type $$name}\n"; } @@ -110,14 +111,17 @@ private function printType(?string $type, bool $nullable, ?Php\PhpNamespace $nam if ($type === null) { return ''; } + if ($namespace) { $type = $namespace->unresolveName($type); } + if ($nullable && strcasecmp($type, 'mixed')) { $type = strpos($type, '|') !== false ? $type . '|null' : '?' . $type; } + return $type; } @@ -137,6 +141,7 @@ public function printParameters($function, Php\PhpNamespace $namespace = null): . '$' . $param->getName() . ($param->hasDefaultValue() && !$variadic ? ' = ' . var_export($param->getDefaultValue(), true) : ''); } + return '(' . implode(', ', $params) . ')'; } diff --git a/src/Latte/Runtime/CachingIterator.php b/src/Latte/Runtime/CachingIterator.php index 11b1a5e6d..3d8849cac 100644 --- a/src/Latte/Runtime/CachingIterator.php +++ b/src/Latte/Runtime/CachingIterator.php @@ -45,7 +45,6 @@ public function __construct($iterator) do { $iterator = $iterator->getIterator(); } while (!$iterator instanceof \Iterator); - } elseif ($iterator instanceof \Traversable) { if (!$iterator instanceof \Iterator) { $iterator = new \IteratorIterator($iterator); @@ -192,6 +191,7 @@ public function &__get(string $name) $ret = $this->$m(); return $ret; } + throw new \LogicException('Attempt to read undeclared property ' . static::class . "::\$$name."); } diff --git a/src/Latte/Runtime/FilterExecutor.php b/src/Latte/Runtime/FilterExecutor.php index 5dc72c1d4..92e480248 100644 --- a/src/Latte/Runtime/FilterExecutor.php +++ b/src/Latte/Runtime/FilterExecutor.php @@ -75,6 +75,7 @@ public function add(?string $name, callable $callback) $this->_static[$name] = [$callback, null]; unset($this->$name); } + return $this; } @@ -107,6 +108,7 @@ public function __get(string $name): callable $args[1] = $args[1]->__toString(); $info->contentType = Engine::CONTENT_HTML; } + $res = $callback(...$args); return $info->contentType === Engine::CONTENT_HTML ? new Html($res) @@ -128,6 +130,7 @@ public function __get(string $name): callable return ($this->$name)(...func_get_args()); } } + $hint = ($t = Helpers::getSuggestion(array_keys($this->_static), $name)) ? ", did you mean '$t'?" : '.'; @@ -152,21 +155,25 @@ public function filterContent(string $name, FilterInfo $info, ...$args) } [$callback, $aware] = $this->prepareFilter($lname); + if ($aware) { // FilterInfo aware filter array_unshift($args, $info); return $callback(...$args); } + // classic filter if ($info->contentType !== Engine::CONTENT_TEXT) { trigger_error("Filter |$name is called with incompatible content type " . strtoupper($info->contentType) . ($info->contentType === Engine::CONTENT_HTML ? ', try to prepend |stripHtml.' : '.'), E_USER_WARNING); } + $res = ($this->$name)(...$args); if ($res instanceof HtmlStringable) { trigger_error("Filter |$name should be changed to content-aware filter."); $info->contentType = Engine::CONTENT_HTML; $res = $res->__toString(); } + return $res; } diff --git a/src/Latte/Runtime/Filters.php b/src/Latte/Runtime/Filters.php index d34e63002..069d301ce 100644 --- a/src/Latte/Runtime/Filters.php +++ b/src/Latte/Runtime/Filters.php @@ -48,6 +48,7 @@ public static function escapeHtmlText($s): string if ($s instanceof HtmlStringable || $s instanceof Nette\Utils\IHtmlString) { return $s->__toString(true); } + $s = htmlspecialchars((string) $s, ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8'); $s = str_replace('{{', '{{', $s); return $s; @@ -66,6 +67,7 @@ public static function escapeHtmlAttr($s, bool $double = true): string if (strpos($s, '`') !== false && strpbrk($s, ' <>"\'') === false) { $s .= ' '; // protection against innerHTML mXSS vulnerability nette/nette#1496 } + $s = htmlspecialchars($s, ENT_QUOTES | ENT_HTML5 | ENT_SUBSTITUTE, 'UTF-8', $double); $s = str_replace('{', '{', $s); return $s; @@ -108,10 +110,12 @@ public static function escapeHtmlComment($s): string if ($s && ($s[0] === '-' || $s[0] === '>' || $s[0] === '!')) { $s = ' ' . $s; } + $s = str_replace('--', '- - ', $s); if (substr($s, -1) === '-') { $s .= ' '; } + return $s; } @@ -357,11 +361,13 @@ public static function spacelessHtmlHandler(string $s, int $phase = null): strin $left = substr($s, 0, strlen($s) - strlen($tmp)); $s = $tmp; } + if ($phase & PHP_OUTPUT_HANDLER_FINAL) { $tmp = rtrim($s); $right = substr($s, strlen($tmp)); $s = $tmp; } + return $left . self::spacelessHtml($s, $strip) . $right; } @@ -390,11 +396,13 @@ public static function indent(FilterInfo $info, string $s, int $level = 1, strin if (preg_last_error()) { throw new Latte\RegexpException(null, preg_last_error()); } + $s = preg_replace('#(?:^|[\r\n]+)(?=[^\r\n])#', '$0' . str_repeat($chars, $level), $s); $s = strtr($s, "\x1F\x1E\x1D\x1A", " \t\r\n"); } else { $s = preg_replace('#(?:^|[\r\n]+)(?=[^\r\n])#', '$0' . str_repeat($chars, $level), $s); } + return $s; } @@ -462,8 +470,10 @@ public static function bytes(float $bytes, int $precision = 2): string if (abs($bytes) < 1024 || $unit === end($units)) { break; } + $bytes /= 1024; } + return round($bytes, $precision) . ' ' . $unit; } @@ -488,6 +498,7 @@ public static function replaceRe(string $subject, string $pattern, string $repla if (preg_last_error()) { throw new Latte\RegexpException(null, preg_last_error()); } + return $res; } @@ -501,6 +512,7 @@ public static function dataStream(string $data, string $type = null): string if ($type === null) { $type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data); } + return 'data:' . ($type ? "$type;" : '') . 'base64,' . base64_encode($data); } @@ -523,9 +535,11 @@ public static function substring($s, int $start, int $length = null): string if ($length === null) { $length = self::strLength($s); } + if (function_exists('mb_substr')) { return mb_substr($s, $start, $length, 'UTF-8'); // MB is much faster } + return iconv_substr($s, $start, $length, 'UTF-8'); } @@ -549,6 +563,7 @@ public static function truncate($s, $length, $append = "\u{2026}"): string return self::substring($s, 0, $length) . $append; } } + return $s; } @@ -632,6 +647,7 @@ public static function trim(FilterInfo $info, $s, string $charlist = " \t\n\r\0\ if (preg_last_error()) { throw new Latte\RegexpException(null, preg_last_error()); } + return $s; } @@ -697,6 +713,7 @@ public static function batch($list, int $length, $rest = null): \Generator $batch[] = $rest; } } + yield $batch; } } @@ -722,6 +739,7 @@ public static function htmlAttributes($attrs): string } else { $s .= ' ' . $key; } + continue; } elseif (is_array($value)) { @@ -734,6 +752,7 @@ public static function htmlAttributes($attrs): string : (is_string($k) ? $k . ':' . $v : $v); } } + if ($tmp === null) { continue; } @@ -754,6 +773,7 @@ public static function htmlAttributes($attrs): string . (strpos($value, '`') !== false && strpbrk($value, ' <>"\'') === false ? ' ' : '') . $q; } + return $s; } } diff --git a/src/Latte/Runtime/SnippetDriver.php b/src/Latte/Runtime/SnippetDriver.php index 18e258a74..c56bd19a2 100644 --- a/src/Latte/Runtime/SnippetDriver.php +++ b/src/Latte/Runtime/SnippetDriver.php @@ -49,6 +49,7 @@ public function enter(string $name, string $type): void if (!$this->renderingSnippets) { return; } + $obStarted = false; if ( ($this->nestingLevel === 0 && $this->bridge->needsRedraw($name)) @@ -60,6 +61,7 @@ public function enter(string $name, string $type): void } elseif ($this->nestingLevel > 0) { $this->nestingLevel++; } + $this->stack[] = [$name, $obStarted]; $this->bridge->markRedrawn($name); } @@ -70,6 +72,7 @@ public function leave(): void if (!$this->renderingSnippets) { return; } + [$name, $obStarted] = array_pop($this->stack); if ($this->nestingLevel > 0 && --$this->nestingLevel === 0) { $content = ob_get_clean(); @@ -91,6 +94,7 @@ public function renderSnippets(array $blocks, array $params): bool if ($this->renderingSnippets || !$this->bridge->isSnippetMode()) { return false; } + $this->renderingSnippets = true; $this->bridge->setSnippetMode(false); foreach ($blocks as $name => $function) { @@ -100,6 +104,7 @@ public function renderSnippets(array $blocks, array $params): bool $function = reset($function); $function($params); } + $this->bridge->setSnippetMode(true); $this->bridge->renderChildren(); return true; diff --git a/src/Latte/Runtime/Template.php b/src/Latte/Runtime/Template.php index 7b7371c34..e50f296d5 100644 --- a/src/Latte/Runtime/Template.php +++ b/src/Latte/Runtime/Template.php @@ -221,6 +221,7 @@ public function createTemplate(string $name, array $params, string $referenceTyp foreach ($referred->blockTypes as $nm => $type) { $this->checkBlockContentType($type, $nm); } + $referred->blockQueue = &$this->blockQueue; $referred->blockTypes = &$this->blockTypes; } diff --git a/src/Latte/Sandbox/SecurityPolicy.php b/src/Latte/Sandbox/SecurityPolicy.php index d99e7206d..27bffd5c8 100644 --- a/src/Latte/Sandbox/SecurityPolicy.php +++ b/src/Latte/Sandbox/SecurityPolicy.php @@ -148,11 +148,13 @@ public function isMethodAllowed(string $class, string $method): bool if (isset($res)) { return $res; } + foreach ($this->methods as $c => $methods) { if (is_a($class, $c, true) && (isset($methods[$method]) || isset($methods['*']))) { return $res = true; } } + return $res = false; } @@ -165,11 +167,13 @@ public function isPropertyAllowed(string $class, string $property): bool if (isset($res)) { return $res; } + foreach ($this->properties as $c => $properties) { if (is_a($class, $c, true) && (isset($properties[$property]) || isset($properties['*']))) { return $res = true; } } + return $res = false; } } diff --git a/src/Latte/exceptions.php b/src/Latte/exceptions.php index a3b4a0e88..2b0c9b798 100644 --- a/src/Latte/exceptions.php +++ b/src/Latte/exceptions.php @@ -34,6 +34,7 @@ public function setSource(string $code, ?int $line, string $name = null): self $this->message = rtrim($this->message, '.') . ' in ' . str_replace(dirname($name, 2), '...', $name) . ($line ? ":$line" : ''); } + return $this; } } diff --git a/tests/Latte/CachingIterator.construct.phpt b/tests/Latte/CachingIterator.construct.phpt index 723895907..eb2823713 100644 --- a/tests/Latte/CachingIterator.construct.phpt +++ b/tests/Latte/CachingIterator.construct.phpt @@ -19,6 +19,7 @@ test('array', function () { foreach (new CachingIterator($arr) as $k => $v) { $tmp[] = "$k => $v"; } + Assert::same([ '0 => Nette', '1 => Framework', @@ -32,6 +33,7 @@ test('stdClass', function () { foreach (new CachingIterator($arr) as $k => $v) { $tmp[] = "$k => $v"; } + Assert::same([ '0 => Nette', '1 => Framework', @@ -45,6 +47,7 @@ test('IteratorAggregate', function () { foreach (new CachingIterator($arr) as $k => $v) { $tmp[] = "$k => $v"; } + Assert::same([ '0 => Nette', '1 => Framework', @@ -57,6 +60,7 @@ test('Iterator', function () { foreach (new CachingIterator($arr->getIterator()) as $k => $v) { $tmp[] = "$k => $v"; } + Assert::same([ '0 => Nette', '1 => Framework', @@ -70,6 +74,7 @@ test('SimpleXMLElement', function () { foreach (new CachingIterator($arr) as $k => $v) { $tmp[] = "$k => $v"; } + Assert::same([ 'item => Nette', 'item => Framework', @@ -100,6 +105,7 @@ test('recursive IteratorAggregate', function () { foreach (new CachingIterator($arr) as $k => $v) { $tmp[] = "$k => $v"; } + Assert::same([ '0 => Nette', '1 => Framework', diff --git a/tests/Latte/Filters.general.phpt b/tests/Latte/Filters.general.phpt index 3adc42a08..051880351 100644 --- a/tests/Latte/Filters.general.phpt +++ b/tests/Latte/Filters.general.phpt @@ -31,6 +31,7 @@ function types() foreach (func_get_args() as $arg) { $res[] = gettype($arg); } + return implode(', ', $res); }