N°7854 - ⬆️ Bump twig version

This commit is contained in:
Eric Espie
2024-12-10 10:11:28 +01:00
parent 1bf53bae2a
commit 346564ca0e
140 changed files with 5096 additions and 3444 deletions

View File

@@ -178,6 +178,7 @@ return array(
'CSVBulkExport' => $baseDir . '/core/csvbulkexport.class.inc.php',
'CSVParser' => $baseDir . '/core/csvparser.class.inc.php',
'CSVParserException' => $baseDir . '/application/exceptions/CSVParserException.php',
'CURLStringFile' => $vendorDir . '/symfony/polyfill-php81/Resources/stubs/CURLStringFile.php',
'CellChangeSpec' => $baseDir . '/core/bulkchange.class.inc.php',
'CellStatus_Ambiguous' => $baseDir . '/core/bulkchange.class.inc.php',
'CellStatus_Issue' => $baseDir . '/core/bulkchange.class.inc.php',
@@ -1503,6 +1504,7 @@ return array(
'RestResultWithObjects' => $baseDir . '/core/restservices.class.inc.php',
'RestResultWithRelations' => $baseDir . '/core/restservices.class.inc.php',
'RestUtils' => $baseDir . '/application/applicationextension.inc.php',
'ReturnTypeWillChange' => $vendorDir . '/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php',
'RotatingLogFileNameBuilder' => $baseDir . '/core/log.class.inc.php',
'RowStatus' => $baseDir . '/core/bulkchange.class.inc.php',
'RowStatus_Disappeared' => $baseDir . '/core/bulkchange.class.inc.php',
@@ -2869,6 +2871,7 @@ return array(
'Symfony\\Polyfill\\Php72\\Php72' => $vendorDir . '/symfony/polyfill-php72/Php72.php',
'Symfony\\Polyfill\\Php80\\Php80' => $vendorDir . '/symfony/polyfill-php80/Php80.php',
'Symfony\\Polyfill\\Php80\\PhpToken' => $vendorDir . '/symfony/polyfill-php80/PhpToken.php',
'Symfony\\Polyfill\\Php81\\Php81' => $vendorDir . '/symfony/polyfill-php81/Php81.php',
'Symfony\\Polyfill\\Php83\\Php83' => $vendorDir . '/symfony/polyfill-php83/Php83.php',
'Symfony\\Runtime\\Symfony\\Component\\Console\\ApplicationRuntime' => $vendorDir . '/symfony/runtime/Internal/Console/ApplicationRuntime.php',
'Symfony\\Runtime\\Symfony\\Component\\Console\\Command\\CommandRuntime' => $vendorDir . '/symfony/runtime/Internal/Console/Command/CommandRuntime.php',
@@ -2916,10 +2919,17 @@ return array(
'TriggerOnStateLeave' => $baseDir . '/core/trigger.class.inc.php',
'TriggerOnThresholdReached' => $baseDir . '/core/trigger.class.inc.php',
'TrueExpression' => $baseDir . '/core/oql/expression.class.inc.php',
'Twig\\AbstractTwigCallable' => $vendorDir . '/twig/twig/src/AbstractTwigCallable.php',
'Twig\\Attribute\\FirstClassTwigCallableReady' => $vendorDir . '/twig/twig/src/Attribute/FirstClassTwigCallableReady.php',
'Twig\\Attribute\\YieldReady' => $vendorDir . '/twig/twig/src/Attribute/YieldReady.php',
'Twig\\Cache\\CacheInterface' => $vendorDir . '/twig/twig/src/Cache/CacheInterface.php',
'Twig\\Cache\\ChainCache' => $vendorDir . '/twig/twig/src/Cache/ChainCache.php',
'Twig\\Cache\\FilesystemCache' => $vendorDir . '/twig/twig/src/Cache/FilesystemCache.php',
'Twig\\Cache\\NullCache' => $vendorDir . '/twig/twig/src/Cache/NullCache.php',
'Twig\\Cache\\ReadOnlyFilesystemCache' => $vendorDir . '/twig/twig/src/Cache/ReadOnlyFilesystemCache.php',
'Twig\\Cache\\RemovableCacheInterface' => $vendorDir . '/twig/twig/src/Cache/RemovableCacheInterface.php',
'Twig\\Compiler' => $vendorDir . '/twig/twig/src/Compiler.php',
'Twig\\DeprecatedCallableInfo' => $vendorDir . '/twig/twig/src/DeprecatedCallableInfo.php',
'Twig\\Environment' => $vendorDir . '/twig/twig/src/Environment.php',
'Twig\\Error\\Error' => $vendorDir . '/twig/twig/src/Error/Error.php',
'Twig\\Error\\LoaderError' => $vendorDir . '/twig/twig/src/Error/LoaderError.php',
@@ -2939,6 +2949,7 @@ return array(
'Twig\\Extension\\SandboxExtension' => $vendorDir . '/twig/twig/src/Extension/SandboxExtension.php',
'Twig\\Extension\\StagingExtension' => $vendorDir . '/twig/twig/src/Extension/StagingExtension.php',
'Twig\\Extension\\StringLoaderExtension' => $vendorDir . '/twig/twig/src/Extension/StringLoaderExtension.php',
'Twig\\Extension\\YieldNotReadyExtension' => $vendorDir . '/twig/twig/src/Extension/YieldNotReadyExtension.php',
'Twig\\FileExtensionEscapingStrategy' => $vendorDir . '/twig/twig/src/FileExtensionEscapingStrategy.php',
'Twig\\Lexer' => $vendorDir . '/twig/twig/src/Lexer.php',
'Twig\\Loader\\ArrayLoader' => $vendorDir . '/twig/twig/src/Loader/ArrayLoader.php',
@@ -2949,21 +2960,23 @@ return array(
'Twig\\NodeTraverser' => $vendorDir . '/twig/twig/src/NodeTraverser.php',
'Twig\\NodeVisitor\\AbstractNodeVisitor' => $vendorDir . '/twig/twig/src/NodeVisitor/AbstractNodeVisitor.php',
'Twig\\NodeVisitor\\EscaperNodeVisitor' => $vendorDir . '/twig/twig/src/NodeVisitor/EscaperNodeVisitor.php',
'Twig\\NodeVisitor\\MacroAutoImportNodeVisitor' => $vendorDir . '/twig/twig/src/NodeVisitor/MacroAutoImportNodeVisitor.php',
'Twig\\NodeVisitor\\NodeVisitorInterface' => $vendorDir . '/twig/twig/src/NodeVisitor/NodeVisitorInterface.php',
'Twig\\NodeVisitor\\OptimizerNodeVisitor' => $vendorDir . '/twig/twig/src/NodeVisitor/OptimizerNodeVisitor.php',
'Twig\\NodeVisitor\\SafeAnalysisNodeVisitor' => $vendorDir . '/twig/twig/src/NodeVisitor/SafeAnalysisNodeVisitor.php',
'Twig\\NodeVisitor\\SandboxNodeVisitor' => $vendorDir . '/twig/twig/src/NodeVisitor/SandboxNodeVisitor.php',
'Twig\\NodeVisitor\\YieldNotReadyNodeVisitor' => $vendorDir . '/twig/twig/src/NodeVisitor/YieldNotReadyNodeVisitor.php',
'Twig\\Node\\AutoEscapeNode' => $vendorDir . '/twig/twig/src/Node/AutoEscapeNode.php',
'Twig\\Node\\BlockNode' => $vendorDir . '/twig/twig/src/Node/BlockNode.php',
'Twig\\Node\\BlockReferenceNode' => $vendorDir . '/twig/twig/src/Node/BlockReferenceNode.php',
'Twig\\Node\\BodyNode' => $vendorDir . '/twig/twig/src/Node/BodyNode.php',
'Twig\\Node\\CaptureNode' => $vendorDir . '/twig/twig/src/Node/CaptureNode.php',
'Twig\\Node\\CheckSecurityCallNode' => $vendorDir . '/twig/twig/src/Node/CheckSecurityCallNode.php',
'Twig\\Node\\CheckSecurityNode' => $vendorDir . '/twig/twig/src/Node/CheckSecurityNode.php',
'Twig\\Node\\CheckToStringNode' => $vendorDir . '/twig/twig/src/Node/CheckToStringNode.php',
'Twig\\Node\\DeprecatedNode' => $vendorDir . '/twig/twig/src/Node/DeprecatedNode.php',
'Twig\\Node\\DoNode' => $vendorDir . '/twig/twig/src/Node/DoNode.php',
'Twig\\Node\\EmbedNode' => $vendorDir . '/twig/twig/src/Node/EmbedNode.php',
'Twig\\Node\\EmptyNode' => $vendorDir . '/twig/twig/src/Node/EmptyNode.php',
'Twig\\Node\\Expression\\AbstractExpression' => $vendorDir . '/twig/twig/src/Node/Expression/AbstractExpression.php',
'Twig\\Node\\Expression\\ArrayExpression' => $vendorDir . '/twig/twig/src/Node/Expression/ArrayExpression.php',
'Twig\\Node\\Expression\\ArrowFunctionExpression' => $vendorDir . '/twig/twig/src/Node/Expression/ArrowFunctionExpression.php',
@@ -2997,15 +3010,20 @@ return array(
'Twig\\Node\\Expression\\Binary\\SpaceshipBinary' => $vendorDir . '/twig/twig/src/Node/Expression/Binary/SpaceshipBinary.php',
'Twig\\Node\\Expression\\Binary\\StartsWithBinary' => $vendorDir . '/twig/twig/src/Node/Expression/Binary/StartsWithBinary.php',
'Twig\\Node\\Expression\\Binary\\SubBinary' => $vendorDir . '/twig/twig/src/Node/Expression/Binary/SubBinary.php',
'Twig\\Node\\Expression\\Binary\\XorBinary' => $vendorDir . '/twig/twig/src/Node/Expression/Binary/XorBinary.php',
'Twig\\Node\\Expression\\BlockReferenceExpression' => $vendorDir . '/twig/twig/src/Node/Expression/BlockReferenceExpression.php',
'Twig\\Node\\Expression\\CallExpression' => $vendorDir . '/twig/twig/src/Node/Expression/CallExpression.php',
'Twig\\Node\\Expression\\ConditionalExpression' => $vendorDir . '/twig/twig/src/Node/Expression/ConditionalExpression.php',
'Twig\\Node\\Expression\\ConstantExpression' => $vendorDir . '/twig/twig/src/Node/Expression/ConstantExpression.php',
'Twig\\Node\\Expression\\FilterExpression' => $vendorDir . '/twig/twig/src/Node/Expression/FilterExpression.php',
'Twig\\Node\\Expression\\Filter\\DefaultFilter' => $vendorDir . '/twig/twig/src/Node/Expression/Filter/DefaultFilter.php',
'Twig\\Node\\Expression\\Filter\\RawFilter' => $vendorDir . '/twig/twig/src/Node/Expression/Filter/RawFilter.php',
'Twig\\Node\\Expression\\FunctionExpression' => $vendorDir . '/twig/twig/src/Node/Expression/FunctionExpression.php',
'Twig\\Node\\Expression\\FunctionNode\\EnumCasesFunction' => $vendorDir . '/twig/twig/src/Node/Expression/FunctionNode/EnumCasesFunction.php',
'Twig\\Node\\Expression\\FunctionNode\\EnumFunction' => $vendorDir . '/twig/twig/src/Node/Expression/FunctionNode/EnumFunction.php',
'Twig\\Node\\Expression\\GetAttrExpression' => $vendorDir . '/twig/twig/src/Node/Expression/GetAttrExpression.php',
'Twig\\Node\\Expression\\InlinePrint' => $vendorDir . '/twig/twig/src/Node/Expression/InlinePrint.php',
'Twig\\Node\\Expression\\MacroReferenceExpression' => $vendorDir . '/twig/twig/src/Node/Expression/MacroReferenceExpression.php',
'Twig\\Node\\Expression\\MethodCallExpression' => $vendorDir . '/twig/twig/src/Node/Expression/MethodCallExpression.php',
'Twig\\Node\\Expression\\NameExpression' => $vendorDir . '/twig/twig/src/Node/Expression/NameExpression.php',
'Twig\\Node\\Expression\\NullCoalesceExpression' => $vendorDir . '/twig/twig/src/Node/Expression/NullCoalesceExpression.php',
@@ -3023,6 +3041,13 @@ return array(
'Twig\\Node\\Expression\\Unary\\NegUnary' => $vendorDir . '/twig/twig/src/Node/Expression/Unary/NegUnary.php',
'Twig\\Node\\Expression\\Unary\\NotUnary' => $vendorDir . '/twig/twig/src/Node/Expression/Unary/NotUnary.php',
'Twig\\Node\\Expression\\Unary\\PosUnary' => $vendorDir . '/twig/twig/src/Node/Expression/Unary/PosUnary.php',
'Twig\\Node\\Expression\\Unary\\SpreadUnary' => $vendorDir . '/twig/twig/src/Node/Expression/Unary/SpreadUnary.php',
'Twig\\Node\\Expression\\Unary\\StringCastUnary' => $vendorDir . '/twig/twig/src/Node/Expression/Unary/StringCastUnary.php',
'Twig\\Node\\Expression\\Variable\\AssignContextVariable' => $vendorDir . '/twig/twig/src/Node/Expression/Variable/AssignContextVariable.php',
'Twig\\Node\\Expression\\Variable\\AssignTemplateVariable' => $vendorDir . '/twig/twig/src/Node/Expression/Variable/AssignTemplateVariable.php',
'Twig\\Node\\Expression\\Variable\\ContextVariable' => $vendorDir . '/twig/twig/src/Node/Expression/Variable/ContextVariable.php',
'Twig\\Node\\Expression\\Variable\\LocalVariable' => $vendorDir . '/twig/twig/src/Node/Expression/Variable/LocalVariable.php',
'Twig\\Node\\Expression\\Variable\\TemplateVariable' => $vendorDir . '/twig/twig/src/Node/Expression/Variable/TemplateVariable.php',
'Twig\\Node\\Expression\\VariadicExpression' => $vendorDir . '/twig/twig/src/Node/Expression/VariadicExpression.php',
'Twig\\Node\\FlushNode' => $vendorDir . '/twig/twig/src/Node/FlushNode.php',
'Twig\\Node\\ForLoopNode' => $vendorDir . '/twig/twig/src/Node/ForLoopNode.php',
@@ -3032,14 +3057,18 @@ return array(
'Twig\\Node\\IncludeNode' => $vendorDir . '/twig/twig/src/Node/IncludeNode.php',
'Twig\\Node\\MacroNode' => $vendorDir . '/twig/twig/src/Node/MacroNode.php',
'Twig\\Node\\ModuleNode' => $vendorDir . '/twig/twig/src/Node/ModuleNode.php',
'Twig\\Node\\NameDeprecation' => $vendorDir . '/twig/twig/src/Node/NameDeprecation.php',
'Twig\\Node\\Node' => $vendorDir . '/twig/twig/src/Node/Node.php',
'Twig\\Node\\NodeCaptureInterface' => $vendorDir . '/twig/twig/src/Node/NodeCaptureInterface.php',
'Twig\\Node\\NodeOutputInterface' => $vendorDir . '/twig/twig/src/Node/NodeOutputInterface.php',
'Twig\\Node\\Nodes' => $vendorDir . '/twig/twig/src/Node/Nodes.php',
'Twig\\Node\\PrintNode' => $vendorDir . '/twig/twig/src/Node/PrintNode.php',
'Twig\\Node\\SandboxNode' => $vendorDir . '/twig/twig/src/Node/SandboxNode.php',
'Twig\\Node\\SetNode' => $vendorDir . '/twig/twig/src/Node/SetNode.php',
'Twig\\Node\\TextNode' => $vendorDir . '/twig/twig/src/Node/TextNode.php',
'Twig\\Node\\TypesNode' => $vendorDir . '/twig/twig/src/Node/TypesNode.php',
'Twig\\Node\\WithNode' => $vendorDir . '/twig/twig/src/Node/WithNode.php',
'Twig\\OperatorPrecedenceChange' => $vendorDir . '/twig/twig/src/OperatorPrecedenceChange.php',
'Twig\\Parser' => $vendorDir . '/twig/twig/src/Parser.php',
'Twig\\Profiler\\Dumper\\BaseDumper' => $vendorDir . '/twig/twig/src/Profiler/Dumper/BaseDumper.php',
'Twig\\Profiler\\Dumper\\BlackfireDumper' => $vendorDir . '/twig/twig/src/Profiler/Dumper/BlackfireDumper.php',
@@ -3052,6 +3081,7 @@ return array(
'Twig\\RuntimeLoader\\ContainerRuntimeLoader' => $vendorDir . '/twig/twig/src/RuntimeLoader/ContainerRuntimeLoader.php',
'Twig\\RuntimeLoader\\FactoryRuntimeLoader' => $vendorDir . '/twig/twig/src/RuntimeLoader/FactoryRuntimeLoader.php',
'Twig\\RuntimeLoader\\RuntimeLoaderInterface' => $vendorDir . '/twig/twig/src/RuntimeLoader/RuntimeLoaderInterface.php',
'Twig\\Runtime\\EscaperRuntime' => $vendorDir . '/twig/twig/src/Runtime/EscaperRuntime.php',
'Twig\\Sandbox\\SecurityError' => $vendorDir . '/twig/twig/src/Sandbox/SecurityError.php',
'Twig\\Sandbox\\SecurityNotAllowedFilterError' => $vendorDir . '/twig/twig/src/Sandbox/SecurityNotAllowedFilterError.php',
'Twig\\Sandbox\\SecurityNotAllowedFunctionError' => $vendorDir . '/twig/twig/src/Sandbox/SecurityNotAllowedFunctionError.php',
@@ -3060,9 +3090,12 @@ return array(
'Twig\\Sandbox\\SecurityNotAllowedTagError' => $vendorDir . '/twig/twig/src/Sandbox/SecurityNotAllowedTagError.php',
'Twig\\Sandbox\\SecurityPolicy' => $vendorDir . '/twig/twig/src/Sandbox/SecurityPolicy.php',
'Twig\\Sandbox\\SecurityPolicyInterface' => $vendorDir . '/twig/twig/src/Sandbox/SecurityPolicyInterface.php',
'Twig\\Sandbox\\SourcePolicyInterface' => $vendorDir . '/twig/twig/src/Sandbox/SourcePolicyInterface.php',
'Twig\\Source' => $vendorDir . '/twig/twig/src/Source.php',
'Twig\\Template' => $vendorDir . '/twig/twig/src/Template.php',
'Twig\\TemplateWrapper' => $vendorDir . '/twig/twig/src/TemplateWrapper.php',
'Twig\\Test\\IntegrationTestCase' => $vendorDir . '/twig/twig/src/Test/IntegrationTestCase.php',
'Twig\\Test\\NodeTestCase' => $vendorDir . '/twig/twig/src/Test/NodeTestCase.php',
'Twig\\Token' => $vendorDir . '/twig/twig/src/Token.php',
'Twig\\TokenParser\\AbstractTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/AbstractTokenParser.php',
'Twig\\TokenParser\\ApplyTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/ApplyTokenParser.php',
@@ -3075,6 +3108,7 @@ return array(
'Twig\\TokenParser\\FlushTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/FlushTokenParser.php',
'Twig\\TokenParser\\ForTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/ForTokenParser.php',
'Twig\\TokenParser\\FromTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/FromTokenParser.php',
'Twig\\TokenParser\\GuardTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/GuardTokenParser.php',
'Twig\\TokenParser\\IfTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/IfTokenParser.php',
'Twig\\TokenParser\\ImportTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/ImportTokenParser.php',
'Twig\\TokenParser\\IncludeTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/IncludeTokenParser.php',
@@ -3082,13 +3116,17 @@ return array(
'Twig\\TokenParser\\SandboxTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/SandboxTokenParser.php',
'Twig\\TokenParser\\SetTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/SetTokenParser.php',
'Twig\\TokenParser\\TokenParserInterface' => $vendorDir . '/twig/twig/src/TokenParser/TokenParserInterface.php',
'Twig\\TokenParser\\TypesTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/TypesTokenParser.php',
'Twig\\TokenParser\\UseTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/UseTokenParser.php',
'Twig\\TokenParser\\WithTokenParser' => $vendorDir . '/twig/twig/src/TokenParser/WithTokenParser.php',
'Twig\\TokenStream' => $vendorDir . '/twig/twig/src/TokenStream.php',
'Twig\\TwigCallableInterface' => $vendorDir . '/twig/twig/src/TwigCallableInterface.php',
'Twig\\TwigFilter' => $vendorDir . '/twig/twig/src/TwigFilter.php',
'Twig\\TwigFunction' => $vendorDir . '/twig/twig/src/TwigFunction.php',
'Twig\\TwigTest' => $vendorDir . '/twig/twig/src/TwigTest.php',
'Twig\\Util\\CallableArgumentsExtractor' => $vendorDir . '/twig/twig/src/Util/CallableArgumentsExtractor.php',
'Twig\\Util\\DeprecationCollector' => $vendorDir . '/twig/twig/src/Util/DeprecationCollector.php',
'Twig\\Util\\ReflectionCallable' => $vendorDir . '/twig/twig/src/Util/ReflectionCallable.php',
'Twig\\Util\\TemplateDirIterator' => $vendorDir . '/twig/twig/src/Util/TemplateDirIterator.php',
'UIExtKeyWidget' => $baseDir . '/application/ui.extkeywidget.class.inc.php',
'UIHTMLEditorWidget' => $baseDir . '/application/ui.htmleditorwidget.class.inc.php',

View File

@@ -12,6 +12,11 @@ return array(
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'662a729f963d39afe703c9d9b7ab4a8c' => $vendorDir . '/symfony/polyfill-php83/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
'23c18046f52bef3eea034657bafda50f' => $vendorDir . '/symfony/polyfill-php81/bootstrap.php',
'89efb1254ef2d1c5d80096acd12c4098' => $vendorDir . '/twig/twig/src/Resources/core.php',
'ffecb95d45175fd40f75be8a23b34f90' => $vendorDir . '/twig/twig/src/Resources/debug.php',
'c7baa00073ee9c61edf148c51917cfb4' => $vendorDir . '/twig/twig/src/Resources/escaper.php',
'f844ccf1d25df8663951193c3fc307c8' => $vendorDir . '/twig/twig/src/Resources/string_loader.php',
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',

View File

@@ -11,6 +11,7 @@ return array(
'TheNetworg\\OAuth2\\Client\\' => array($vendorDir . '/thenetworg/oauth2-azure/src'),
'Symfony\\Runtime\\Symfony\\Component\\' => array($vendorDir . '/symfony/runtime/Internal'),
'Symfony\\Polyfill\\Php83\\' => array($vendorDir . '/symfony/polyfill-php83'),
'Symfony\\Polyfill\\Php81\\' => array($vendorDir . '/symfony/polyfill-php81'),
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Polyfill\\Php72\\' => array($vendorDir . '/symfony/polyfill-php72'),
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),

View File

@@ -13,6 +13,11 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
'662a729f963d39afe703c9d9b7ab4a8c' => __DIR__ . '/..' . '/symfony/polyfill-php83/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
'23c18046f52bef3eea034657bafda50f' => __DIR__ . '/..' . '/symfony/polyfill-php81/bootstrap.php',
'89efb1254ef2d1c5d80096acd12c4098' => __DIR__ . '/..' . '/twig/twig/src/Resources/core.php',
'ffecb95d45175fd40f75be8a23b34f90' => __DIR__ . '/..' . '/twig/twig/src/Resources/debug.php',
'c7baa00073ee9c61edf148c51917cfb4' => __DIR__ . '/..' . '/twig/twig/src/Resources/escaper.php',
'f844ccf1d25df8663951193c3fc307c8' => __DIR__ . '/..' . '/twig/twig/src/Resources/string_loader.php',
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
@@ -39,6 +44,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
array (
'Symfony\\Runtime\\Symfony\\Component\\' => 34,
'Symfony\\Polyfill\\Php83\\' => 23,
'Symfony\\Polyfill\\Php81\\' => 23,
'Symfony\\Polyfill\\Php80\\' => 23,
'Symfony\\Polyfill\\Php72\\' => 23,
'Symfony\\Polyfill\\Mbstring\\' => 26,
@@ -132,6 +138,10 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php83',
),
'Symfony\\Polyfill\\Php81\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php81',
),
'Symfony\\Polyfill\\Php80\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
@@ -558,6 +568,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'CSVBulkExport' => __DIR__ . '/../..' . '/core/csvbulkexport.class.inc.php',
'CSVParser' => __DIR__ . '/../..' . '/core/csvparser.class.inc.php',
'CSVParserException' => __DIR__ . '/../..' . '/application/exceptions/CSVParserException.php',
'CURLStringFile' => __DIR__ . '/..' . '/symfony/polyfill-php81/Resources/stubs/CURLStringFile.php',
'CellChangeSpec' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
'CellStatus_Ambiguous' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
'CellStatus_Issue' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
@@ -1883,6 +1894,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'RestResultWithObjects' => __DIR__ . '/../..' . '/core/restservices.class.inc.php',
'RestResultWithRelations' => __DIR__ . '/../..' . '/core/restservices.class.inc.php',
'RestUtils' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
'ReturnTypeWillChange' => __DIR__ . '/..' . '/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php',
'RotatingLogFileNameBuilder' => __DIR__ . '/../..' . '/core/log.class.inc.php',
'RowStatus' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
'RowStatus_Disappeared' => __DIR__ . '/../..' . '/core/bulkchange.class.inc.php',
@@ -3249,6 +3261,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Symfony\\Polyfill\\Php72\\Php72' => __DIR__ . '/..' . '/symfony/polyfill-php72/Php72.php',
'Symfony\\Polyfill\\Php80\\Php80' => __DIR__ . '/..' . '/symfony/polyfill-php80/Php80.php',
'Symfony\\Polyfill\\Php80\\PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/PhpToken.php',
'Symfony\\Polyfill\\Php81\\Php81' => __DIR__ . '/..' . '/symfony/polyfill-php81/Php81.php',
'Symfony\\Polyfill\\Php83\\Php83' => __DIR__ . '/..' . '/symfony/polyfill-php83/Php83.php',
'Symfony\\Runtime\\Symfony\\Component\\Console\\ApplicationRuntime' => __DIR__ . '/..' . '/symfony/runtime/Internal/Console/ApplicationRuntime.php',
'Symfony\\Runtime\\Symfony\\Component\\Console\\Command\\CommandRuntime' => __DIR__ . '/..' . '/symfony/runtime/Internal/Console/Command/CommandRuntime.php',
@@ -3296,10 +3309,17 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'TriggerOnStateLeave' => __DIR__ . '/../..' . '/core/trigger.class.inc.php',
'TriggerOnThresholdReached' => __DIR__ . '/../..' . '/core/trigger.class.inc.php',
'TrueExpression' => __DIR__ . '/../..' . '/core/oql/expression.class.inc.php',
'Twig\\AbstractTwigCallable' => __DIR__ . '/..' . '/twig/twig/src/AbstractTwigCallable.php',
'Twig\\Attribute\\FirstClassTwigCallableReady' => __DIR__ . '/..' . '/twig/twig/src/Attribute/FirstClassTwigCallableReady.php',
'Twig\\Attribute\\YieldReady' => __DIR__ . '/..' . '/twig/twig/src/Attribute/YieldReady.php',
'Twig\\Cache\\CacheInterface' => __DIR__ . '/..' . '/twig/twig/src/Cache/CacheInterface.php',
'Twig\\Cache\\ChainCache' => __DIR__ . '/..' . '/twig/twig/src/Cache/ChainCache.php',
'Twig\\Cache\\FilesystemCache' => __DIR__ . '/..' . '/twig/twig/src/Cache/FilesystemCache.php',
'Twig\\Cache\\NullCache' => __DIR__ . '/..' . '/twig/twig/src/Cache/NullCache.php',
'Twig\\Cache\\ReadOnlyFilesystemCache' => __DIR__ . '/..' . '/twig/twig/src/Cache/ReadOnlyFilesystemCache.php',
'Twig\\Cache\\RemovableCacheInterface' => __DIR__ . '/..' . '/twig/twig/src/Cache/RemovableCacheInterface.php',
'Twig\\Compiler' => __DIR__ . '/..' . '/twig/twig/src/Compiler.php',
'Twig\\DeprecatedCallableInfo' => __DIR__ . '/..' . '/twig/twig/src/DeprecatedCallableInfo.php',
'Twig\\Environment' => __DIR__ . '/..' . '/twig/twig/src/Environment.php',
'Twig\\Error\\Error' => __DIR__ . '/..' . '/twig/twig/src/Error/Error.php',
'Twig\\Error\\LoaderError' => __DIR__ . '/..' . '/twig/twig/src/Error/LoaderError.php',
@@ -3319,6 +3339,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Twig\\Extension\\SandboxExtension' => __DIR__ . '/..' . '/twig/twig/src/Extension/SandboxExtension.php',
'Twig\\Extension\\StagingExtension' => __DIR__ . '/..' . '/twig/twig/src/Extension/StagingExtension.php',
'Twig\\Extension\\StringLoaderExtension' => __DIR__ . '/..' . '/twig/twig/src/Extension/StringLoaderExtension.php',
'Twig\\Extension\\YieldNotReadyExtension' => __DIR__ . '/..' . '/twig/twig/src/Extension/YieldNotReadyExtension.php',
'Twig\\FileExtensionEscapingStrategy' => __DIR__ . '/..' . '/twig/twig/src/FileExtensionEscapingStrategy.php',
'Twig\\Lexer' => __DIR__ . '/..' . '/twig/twig/src/Lexer.php',
'Twig\\Loader\\ArrayLoader' => __DIR__ . '/..' . '/twig/twig/src/Loader/ArrayLoader.php',
@@ -3329,21 +3350,23 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Twig\\NodeTraverser' => __DIR__ . '/..' . '/twig/twig/src/NodeTraverser.php',
'Twig\\NodeVisitor\\AbstractNodeVisitor' => __DIR__ . '/..' . '/twig/twig/src/NodeVisitor/AbstractNodeVisitor.php',
'Twig\\NodeVisitor\\EscaperNodeVisitor' => __DIR__ . '/..' . '/twig/twig/src/NodeVisitor/EscaperNodeVisitor.php',
'Twig\\NodeVisitor\\MacroAutoImportNodeVisitor' => __DIR__ . '/..' . '/twig/twig/src/NodeVisitor/MacroAutoImportNodeVisitor.php',
'Twig\\NodeVisitor\\NodeVisitorInterface' => __DIR__ . '/..' . '/twig/twig/src/NodeVisitor/NodeVisitorInterface.php',
'Twig\\NodeVisitor\\OptimizerNodeVisitor' => __DIR__ . '/..' . '/twig/twig/src/NodeVisitor/OptimizerNodeVisitor.php',
'Twig\\NodeVisitor\\SafeAnalysisNodeVisitor' => __DIR__ . '/..' . '/twig/twig/src/NodeVisitor/SafeAnalysisNodeVisitor.php',
'Twig\\NodeVisitor\\SandboxNodeVisitor' => __DIR__ . '/..' . '/twig/twig/src/NodeVisitor/SandboxNodeVisitor.php',
'Twig\\NodeVisitor\\YieldNotReadyNodeVisitor' => __DIR__ . '/..' . '/twig/twig/src/NodeVisitor/YieldNotReadyNodeVisitor.php',
'Twig\\Node\\AutoEscapeNode' => __DIR__ . '/..' . '/twig/twig/src/Node/AutoEscapeNode.php',
'Twig\\Node\\BlockNode' => __DIR__ . '/..' . '/twig/twig/src/Node/BlockNode.php',
'Twig\\Node\\BlockReferenceNode' => __DIR__ . '/..' . '/twig/twig/src/Node/BlockReferenceNode.php',
'Twig\\Node\\BodyNode' => __DIR__ . '/..' . '/twig/twig/src/Node/BodyNode.php',
'Twig\\Node\\CaptureNode' => __DIR__ . '/..' . '/twig/twig/src/Node/CaptureNode.php',
'Twig\\Node\\CheckSecurityCallNode' => __DIR__ . '/..' . '/twig/twig/src/Node/CheckSecurityCallNode.php',
'Twig\\Node\\CheckSecurityNode' => __DIR__ . '/..' . '/twig/twig/src/Node/CheckSecurityNode.php',
'Twig\\Node\\CheckToStringNode' => __DIR__ . '/..' . '/twig/twig/src/Node/CheckToStringNode.php',
'Twig\\Node\\DeprecatedNode' => __DIR__ . '/..' . '/twig/twig/src/Node/DeprecatedNode.php',
'Twig\\Node\\DoNode' => __DIR__ . '/..' . '/twig/twig/src/Node/DoNode.php',
'Twig\\Node\\EmbedNode' => __DIR__ . '/..' . '/twig/twig/src/Node/EmbedNode.php',
'Twig\\Node\\EmptyNode' => __DIR__ . '/..' . '/twig/twig/src/Node/EmptyNode.php',
'Twig\\Node\\Expression\\AbstractExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/AbstractExpression.php',
'Twig\\Node\\Expression\\ArrayExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/ArrayExpression.php',
'Twig\\Node\\Expression\\ArrowFunctionExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/ArrowFunctionExpression.php',
@@ -3377,15 +3400,20 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Twig\\Node\\Expression\\Binary\\SpaceshipBinary' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Binary/SpaceshipBinary.php',
'Twig\\Node\\Expression\\Binary\\StartsWithBinary' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Binary/StartsWithBinary.php',
'Twig\\Node\\Expression\\Binary\\SubBinary' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Binary/SubBinary.php',
'Twig\\Node\\Expression\\Binary\\XorBinary' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Binary/XorBinary.php',
'Twig\\Node\\Expression\\BlockReferenceExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/BlockReferenceExpression.php',
'Twig\\Node\\Expression\\CallExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/CallExpression.php',
'Twig\\Node\\Expression\\ConditionalExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/ConditionalExpression.php',
'Twig\\Node\\Expression\\ConstantExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/ConstantExpression.php',
'Twig\\Node\\Expression\\FilterExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/FilterExpression.php',
'Twig\\Node\\Expression\\Filter\\DefaultFilter' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Filter/DefaultFilter.php',
'Twig\\Node\\Expression\\Filter\\RawFilter' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Filter/RawFilter.php',
'Twig\\Node\\Expression\\FunctionExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/FunctionExpression.php',
'Twig\\Node\\Expression\\FunctionNode\\EnumCasesFunction' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/FunctionNode/EnumCasesFunction.php',
'Twig\\Node\\Expression\\FunctionNode\\EnumFunction' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/FunctionNode/EnumFunction.php',
'Twig\\Node\\Expression\\GetAttrExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/GetAttrExpression.php',
'Twig\\Node\\Expression\\InlinePrint' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/InlinePrint.php',
'Twig\\Node\\Expression\\MacroReferenceExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/MacroReferenceExpression.php',
'Twig\\Node\\Expression\\MethodCallExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/MethodCallExpression.php',
'Twig\\Node\\Expression\\NameExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/NameExpression.php',
'Twig\\Node\\Expression\\NullCoalesceExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/NullCoalesceExpression.php',
@@ -3403,6 +3431,13 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Twig\\Node\\Expression\\Unary\\NegUnary' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Unary/NegUnary.php',
'Twig\\Node\\Expression\\Unary\\NotUnary' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Unary/NotUnary.php',
'Twig\\Node\\Expression\\Unary\\PosUnary' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Unary/PosUnary.php',
'Twig\\Node\\Expression\\Unary\\SpreadUnary' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Unary/SpreadUnary.php',
'Twig\\Node\\Expression\\Unary\\StringCastUnary' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Unary/StringCastUnary.php',
'Twig\\Node\\Expression\\Variable\\AssignContextVariable' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Variable/AssignContextVariable.php',
'Twig\\Node\\Expression\\Variable\\AssignTemplateVariable' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Variable/AssignTemplateVariable.php',
'Twig\\Node\\Expression\\Variable\\ContextVariable' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Variable/ContextVariable.php',
'Twig\\Node\\Expression\\Variable\\LocalVariable' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Variable/LocalVariable.php',
'Twig\\Node\\Expression\\Variable\\TemplateVariable' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/Variable/TemplateVariable.php',
'Twig\\Node\\Expression\\VariadicExpression' => __DIR__ . '/..' . '/twig/twig/src/Node/Expression/VariadicExpression.php',
'Twig\\Node\\FlushNode' => __DIR__ . '/..' . '/twig/twig/src/Node/FlushNode.php',
'Twig\\Node\\ForLoopNode' => __DIR__ . '/..' . '/twig/twig/src/Node/ForLoopNode.php',
@@ -3412,14 +3447,18 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Twig\\Node\\IncludeNode' => __DIR__ . '/..' . '/twig/twig/src/Node/IncludeNode.php',
'Twig\\Node\\MacroNode' => __DIR__ . '/..' . '/twig/twig/src/Node/MacroNode.php',
'Twig\\Node\\ModuleNode' => __DIR__ . '/..' . '/twig/twig/src/Node/ModuleNode.php',
'Twig\\Node\\NameDeprecation' => __DIR__ . '/..' . '/twig/twig/src/Node/NameDeprecation.php',
'Twig\\Node\\Node' => __DIR__ . '/..' . '/twig/twig/src/Node/Node.php',
'Twig\\Node\\NodeCaptureInterface' => __DIR__ . '/..' . '/twig/twig/src/Node/NodeCaptureInterface.php',
'Twig\\Node\\NodeOutputInterface' => __DIR__ . '/..' . '/twig/twig/src/Node/NodeOutputInterface.php',
'Twig\\Node\\Nodes' => __DIR__ . '/..' . '/twig/twig/src/Node/Nodes.php',
'Twig\\Node\\PrintNode' => __DIR__ . '/..' . '/twig/twig/src/Node/PrintNode.php',
'Twig\\Node\\SandboxNode' => __DIR__ . '/..' . '/twig/twig/src/Node/SandboxNode.php',
'Twig\\Node\\SetNode' => __DIR__ . '/..' . '/twig/twig/src/Node/SetNode.php',
'Twig\\Node\\TextNode' => __DIR__ . '/..' . '/twig/twig/src/Node/TextNode.php',
'Twig\\Node\\TypesNode' => __DIR__ . '/..' . '/twig/twig/src/Node/TypesNode.php',
'Twig\\Node\\WithNode' => __DIR__ . '/..' . '/twig/twig/src/Node/WithNode.php',
'Twig\\OperatorPrecedenceChange' => __DIR__ . '/..' . '/twig/twig/src/OperatorPrecedenceChange.php',
'Twig\\Parser' => __DIR__ . '/..' . '/twig/twig/src/Parser.php',
'Twig\\Profiler\\Dumper\\BaseDumper' => __DIR__ . '/..' . '/twig/twig/src/Profiler/Dumper/BaseDumper.php',
'Twig\\Profiler\\Dumper\\BlackfireDumper' => __DIR__ . '/..' . '/twig/twig/src/Profiler/Dumper/BlackfireDumper.php',
@@ -3432,6 +3471,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Twig\\RuntimeLoader\\ContainerRuntimeLoader' => __DIR__ . '/..' . '/twig/twig/src/RuntimeLoader/ContainerRuntimeLoader.php',
'Twig\\RuntimeLoader\\FactoryRuntimeLoader' => __DIR__ . '/..' . '/twig/twig/src/RuntimeLoader/FactoryRuntimeLoader.php',
'Twig\\RuntimeLoader\\RuntimeLoaderInterface' => __DIR__ . '/..' . '/twig/twig/src/RuntimeLoader/RuntimeLoaderInterface.php',
'Twig\\Runtime\\EscaperRuntime' => __DIR__ . '/..' . '/twig/twig/src/Runtime/EscaperRuntime.php',
'Twig\\Sandbox\\SecurityError' => __DIR__ . '/..' . '/twig/twig/src/Sandbox/SecurityError.php',
'Twig\\Sandbox\\SecurityNotAllowedFilterError' => __DIR__ . '/..' . '/twig/twig/src/Sandbox/SecurityNotAllowedFilterError.php',
'Twig\\Sandbox\\SecurityNotAllowedFunctionError' => __DIR__ . '/..' . '/twig/twig/src/Sandbox/SecurityNotAllowedFunctionError.php',
@@ -3440,9 +3480,12 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Twig\\Sandbox\\SecurityNotAllowedTagError' => __DIR__ . '/..' . '/twig/twig/src/Sandbox/SecurityNotAllowedTagError.php',
'Twig\\Sandbox\\SecurityPolicy' => __DIR__ . '/..' . '/twig/twig/src/Sandbox/SecurityPolicy.php',
'Twig\\Sandbox\\SecurityPolicyInterface' => __DIR__ . '/..' . '/twig/twig/src/Sandbox/SecurityPolicyInterface.php',
'Twig\\Sandbox\\SourcePolicyInterface' => __DIR__ . '/..' . '/twig/twig/src/Sandbox/SourcePolicyInterface.php',
'Twig\\Source' => __DIR__ . '/..' . '/twig/twig/src/Source.php',
'Twig\\Template' => __DIR__ . '/..' . '/twig/twig/src/Template.php',
'Twig\\TemplateWrapper' => __DIR__ . '/..' . '/twig/twig/src/TemplateWrapper.php',
'Twig\\Test\\IntegrationTestCase' => __DIR__ . '/..' . '/twig/twig/src/Test/IntegrationTestCase.php',
'Twig\\Test\\NodeTestCase' => __DIR__ . '/..' . '/twig/twig/src/Test/NodeTestCase.php',
'Twig\\Token' => __DIR__ . '/..' . '/twig/twig/src/Token.php',
'Twig\\TokenParser\\AbstractTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/AbstractTokenParser.php',
'Twig\\TokenParser\\ApplyTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/ApplyTokenParser.php',
@@ -3455,6 +3498,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Twig\\TokenParser\\FlushTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/FlushTokenParser.php',
'Twig\\TokenParser\\ForTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/ForTokenParser.php',
'Twig\\TokenParser\\FromTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/FromTokenParser.php',
'Twig\\TokenParser\\GuardTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/GuardTokenParser.php',
'Twig\\TokenParser\\IfTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/IfTokenParser.php',
'Twig\\TokenParser\\ImportTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/ImportTokenParser.php',
'Twig\\TokenParser\\IncludeTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/IncludeTokenParser.php',
@@ -3462,13 +3506,17 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Twig\\TokenParser\\SandboxTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/SandboxTokenParser.php',
'Twig\\TokenParser\\SetTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/SetTokenParser.php',
'Twig\\TokenParser\\TokenParserInterface' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/TokenParserInterface.php',
'Twig\\TokenParser\\TypesTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/TypesTokenParser.php',
'Twig\\TokenParser\\UseTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/UseTokenParser.php',
'Twig\\TokenParser\\WithTokenParser' => __DIR__ . '/..' . '/twig/twig/src/TokenParser/WithTokenParser.php',
'Twig\\TokenStream' => __DIR__ . '/..' . '/twig/twig/src/TokenStream.php',
'Twig\\TwigCallableInterface' => __DIR__ . '/..' . '/twig/twig/src/TwigCallableInterface.php',
'Twig\\TwigFilter' => __DIR__ . '/..' . '/twig/twig/src/TwigFilter.php',
'Twig\\TwigFunction' => __DIR__ . '/..' . '/twig/twig/src/TwigFunction.php',
'Twig\\TwigTest' => __DIR__ . '/..' . '/twig/twig/src/TwigTest.php',
'Twig\\Util\\CallableArgumentsExtractor' => __DIR__ . '/..' . '/twig/twig/src/Util/CallableArgumentsExtractor.php',
'Twig\\Util\\DeprecationCollector' => __DIR__ . '/..' . '/twig/twig/src/Util/DeprecationCollector.php',
'Twig\\Util\\ReflectionCallable' => __DIR__ . '/..' . '/twig/twig/src/Util/ReflectionCallable.php',
'Twig\\Util\\TemplateDirIterator' => __DIR__ . '/..' . '/twig/twig/src/Util/TemplateDirIterator.php',
'UIExtKeyWidget' => __DIR__ . '/../..' . '/application/ui.extkeywidget.class.inc.php',
'UIHTMLEditorWidget' => __DIR__ . '/../..' . '/application/ui.htmleditorwidget.class.inc.php',

View File

@@ -4162,6 +4162,85 @@
],
"install-path": "../symfony/polyfill-php80"
},
{
"name": "symfony/polyfill-php81",
"version": "v1.31.0",
"version_normalized": "1.31.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php81.git",
"reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
"reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"time": "2024-09-09T11:45:10+00:00",
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"installation-source": "dist",
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php81\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/polyfill-php81"
},
{
"name": "symfony/polyfill-php83",
"version": "v1.28.0",
@@ -5398,33 +5477,41 @@
},
{
"name": "twig/twig",
"version": "v3.8.0",
"version_normalized": "3.8.0.0",
"version": "v3.16.0",
"version_normalized": "3.16.0.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d"
"reference": "475ad2dc97d65d8631393e721e7e44fb544f0561"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/9d15f0ac07f44dc4217883ec6ae02fd555c6f71d",
"reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/475ad2dc97d65d8631393e721e7e44fb544f0561",
"reference": "475ad2dc97d65d8631393e721e7e44fb544f0561",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"php": ">=8.0.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-mbstring": "^1.3",
"symfony/polyfill-php80": "^1.22"
"symfony/polyfill-php81": "^1.29"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"psr/container": "^1.0|^2.0",
"symfony/phpunit-bridge": "^5.4.9|^6.3|^7.0"
"symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
},
"time": "2023-11-21T18:54:41+00:00",
"time": "2024-11-29T08:27:05+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"files": [
"src/Resources/core.php",
"src/Resources/debug.php",
"src/Resources/escaper.php",
"src/Resources/string_loader.php"
],
"psr-4": {
"Twig\\": "src/"
}
@@ -5457,7 +5544,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.8.0"
"source": "https://github.com/twigphp/Twig/tree/v3.16.0"
},
"funding": [
{

View File

@@ -3,7 +3,7 @@
'name' => 'combodo/itop',
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
'reference' => '5ae2fdee94b925808451355b98b941351e0b4fcd',
'reference' => '1bf53bae2a481c76bba07e2f6d3447b4008efacd',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -22,7 +22,7 @@
'combodo/itop' => array(
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
'reference' => '5ae2fdee94b925808451355b98b941351e0b4fcd',
'reference' => '1bf53bae2a481c76bba07e2f6d3447b4008efacd',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -587,6 +587,15 @@
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-php81' => array(
'pretty_version' => 'v1.31.0',
'version' => '1.31.0.0',
'reference' => '4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php81',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-php83' => array(
'pretty_version' => 'v1.28.0',
'version' => '1.28.0.0',
@@ -729,9 +738,9 @@
'dev_requirement' => false,
),
'twig/twig' => array(
'pretty_version' => 'v3.8.0',
'version' => '3.8.0.0',
'reference' => '9d15f0ac07f44dc4217883ec6ae02fd555c6f71d',
'pretty_version' => 'v3.16.0',
'version' => '3.16.0.0',
'reference' => '475ad2dc97d65d8631393e721e7e44fb544f0561',
'type' => 'library',
'install_path' => __DIR__ . '/../twig/twig',
'aliases' => array(),

View File

@@ -1,3 +1,202 @@
# 3.16.0 (2024-XX-XX)
* Deprecate `InlinePrint`
* Fix having macro variables starting with an underscore
* Deprecate not passing a `Source` instance to `TokenStream`
* Deprecate returning `null` from `TwigFilter::getSafe()` and `TwigFunction::getSafe()`, return `[]` instead
# 3.15.0 (2024-11-17)
* [BC BREAK] Add support for accessing class constants with the dot operator;
this can be a BC break if you don't use UPPERCASE constant names
* Add Spanish inflector support for the `plural` and `singular` filters in the String extension
* Deprecate `TempNameExpression` in favor of `LocalVariable`
* Deprecate `NameExpression` in favor of `ContextVariable`
* Deprecate `AssignNameExpression` in favor of `AssignContextVariable`
* Remove `MacroAutoImportNodeVisitor`
* Deprecate `MethodCallExpression` in favor of `MacroReferenceExpression`
* Fix support for the "is defined" test on `_self.xxx` (auto-imported) macros
* Fix support for the "is defined" test on inherited macros
* Add named arguments support for the dot operator arguments (`foo.bar(some: arg)`)
* Add named arguments support for macros
* Add a new `guard` tag that allows to test if some Twig callables are available at compilation time
* Allow arrow functions everywhere
* Deprecate passing a string or an array to Twig callable arguments accepting arrow functions (pass a `\Closure`)
* Add support for triggering deprecations for future operator precedence changes
* Deprecate using the `not` unary operator in an expression with ``*``, ``/``, ``//``, or ``%`` without using explicit parentheses to clarify precedence
* Deprecate using the `??` binary operator without explicit parentheses
* Deprecate using the `~` binary operator in an expression with `+` or `-` without using parentheses to clarify precedence
* Deprecate not passing `AbstractExpression` args to most constructor arguments for classes extending `AbstractExpression`
* Fix `power` expressions with a negative number in parenthesis (`(-1) ** 2`)
* Deprecate instantiating `Node` directly. Use `EmptyNode` or `Nodes` instead.
* Add support for inline comments
* Add `Profile::getStartTime()` and `Profile::getEndTime()`
* Fix "ignore missing" when used on an "embed" tag
* Fix the possibility to override an aliased block (via use)
* Add template cache hot reload
* Allow Twig callable argument names to be free-form (snake-case or camelCase) independently of the PHP callable signature
They were automatically converted to snake-cased before
* Deprecate the `attribute` function; use the `.` notation and wrap the name with parenthesis instead
* Add support for argument unpackaging
* Add JSON support for the file extension escaping strategy
* Support Markup instances (and any other \Stringable) as dynamic mapping keys
* Deprecate the `sandbox` tag
* Improve the way one can deprecate a Twig callable (use `deprecation_info` instead of the other callable options)
* Add the `enum` function
* Add support for logical `xor` operator
# 3.14.2 (2024-11-07)
* Fix an infinite recursion in the sandbox code
# 3.14.1 (2024-11-06)
* [BC BREAK] Fix a security issue in the sandbox mode allowing an attacker to call attributes on Array-like objects
They are now checked via the property policy
* Fix a security issue in the sandbox mode allowing an attacker to be able to call `toString()`
under some circumstances on an object even if the `__toString()` method is not allowed by the security policy
# 3.14.0 (2024-09-09)
* Fix a security issue when an included sandboxed template has been loaded before without the sandbox context
* Add the possibility to reset globals via `Environment::resetGlobals()`
* Deprecate `Environment::mergeGlobals()`
# 3.13.0 (2024-09-07)
* Add the `types` tag (experimental)
* Deprecate the `Twig\Test\NodeTestCase::getTests()` data provider, override `provideTests()` instead.
* Mark `Twig\Test\NodeTestCase::getEnvironment()` as final, override `createEnvironment()` instead.
* Deprecate `Twig\Test\NodeTestCase::getVariableGetter()`, call `createVariableGetter()` instead.
* Deprecate `Twig\Test\NodeTestCase::getAttributeGetter()`, call `createAttributeGetter()` instead.
* Deprecate not overriding `Twig\Test\IntegrationTestCase::getFixturesDirectory()`, this method will be abstract in 4.0
* Marked `Twig\Test\IntegrationTestCase::getTests()` and `getLegacyTests()` as final
# 3.12.0 (2024-08-29)
* Deprecate the fact that the `extends` and `use` tags are always allowed in a sandboxed template.
This behavior will change in 4.0 where these tags will need to be explicitly allowed like any other tag.
* Deprecate the "tag" constructor argument of the "Twig\Node\Node" class as the tag is now automatically set by the Parser when needed
* Fix precedence of two-word tests when the first word is a valid test
* Deprecate the `spaceless` filter
* Deprecate some internal methods from `Parser`: `getBlockStack()`, `hasBlock()`, `getBlock()`, `hasMacro()`, `hasTraits()`, `getParent()`
* Deprecate passing `null` to `Twig\Parser::setParent()`
* Update `Node::__toString()` to include the node tag if set
* Add support for integers in methods of `Twig\Node\Node` that take a Node name
* Deprecate not passing a `BodyNode` instance as the body of a `ModuleNode` or `MacroNode` constructor
* Deprecate returning "null" from "TokenParserInterface::parse()".
* Deprecate `OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES`
* Fix performance regression when `use_yield` is `false` (which is the default)
* Improve compatibility when `use_yield` is `false` (as extensions still using `echo` will work as is)
* Accept colons (`:`) in addition to equals (`=`) to separate argument names and values in named arguments
* Add the `html_cva` function (in the HTML extra package)
* Add support for named arguments to the `block` and `attribute` functions
* Throw a SyntaxError exception at compile time when a Twig callable has not the minimum number of required arguments
* Add a `CallableArgumentsExtractor` class
* Deprecate passing a name to `FunctionExpression`, `FilterExpression`, and `TestExpression`;
pass a `TwigFunction`, `TwigFilter`, or `TestFilter` instead
* Deprecate all Twig callable attributes on `FunctionExpression`, `FilterExpression`, and `TestExpression`
* Deprecate the `filter` node of `FilterExpression`
* Add the notion of Twig callables (functions, filters, and tests)
* Bump minimum PHP version to 8.0
* Fix integration tests when a test has more than one data/expect section and deprecations
* Add the `enum_cases` function
# 3.11.2 (2024-11-06)
* [BC BREAK] Fix a security issue in the sandbox mode allowing an attacker to call attributes on Array-like objects
They are now checked via the property policy
* Fix a security issue in the sandbox mode allowing an attacker to be able to call `toString()`
under some circumstances on an object even if the `__toString()` method is not allowed by the security policy
# 3.11.1 (2024-09-10)
* Fix a security issue when an included sandboxed template has been loaded before without the sandbox context
# 3.11.0 (2024-08-08)
* Deprecate `OptimizerNodeVisitor::OPTIMIZE_RAW_FILTER`
* Add `Twig\Cache\ChainCache` and `Twig\Cache\ReadOnlyFilesystemCache`
* Add the possibility to deprecate attributes and nodes on `Node`
* Add the possibility to add a package and a version to the `deprecated` tag
* Add the possibility to add a package for filter/function/test deprecations
* Mark `ConstantExpression` as being `@final`
* Add the `find` filter
* Fix optimizer mode validation in `OptimizerNodeVisitor`
* Add the possibility to yield from a generator in `PrintNode`
* Add the `shuffle` filter
* Add the `singular` and `plural` filters in `StringExtension`
* Deprecate the second argument of `Twig\Node\Expression\CallExpression::compileArguments()`
* Deprecate `Twig\ExpressionParser\parseHashExpression()` in favor of
`Twig\ExpressionParser::parseMappingExpression()`
* Deprecate `Twig\ExpressionParser\parseArrayExpression()` in favor of
`Twig\ExpressionParser::parseSequenceExpression()`
* Add `sequence` and `mapping` tests
* Deprecate `Twig\Node\Expression\NameExpression::isSimple()` and
`Twig\Node\Expression\NameExpression::isSpecial()`
# 3.10.3 (2024-05-16)
* Fix missing ; in generated code
# 3.10.2 (2024-05-14)
* Fix support for the deprecated escaper signature
# 3.10.1 (2024-05-12)
* Fix BC break on escaper extension
* Fix constant return type
# 3.10.0 (2024-05-11)
* Make `CoreExtension::formatDate`, `CoreExtension::convertDate`, and
`CoreExtension::formatNumber` part of the public API
* Add `needs_charset` option for filters and functions
* Extract the escaping logic from the `EscaperExtension` class to a new
`EscaperRuntime` class.
The following methods from ``Twig\\Extension\\EscaperExtension`` are
deprecated: ``setEscaper()``, ``getEscapers()``, ``setSafeClasses``,
``addSafeClasses()``. Use the same methods on the
``Twig\\Runtime\\EscaperRuntime`` class instead.
* Fix capturing output from extensions that still use echo
* Fix a PHP warning in the Lexer on malformed templates
* Fix blocks not available under some circumstances
* Synchronize source context in templates when setting a Node on a Node
# 3.9.3 (2024-04-18)
* Add missing `twig_escape_filter_is_safe` deprecated function
* Fix yield usage with CaptureNode
* Add missing unwrap call when using a TemplateWrapper instance internally
* Ensure Lexer is initialized early on
# 3.9.2 (2024-04-17)
* Fix usage of display_end hook
# 3.9.1 (2024-04-17)
* Fix missing `$blocks` variable in `CaptureNode`
# 3.9.0 (2024-04-16)
* Add support for PHP 8.4
* Deprecate AbstractNodeVisitor
* Deprecate passing Template to Environment::resolveTemplate(), Environment::load(), and Template::loadTemplate()
* Add a new "yield" mode for output generation;
Node implementations that use "echo" or "print" should use "yield" instead;
all Node implementations should be flagged with `#[YieldReady]` once they've been made ready for "yield";
the "use_yield" Environment option can be turned on when all nodes have been made `#[YieldReady]`;
"yield" will be the only strategy supported in the next major version
* Add return type for Symfony 7 compatibility
* Fix premature loop exit in Security Policy lookup of allowed methods/properties
* Deprecate all internal extension functions in favor of methods on the extension classes
* Mark all extension functions as @internal
* Add SourcePolicyInterface to selectively enable the Sandbox based on a template's Source
* Throw a proper Twig exception when using cycle on an empty array
# 3.8.0 (2023-11-21)
* Catch errors thrown during template rendering
@@ -186,7 +385,7 @@
* removed Parser::isReservedMacroName()
* removed SanboxedPrintNode
* removed Node::setTemplateName()
* made classes maked as "@final" final
* made classes marked as "@final" final
* removed InitRuntimeInterface, ExistsLoaderInterface, and SourceContextLoaderInterface
* removed the "spaceless" tag
* removed Twig\Environment::getBaseTemplateClass() and Twig\Environment::setBaseTemplateClass()

View File

@@ -11,7 +11,7 @@ Sponsors
.. raw:: html
<a href="https://blackfire.io/docs/introduction?utm_source=twig&utm_medium=github_readme&utm_campaign=logo">
<a href="https://docs.blackfire.io/introduction?utm_source=twig&utm_medium=github_readme&utm_campaign=logo">
<img src="https://static.blackfire.io/assets/intemporals/logo/png/blackfire-io_secondary_horizontal_transparent.png?1" width="255px" alt="Blackfire.io">
</a>

View File

@@ -24,16 +24,24 @@
}
],
"require": {
"php": ">=7.2.5",
"symfony/polyfill-php80": "^1.22",
"php": ">=8.0.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-mbstring": "^1.3",
"symfony/polyfill-ctype": "^1.8"
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-php81": "^1.29"
},
"require-dev": {
"symfony/phpunit-bridge": "^5.4.9|^6.3|^7.0",
"psr/container": "^1.0|^2.0"
"symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0",
"psr/container": "^1.0|^2.0",
"phpstan/phpstan": "^2.0"
},
"autoload": {
"files": [
"src/Resources/core.php",
"src/Resources/debug.php",
"src/Resources/escaper.php",
"src/Resources/string_loader.php"
],
"psr-4" : {
"Twig\\" : "src/"
}

View File

@@ -16,7 +16,7 @@ namespace Twig\Cache;
*
* @author Andrew Tch <andrew@noop.lv>
*/
class FilesystemCache implements CacheInterface
class FilesystemCache implements CacheInterface, RemovableCacheInterface
{
public const FORCE_BYTECODE_INVALIDATION = 1;
@@ -50,11 +50,11 @@ class FilesystemCache implements CacheInterface
if (false === @mkdir($dir, 0777, true)) {
clearstatcache(true, $dir);
if (!is_dir($dir)) {
throw new \RuntimeException(sprintf('Unable to create the cache directory (%s).', $dir));
throw new \RuntimeException(\sprintf('Unable to create the cache directory (%s).', $dir));
}
}
} elseif (!is_writable($dir)) {
throw new \RuntimeException(sprintf('Unable to write in the cache directory (%s).', $dir));
throw new \RuntimeException(\sprintf('Unable to write in the cache directory (%s).', $dir));
}
$tmpFile = tempnam($dir, basename($key));
@@ -73,7 +73,15 @@ class FilesystemCache implements CacheInterface
return;
}
throw new \RuntimeException(sprintf('Failed to write cache file "%s".', $key));
throw new \RuntimeException(\sprintf('Failed to write cache file "%s".', $key));
}
public function remove(string $name, string $cls): void
{
$key = $this->generateKey($name, $cls);
if (!@unlink($key) && file_exists($key)) {
throw new \RuntimeException(\sprintf('Failed to delete cache file "%s".', $key));
}
}
public function getTimestamp(string $key): int

View File

@@ -16,7 +16,7 @@ namespace Twig\Cache;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class NullCache implements CacheInterface
final class NullCache implements CacheInterface, RemovableCacheInterface
{
public function generateKey(string $name, string $className): string
{
@@ -35,4 +35,8 @@ final class NullCache implements CacheInterface
{
return 0;
}
public function remove(string $name, string $cls): void
{
}
}

View File

@@ -22,15 +22,16 @@ class Compiler
private $lastLine;
private $source;
private $indentation;
private $env;
private $debugInfo = [];
private $sourceOffset;
private $sourceLine;
private $varNameSalt = 0;
private $didUseEcho = false;
private $didUseEchoStack = [];
public function __construct(Environment $env)
{
$this->env = $env;
public function __construct(
private Environment $env,
) {
}
public function getEnvironment(): Environment
@@ -66,9 +67,20 @@ class Compiler
public function compile(Node $node, int $indentation = 0)
{
$this->reset($indentation);
$node->compile($this);
$this->didUseEchoStack[] = $this->didUseEcho;
return $this;
try {
$this->didUseEcho = false;
$node->compile($this);
if ($this->didUseEcho) {
trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[\Twig\Attribute\YieldReady].', $this->didUseEcho, \get_class($node));
}
return $this;
} finally {
$this->didUseEcho = array_pop($this->didUseEchoStack);
}
}
/**
@@ -76,13 +88,24 @@ class Compiler
*/
public function subcompile(Node $node, bool $raw = true)
{
if (false === $raw) {
if (!$raw) {
$this->source .= str_repeat(' ', $this->indentation * 4);
}
$node->compile($this);
$this->didUseEchoStack[] = $this->didUseEcho;
return $this;
try {
$this->didUseEcho = false;
$node->compile($this);
if ($this->didUseEcho) {
trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[\Twig\Attribute\YieldReady].', $this->didUseEcho, \get_class($node));
}
return $this;
} finally {
$this->didUseEcho = array_pop($this->didUseEchoStack);
}
}
/**
@@ -92,6 +115,7 @@ class Compiler
*/
public function raw(string $string)
{
$this->checkForEcho($string);
$this->source .= $string;
return $this;
@@ -105,6 +129,7 @@ class Compiler
public function write(...$strings)
{
foreach ($strings as $string) {
$this->checkForEcho($string);
$this->source .= str_repeat(' ', $this->indentation * 4).$string;
}
@@ -118,7 +143,7 @@ class Compiler
*/
public function string(string $value)
{
$this->source .= sprintf('"%s"', addcslashes($value, "\0\t\"\$\\"));
$this->source .= \sprintf('"%s"', addcslashes($value, "\0\t\"\$\\"));
return $this;
}
@@ -170,7 +195,7 @@ class Compiler
public function addDebugInfo(Node $node)
{
if ($node->getTemplateLine() != $this->lastLine) {
$this->write(sprintf("// line %d\n", $node->getTemplateLine()));
$this->write(\sprintf("// line %d\n", $node->getTemplateLine()));
$this->sourceLine += substr_count($this->source, "\n", $this->sourceOffset);
$this->sourceOffset = \strlen($this->source);
@@ -218,6 +243,15 @@ class Compiler
public function getVarName(): string
{
return sprintf('__internal_compile_%d', $this->varNameSalt++);
return \sprintf('_v%d', $this->varNameSalt++);
}
private function checkForEcho(string $string): void
{
if ($this->didUseEcho) {
return;
}
$this->didUseEcho = preg_match('/^\s*+(echo|print)\b/', $string, $m) ? $m[1] : false;
}
}

View File

@@ -14,6 +14,7 @@ namespace Twig;
use Twig\Cache\CacheInterface;
use Twig\Cache\FilesystemCache;
use Twig\Cache\NullCache;
use Twig\Cache\RemovableCacheInterface;
use Twig\Error\Error;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
@@ -22,6 +23,7 @@ use Twig\Extension\CoreExtension;
use Twig\Extension\EscaperExtension;
use Twig\Extension\ExtensionInterface;
use Twig\Extension\OptimizerExtension;
use Twig\Extension\YieldNotReadyExtension;
use Twig\Loader\ArrayLoader;
use Twig\Loader\ChainLoader;
use Twig\Loader\LoaderInterface;
@@ -30,6 +32,8 @@ use Twig\Node\Expression\Unary\AbstractUnary;
use Twig\Node\ModuleNode;
use Twig\Node\Node;
use Twig\NodeVisitor\NodeVisitorInterface;
use Twig\Runtime\EscaperRuntime;
use Twig\RuntimeLoader\FactoryRuntimeLoader;
use Twig\RuntimeLoader\RuntimeLoaderInterface;
use Twig\TokenParser\TokenParserInterface;
@@ -40,10 +44,10 @@ use Twig\TokenParser\TokenParserInterface;
*/
class Environment
{
public const VERSION = '3.8.0';
public const VERSION_ID = 30800;
public const VERSION = '3.16.0';
public const VERSION_ID = 31600;
public const MAJOR_VERSION = 3;
public const MINOR_VERSION = 8;
public const MINOR_VERSION = 16;
public const RELEASE_VERSION = 0;
public const EXTRA_VERSION = '';
@@ -60,12 +64,15 @@ class Environment
private $resolvedGlobals;
private $loadedTemplates;
private $strictVariables;
private $templateClassPrefix = '__TwigTemplate_';
private $originalCache;
private $extensionSet;
private $runtimeLoaders = [];
private $runtimes = [];
private $optionsHash;
/** @var bool */
private $useYield;
private $defaultRuntimeLoader;
private array $hotCache = [];
/**
* Constructor.
@@ -97,8 +104,12 @@ class Environment
* * optimizations: A flag that indicates which optimizations to apply
* (default to -1 which means that all optimizations are enabled;
* set it to 0 to disable).
*
* * use_yield: true: forces templates to exclusively use "yield" instead of "echo" (all extensions must be yield ready)
* false (default): allows templates to use a mix of "yield" and "echo" calls to allow for a progressive migration
* Switch to "true" when possible as this will be the only supported mode in Twig 4.0
*/
public function __construct(LoaderInterface $loader, $options = [])
public function __construct(LoaderInterface $loader, array $options = [])
{
$this->setLoader($loader);
@@ -110,20 +121,38 @@ class Environment
'cache' => false,
'auto_reload' => null,
'optimizations' => -1,
'use_yield' => false,
], $options);
$this->useYield = (bool) $options['use_yield'];
$this->debug = (bool) $options['debug'];
$this->setCharset($options['charset'] ?? 'UTF-8');
$this->autoReload = null === $options['auto_reload'] ? $this->debug : (bool) $options['auto_reload'];
$this->strictVariables = (bool) $options['strict_variables'];
$this->setCache($options['cache']);
$this->extensionSet = new ExtensionSet();
$this->defaultRuntimeLoader = new FactoryRuntimeLoader([
EscaperRuntime::class => function () { return new EscaperRuntime($this->charset); },
]);
$this->addExtension(new CoreExtension());
$this->addExtension(new EscaperExtension($options['autoescape']));
$escaperExt = new EscaperExtension($options['autoescape']);
$escaperExt->setEnvironment($this, false);
$this->addExtension($escaperExt);
if (\PHP_VERSION_ID >= 80000) {
$this->addExtension(new YieldNotReadyExtension($this->useYield));
}
$this->addExtension(new OptimizerExtension($options['optimizations']));
}
/**
* @internal
*/
public function useYield(): bool
{
return $this->useYield;
}
/**
* Enables debugging mode.
*/
@@ -206,6 +235,18 @@ class Environment
return $this->strictVariables;
}
public function removeCache(string $name): void
{
$cls = $this->getTemplateClass($name);
$this->hotCache[$name] = $cls.'_'.bin2hex(random_bytes(16));
if ($this->cache instanceof RemovableCacheInterface) {
$this->cache->remove($name, $cls);
} else {
throw new \LogicException(\sprintf('The "%s" cache class does not support removing template cache as it does not implement the "RemovableCacheInterface" interface.', \get_class($this->cache)));
}
}
/**
* Gets the current cache implementation.
*
@@ -249,7 +290,6 @@ class Environment
*
* * The cache key for the given template;
* * The currently enabled extensions;
* * Whether the Twig C extension is available or not;
* * PHP version;
* * Twig version;
* * Options with what environment was created.
@@ -259,11 +299,11 @@ class Environment
*
* @internal
*/
public function getTemplateClass(string $name, int $index = null): string
public function getTemplateClass(string $name, ?int $index = null): string
{
$key = $this->getLoader()->getCacheKey($name).$this->optionsHash;
$key = ($this->hotCache[$name] ?? $this->getLoader()->getCacheKey($name)).$this->optionsHash;
return $this->templateClassPrefix.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $key).(null === $index ? '' : '___'.$index);
return '__TwigTemplate_'.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $key).(null === $index ? '' : '___'.$index);
}
/**
@@ -308,6 +348,11 @@ class Environment
if ($name instanceof TemplateWrapper) {
return $name;
}
if ($name instanceof Template) {
trigger_deprecation('twig/twig', '3.9', 'Passing a "%s" instance to "%s" is deprecated.', self::class, __METHOD__);
return $name;
}
return new TemplateWrapper($this, $this->loadTemplate($this->getTemplateClass($name), $name));
}
@@ -318,8 +363,8 @@ class Environment
* This method is for internal use only and should never be called
* directly.
*
* @param string $name The template name
* @param int $index The index if it is an embedded template
* @param string $name The template name
* @param int|null $index The index if it is an embedded template
*
* @throws LoaderError When the template cannot be found
* @throws RuntimeError When a previously generated cache is corrupted
@@ -327,7 +372,7 @@ class Environment
*
* @internal
*/
public function loadTemplate(string $cls, string $name, int $index = null): Template
public function loadTemplate(string $cls, string $name, ?int $index = null): Template
{
$mainCls = $cls;
if (null !== $index) {
@@ -348,8 +393,10 @@ class Environment
if (!class_exists($cls, false)) {
$source = $this->getLoader()->getSourceContext($name);
$content = $this->compileSource($source);
$this->cache->write($key, $content);
$this->cache->load($key);
if (!isset($this->hotCache[$name])) {
$this->cache->write($key, $content);
$this->cache->load($key);
}
if (!class_exists($mainCls, false)) {
/* Last line of defense if either $this->bcWriteCacheFile was used,
@@ -361,7 +408,7 @@ class Environment
}
if (!class_exists($cls, false)) {
throw new RuntimeError(sprintf('Failed to load Twig template "%s", index "%s": cache might be corrupted.', $name, $index), -1, $source);
throw new RuntimeError(\sprintf('Failed to load Twig template "%s", index "%s": cache might be corrupted.', $name, $index), -1, $source);
}
}
}
@@ -376,19 +423,19 @@ class Environment
*
* This method should not be used as a generic way to load templates.
*
* @param string $template The template source
* @param string $name An optional name of the template to be used in error messages
* @param string $template The template source
* @param string|null $name An optional name of the template to be used in error messages
*
* @throws LoaderError When the template cannot be found
* @throws SyntaxError When an error occurred during compilation
*/
public function createTemplate(string $template, string $name = null): TemplateWrapper
public function createTemplate(string $template, ?string $name = null): TemplateWrapper
{
$hash = hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $template, false);
if (null !== $name) {
$name = sprintf('%s (string template %s)', $name, $hash);
$name = \sprintf('%s (string template %s)', $name, $hash);
} else {
$name = sprintf('__string_template__%s', $hash);
$name = \sprintf('__string_template__%s', $hash);
}
$loader = new ChainLoader([
@@ -421,10 +468,10 @@ class Environment
/**
* Tries to load a template consecutively from an array.
*
* Similar to load() but it also accepts instances of \Twig\Template and
* \Twig\TemplateWrapper, and an array of templates where each is tried to be loaded.
* Similar to load() but it also accepts instances of \Twig\TemplateWrapper
* and an array of templates where each is tried to be loaded.
*
* @param string|TemplateWrapper|array $names A template or an array of templates to try consecutively
* @param string|TemplateWrapper|array<string|TemplateWrapper> $names A template or an array of templates to try consecutively
*
* @throws LoaderError When none of the templates can be found
* @throws SyntaxError When an error occurred during compilation
@@ -438,7 +485,9 @@ class Environment
$count = \count($names);
foreach ($names as $name) {
if ($name instanceof Template) {
return $name;
trigger_deprecation('twig/twig', '3.9', 'Passing a "%s" instance to "%s" is deprecated.', Template::class, __METHOD__);
return new TemplateWrapper($this, $name);
}
if ($name instanceof TemplateWrapper) {
return $name;
@@ -451,7 +500,7 @@ class Environment
return $this->load($name);
}
throw new LoaderError(sprintf('Unable to find one of the following templates: "%s".', implode('", "', $names)));
throw new LoaderError(\sprintf('Unable to find one of the following templates: "%s".', implode('", "', $names)));
}
public function setLexer(Lexer $lexer)
@@ -520,7 +569,7 @@ class Environment
$e->setSourceContext($source);
throw $e;
} catch (\Exception $e) {
throw new SyntaxError(sprintf('An exception has been thrown during the compilation of a template ("%s").', $e->getMessage()), -1, $source, $e);
throw new SyntaxError(\sprintf('An exception has been thrown during the compilation of a template ("%s").', $e->getMessage()), -1, $source, $e);
}
}
@@ -536,7 +585,7 @@ class Environment
public function setCharset(string $charset)
{
if ('UTF8' === $charset = null === $charset ? null : strtoupper($charset)) {
if ('UTF8' === $charset = strtoupper($charset ?: '')) {
// iconv on Windows requires "UTF-8" instead of "UTF8"
$charset = 'UTF-8';
}
@@ -594,7 +643,11 @@ class Environment
}
}
throw new RuntimeError(sprintf('Unable to load the "%s" runtime.', $class));
if (null !== $runtime = $this->defaultRuntimeLoader->load($class)) {
return $this->runtimes[$class] = $runtime;
}
throw new RuntimeError(\sprintf('Unable to load the "%s" runtime.', $class));
}
public function addExtension(ExtensionInterface $extension)
@@ -765,7 +818,7 @@ class Environment
public function addGlobal(string $name, $value)
{
if ($this->extensionSet->isInitialized() && !\array_key_exists($name, $this->getGlobals())) {
throw new \LogicException(sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name));
throw new \LogicException(\sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name));
}
if (null !== $this->resolvedGlobals) {
@@ -776,8 +829,6 @@ class Environment
}
/**
* @internal
*
* @return array<string, mixed>
*/
public function getGlobals(): array
@@ -793,17 +844,20 @@ class Environment
return array_merge($this->extensionSet->getGlobals(), $this->globals);
}
public function resetGlobals(): void
{
$this->resolvedGlobals = null;
$this->extensionSet->resetGlobals();
}
/**
* @deprecated since Twig 3.14
*/
public function mergeGlobals(array $context): array
{
// we don't use array_merge as the context being generally
// bigger than globals, this code is faster.
foreach ($this->getGlobals() as $key => $value) {
if (!\array_key_exists($key, $context)) {
$context[$key] = $value;
}
}
trigger_deprecation('twig/twig', '3.14', 'The "%s" method is deprecated.', __METHOD__);
return $context;
return $context + $this->getGlobals();
}
/**
@@ -835,6 +889,7 @@ class Environment
self::VERSION,
(int) $this->debug,
(int) $this->strictVariables,
$this->useYield ? '1' : '0',
]);
}
}

View File

@@ -53,7 +53,7 @@ class Error extends \Exception
* @param int $lineno The template line where the error occurred
* @param Source|null $source The source context where the error occurred
*/
public function __construct(string $message, int $lineno = -1, Source $source = null, \Throwable $previous = null)
public function __construct(string $message, int $lineno = -1, ?Source $source = null, ?\Throwable $previous = null)
{
parent::__construct('', 0, $previous);
@@ -93,7 +93,7 @@ class Error extends \Exception
return $this->name ? new Source($this->sourceCode, $this->name, $this->sourcePath) : null;
}
public function setSourceContext(Source $source = null): void
public function setSourceContext(?Source $source = null): void
{
if (null === $source) {
$this->sourceCode = $this->name = $this->sourcePath = null;
@@ -142,16 +142,16 @@ class Error extends \Exception
}
if ($this->name) {
if (\is_string($this->name) || (\is_object($this->name) && method_exists($this->name, '__toString'))) {
$name = sprintf('"%s"', $this->name);
if (\is_string($this->name) || $this->name instanceof \Stringable) {
$name = \sprintf('"%s"', $this->name);
} else {
$name = json_encode($this->name);
}
$this->message .= sprintf(' in %s', $name);
$this->message .= \sprintf(' in %s', $name);
}
if ($this->lineno && $this->lineno >= 0) {
$this->message .= sprintf(' at line %d', $this->lineno);
$this->message .= \sprintf(' at line %d', $this->lineno);
}
if ($dot) {

View File

@@ -41,6 +41,6 @@ class SyntaxError extends Error
asort($alternatives);
$this->appendMessage(sprintf(' Did you mean "%s"?', implode('", "', array_keys($alternatives))));
$this->appendMessage(\sprintf(' Did you mean "%s"?', implode('", "', array_keys($alternatives))));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -40,6 +40,6 @@ abstract class AbstractExtension implements ExtensionInterface
public function getOperators()
{
return [];
return [[], []];
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,11 @@
* file that was distributed with this source code.
*/
namespace Twig\Extension {
namespace Twig\Extension;
use Twig\Environment;
use Twig\Template;
use Twig\TemplateWrapper;
use Twig\TwigFunction;
final class DebugExtension extends AbstractExtension
@@ -18,47 +22,41 @@ final class DebugExtension extends AbstractExtension
{
// dump is safe if var_dump is overridden by xdebug
$isDumpOutputHtmlSafe = \extension_loaded('xdebug')
// false means that it was not set (and the default is on) or it explicitly enabled
&& (false === \ini_get('xdebug.overload_var_dump') || \ini_get('xdebug.overload_var_dump'))
// false means that it was not set (and the default is on) or it explicitly enabled
// xdebug.overload_var_dump produces HTML only when html_errors is also enabled
// Xdebug overloads var_dump in develop mode when html_errors is enabled
&& str_contains(\ini_get('xdebug.mode'), 'develop')
&& (false === \ini_get('html_errors') || \ini_get('html_errors'))
|| 'cli' === \PHP_SAPI
;
return [
new TwigFunction('dump', 'twig_var_dump', ['is_safe' => $isDumpOutputHtmlSafe ? ['html'] : [], 'needs_context' => true, 'needs_environment' => true, 'is_variadic' => true]),
new TwigFunction('dump', [self::class, 'dump'], ['is_safe' => $isDumpOutputHtmlSafe ? ['html'] : [], 'needs_context' => true, 'needs_environment' => true, 'is_variadic' => true]),
];
}
}
}
namespace {
use Twig\Environment;
use Twig\Template;
use Twig\TemplateWrapper;
function twig_var_dump(Environment $env, $context, ...$vars)
{
if (!$env->isDebug()) {
return;
}
ob_start();
if (!$vars) {
$vars = [];
foreach ($context as $key => $value) {
if (!$value instanceof Template && !$value instanceof TemplateWrapper) {
$vars[$key] = $value;
}
/**
* @internal
*/
public static function dump(Environment $env, $context, ...$vars)
{
if (!$env->isDebug()) {
return;
}
var_dump($vars);
} else {
var_dump(...$vars);
}
ob_start();
return ob_get_clean();
}
if (!$vars) {
$vars = [];
foreach ($context as $key => $value) {
if (!$value instanceof Template && !$value instanceof TemplateWrapper) {
$vars[$key] = $value;
}
}
var_dump($vars);
} else {
var_dump(...$vars);
}
return ob_get_clean();
}
}

View File

@@ -9,22 +9,24 @@
* file that was distributed with this source code.
*/
namespace Twig\Extension {
namespace Twig\Extension;
use Twig\Environment;
use Twig\FileExtensionEscapingStrategy;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\Filter\RawFilter;
use Twig\Node\Node;
use Twig\NodeVisitor\EscaperNodeVisitor;
use Twig\Runtime\EscaperRuntime;
use Twig\TokenParser\AutoEscapeTokenParser;
use Twig\TwigFilter;
final class EscaperExtension extends AbstractExtension
{
private $defaultStrategy;
private $environment;
private $escapers = [];
/** @internal */
public $safeClasses = [];
/** @internal */
public $safeLookup = [];
private $escaper;
private $defaultStrategy;
/**
* @param string|false|callable $defaultStrategy An escaping strategy
@@ -49,19 +51,43 @@ final class EscaperExtension extends AbstractExtension
public function getFilters(): array
{
return [
new TwigFilter('escape', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']),
new TwigFilter('e', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']),
new TwigFilter('raw', 'twig_raw_filter', ['is_safe' => ['all']]),
new TwigFilter('escape', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]),
new TwigFilter('e', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]),
new TwigFilter('raw', null, ['is_safe' => ['all'], 'node_class' => RawFilter::class]),
];
}
/**
* @deprecated since Twig 3.10
*/
public function setEnvironment(Environment $environment): void
{
$triggerDeprecation = \func_num_args() > 1 ? func_get_arg(1) : true;
if ($triggerDeprecation) {
trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated and not needed if you are using methods from "Twig\Runtime\EscaperRuntime".', __METHOD__);
}
$this->environment = $environment;
$this->escaper = $environment->getRuntime(EscaperRuntime::class);
}
/**
* @deprecated since Twig 3.10
*/
public function setEscaperRuntime(EscaperRuntime $escaper)
{
trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated and not needed if you are using methods from "Twig\Runtime\EscaperRuntime".', __METHOD__);
$this->escaper = $escaper;
}
/**
* Sets the default strategy to use when not defined by the user.
*
* The strategy can be a valid PHP callback that takes the template
* name as an argument and returns the strategy to use.
*
* @param string|false|callable $defaultStrategy An escaping strategy
* @param string|false|callable(string $templateName): string $defaultStrategy An escaping strategy
*/
public function setDefaultStrategy($defaultStrategy): void
{
@@ -93,324 +119,82 @@ final class EscaperExtension extends AbstractExtension
/**
* Defines a new escaper to be used via the escape filter.
*
* @param string $strategy The strategy name that should be used as a strategy in the escape call
* @param callable $callable A valid PHP callable
* @param string $strategy The strategy name that should be used as a strategy in the escape call
* @param callable(Environment, string, string): string $callable A valid PHP callable
*
* @deprecated since Twig 3.10
*/
public function setEscaper($strategy, callable $callable)
{
trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setEscaper()" method instead (be warned that Environment is not passed anymore to the callable).', __METHOD__);
if (!isset($this->environment)) {
throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__));
}
$this->escapers[$strategy] = $callable;
$callable = function ($string, $charset) use ($callable) {
return $callable($this->environment, $string, $charset);
};
$this->escaper->setEscaper($strategy, $callable);
}
/**
* Gets all defined escapers.
*
* @return callable[] An array of escapers
* @return array<string, callable(Environment, string, string): string> An array of escapers
*
* @deprecated since Twig 3.10
*/
public function getEscapers()
{
trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::getEscaper()" method instead.', __METHOD__);
return $this->escapers;
}
/**
* @deprecated since Twig 3.10
*/
public function setSafeClasses(array $safeClasses = [])
{
$this->safeClasses = [];
$this->safeLookup = [];
foreach ($safeClasses as $class => $strategies) {
$this->addSafeClass($class, $strategies);
trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setSafeClasses()" method instead.', __METHOD__);
if (!isset($this->escaper)) {
throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__));
}
$this->escaper->setSafeClasses($safeClasses);
}
/**
* @deprecated since Twig 3.10
*/
public function addSafeClass(string $class, array $strategies)
{
$class = ltrim($class, '\\');
if (!isset($this->safeClasses[$class])) {
$this->safeClasses[$class] = [];
}
$this->safeClasses[$class] = array_merge($this->safeClasses[$class], $strategies);
trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::addSafeClass()" method instead.', __METHOD__);
foreach ($strategies as $strategy) {
$this->safeLookup[$strategy][$class] = true;
}
}
}
}
namespace {
use Twig\Environment;
use Twig\Error\RuntimeError;
use Twig\Extension\EscaperExtension;
use Twig\Markup;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Node;
/**
* Marks a variable as being safe.
*
* @param string $string A PHP variable
*/
function twig_raw_filter($string)
{
return $string;
}
/**
* Escapes a string.
*
* @param mixed $string The value to be escaped
* @param string $strategy The escaping strategy
* @param string $charset The charset
* @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false)
*
* @return string
*/
function twig_escape_filter(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false)
{
if ($autoescape && $string instanceof Markup) {
return $string;
}
if (!\is_string($string)) {
if (\is_object($string) && method_exists($string, '__toString')) {
if ($autoescape) {
$c = \get_class($string);
$ext = $env->getExtension(EscaperExtension::class);
if (!isset($ext->safeClasses[$c])) {
$ext->safeClasses[$c] = [];
foreach (class_parents($string) + class_implements($string) as $class) {
if (isset($ext->safeClasses[$class])) {
$ext->safeClasses[$c] = array_unique(array_merge($ext->safeClasses[$c], $ext->safeClasses[$class]));
foreach ($ext->safeClasses[$class] as $s) {
$ext->safeLookup[$s][$c] = true;
}
}
}
}
if (isset($ext->safeLookup[$strategy][$c]) || isset($ext->safeLookup['all'][$c])) {
return (string) $string;
}
}
$string = (string) $string;
} elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) {
return $string;
}
}
if ('' === $string) {
return '';
}
if (null === $charset) {
$charset = $env->getCharset();
}
switch ($strategy) {
case 'html':
// see https://www.php.net/htmlspecialchars
// Using a static variable to avoid initializing the array
// each time the function is called. Moving the declaration on the
// top of the function slow downs other escaping strategies.
static $htmlspecialcharsCharsets = [
'ISO-8859-1' => true, 'ISO8859-1' => true,
'ISO-8859-15' => true, 'ISO8859-15' => true,
'utf-8' => true, 'UTF-8' => true,
'CP866' => true, 'IBM866' => true, '866' => true,
'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true,
'1251' => true,
'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true,
'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true,
'BIG5' => true, '950' => true,
'GB2312' => true, '936' => true,
'BIG5-HKSCS' => true,
'SHIFT_JIS' => true, 'SJIS' => true, '932' => true,
'EUC-JP' => true, 'EUCJP' => true,
'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true,
];
if (isset($htmlspecialcharsCharsets[$charset])) {
return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset);
}
if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) {
// cache the lowercase variant for future iterations
$htmlspecialcharsCharsets[$charset] = true;
return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset);
}
$string = twig_convert_encoding($string, 'UTF-8', $charset);
$string = htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8');
return iconv('UTF-8', $charset, $string);
case 'js':
// escape all non-alphanumeric characters
// into their \x or \uHHHH representations
if ('UTF-8' !== $charset) {
$string = twig_convert_encoding($string, 'UTF-8', $charset);
}
if (!preg_match('//u', $string)) {
throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
}
$string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) {
$char = $matches[0];
/*
* A few characters have short escape sequences in JSON and JavaScript.
* Escape sequences supported only by JavaScript, not JSON, are omitted.
* \" is also supported but omitted, because the resulting string is not HTML safe.
*/
static $shortMap = [
'\\' => '\\\\',
'/' => '\\/',
"\x08" => '\b',
"\x0C" => '\f',
"\x0A" => '\n',
"\x0D" => '\r',
"\x09" => '\t',
];
if (isset($shortMap[$char])) {
return $shortMap[$char];
}
$codepoint = mb_ord($char, 'UTF-8');
if (0x10000 > $codepoint) {
return sprintf('\u%04X', $codepoint);
}
// Split characters outside the BMP into surrogate pairs
// https://tools.ietf.org/html/rfc2781.html#section-2.1
$u = $codepoint - 0x10000;
$high = 0xD800 | ($u >> 10);
$low = 0xDC00 | ($u & 0x3FF);
return sprintf('\u%04X\u%04X', $high, $low);
}, $string);
if ('UTF-8' !== $charset) {
$string = iconv('UTF-8', $charset, $string);
}
return $string;
case 'css':
if ('UTF-8' !== $charset) {
$string = twig_convert_encoding($string, 'UTF-8', $charset);
}
if (!preg_match('//u', $string)) {
throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
}
$string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) {
$char = $matches[0];
return sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8'));
}, $string);
if ('UTF-8' !== $charset) {
$string = iconv('UTF-8', $charset, $string);
}
return $string;
case 'html_attr':
if ('UTF-8' !== $charset) {
$string = twig_convert_encoding($string, 'UTF-8', $charset);
}
if (!preg_match('//u', $string)) {
throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
}
$string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) {
/**
* This function is adapted from code coming from Zend Framework.
*
* @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://framework.zend.com/license/new-bsd New BSD License
*/
$chr = $matches[0];
$ord = \ord($chr);
/*
* The following replaces characters undefined in HTML with the
* hex entity for the Unicode replacement character.
*/
if (($ord <= 0x1F && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7F && $ord <= 0x9F)) {
return '&#xFFFD;';
}
/*
* Check if the current character to escape has a name entity we should
* replace it with while grabbing the hex value of the character.
*/
if (1 === \strlen($chr)) {
/*
* While HTML supports far more named entities, the lowest common denominator
* has become HTML5's XML Serialisation which is restricted to the those named
* entities that XML supports. Using HTML entities would result in this error:
* XML Parsing Error: undefined entity
*/
static $entityMap = [
34 => '&quot;', /* quotation mark */
38 => '&amp;', /* ampersand */
60 => '&lt;', /* less-than sign */
62 => '&gt;', /* greater-than sign */
];
if (isset($entityMap[$ord])) {
return $entityMap[$ord];
}
return sprintf('&#x%02X;', $ord);
}
/*
* Per OWASP recommendations, we'll use hex entities for any other
* characters where a named entity does not exist.
*/
return sprintf('&#x%04X;', mb_ord($chr, 'UTF-8'));
}, $string);
if ('UTF-8' !== $charset) {
$string = iconv('UTF-8', $charset, $string);
}
return $string;
case 'url':
return rawurlencode($string);
default:
$escapers = $env->getExtension(EscaperExtension::class)->getEscapers();
if (\array_key_exists($strategy, $escapers)) {
return $escapers[$strategy]($env, $string, $charset);
}
$validStrategies = implode(', ', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers)));
throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies));
}
}
/**
* @internal
*/
function twig_escape_filter_is_safe(Node $filterArgs)
{
foreach ($filterArgs as $arg) {
if ($arg instanceof ConstantExpression) {
return [$arg->getAttribute('value')];
if (!isset($this->escaper)) {
throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__));
}
return [];
$this->escaper->addSafeClass($class, $strategies);
}
return ['html'];
}
/**
* @internal
*/
public static function escapeFilterIsSafe(Node $filterArgs)
{
foreach ($filterArgs as $arg) {
if ($arg instanceof ConstantExpression) {
return [$arg->getAttribute('value')];
}
return [];
}
return ['html'];
}
}

View File

@@ -12,9 +12,9 @@
namespace Twig\Extension;
use Twig\ExpressionParser;
use Twig\Node\Expression\Binary\AbstractBinary;
use Twig\Node\Expression\Unary\AbstractUnary;
use Twig\Node\Expression\AbstractExpression;
use Twig\NodeVisitor\NodeVisitorInterface;
use Twig\OperatorPrecedenceChange;
use Twig\TokenParser\TokenParserInterface;
use Twig\TwigFilter;
use Twig\TwigFunction;
@@ -68,8 +68,8 @@ interface ExtensionInterface
* @return array<array> First array of unary operators, second array of binary operators
*
* @psalm-return array{
* array<string, array{precedence: int, class: class-string<AbstractUnary>}>,
* array<string, array{precedence: int, class: class-string<AbstractBinary>, associativity: ExpressionParser::OPERATOR_*}>
* array<string, array{precedence: int, precedence_change?: OperatorPrecedenceChange, class: class-string<AbstractExpression>}>,
* array<string, array{precedence: int, precedence_change?: OperatorPrecedenceChange, class?: class-string<AbstractExpression>, associativity: ExpressionParser::OPERATOR_*}>
* }
*/
public function getOperators();

View File

@@ -12,10 +12,7 @@
namespace Twig\Extension;
/**
* Enables usage of the deprecated Twig\Extension\AbstractExtension::getGlobals() method.
*
* Explicitly implement this interface if you really need to implement the
* deprecated getGlobals() method in your extensions.
* Allows Twig extensions to add globals to the context.
*
* @author Fabien Potencier <fabien@symfony.com>
*/

View File

@@ -15,11 +15,9 @@ use Twig\NodeVisitor\OptimizerNodeVisitor;
final class OptimizerExtension extends AbstractExtension
{
private $optimizers;
public function __construct(int $optimizers = -1)
{
$this->optimizers = $optimizers;
public function __construct(
private int $optimizers = -1,
) {
}
public function getNodeVisitors(): array

View File

@@ -15,6 +15,7 @@ use Twig\NodeVisitor\SandboxNodeVisitor;
use Twig\Sandbox\SecurityNotAllowedMethodError;
use Twig\Sandbox\SecurityNotAllowedPropertyError;
use Twig\Sandbox\SecurityPolicyInterface;
use Twig\Sandbox\SourcePolicyInterface;
use Twig\Source;
use Twig\TokenParser\SandboxTokenParser;
@@ -23,11 +24,13 @@ final class SandboxExtension extends AbstractExtension
private $sandboxedGlobally;
private $sandboxed;
private $policy;
private $sourcePolicy;
public function __construct(SecurityPolicyInterface $policy, $sandboxed = false)
public function __construct(SecurityPolicyInterface $policy, $sandboxed = false, ?SourcePolicyInterface $sourcePolicy = null)
{
$this->policy = $policy;
$this->sandboxedGlobally = $sandboxed;
$this->sourcePolicy = $sourcePolicy;
}
public function getTokenParsers(): array
@@ -50,9 +53,9 @@ final class SandboxExtension extends AbstractExtension
$this->sandboxed = false;
}
public function isSandboxed(): bool
public function isSandboxed(?Source $source = null): bool
{
return $this->sandboxedGlobally || $this->sandboxed;
return $this->sandboxedGlobally || $this->sandboxed || $this->isSourceSandboxed($source);
}
public function isSandboxedGlobally(): bool
@@ -60,6 +63,15 @@ final class SandboxExtension extends AbstractExtension
return $this->sandboxedGlobally;
}
private function isSourceSandboxed(?Source $source): bool
{
if (null === $source || null === $this->sourcePolicy) {
return false;
}
return $this->sourcePolicy->enableSandbox($source);
}
public function setSecurityPolicy(SecurityPolicyInterface $policy)
{
$this->policy = $policy;
@@ -70,16 +82,16 @@ final class SandboxExtension extends AbstractExtension
return $this->policy;
}
public function checkSecurity($tags, $filters, $functions): void
public function checkSecurity($tags, $filters, $functions, ?Source $source = null): void
{
if ($this->isSandboxed()) {
if ($this->isSandboxed($source)) {
$this->policy->checkSecurity($tags, $filters, $functions);
}
}
public function checkMethodAllowed($obj, $method, int $lineno = -1, Source $source = null): void
public function checkMethodAllowed($obj, $method, int $lineno = -1, ?Source $source = null): void
{
if ($this->isSandboxed()) {
if ($this->isSandboxed($source)) {
try {
$this->policy->checkMethodAllowed($obj, $method);
} catch (SecurityNotAllowedMethodError $e) {
@@ -91,9 +103,9 @@ final class SandboxExtension extends AbstractExtension
}
}
public function checkPropertyAllowed($obj, $property, int $lineno = -1, Source $source = null): void
public function checkPropertyAllowed($obj, $property, int $lineno = -1, ?Source $source = null): void
{
if ($this->isSandboxed()) {
if ($this->isSandboxed($source)) {
try {
$this->policy->checkPropertyAllowed($obj, $property);
} catch (SecurityNotAllowedPropertyError $e) {
@@ -105,9 +117,15 @@ final class SandboxExtension extends AbstractExtension
}
}
public function ensureToStringAllowed($obj, int $lineno = -1, Source $source = null)
public function ensureToStringAllowed($obj, int $lineno = -1, ?Source $source = null)
{
if ($this->isSandboxed() && \is_object($obj) && method_exists($obj, '__toString')) {
if (\is_array($obj)) {
$this->ensureToStringAllowedForArray($obj, $lineno, $source);
return $obj;
}
if ($obj instanceof \Stringable && $this->isSandboxed($source)) {
try {
$this->policy->checkMethodAllowed($obj, '__toString');
} catch (SecurityNotAllowedMethodError $e) {
@@ -120,4 +138,28 @@ final class SandboxExtension extends AbstractExtension
return $obj;
}
private function ensureToStringAllowedForArray(array $obj, int $lineno, ?Source $source, array &$stack = []): void
{
foreach ($obj as $k => $v) {
if (!$v) {
continue;
}
if (!\is_array($v)) {
$this->ensureToStringAllowed($v, $lineno, $source);
continue;
}
if ($r = \ReflectionReference::fromArrayElement($obj, $k)) {
if (isset($stack[$r->getId()])) {
continue;
}
$stack[$r->getId()] = true;
}
$this->ensureToStringAllowedForArray($v, $lineno, $source, $stack);
}
}
}

View File

@@ -35,7 +35,7 @@ final class StagingExtension extends AbstractExtension
public function addFunction(TwigFunction $function): void
{
if (isset($this->functions[$function->getName()])) {
throw new \LogicException(sprintf('Function "%s" is already registered.', $function->getName()));
throw new \LogicException(\sprintf('Function "%s" is already registered.', $function->getName()));
}
$this->functions[$function->getName()] = $function;
@@ -49,7 +49,7 @@ final class StagingExtension extends AbstractExtension
public function addFilter(TwigFilter $filter): void
{
if (isset($this->filters[$filter->getName()])) {
throw new \LogicException(sprintf('Filter "%s" is already registered.', $filter->getName()));
throw new \LogicException(\sprintf('Filter "%s" is already registered.', $filter->getName()));
}
$this->filters[$filter->getName()] = $filter;
@@ -73,7 +73,7 @@ final class StagingExtension extends AbstractExtension
public function addTokenParser(TokenParserInterface $parser): void
{
if (isset($this->tokenParsers[$parser->getTag()])) {
throw new \LogicException(sprintf('Tag "%s" is already registered.', $parser->getTag()));
throw new \LogicException(\sprintf('Tag "%s" is already registered.', $parser->getTag()));
}
$this->tokenParsers[$parser->getTag()] = $parser;
@@ -87,7 +87,7 @@ final class StagingExtension extends AbstractExtension
public function addTest(TwigTest $test): void
{
if (isset($this->tests[$test->getName()])) {
throw new \LogicException(sprintf('Test "%s" is already registered.', $test->getName()));
throw new \LogicException(\sprintf('Test "%s" is already registered.', $test->getName()));
}
$this->tests[$test->getName()] = $test;

View File

@@ -9,7 +9,10 @@
* file that was distributed with this source code.
*/
namespace Twig\Extension {
namespace Twig\Extension;
use Twig\Environment;
use Twig\TemplateWrapper;
use Twig\TwigFunction;
final class StringLoaderExtension extends AbstractExtension
@@ -17,26 +20,21 @@ final class StringLoaderExtension extends AbstractExtension
public function getFunctions(): array
{
return [
new TwigFunction('template_from_string', 'twig_template_from_string', ['needs_environment' => true]),
new TwigFunction('template_from_string', [self::class, 'templateFromString'], ['needs_environment' => true]),
];
}
}
}
namespace {
use Twig\Environment;
use Twig\TemplateWrapper;
/**
* Loads a template from a string.
*
* {{ include(template_from_string("Hello {{ name }}")) }}
*
* @param string $template A template as a string or object implementing __toString()
* @param string $name An optional name of the template to be used in error messages
*/
function twig_template_from_string(Environment $env, $template, string $name = null): TemplateWrapper
{
return $env->createTemplate((string) $template, $name);
}
/**
* Loads a template from a string.
*
* {{ include(template_from_string("Hello {{ name }}")) }}
*
* @param string|null $name An optional name of the template to be used in error messages
*
* @internal
*/
public static function templateFromString(Environment $env, string|\Stringable $template, ?string $name = null): TemplateWrapper
{
return $env->createTemplate((string) $template, $name);
}
}

View File

@@ -15,8 +15,7 @@ use Twig\Error\RuntimeError;
use Twig\Extension\ExtensionInterface;
use Twig\Extension\GlobalsInterface;
use Twig\Extension\StagingExtension;
use Twig\Node\Expression\Binary\AbstractBinary;
use Twig\Node\Expression\Unary\AbstractUnary;
use Twig\Node\Expression\AbstractExpression;
use Twig\NodeVisitor\NodeVisitorInterface;
use Twig\TokenParser\TokenParserInterface;
@@ -35,15 +34,21 @@ final class ExtensionSet
private $visitors;
/** @var array<string, TwigFilter> */
private $filters;
/** @var array<string, TwigFilter> */
private $dynamicFilters;
/** @var array<string, TwigTest> */
private $tests;
/** @var array<string, TwigTest> */
private $dynamicTests;
/** @var array<string, TwigFunction> */
private $functions;
/** @var array<string, array{precedence: int, class: class-string<AbstractUnary>}> */
/** @var array<string, TwigFunction> */
private $dynamicFunctions;
/** @var array<string, array{precedence: int, class: class-string<AbstractExpression>}> */
private $unaryOperators;
/** @var array<string, array{precedence: int, class: class-string<AbstractBinary>, associativity: ExpressionParser::OPERATOR_*}> */
/** @var array<string, array{precedence: int, class?: class-string<AbstractExpression>, associativity: ExpressionParser::OPERATOR_*}> */
private $binaryOperators;
/** @var array<string, mixed> */
/** @var array<string, mixed>|null */
private $globals;
private $functionCallbacks = [];
private $filterCallbacks = [];
@@ -70,7 +75,7 @@ final class ExtensionSet
$class = ltrim($class, '\\');
if (!isset($this->extensions[$class])) {
throw new RuntimeError(sprintf('The "%s" extension is not enabled.', $class));
throw new RuntimeError(\sprintf('The "%s" extension is not enabled.', $class));
}
return $this->extensions[$class];
@@ -125,11 +130,11 @@ final class ExtensionSet
$class = \get_class($extension);
if ($this->initialized) {
throw new \LogicException(sprintf('Unable to register extension "%s" as extensions have already been initialized.', $class));
throw new \LogicException(\sprintf('Unable to register extension "%s" as extensions have already been initialized.', $class));
}
if (isset($this->extensions[$class])) {
throw new \LogicException(sprintf('Unable to register extension "%s" as it is already registered.', $class));
throw new \LogicException(\sprintf('Unable to register extension "%s" as it is already registered.', $class));
}
$this->extensions[$class] = $extension;
@@ -138,7 +143,7 @@ final class ExtensionSet
public function addFunction(TwigFunction $function): void
{
if ($this->initialized) {
throw new \LogicException(sprintf('Unable to add function "%s" as extensions have already been initialized.', $function->getName()));
throw new \LogicException(\sprintf('Unable to add function "%s" as extensions have already been initialized.', $function->getName()));
}
$this->staging->addFunction($function);
@@ -166,14 +171,11 @@ final class ExtensionSet
return $this->functions[$name];
}
foreach ($this->functions as $pattern => $function) {
$pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);
if ($count && preg_match('#^'.$pattern.'$#', $name, $matches)) {
foreach ($this->dynamicFunctions as $pattern => $function) {
if (preg_match($pattern, $name, $matches)) {
array_shift($matches);
$function->setArguments($matches);
return $function;
return $function->withDynamicArguments($name, $function->getName(), $matches);
}
}
@@ -194,7 +196,7 @@ final class ExtensionSet
public function addFilter(TwigFilter $filter): void
{
if ($this->initialized) {
throw new \LogicException(sprintf('Unable to add filter "%s" as extensions have already been initialized.', $filter->getName()));
throw new \LogicException(\sprintf('Unable to add filter "%s" as extensions have already been initialized.', $filter->getName()));
}
$this->staging->addFilter($filter);
@@ -222,14 +224,11 @@ final class ExtensionSet
return $this->filters[$name];
}
foreach ($this->filters as $pattern => $filter) {
$pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);
if ($count && preg_match('#^'.$pattern.'$#', $name, $matches)) {
foreach ($this->dynamicFilters as $pattern => $filter) {
if (preg_match($pattern, $name, $matches)) {
array_shift($matches);
$filter->setArguments($matches);
return $filter;
return $filter->withDynamicArguments($name, $filter->getName(), $matches);
}
}
@@ -328,12 +327,7 @@ final class ExtensionSet
continue;
}
$extGlobals = $extension->getGlobals();
if (!\is_array($extGlobals)) {
throw new \UnexpectedValueException(sprintf('"%s::getGlobals()" must return an array of globals.', \get_class($extension)));
}
$globals = array_merge($globals, $extGlobals);
$globals = array_merge($globals, $extension->getGlobals());
}
if ($this->initialized) {
@@ -343,10 +337,15 @@ final class ExtensionSet
return $globals;
}
public function resetGlobals(): void
{
$this->globals = null;
}
public function addTest(TwigTest $test): void
{
if ($this->initialized) {
throw new \LogicException(sprintf('Unable to add test "%s" as extensions have already been initialized.', $test->getName()));
throw new \LogicException(\sprintf('Unable to add test "%s" as extensions have already been initialized.', $test->getName()));
}
$this->staging->addTest($test);
@@ -374,16 +373,11 @@ final class ExtensionSet
return $this->tests[$name];
}
foreach ($this->tests as $pattern => $test) {
$pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);
foreach ($this->dynamicTests as $pattern => $test) {
if (preg_match($pattern, $name, $matches)) {
array_shift($matches);
if ($count) {
if (preg_match('#^'.$pattern.'$#', $name, $matches)) {
array_shift($matches);
$test->setArguments($matches);
return $test;
}
return $test->withDynamicArguments($name, $test->getName(), $matches);
}
}
@@ -391,7 +385,7 @@ final class ExtensionSet
}
/**
* @return array<string, array{precedence: int, class: class-string<AbstractUnary>}>
* @return array<string, array{precedence: int, class: class-string<AbstractExpression>}>
*/
public function getUnaryOperators(): array
{
@@ -403,7 +397,7 @@ final class ExtensionSet
}
/**
* @return array<string, array{precedence: int, class: class-string<AbstractBinary>, associativity: ExpressionParser::OPERATOR_*}>
* @return array<string, array{precedence: int, class?: class-string<AbstractExpression>, associativity: ExpressionParser::OPERATOR_*}>
*/
public function getBinaryOperators(): array
{
@@ -420,6 +414,9 @@ final class ExtensionSet
$this->filters = [];
$this->functions = [];
$this->tests = [];
$this->dynamicFilters = [];
$this->dynamicFunctions = [];
$this->dynamicTests = [];
$this->visitors = [];
$this->unaryOperators = [];
$this->binaryOperators = [];
@@ -436,17 +433,26 @@ final class ExtensionSet
{
// filters
foreach ($extension->getFilters() as $filter) {
$this->filters[$filter->getName()] = $filter;
$this->filters[$name = $filter->getName()] = $filter;
if (str_contains($name, '*')) {
$this->dynamicFilters['#^'.str_replace('\\*', '(.*?)', preg_quote($name, '#')).'$#'] = $filter;
}
}
// functions
foreach ($extension->getFunctions() as $function) {
$this->functions[$function->getName()] = $function;
$this->functions[$name = $function->getName()] = $function;
if (str_contains($name, '*')) {
$this->dynamicFunctions['#^'.str_replace('\\*', '(.*?)', preg_quote($name, '#')).'$#'] = $function;
}
}
// tests
foreach ($extension->getTests() as $test) {
$this->tests[$test->getName()] = $test;
$this->tests[$name = $test->getName()] = $test;
if (str_contains($name, '*')) {
$this->dynamicTests['#^'.str_replace('\\*', '(.*?)', preg_quote($name, '#')).'$#'] = $test;
}
}
// token parsers
@@ -466,11 +472,11 @@ final class ExtensionSet
// operators
if ($operators = $extension->getOperators()) {
if (!\is_array($operators)) {
throw new \InvalidArgumentException(sprintf('"%s::getOperators()" must return an array with operators, got "%s".', \get_class($extension), \is_object($operators) ? \get_class($operators) : \gettype($operators).(\is_resource($operators) ? '' : '#'.$operators)));
throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array with operators, got "%s".', \get_class($extension), get_debug_type($operators).(\is_resource($operators) ? '' : '#'.$operators)));
}
if (2 !== \count($operators)) {
throw new \InvalidArgumentException(sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators)));
throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators)));
}
$this->unaryOperators = array_merge($this->unaryOperators, $operators[0]);

View File

@@ -45,6 +45,7 @@ class FileExtensionEscapingStrategy
switch ($extension) {
case 'js':
case 'json':
return 'js';
case 'css':

View File

@@ -48,8 +48,17 @@ class Lexer
public const REGEX_STRING = '/"([^#"\\\\]*(?:\\\\.[^#"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As';
public const REGEX_DQ_STRING_DELIM = '/"/A';
public const REGEX_DQ_STRING_PART = '/[^#"\\\\]*(?:(?:\\\\.|#(?!\{))[^#"\\\\]*)*/As';
public const REGEX_INLINE_COMMENT = '/#[^\n]*/A';
public const PUNCTUATION = '()[]{}?:.,|';
private const SPECIAL_CHARS = [
'f' => "\f",
'n' => "\n",
'r' => "\r",
't' => "\t",
'v' => "\v",
];
public function __construct(Environment $env, array $options = [])
{
$this->env = $env;
@@ -71,8 +80,6 @@ class Lexer
return;
}
$this->isInitialized = true;
// when PHP 7.3 is the min version, we will be able to remove the '#' part in preg_quote as it's part of the default
$this->regexes = [
// }}
@@ -160,6 +167,8 @@ class Lexer
'interpolation_start' => '{'.preg_quote($this->options['interpolation'][0], '#').'\s*}A',
'interpolation_end' => '{\s*'.preg_quote($this->options['interpolation'][1], '#').'}A',
];
$this->isInitialized = true;
}
public function tokenize(Source $source): TokenStream
@@ -207,11 +216,11 @@ class Lexer
}
}
$this->pushToken(/* Token::EOF_TYPE */ -1);
$this->pushToken(Token::EOF_TYPE);
if (!empty($this->brackets)) {
list($expect, $lineno) = array_pop($this->brackets);
throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
if ($this->brackets) {
[$expect, $lineno] = array_pop($this->brackets);
throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
}
return new TokenStream($this->tokens, $this->source);
@@ -221,7 +230,7 @@ class Lexer
{
// if no matches are left we return the rest of the template as simple text token
if ($this->position == \count($this->positions[0]) - 1) {
$this->pushToken(/* Token::TEXT_TYPE */ 0, substr($this->code, $this->cursor));
$this->pushToken(Token::TEXT_TYPE, substr($this->code, $this->cursor));
$this->cursor = $this->end;
return;
@@ -250,7 +259,7 @@ class Lexer
$text = rtrim($text, " \t\0\x0B");
}
}
$this->pushToken(/* Token::TEXT_TYPE */ 0, $text);
$this->pushToken(Token::TEXT_TYPE, $text);
$this->moveCursor($textContent.$position[0]);
switch ($this->positions[1][$this->position][0]) {
@@ -268,14 +277,14 @@ class Lexer
$this->moveCursor($match[0]);
$this->lineno = (int) $match[1];
} else {
$this->pushToken(/* Token::BLOCK_START_TYPE */ 1);
$this->pushToken(Token::BLOCK_START_TYPE);
$this->pushState(self::STATE_BLOCK);
$this->currentVarBlockLine = $this->lineno;
}
break;
case $this->options['tag_variable'][0]:
$this->pushToken(/* Token::VAR_START_TYPE */ 2);
$this->pushToken(Token::VAR_START_TYPE);
$this->pushState(self::STATE_VAR);
$this->currentVarBlockLine = $this->lineno;
break;
@@ -284,8 +293,8 @@ class Lexer
private function lexBlock(): void
{
if (empty($this->brackets) && preg_match($this->regexes['lex_block'], $this->code, $match, 0, $this->cursor)) {
$this->pushToken(/* Token::BLOCK_END_TYPE */ 3);
if (!$this->brackets && preg_match($this->regexes['lex_block'], $this->code, $match, 0, $this->cursor)) {
$this->pushToken(Token::BLOCK_END_TYPE);
$this->moveCursor($match[0]);
$this->popState();
} else {
@@ -295,8 +304,8 @@ class Lexer
private function lexVar(): void
{
if (empty($this->brackets) && preg_match($this->regexes['lex_var'], $this->code, $match, 0, $this->cursor)) {
$this->pushToken(/* Token::VAR_END_TYPE */ 4);
if (!$this->brackets && preg_match($this->regexes['lex_var'], $this->code, $match, 0, $this->cursor)) {
$this->pushToken(Token::VAR_END_TYPE);
$this->moveCursor($match[0]);
$this->popState();
} else {
@@ -311,7 +320,7 @@ class Lexer
$this->moveCursor($match[0]);
if ($this->cursor >= $this->end) {
throw new SyntaxError(sprintf('Unclosed "%s".', self::STATE_BLOCK === $this->state ? 'block' : 'variable'), $this->currentVarBlockLine, $this->source);
throw new SyntaxError(\sprintf('Unclosed "%s".', self::STATE_BLOCK === $this->state ? 'block' : 'variable'), $this->currentVarBlockLine, $this->source);
}
}
@@ -321,18 +330,18 @@ class Lexer
$this->moveCursor('...');
}
// arrow function
elseif ('=' === $this->code[$this->cursor] && '>' === $this->code[$this->cursor + 1]) {
elseif ('=' === $this->code[$this->cursor] && ($this->cursor + 1 < $this->end) && '>' === $this->code[$this->cursor + 1]) {
$this->pushToken(Token::ARROW_TYPE, '=>');
$this->moveCursor('=>');
}
// operators
elseif (preg_match($this->regexes['operator'], $this->code, $match, 0, $this->cursor)) {
$this->pushToken(/* Token::OPERATOR_TYPE */ 8, preg_replace('/\s+/', ' ', $match[0]));
$this->pushToken(Token::OPERATOR_TYPE, preg_replace('/\s+/', ' ', $match[0]));
$this->moveCursor($match[0]);
}
// names
elseif (preg_match(self::REGEX_NAME, $this->code, $match, 0, $this->cursor)) {
$this->pushToken(/* Token::NAME_TYPE */ 5, $match[0]);
$this->pushToken(Token::NAME_TYPE, $match[0]);
$this->moveCursor($match[0]);
}
// numbers
@@ -341,7 +350,7 @@ class Lexer
if (ctype_digit($match[0]) && $number <= \PHP_INT_MAX) {
$number = (int) $match[0]; // integers lower than the maximum
}
$this->pushToken(/* Token::NUMBER_TYPE */ 6, $number);
$this->pushToken(Token::NUMBER_TYPE, $number);
$this->moveCursor($match[0]);
}
// punctuation
@@ -352,22 +361,22 @@ class Lexer
}
// closing bracket
elseif (str_contains(')]}', $this->code[$this->cursor])) {
if (empty($this->brackets)) {
throw new SyntaxError(sprintf('Unexpected "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
if (!$this->brackets) {
throw new SyntaxError(\sprintf('Unexpected "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
}
list($expect, $lineno) = array_pop($this->brackets);
[$expect, $lineno] = array_pop($this->brackets);
if ($this->code[$this->cursor] != strtr($expect, '([{', ')]}')) {
throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
}
}
$this->pushToken(/* Token::PUNCTUATION_TYPE */ 9, $this->code[$this->cursor]);
$this->pushToken(Token::PUNCTUATION_TYPE, $this->code[$this->cursor]);
++$this->cursor;
}
// strings
elseif (preg_match(self::REGEX_STRING, $this->code, $match, 0, $this->cursor)) {
$this->pushToken(/* Token::STRING_TYPE */ 7, stripcslashes(substr($match[0], 1, -1)));
$this->pushToken(Token::STRING_TYPE, $this->stripcslashes(substr($match[0], 1, -1), substr($match[0], 0, 1)));
$this->moveCursor($match[0]);
}
// opening double quoted string
@@ -376,12 +385,73 @@ class Lexer
$this->pushState(self::STATE_STRING);
$this->moveCursor($match[0]);
}
// inline comment
elseif (preg_match(self::REGEX_INLINE_COMMENT, $this->code, $match, 0, $this->cursor)) {
$this->moveCursor($match[0]);
}
// unlexable
else {
throw new SyntaxError(sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
throw new SyntaxError(\sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
}
}
private function stripcslashes(string $str, string $quoteType): string
{
$result = '';
$length = \strlen($str);
$i = 0;
while ($i < $length) {
if (false === $pos = strpos($str, '\\', $i)) {
$result .= substr($str, $i);
break;
}
$result .= substr($str, $i, $pos - $i);
$i = $pos + 1;
if ($i >= $length) {
$result .= '\\';
break;
}
$nextChar = $str[$i];
if (isset(self::SPECIAL_CHARS[$nextChar])) {
$result .= self::SPECIAL_CHARS[$nextChar];
} elseif ('\\' === $nextChar) {
$result .= $nextChar;
} elseif ("'" === $nextChar || '"' === $nextChar) {
if ($nextChar !== $quoteType) {
trigger_deprecation('twig/twig', '3.12', 'Character "%s" should not be escaped; the "\" character is ignored in Twig 3 but will not be in Twig 4. Please remove the extra "\" character at position %d in "%s" at line %d.', $nextChar, $i + 1, $this->source->getName(), $this->lineno);
}
$result .= $nextChar;
} elseif ('#' === $nextChar && $i + 1 < $length && '{' === $str[$i + 1]) {
$result .= '#{';
++$i;
} elseif ('x' === $nextChar && $i + 1 < $length && ctype_xdigit($str[$i + 1])) {
$hex = $str[++$i];
if ($i + 1 < $length && ctype_xdigit($str[$i + 1])) {
$hex .= $str[++$i];
}
$result .= \chr(hexdec($hex));
} elseif (ctype_digit($nextChar) && $nextChar < '8') {
$octal = $nextChar;
while ($i + 1 < $length && ctype_digit($str[$i + 1]) && $str[$i + 1] < '8' && \strlen($octal) < 3) {
$octal .= $str[++$i];
}
$result .= \chr(octdec($octal));
} else {
trigger_deprecation('twig/twig', '3.12', 'Character "%s" should not be escaped; the "\" character is ignored in Twig 3 but will not be in Twig 4. Please remove the extra "\" character at position %d in "%s" at line %d.', $nextChar, $i + 1, $this->source->getName(), $this->lineno);
$result .= $nextChar;
}
++$i;
}
return $result;
}
private function lexRawData(): void
{
if (!preg_match($this->regexes['lex_raw_data'], $this->code, $match, \PREG_OFFSET_CAPTURE, $this->cursor)) {
@@ -403,7 +473,7 @@ class Lexer
}
}
$this->pushToken(/* Token::TEXT_TYPE */ 0, $text);
$this->pushToken(Token::TEXT_TYPE, $text);
}
private function lexComment(): void
@@ -419,23 +489,23 @@ class Lexer
{
if (preg_match($this->regexes['interpolation_start'], $this->code, $match, 0, $this->cursor)) {
$this->brackets[] = [$this->options['interpolation'][0], $this->lineno];
$this->pushToken(/* Token::INTERPOLATION_START_TYPE */ 10);
$this->pushToken(Token::INTERPOLATION_START_TYPE);
$this->moveCursor($match[0]);
$this->pushState(self::STATE_INTERPOLATION);
} elseif (preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, 0, $this->cursor) && '' !== $match[0]) {
$this->pushToken(/* Token::STRING_TYPE */ 7, stripcslashes($match[0]));
$this->pushToken(Token::STRING_TYPE, $this->stripcslashes($match[0], '"'));
$this->moveCursor($match[0]);
} elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) {
list($expect, $lineno) = array_pop($this->brackets);
[$expect, $lineno] = array_pop($this->brackets);
if ('"' != $this->code[$this->cursor]) {
throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
}
$this->popState();
++$this->cursor;
} else {
// unlexable
throw new SyntaxError(sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
throw new SyntaxError(\sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
}
}
@@ -444,7 +514,7 @@ class Lexer
$bracket = end($this->brackets);
if ($this->options['interpolation'][0] === $bracket[0] && preg_match($this->regexes['interpolation_end'], $this->code, $match, 0, $this->cursor)) {
array_pop($this->brackets);
$this->pushToken(/* Token::INTERPOLATION_END_TYPE */ 11);
$this->pushToken(Token::INTERPOLATION_END_TYPE);
$this->moveCursor($match[0]);
$this->popState();
} else {
@@ -455,7 +525,7 @@ class Lexer
private function pushToken($type, $value = ''): void
{
// do not push empty text tokens
if (/* Token::TEXT_TYPE */ 0 === $type && '' === $value) {
if (Token::TEXT_TYPE === $type && '' === $value) {
return;
}

View File

@@ -28,14 +28,12 @@ use Twig\Source;
*/
final class ArrayLoader implements LoaderInterface
{
private $templates = [];
/**
* @param array $templates An array of templates (keys are the names, and values are the source code)
*/
public function __construct(array $templates = [])
{
$this->templates = $templates;
public function __construct(
private array $templates = [],
) {
}
public function setTemplate(string $name, string $template): void
@@ -46,7 +44,7 @@ final class ArrayLoader implements LoaderInterface
public function getSourceContext(string $name): Source
{
if (!isset($this->templates[$name])) {
throw new LoaderError(sprintf('Template "%s" is not defined.', $name));
throw new LoaderError(\sprintf('Template "%s" is not defined.', $name));
}
return new Source($this->templates[$name], $name);
@@ -60,7 +58,7 @@ final class ArrayLoader implements LoaderInterface
public function getCacheKey(string $name): string
{
if (!isset($this->templates[$name])) {
throw new LoaderError(sprintf('Template "%s" is not defined.', $name));
throw new LoaderError(\sprintf('Template "%s" is not defined.', $name));
}
return $name.':'.$this->templates[$name];
@@ -69,7 +67,7 @@ final class ArrayLoader implements LoaderInterface
public function isFresh(string $name, int $time): bool
{
if (!isset($this->templates[$name])) {
throw new LoaderError(sprintf('Template "%s" is not defined.', $name));
throw new LoaderError(\sprintf('Template "%s" is not defined.', $name));
}
return true;

View File

@@ -21,22 +21,28 @@ use Twig\Source;
*/
final class ChainLoader implements LoaderInterface
{
/**
* @var array<string, bool>
*/
private $hasSourceCache = [];
private $loaders = [];
/**
* @param LoaderInterface[] $loaders
* @param iterable<LoaderInterface> $loaders
*/
public function __construct(array $loaders = [])
{
foreach ($loaders as $loader) {
$this->addLoader($loader);
}
public function __construct(
private iterable $loaders = [],
) {
}
public function addLoader(LoaderInterface $loader): void
{
$this->loaders[] = $loader;
$current = $this->loaders;
$this->loaders = (static function () use ($current, $loader): \Generator {
yield from $current;
yield $loader;
})();
$this->hasSourceCache = [];
}
@@ -45,13 +51,18 @@ final class ChainLoader implements LoaderInterface
*/
public function getLoaders(): array
{
if (!\is_array($this->loaders)) {
$this->loaders = iterator_to_array($this->loaders, false);
}
return $this->loaders;
}
public function getSourceContext(string $name): Source
{
$exceptions = [];
foreach ($this->loaders as $loader) {
foreach ($this->getLoaders() as $loader) {
if (!$loader->exists($name)) {
continue;
}
@@ -63,7 +74,7 @@ final class ChainLoader implements LoaderInterface
}
}
throw new LoaderError(sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : ''));
throw new LoaderError(\sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : ''));
}
public function exists(string $name): bool
@@ -72,7 +83,7 @@ final class ChainLoader implements LoaderInterface
return $this->hasSourceCache[$name];
}
foreach ($this->loaders as $loader) {
foreach ($this->getLoaders() as $loader) {
if ($loader->exists($name)) {
return $this->hasSourceCache[$name] = true;
}
@@ -84,7 +95,8 @@ final class ChainLoader implements LoaderInterface
public function getCacheKey(string $name): string
{
$exceptions = [];
foreach ($this->loaders as $loader) {
foreach ($this->getLoaders() as $loader) {
if (!$loader->exists($name)) {
continue;
}
@@ -96,13 +108,14 @@ final class ChainLoader implements LoaderInterface
}
}
throw new LoaderError(sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : ''));
throw new LoaderError(\sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : ''));
}
public function isFresh(string $name, int $time): bool
{
$exceptions = [];
foreach ($this->loaders as $loader) {
foreach ($this->getLoaders() as $loader) {
if (!$loader->exists($name)) {
continue;
}
@@ -114,6 +127,6 @@ final class ChainLoader implements LoaderInterface
}
}
throw new LoaderError(sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : ''));
throw new LoaderError(\sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : ''));
}
}

View File

@@ -24,6 +24,9 @@ class FilesystemLoader implements LoaderInterface
/** Identifier of the main namespace. */
public const MAIN_NAMESPACE = '__main__';
/**
* @var array<string, list<string>>
*/
protected $paths = [];
protected $cache = [];
protected $errorCache = [];
@@ -31,10 +34,10 @@ class FilesystemLoader implements LoaderInterface
private $rootPath;
/**
* @param string|array $paths A path or an array of paths where to look for templates
* @param string|string[] $paths A path or an array of paths where to look for templates
* @param string|null $rootPath The root path common to all relative paths (null for getcwd())
*/
public function __construct($paths = [], string $rootPath = null)
public function __construct($paths = [], ?string $rootPath = null)
{
$this->rootPath = ($rootPath ?? getcwd()).\DIRECTORY_SEPARATOR;
if (null !== $rootPath && false !== ($realPath = realpath($rootPath))) {
@@ -48,6 +51,8 @@ class FilesystemLoader implements LoaderInterface
/**
* Returns the paths to the templates.
*
* @return list<string>
*/
public function getPaths(string $namespace = self::MAIN_NAMESPACE): array
{
@@ -58,6 +63,8 @@ class FilesystemLoader implements LoaderInterface
* Returns the path namespaces.
*
* The main namespace is always defined.
*
* @return list<string>
*/
public function getNamespaces(): array
{
@@ -65,7 +72,7 @@ class FilesystemLoader implements LoaderInterface
}
/**
* @param string|array $paths A path or an array of paths where to look for templates
* @param string|string[] $paths A path or an array of paths where to look for templates
*/
public function setPaths($paths, string $namespace = self::MAIN_NAMESPACE): void
{
@@ -89,7 +96,7 @@ class FilesystemLoader implements LoaderInterface
$checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path;
if (!is_dir($checkPath)) {
throw new LoaderError(sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath));
throw new LoaderError(\sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath));
}
$this->paths[$namespace][] = rtrim($path, '/\\');
@@ -105,7 +112,7 @@ class FilesystemLoader implements LoaderInterface
$checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path;
if (!is_dir($checkPath)) {
throw new LoaderError(sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath));
throw new LoaderError(\sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath));
}
$path = rtrim($path, '/\\');
@@ -183,7 +190,7 @@ class FilesystemLoader implements LoaderInterface
}
try {
list($namespace, $shortname) = $this->parseName($name);
[$namespace, $shortname] = $this->parseName($name);
$this->validateName($shortname);
} catch (LoaderError $e) {
@@ -195,7 +202,7 @@ class FilesystemLoader implements LoaderInterface
}
if (!isset($this->paths[$namespace])) {
$this->errorCache[$name] = sprintf('There are no registered paths for namespace "%s".', $namespace);
$this->errorCache[$name] = \sprintf('There are no registered paths for namespace "%s".', $namespace);
if (!$throw) {
return null;
@@ -218,7 +225,7 @@ class FilesystemLoader implements LoaderInterface
}
}
$this->errorCache[$name] = sprintf('Unable to find template "%s" (looked into: %s).', $name, implode(', ', $this->paths[$namespace]));
$this->errorCache[$name] = \sprintf('Unable to find template "%s" (looked into: %s).', $name, implode(', ', $this->paths[$namespace]));
if (!$throw) {
return null;
@@ -236,7 +243,7 @@ class FilesystemLoader implements LoaderInterface
{
if (isset($name[0]) && '@' == $name[0]) {
if (false === $pos = strpos($name, '/')) {
throw new LoaderError(sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name));
throw new LoaderError(\sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name));
}
$namespace = substr($name, 1, $pos - 1);
@@ -265,7 +272,7 @@ class FilesystemLoader implements LoaderInterface
}
if ($level < 0) {
throw new LoaderError(sprintf('Looks like you try to load a template outside configured directories (%s).', $name));
throw new LoaderError(\sprintf('Looks like you try to load a template outside configured directories (%s).', $name));
}
}
}

View File

@@ -16,10 +16,10 @@ namespace Twig;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Markup implements \Countable, \JsonSerializable
class Markup implements \Countable, \JsonSerializable, \Stringable
{
private $content;
private $charset;
private ?string $charset;
public function __construct($content, $charset)
{
@@ -32,6 +32,11 @@ class Markup implements \Countable, \JsonSerializable
return $this->content;
}
public function getCharset(): string
{
return $this->charset;
}
/**
* @return int
*/

View File

@@ -11,6 +11,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
@@ -24,11 +25,12 @@ use Twig\Compiler;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class AutoEscapeNode extends Node
{
public function __construct($value, Node $body, int $lineno, string $tag = 'autoescape')
public function __construct($value, Node $body, int $lineno)
{
parent::__construct(['body' => $body], ['value' => $value], $lineno, $tag);
parent::__construct(['body' => $body], ['value' => $value], $lineno);
}
public function compile(Compiler $compiler): void

View File

@@ -12,6 +12,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
@@ -19,24 +20,29 @@ use Twig\Compiler;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class BlockNode extends Node
{
public function __construct(string $name, Node $body, int $lineno, string $tag = null)
public function __construct(string $name, Node $body, int $lineno)
{
parent::__construct(['body' => $body], ['name' => $name], $lineno, $tag);
parent::__construct(['body' => $body], ['name' => $name], $lineno);
}
public function compile(Compiler $compiler): void
{
$compiler
->addDebugInfo($this)
->write(sprintf("public function block_%s(\$context, array \$blocks = [])\n", $this->getAttribute('name')), "{\n")
->write("/**\n")
->write(" * @return iterable<null|scalar|\Stringable>\n")
->write(" */\n")
->write(\sprintf("public function block_%s(array \$context, array \$blocks = []): iterable\n", $this->getAttribute('name')), "{\n")
->indent()
->write("\$macros = \$this->macros;\n")
;
$compiler
->subcompile($this->getNode('body'))
->write("yield from [];\n")
->outdent()
->write("}\n\n")
;

View File

@@ -12,6 +12,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
@@ -19,18 +20,19 @@ use Twig\Compiler;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class BlockReferenceNode extends Node implements NodeOutputInterface
{
public function __construct(string $name, int $lineno, string $tag = null)
public function __construct(string $name, int $lineno)
{
parent::__construct([], ['name' => $name], $lineno, $tag);
parent::__construct([], ['name' => $name], $lineno);
}
public function compile(Compiler $compiler): void
{
$compiler
->addDebugInfo($this)
->write(sprintf("\$this->displayBlock('%s', \$context, \$blocks);\n", $this->getAttribute('name')))
->write(\sprintf("yield from \$this->unwrap()->yieldBlock('%s', \$context, \$blocks);\n", $this->getAttribute('name')))
;
}
}

View File

@@ -11,11 +11,14 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
/**
* Represents a body node.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class BodyNode extends Node
{
}

View File

@@ -11,17 +11,19 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class CheckSecurityCallNode extends Node
{
public function compile(Compiler $compiler)
{
$compiler
->write("\$this->sandbox = \$this->env->getExtension('\Twig\Extension\SandboxExtension');\n")
->write("\$this->sandbox = \$this->extensions[SandboxExtension::class];\n")
->write("\$this->checkSecurity();\n")
;
}

View File

@@ -11,17 +11,24 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class CheckSecurityNode extends Node
{
private $usedFilters;
private $usedTags;
private $usedFunctions;
/**
* @param array<string, int> $usedFilters
* @param array<string, int> $usedTags
* @param array<string, int> $usedFunctions
*/
public function __construct(array $usedFilters, array $usedTags, array $usedFunctions)
{
$this->usedFilters = $usedFilters;
@@ -33,32 +40,22 @@ class CheckSecurityNode extends Node
public function compile(Compiler $compiler): void
{
$tags = $filters = $functions = [];
foreach (['tags', 'filters', 'functions'] as $type) {
foreach ($this->{'used'.ucfirst($type)} as $name => $node) {
if ($node instanceof Node) {
${$type}[$name] = $node->getTemplateLine();
} else {
${$type}[$node] = null;
}
}
}
$compiler
->write("\n")
->write("public function checkSecurity()\n")
->write("{\n")
->indent()
->write('static $tags = ')->repr(array_filter($tags))->raw(";\n")
->write('static $filters = ')->repr(array_filter($filters))->raw(";\n")
->write('static $functions = ')->repr(array_filter($functions))->raw(";\n\n")
->write('static $tags = ')->repr(array_filter($this->usedTags))->raw(";\n")
->write('static $filters = ')->repr(array_filter($this->usedFilters))->raw(";\n")
->write('static $functions = ')->repr(array_filter($this->usedFunctions))->raw(";\n\n")
->write("try {\n")
->indent()
->write("\$this->sandbox->checkSecurity(\n")
->indent()
->write(!$tags ? "[],\n" : "['".implode("', '", array_keys($tags))."'],\n")
->write(!$filters ? "[],\n" : "['".implode("', '", array_keys($filters))."'],\n")
->write(!$functions ? "[]\n" : "['".implode("', '", array_keys($functions))."']\n")
->write(!$this->usedTags ? "[],\n" : "['".implode("', '", array_keys($this->usedTags))."'],\n")
->write(!$this->usedFilters ? "[],\n" : "['".implode("', '", array_keys($this->usedFilters))."'],\n")
->write(!$this->usedFunctions ? "[],\n" : "['".implode("', '", array_keys($this->usedFunctions))."'],\n")
->write("\$this->source\n")
->outdent()
->write(");\n")
->outdent()

View File

@@ -11,6 +11,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
@@ -24,11 +25,12 @@ use Twig\Node\Expression\AbstractExpression;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class CheckToStringNode extends AbstractExpression
{
public function __construct(AbstractExpression $expr)
{
parent::__construct(['expr' => $expr], [], $expr->getTemplateLine(), $expr->getNodeTag());
parent::__construct(['expr' => $expr], [], $expr->getTemplateLine());
}
public function compile(Compiler $compiler): void

View File

@@ -11,6 +11,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ConstantExpression;
@@ -20,11 +21,12 @@ use Twig\Node\Expression\ConstantExpression;
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
#[YieldReady]
class DeprecatedNode extends Node
{
public function __construct(AbstractExpression $expr, int $lineno, string $tag = null)
public function __construct(AbstractExpression $expr, int $lineno)
{
parent::__construct(['expr' => $expr], [], $lineno, $tag);
parent::__construct(['expr' => $expr], [], $lineno);
}
public function compile(Compiler $compiler): void
@@ -33,21 +35,39 @@ class DeprecatedNode extends Node
$expr = $this->getNode('expr');
if ($expr instanceof ConstantExpression) {
$compiler->write('@trigger_error(')
->subcompile($expr);
} else {
if (!$expr instanceof ConstantExpression) {
$varName = $compiler->getVarName();
$compiler->write(sprintf('$%s = ', $varName))
$compiler
->write(\sprintf('$%s = ', $varName))
->subcompile($expr)
->raw(";\n")
->write(sprintf('@trigger_error($%s', $varName));
;
}
$compiler->write('trigger_deprecation(');
if ($this->hasNode('package')) {
$compiler->subcompile($this->getNode('package'));
} else {
$compiler->raw("''");
}
$compiler->raw(', ');
if ($this->hasNode('version')) {
$compiler->subcompile($this->getNode('version'));
} else {
$compiler->raw("''");
}
$compiler->raw(', ');
if ($expr instanceof ConstantExpression) {
$compiler->subcompile($expr);
} else {
$compiler->write(\sprintf('$%s', $varName));
}
$compiler
->raw('.')
->string(sprintf(' ("%s" at line %d).', $this->getTemplateName(), $this->getTemplateLine()))
->raw(", E_USER_DEPRECATED);\n")
->string(\sprintf(' in "%s" at line %d.', $this->getTemplateName(), $this->getTemplateLine()))
->raw(");\n")
;
}
}

View File

@@ -11,6 +11,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
@@ -19,11 +20,12 @@ use Twig\Node\Expression\AbstractExpression;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class DoNode extends Node
{
public function __construct(AbstractExpression $expr, int $lineno, string $tag = null)
public function __construct(AbstractExpression $expr, int $lineno)
{
parent::__construct(['expr' => $expr], [], $lineno, $tag);
parent::__construct(['expr' => $expr], [], $lineno);
}
public function compile(Compiler $compiler): void

View File

@@ -11,6 +11,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ConstantExpression;
@@ -20,21 +21,22 @@ use Twig\Node\Expression\ConstantExpression;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class EmbedNode extends IncludeNode
{
// we don't inject the module to avoid node visitors to traverse it twice (as it will be already visited in the main module)
public function __construct(string $name, int $index, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno, string $tag = null)
public function __construct(string $name, int $index, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno)
{
parent::__construct(new ConstantExpression('not_used', $lineno), $variables, $only, $ignoreMissing, $lineno, $tag);
parent::__construct(new ConstantExpression('not_used', $lineno), $variables, $only, $ignoreMissing, $lineno);
$this->setAttribute('name', $name);
$this->setAttribute('index', $index);
}
protected function addGetTemplate(Compiler $compiler): void
protected function addGetTemplate(Compiler $compiler, string $template = ''): void
{
$compiler
->write('$this->loadTemplate(')
->raw('$this->loadTemplate(')
->string($this->getAttribute('name'))
->raw(', ')
->repr($this->getTemplateName())
@@ -44,5 +46,11 @@ class EmbedNode extends IncludeNode
->string($this->getAttribute('index'))
->raw(')')
;
if ($this->getAttribute('ignore_missing')) {
$compiler
->raw(";\n")
->write(\sprintf("\$%s->getParent(\$context);\n", $template))
;
}
}
}

View File

@@ -21,4 +21,23 @@ use Twig\Node\Node;
*/
abstract class AbstractExpression extends Node
{
public function isGenerator(): bool
{
return $this->hasAttribute('is_generator') && $this->getAttribute('is_generator');
}
/**
* @return static
*/
public function setExplicitParentheses(): self
{
$this->setAttribute('with_parentheses', true);
return $this;
}
public function hasExplicitParentheses(): bool
{
return $this->hasAttribute('with_parentheses') && $this->getAttribute('with_parentheses');
}
}

View File

@@ -12,6 +12,7 @@
namespace Twig\Node\Expression;
use Twig\Compiler;
use Twig\Node\Expression\Unary\StringCastUnary;
class ArrayExpression extends AbstractExpression
{
@@ -55,7 +56,7 @@ class ArrayExpression extends AbstractExpression
return false;
}
public function addElement(AbstractExpression $value, AbstractExpression $key = null): void
public function addElement(AbstractExpression $value, ?AbstractExpression $key = null): void
{
if (null === $key) {
$key = new ConstantExpression(++$this->index, $value->getTemplateLine());
@@ -70,7 +71,7 @@ class ArrayExpression extends AbstractExpression
$needsArrayMergeSpread = \PHP_VERSION_ID < 80100 && $this->hasSpreadItem($keyValuePairs);
if ($needsArrayMergeSpread) {
$compiler->raw('twig_array_merge(');
$compiler->raw('CoreExtension::merge(');
}
$compiler->raw('[');
$first = true;
@@ -97,7 +98,17 @@ class ArrayExpression extends AbstractExpression
$compiler->raw('...')->subcompile($pair['value']);
++$nextIndex;
} else {
$key = $pair['key'] instanceof ConstantExpression ? $pair['key']->getAttribute('value') : null;
$key = null;
if ($pair['key'] instanceof NameExpression) {
$pair['key'] = new StringCastUnary($pair['key'], $pair['key']->getTemplateLine());
}
if ($pair['key'] instanceof TempNameExpression) {
$key = $pair['key']->getAttribute('name');
$pair['key'] = new ConstantExpression($key, $pair['key']->getTemplateLine());
}
if ($pair['key'] instanceof ConstantExpression) {
$key = $pair['key']->getAttribute('value');
}
if ($nextIndex !== $key) {
if (\is_int($key)) {

View File

@@ -21,9 +21,9 @@ use Twig\Node\Node;
*/
class ArrowFunctionExpression extends AbstractExpression
{
public function __construct(AbstractExpression $expr, Node $names, $lineno, $tag = null)
public function __construct(AbstractExpression $expr, Node $names, $lineno)
{
parent::__construct(['expr' => $expr, 'names' => $names], [], $lineno, $tag);
parent::__construct(['expr' => $expr, 'names' => $names], [], $lineno);
}
public function compile(Compiler $compiler): void

View File

@@ -13,9 +13,25 @@
namespace Twig\Node\Expression;
use Twig\Compiler;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\Variable\AssignContextVariable;
class AssignNameExpression extends NameExpression
{
public function __construct(string $name, int $lineno)
{
if (self::class === static::class) {
trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated, use "%s" instead.', self::class, AssignContextVariable::class);
}
// All names supported by ExpressionParser::parsePrimaryExpression() should be excluded
if (\in_array(strtolower($name), ['true', 'false', 'none', 'null'])) {
throw new SyntaxError(\sprintf('You cannot assign a value to "%s".', $name), $lineno);
}
parent::__construct($name, $lineno);
}
public function compile(Compiler $compiler): void
{
$compiler

View File

@@ -18,8 +18,19 @@ use Twig\Node\Node;
abstract class AbstractBinary extends AbstractExpression
{
/**
* @param AbstractExpression $left
* @param AbstractExpression $right
*/
public function __construct(Node $left, Node $right, int $lineno)
{
if (!$left instanceof AbstractExpression) {
trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "left" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($left));
}
if (!$right instanceof AbstractExpression) {
trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "right" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($right));
}
parent::__construct(['left' => $left, 'right' => $right], [], $lineno);
}

View File

@@ -20,11 +20,11 @@ class EndsWithBinary extends AbstractBinary
$left = $compiler->getVarName();
$right = $compiler->getVarName();
$compiler
->raw(sprintf('(is_string($%s = ', $left))
->raw(\sprintf('(is_string($%s = ', $left))
->subcompile($this->getNode('left'))
->raw(sprintf(') && is_string($%s = ', $right))
->raw(\sprintf(') && is_string($%s = ', $right))
->subcompile($this->getNode('right'))
->raw(sprintf(') && str_ends_with($%1$s, $%2$s))', $left, $right))
->raw(\sprintf(') && str_ends_with($%1$s, $%2$s))', $left, $right))
;
}

View File

@@ -24,7 +24,7 @@ class EqualBinary extends AbstractBinary
}
$compiler
->raw('(0 === twig_compare(')
->raw('(0 === CoreExtension::compare(')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))

View File

@@ -24,7 +24,7 @@ class GreaterBinary extends AbstractBinary
}
$compiler
->raw('(1 === twig_compare(')
->raw('(1 === CoreExtension::compare(')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))

View File

@@ -24,7 +24,7 @@ class GreaterEqualBinary extends AbstractBinary
}
$compiler
->raw('(0 <= twig_compare(')
->raw('(0 <= CoreExtension::compare(')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))

View File

@@ -18,7 +18,7 @@ class HasEveryBinary extends AbstractBinary
public function compile(Compiler $compiler): void
{
$compiler
->raw('twig_array_every($this->env, ')
->raw('CoreExtension::arrayEvery($this->env, ')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))

View File

@@ -18,7 +18,7 @@ class HasSomeBinary extends AbstractBinary
public function compile(Compiler $compiler): void
{
$compiler
->raw('twig_array_some($this->env, ')
->raw('CoreExtension::arraySome($this->env, ')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))

View File

@@ -18,7 +18,7 @@ class InBinary extends AbstractBinary
public function compile(Compiler $compiler): void
{
$compiler
->raw('twig_in_filter(')
->raw('CoreExtension::inFilter(')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))

View File

@@ -24,7 +24,7 @@ class LessBinary extends AbstractBinary
}
$compiler
->raw('(-1 === twig_compare(')
->raw('(-1 === CoreExtension::compare(')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))

View File

@@ -24,7 +24,7 @@ class LessEqualBinary extends AbstractBinary
}
$compiler
->raw('(0 >= twig_compare(')
->raw('(0 >= CoreExtension::compare(')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))

View File

@@ -12,13 +12,31 @@
namespace Twig\Node\Expression\Binary;
use Twig\Compiler;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Node;
class MatchesBinary extends AbstractBinary
{
public function __construct(Node $left, Node $right, int $lineno)
{
if ($right instanceof ConstantExpression) {
$regexp = $right->getAttribute('value');
set_error_handler(static fn ($t, $m) => throw new SyntaxError(\sprintf('Regexp "%s" passed to "matches" is not valid: %s.', $regexp, substr($m, 14)), $lineno));
try {
preg_match($regexp, '');
} finally {
restore_error_handler();
}
}
parent::__construct($left, $right, $lineno);
}
public function compile(Compiler $compiler): void
{
$compiler
->raw('twig_matches(')
->raw('CoreExtension::matches(')
->subcompile($this->getNode('right'))
->raw(', ')
->subcompile($this->getNode('left'))

View File

@@ -24,7 +24,7 @@ class NotEqualBinary extends AbstractBinary
}
$compiler
->raw('(0 !== twig_compare(')
->raw('(0 !== CoreExtension::compare(')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))

View File

@@ -18,7 +18,7 @@ class NotInBinary extends AbstractBinary
public function compile(Compiler $compiler): void
{
$compiler
->raw('!twig_in_filter(')
->raw('!CoreExtension::inFilter(')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))

View File

@@ -20,11 +20,11 @@ class StartsWithBinary extends AbstractBinary
$left = $compiler->getVarName();
$right = $compiler->getVarName();
$compiler
->raw(sprintf('(is_string($%s = ', $left))
->raw(\sprintf('(is_string($%s = ', $left))
->subcompile($this->getNode('left'))
->raw(sprintf(') && is_string($%s = ', $right))
->raw(\sprintf(') && is_string($%s = ', $right))
->subcompile($this->getNode('right'))
->raw(sprintf(') && str_starts_with($%1$s, $%2$s))', $left, $right))
->raw(\sprintf(') && str_starts_with($%1$s, $%2$s))', $left, $right))
;
}

View File

@@ -22,14 +22,21 @@ use Twig\Node\Node;
*/
class BlockReferenceExpression extends AbstractExpression
{
public function __construct(Node $name, ?Node $template, int $lineno, string $tag = null)
/**
* @param AbstractExpression $name
*/
public function __construct(Node $name, ?Node $template, int $lineno)
{
if (!$name instanceof AbstractExpression) {
trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($name));
}
$nodes = ['name' => $name];
if (null !== $template) {
$nodes['template'] = $template;
}
parent::__construct($nodes, ['is_defined_test' => false, 'output' => false], $lineno, $tag);
parent::__construct($nodes, ['is_defined_test' => false, 'output' => false], $lineno);
}
public function compile(Compiler $compiler): void
@@ -40,8 +47,9 @@ class BlockReferenceExpression extends AbstractExpression
if ($this->getAttribute('output')) {
$compiler->addDebugInfo($this);
$compiler->write('yield from ');
$this
->compileTemplateCall($compiler, 'displayBlock')
->compileTemplateCall($compiler, 'yieldBlock')
->raw(";\n");
} else {
$this->compileTemplateCall($compiler, 'renderBlock');
@@ -65,7 +73,7 @@ class BlockReferenceExpression extends AbstractExpression
;
}
$compiler->raw(sprintf('->%s', $method));
$compiler->raw(\sprintf('->unwrap()->%s', $method));
return $this->compileBlockArguments($compiler);
}

View File

@@ -15,40 +15,49 @@ use Twig\Compiler;
use Twig\Error\SyntaxError;
use Twig\Extension\ExtensionInterface;
use Twig\Node\Node;
use Twig\TwigCallableInterface;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Twig\TwigTest;
use Twig\Util\CallableArgumentsExtractor;
use Twig\Util\ReflectionCallable;
abstract class CallExpression extends AbstractExpression
{
private $reflector;
private $reflector = null;
protected function compileCallable(Compiler $compiler)
{
$callable = $this->getAttribute('callable');
$twigCallable = $this->getTwigCallable();
$callable = $twigCallable->getCallable();
if (\is_string($callable) && !str_contains($callable, '::')) {
$compiler->raw($callable);
} else {
[$r, $callable] = $this->reflectCallable($callable);
$rc = $this->reflectCallable($twigCallable);
$r = $rc->getReflector();
$callable = $rc->getCallable();
if (\is_string($callable)) {
$compiler->raw($callable);
} elseif (\is_array($callable) && \is_string($callable[0])) {
if (!$r instanceof \ReflectionMethod || $r->isStatic()) {
$compiler->raw(sprintf('%s::%s', $callable[0], $callable[1]));
$compiler->raw(\sprintf('%s::%s', $callable[0], $callable[1]));
} else {
$compiler->raw(sprintf('$this->env->getRuntime(\'%s\')->%s', $callable[0], $callable[1]));
$compiler->raw(\sprintf('$this->env->getRuntime(\'%s\')->%s', $callable[0], $callable[1]));
}
} elseif (\is_array($callable) && $callable[0] instanceof ExtensionInterface) {
$class = \get_class($callable[0]);
if (!$compiler->getEnvironment()->hasExtension($class)) {
// Compile a non-optimized call to trigger a \Twig\Error\RuntimeError, which cannot be a compile-time error
$compiler->raw(sprintf('$this->env->getExtension(\'%s\')', $class));
$compiler->raw(\sprintf('$this->env->getExtension(\'%s\')', $class));
} else {
$compiler->raw(sprintf('$this->extensions[\'%s\']', ltrim($class, '\\')));
$compiler->raw(\sprintf('$this->extensions[\'%s\']', ltrim($class, '\\')));
}
$compiler->raw(sprintf('->%s', $callable[1]));
$compiler->raw(\sprintf('->%s', $callable[1]));
} else {
$compiler->raw(sprintf('$this->env->get%s(\'%s\')->getCallable()', ucfirst($this->getAttribute('type')), $this->getAttribute('name')));
$compiler->raw(\sprintf('$this->env->get%s(\'%s\')->getCallable()', ucfirst($this->getAttribute('type')), $twigCallable->getDynamicName()));
}
}
@@ -57,16 +66,30 @@ abstract class CallExpression extends AbstractExpression
protected function compileArguments(Compiler $compiler, $isArray = false): void
{
if (\func_num_args() >= 2) {
trigger_deprecation('twig/twig', '3.11', 'Passing a second argument to "%s()" is deprecated.', __METHOD__);
}
$compiler->raw($isArray ? '[' : '(');
$first = true;
if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) {
$twigCallable = $this->getAttribute('twig_callable');
if ($twigCallable->needsCharset()) {
$compiler->raw('$this->env->getCharset()');
$first = false;
}
if ($twigCallable->needsEnvironment()) {
if (!$first) {
$compiler->raw(', ');
}
$compiler->raw('$this->env');
$first = false;
}
if ($this->hasAttribute('needs_context') && $this->getAttribute('needs_context')) {
if ($twigCallable->needsContext()) {
if (!$first) {
$compiler->raw(', ');
}
@@ -74,14 +97,12 @@ abstract class CallExpression extends AbstractExpression
$first = false;
}
if ($this->hasAttribute('arguments')) {
foreach ($this->getAttribute('arguments') as $argument) {
if (!$first) {
$compiler->raw(', ');
}
$compiler->string($argument);
$first = false;
foreach ($twigCallable->getArguments() as $argument) {
if (!$first) {
$compiler->raw(', ');
}
$compiler->string($argument);
$first = false;
}
if ($this->hasNode('node')) {
@@ -93,8 +114,7 @@ abstract class CallExpression extends AbstractExpression
}
if ($this->hasNode('arguments')) {
$callable = $this->getAttribute('callable');
$arguments = $this->getArguments($callable, $this->getNode('arguments'));
$arguments = (new CallableArgumentsExtractor($this, $this->getTwigCallable()))->extractArguments($this->getNode('arguments'));
foreach ($arguments as $node) {
if (!$first) {
$compiler->raw(', ');
@@ -107,8 +127,13 @@ abstract class CallExpression extends AbstractExpression
$compiler->raw($isArray ? ']' : ')');
}
/**
* @deprecated since Twig 3.12, use Twig\Util\CallableArgumentsExtractor::getArguments() instead
*/
protected function getArguments($callable, $arguments)
{
trigger_deprecation('twig/twig', '3.12', 'The "%s()" method is deprecated, use Twig\Util\CallableArgumentsExtractor::getArguments() instead.', __METHOD__);
$callType = $this->getAttribute('type');
$callName = $this->getAttribute('name');
@@ -119,28 +144,28 @@ abstract class CallExpression extends AbstractExpression
$named = true;
$name = $this->normalizeName($name);
} elseif ($named) {
throw new SyntaxError(sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $callType, $callName), $this->getTemplateLine(), $this->getSourceContext());
throw new SyntaxError(\sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $callType, $callName), $this->getTemplateLine(), $this->getSourceContext());
}
$parameters[$name] = $node;
}
$isVariadic = $this->hasAttribute('is_variadic') && $this->getAttribute('is_variadic');
$isVariadic = $this->getAttribute('twig_callable')->isVariadic();
if (!$named && !$isVariadic) {
return $parameters;
}
if (!$callable) {
if ($named) {
$message = sprintf('Named arguments are not supported for %s "%s".', $callType, $callName);
$message = \sprintf('Named arguments are not supported for %s "%s".', $callType, $callName);
} else {
$message = sprintf('Arbitrary positional arguments are not supported for %s "%s".', $callType, $callName);
$message = \sprintf('Arbitrary positional arguments are not supported for %s "%s".', $callType, $callName);
}
throw new \LogicException($message);
}
list($callableParameters, $isPhpVariadic) = $this->getCallableParameters($callable, $isVariadic);
[$callableParameters, $isPhpVariadic] = $this->getCallableParameters($callable, $isVariadic);
$arguments = [];
$names = [];
$missingArguments = [];
@@ -160,11 +185,11 @@ abstract class CallExpression extends AbstractExpression
if (\array_key_exists($name, $parameters)) {
if (\array_key_exists($pos, $parameters)) {
throw new SyntaxError(sprintf('Argument "%s" is defined twice for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext());
throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext());
}
if (\count($missingArguments)) {
throw new SyntaxError(sprintf(
throw new SyntaxError(\sprintf(
'Argument "%s" could not be assigned for %s "%s(%s)" because it is mapped to an internal PHP function which cannot determine default value for optional argument%s "%s".',
$name, $callType, $callName, implode(', ', $names), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments)
), $this->getTemplateLine(), $this->getSourceContext());
@@ -183,13 +208,13 @@ abstract class CallExpression extends AbstractExpression
} elseif ($callableParameter->isDefaultValueAvailable()) {
$optionalArguments[] = new ConstantExpression($callableParameter->getDefaultValue(), -1);
} elseif ($callableParameter->isOptional()) {
if (empty($parameters)) {
if (!$parameters) {
break;
} else {
$missingArguments[] = $name;
}
} else {
throw new SyntaxError(sprintf('Value for argument "%s" is required for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext());
throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext());
}
}
@@ -210,7 +235,7 @@ abstract class CallExpression extends AbstractExpression
}
}
if (!empty($parameters)) {
if ($parameters) {
$unknownParameter = null;
foreach ($parameters as $parameter) {
if ($parameter instanceof Node) {
@@ -220,7 +245,7 @@ abstract class CallExpression extends AbstractExpression
}
throw new SyntaxError(
sprintf(
\sprintf(
'Unknown argument%s "%s" for %s "%s(%s)".',
\count($parameters) > 1 ? 's' : '', implode('", "', array_keys($parameters)), $callType, $callName, implode(', ', $names)
),
@@ -232,90 +257,106 @@ abstract class CallExpression extends AbstractExpression
return $arguments;
}
/**
* @deprecated since Twig 3.12
*/
protected function normalizeName(string $name): string
{
trigger_deprecation('twig/twig', '3.12', 'The "%s()" method is deprecated.', __METHOD__);
return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $name));
}
// To be removed in 4.0
private function getCallableParameters($callable, bool $isVariadic): array
{
[$r, , $callableName] = $this->reflectCallable($callable);
$twigCallable = $this->getAttribute('twig_callable');
$rc = $this->reflectCallable($twigCallable);
$r = $rc->getReflector();
$callableName = $rc->getName();
$parameters = $r->getParameters();
if ($this->hasNode('node')) {
array_shift($parameters);
}
if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) {
if ($twigCallable->needsCharset()) {
array_shift($parameters);
}
if ($this->hasAttribute('needs_context') && $this->getAttribute('needs_context')) {
if ($twigCallable->needsEnvironment()) {
array_shift($parameters);
}
if ($this->hasAttribute('arguments') && null !== $this->getAttribute('arguments')) {
foreach ($this->getAttribute('arguments') as $argument) {
array_shift($parameters);
}
if ($twigCallable->needsContext()) {
array_shift($parameters);
}
foreach ($twigCallable->getArguments() as $argument) {
array_shift($parameters);
}
$isPhpVariadic = false;
if ($isVariadic) {
$argument = end($parameters);
$isArray = $argument && $argument->hasType() && 'array' === $argument->getType()->getName();
$isArray = $argument && $argument->hasType() && $argument->getType() instanceof \ReflectionNamedType && 'array' === $argument->getType()->getName();
if ($isArray && $argument->isDefaultValueAvailable() && [] === $argument->getDefaultValue()) {
array_pop($parameters);
} elseif ($argument && $argument->isVariadic()) {
array_pop($parameters);
$isPhpVariadic = true;
} else {
throw new \LogicException(sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $callableName, $this->getAttribute('type'), $this->getAttribute('name')));
throw new \LogicException(\sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $callableName, $this->getAttribute('type'), $twigCallable->getName()));
}
}
return [$parameters, $isPhpVariadic];
}
private function reflectCallable($callable)
private function reflectCallable(TwigCallableInterface $callable): ReflectionCallable
{
if (null !== $this->reflector) {
return $this->reflector;
if (!$this->reflector) {
$this->reflector = new ReflectionCallable($callable);
}
if (\is_string($callable) && false !== $pos = strpos($callable, '::')) {
$callable = [substr($callable, 0, $pos), substr($callable, 2 + $pos)];
}
return $this->reflector;
}
if (\is_array($callable) && method_exists($callable[0], $callable[1])) {
$r = new \ReflectionMethod($callable[0], $callable[1]);
/**
* Overrides the Twig callable based on attributes (as potentially, attributes changed between the creation and the compilation of the node).
*
* To be removed in 4.0 and replace by $this->getAttribute('twig_callable').
*/
private function getTwigCallable(): TwigCallableInterface
{
$current = $this->getAttribute('twig_callable');
return $this->reflector = [$r, $callable, $r->class.'::'.$r->name];
}
$this->setAttribute('twig_callable', match ($this->getAttribute('type')) {
'test' => (new TwigTest(
$this->getAttribute('name'),
$this->hasAttribute('callable') ? $this->getAttribute('callable') : $current->getCallable(),
[
'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(),
],
))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ? $this->getAttribute('arguments') : $current->getArguments()),
'function' => (new TwigFunction(
$this->hasAttribute('name') ? $this->getAttribute('name') : $current->getName(),
$this->hasAttribute('callable') ? $this->getAttribute('callable') : $current->getCallable(),
[
'needs_environment' => $this->hasAttribute('needs_environment') ? $this->getAttribute('needs_environment') : $current->needsEnvironment(),
'needs_context' => $this->hasAttribute('needs_context') ? $this->getAttribute('needs_context') : $current->needsContext(),
'needs_charset' => $this->hasAttribute('needs_charset') ? $this->getAttribute('needs_charset') : $current->needsCharset(),
'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(),
],
))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ? $this->getAttribute('arguments') : $current->getArguments()),
'filter' => (new TwigFilter(
$this->getAttribute('name'),
$this->hasAttribute('callable') ? $this->getAttribute('callable') : $current->getCallable(),
[
'needs_environment' => $this->hasAttribute('needs_environment') ? $this->getAttribute('needs_environment') : $current->needsEnvironment(),
'needs_context' => $this->hasAttribute('needs_context') ? $this->getAttribute('needs_context') : $current->needsContext(),
'needs_charset' => $this->hasAttribute('needs_charset') ? $this->getAttribute('needs_charset') : $current->needsCharset(),
'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(),
],
))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ? $this->getAttribute('arguments') : $current->getArguments()),
});
$checkVisibility = $callable instanceof \Closure;
try {
$closure = \Closure::fromCallable($callable);
} catch (\TypeError $e) {
throw new \LogicException(sprintf('Callback for %s "%s" is not callable in the current scope.', $this->getAttribute('type'), $this->getAttribute('name')), 0, $e);
}
$r = new \ReflectionFunction($closure);
if (str_contains($r->name, '{closure}')) {
return $this->reflector = [$r, $callable, 'Closure'];
}
if ($object = $r->getClosureThis()) {
$callable = [$object, $r->name];
$callableName = get_debug_type($object).'::'.$r->name;
} elseif (\PHP_VERSION_ID >= 80111 && $class = $r->getClosureCalledClass()) {
$callableName = $class->name.'::'.$r->name;
} elseif (\PHP_VERSION_ID < 80111 && $class = $r->getClosureScopeClass()) {
$callableName = (\is_array($callable) ? $callable[0] : $class->name).'::'.$r->name;
} else {
$callable = $callableName = $r->name;
}
if ($checkVisibility && \is_array($callable) && method_exists(...$callable) && !(new \ReflectionMethod(...$callable))->isPublic()) {
$callable = $r->getClosure();
}
return $this->reflector = [$r, $callable, $callableName];
return $this->getAttribute('twig_callable');
}
}

View File

@@ -14,6 +14,9 @@ namespace Twig\Node\Expression;
use Twig\Compiler;
/**
* @final
*/
class ConstantExpression extends AbstractExpression
{
public function __construct($value, int $lineno)

View File

@@ -11,7 +11,11 @@
namespace Twig\Node\Expression\Filter;
use Twig\Attribute\FirstClassTwigCallableReady;
use Twig\Compiler;
use Twig\Extension\CoreExtension;
use Twig\Node\EmptyNode;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ConditionalExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FilterExpression;
@@ -19,6 +23,8 @@ use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Node\Expression\Test\DefinedTest;
use Twig\Node\Node;
use Twig\TwigFilter;
use Twig\TwigTest;
/**
* Returns the value or the default value when it is undefined or empty.
@@ -29,20 +35,34 @@ use Twig\Node\Node;
*/
class DefaultFilter extends FilterExpression
{
public function __construct(Node $node, ConstantExpression $filterName, Node $arguments, int $lineno, string $tag = null)
/**
* @param AbstractExpression $node
*/
#[FirstClassTwigCallableReady]
public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno)
{
$default = new FilterExpression($node, new ConstantExpression('default', $node->getTemplateLine()), $arguments, $node->getTemplateLine());
if (!$node instanceof AbstractExpression) {
trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node));
}
if ('default' === $filterName->getAttribute('value') && ($node instanceof NameExpression || $node instanceof GetAttrExpression)) {
$test = new DefinedTest(clone $node, 'defined', new Node(), $node->getTemplateLine());
$false = \count($arguments) ? $arguments->getNode(0) : new ConstantExpression('', $node->getTemplateLine());
if ($filter instanceof TwigFilter) {
$name = $filter->getName();
$default = new FilterExpression($node, $filter, $arguments, $node->getTemplateLine());
} else {
$name = $filter->getAttribute('value');
$default = new FilterExpression($node, new TwigFilter('default', [CoreExtension::class, 'default']), $arguments, $node->getTemplateLine());
}
if ('default' === $name && ($node instanceof NameExpression || $node instanceof GetAttrExpression)) {
$test = new DefinedTest(clone $node, new TwigTest('defined'), new EmptyNode(), $node->getTemplateLine());
$false = \count($arguments) ? $arguments->getNode('0') : new ConstantExpression('', $node->getTemplateLine());
$node = new ConditionalExpression($test, $default, $false, $node->getTemplateLine());
} else {
$node = $default;
}
parent::__construct($node, $filterName, $arguments, $lineno, $tag);
parent::__construct($node, $filter, $arguments, $lineno);
}
public function compile(Compiler $compiler): void

View File

@@ -12,28 +12,68 @@
namespace Twig\Node\Expression;
use Twig\Attribute\FirstClassTwigCallableReady;
use Twig\Compiler;
use Twig\Node\NameDeprecation;
use Twig\Node\Node;
use Twig\TwigFilter;
class FilterExpression extends CallExpression
{
public function __construct(Node $node, ConstantExpression $filterName, Node $arguments, int $lineno, string $tag = null)
/**
* @param AbstractExpression $node
*/
#[FirstClassTwigCallableReady]
public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno)
{
parent::__construct(['node' => $node, 'filter' => $filterName, 'arguments' => $arguments], [], $lineno, $tag);
if (!$node instanceof AbstractExpression) {
trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node));
}
if ($filter instanceof TwigFilter) {
$name = $filter->getName();
$filterName = new ConstantExpression($name, $lineno);
} else {
$name = $filter->getAttribute('value');
$filterName = $filter;
trigger_deprecation('twig/twig', '3.12', 'Not passing an instance of "TwigFilter" when creating a "%s" filter of type "%s" is deprecated.', $name, static::class);
}
parent::__construct(['node' => $node, 'filter' => $filterName, 'arguments' => $arguments], ['name' => $name, 'type' => 'filter'], $lineno);
if ($filter instanceof TwigFilter) {
$this->setAttribute('twig_callable', $filter);
}
$this->deprecateNode('filter', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('needs_charset', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('needs_environment', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('needs_context', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('arguments', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('callable', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('is_variadic', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('dynamic_name', new NameDeprecation('twig/twig', '3.12'));
}
public function compile(Compiler $compiler): void
{
$name = $this->getNode('filter')->getAttribute('value');
$filter = $compiler->getEnvironment()->getFilter($name);
$name = $this->getNode('filter', false)->getAttribute('value');
if ($name !== $this->getAttribute('name')) {
trigger_deprecation('twig/twig', '3.11', 'Changing the value of a "filter" node in a NodeVisitor class is not supported anymore.');
$this->removeAttribute('twig_callable');
}
if ('raw' === $name) {
trigger_deprecation('twig/twig', '3.11', 'Creating the "raw" filter via "FilterExpression" is deprecated; use "RawFilter" instead.');
$this->setAttribute('name', $name);
$this->setAttribute('type', 'filter');
$this->setAttribute('needs_environment', $filter->needsEnvironment());
$this->setAttribute('needs_context', $filter->needsContext());
$this->setAttribute('arguments', $filter->getArguments());
$this->setAttribute('callable', $filter->getCallable());
$this->setAttribute('is_variadic', $filter->isVariadic());
$compiler->subcompile($this->getNode('node'));
return;
}
if (!$this->hasAttribute('twig_callable')) {
$this->setAttribute('twig_callable', $compiler->getEnvironment()->getFilter($name));
}
$this->compileCallable($compiler);
}

View File

@@ -11,32 +11,57 @@
namespace Twig\Node\Expression;
use Twig\Attribute\FirstClassTwigCallableReady;
use Twig\Compiler;
use Twig\Node\NameDeprecation;
use Twig\Node\Node;
use Twig\TwigFunction;
class FunctionExpression extends CallExpression
{
public function __construct(string $name, Node $arguments, int $lineno)
#[FirstClassTwigCallableReady]
public function __construct(TwigFunction|string $function, Node $arguments, int $lineno)
{
parent::__construct(['arguments' => $arguments], ['name' => $name, 'is_defined_test' => false], $lineno);
if ($function instanceof TwigFunction) {
$name = $function->getName();
} else {
$name = $function;
trigger_deprecation('twig/twig', '3.12', 'Not passing an instance of "TwigFunction" when creating a "%s" function of type "%s" is deprecated.', $name, static::class);
}
parent::__construct(['arguments' => $arguments], ['name' => $name, 'type' => 'function', 'is_defined_test' => false], $lineno);
if ($function instanceof TwigFunction) {
$this->setAttribute('twig_callable', $function);
}
$this->deprecateAttribute('needs_charset', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('needs_environment', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('needs_context', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('arguments', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('callable', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('is_variadic', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('dynamic_name', new NameDeprecation('twig/twig', '3.12'));
}
public function compile(Compiler $compiler)
{
$name = $this->getAttribute('name');
$function = $compiler->getEnvironment()->getFunction($name);
$this->setAttribute('name', $name);
$this->setAttribute('type', 'function');
$this->setAttribute('needs_environment', $function->needsEnvironment());
$this->setAttribute('needs_context', $function->needsContext());
$this->setAttribute('arguments', $function->getArguments());
$callable = $function->getCallable();
if ('constant' === $name && $this->getAttribute('is_defined_test')) {
$callable = 'twig_constant_is_defined';
if ($this->hasAttribute('twig_callable')) {
$name = $this->getAttribute('twig_callable')->getName();
if ($name !== $this->getAttribute('name')) {
trigger_deprecation('twig/twig', '3.12', 'Changing the value of a "function" node in a NodeVisitor class is not supported anymore.');
$this->removeAttribute('twig_callable');
}
}
if (!$this->hasAttribute('twig_callable')) {
$this->setAttribute('twig_callable', $compiler->getEnvironment()->getFunction($name));
}
if ('constant' === $name && $this->getAttribute('is_defined_test')) {
$this->getNode('arguments')->setNode('checkDefined', new ConstantExpression(true, $this->getTemplateLine()));
}
$this->setAttribute('callable', $callable);
$this->setAttribute('is_variadic', $function->isVariadic());
$this->compileCallable($compiler);
}

View File

@@ -18,6 +18,10 @@ use Twig\Template;
class GetAttrExpression extends AbstractExpression
{
/**
* @param ArrayExpression|NameExpression|null $arguments
*/
public function __construct(AbstractExpression $node, AbstractExpression $attribute, ?AbstractExpression $arguments, string $type, int $lineno)
{
$nodes = ['node' => $node, 'attribute' => $attribute];
@@ -25,12 +29,17 @@ class GetAttrExpression extends AbstractExpression
$nodes['arguments'] = $arguments;
}
if ($arguments && !$arguments instanceof ArrayExpression && !$arguments instanceof NameExpression) {
trigger_deprecation('twig/twig', '3.15', \sprintf('Not passing a "%s" instance as the "arguments" argument of the "%s" constructor is deprecated ("%s" given).', ArrayExpression::class, static::class, $arguments::class));
}
parent::__construct($nodes, ['type' => $type, 'is_defined_test' => false, 'ignore_strict_check' => false, 'optimizable' => true], $lineno);
}
public function compile(Compiler $compiler): void
{
$env = $compiler->getEnvironment();
$arrayAccessSandbox = false;
// optimize array calls
if (
@@ -44,20 +53,38 @@ class GetAttrExpression extends AbstractExpression
->raw('(('.$var.' = ')
->subcompile($this->getNode('node'))
->raw(') && is_array(')
->raw($var)
->raw($var);
if (!$env->hasExtension(SandboxExtension::class)) {
$compiler
->raw(') || ')
->raw($var)
->raw(' instanceof ArrayAccess ? (')
->raw($var)
->raw('[')
->subcompile($this->getNode('attribute'))
->raw('] ?? null) : null)')
;
return;
}
$arrayAccessSandbox = true;
$compiler
->raw(') || ')
->raw($var)
->raw(' instanceof ArrayAccess ? (')
->raw(' instanceof ArrayAccess && in_array(')
->raw($var.'::class')
->raw(', CoreExtension::ARRAY_LIKE_CLASSES, true) ? (')
->raw($var)
->raw('[')
->subcompile($this->getNode('attribute'))
->raw('] ?? null) : null)')
->raw('] ?? null) : ')
;
return;
}
$compiler->raw('twig_get_attribute($this->env, $this->source, ');
$compiler->raw('CoreExtension::getAttribute($this->env, $this->source, ');
if ($this->getAttribute('ignore_strict_check')) {
$this->getNode('node')->setAttribute('ignore_strict_check', true);
@@ -83,5 +110,9 @@ class GetAttrExpression extends AbstractExpression
->raw(', ')->repr($this->getNode('node')->getTemplateLine())
->raw(')')
;
if ($arrayAccessSandbox) {
$compiler->raw(')');
}
}
}

View File

@@ -19,17 +19,21 @@ use Twig\Node\Node;
*/
final class InlinePrint extends AbstractExpression
{
/**
* @param AbstractExpression $node
*/
public function __construct(Node $node, int $lineno)
{
trigger_deprecation('twig/twig', '3.16', \sprintf('The "%s" class is deprecated with no replacement.', static::class));
parent::__construct(['node' => $node], [], $lineno);
}
public function compile(Compiler $compiler): void
{
$compiler
->raw('print (')
->raw('yield ')
->subcompile($this->getNode('node'))
->raw(')')
;
}
}

View File

@@ -17,6 +17,8 @@ class MethodCallExpression extends AbstractExpression
{
public function __construct(AbstractExpression $node, string $method, ArrayExpression $arguments, int $lineno)
{
trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated, use "%s" instead.', __CLASS__, MacroReferenceExpression::class);
parent::__construct(['node' => $node, 'arguments' => $arguments], ['method' => $method, 'safe' => false, 'is_defined_test' => false], $lineno);
if ($node instanceof NameExpression) {
@@ -39,23 +41,13 @@ class MethodCallExpression extends AbstractExpression
}
$compiler
->raw('twig_call_macro($macros[')
->raw('CoreExtension::callMacro($macros[')
->repr($this->getNode('node')->getAttribute('name'))
->raw('], ')
->repr($this->getAttribute('method'))
->raw(', [')
;
$first = true;
foreach ($this->getNode('arguments')->getKeyValuePairs() as $pair) {
if (!$first) {
$compiler->raw(', ');
}
$first = false;
$compiler->subcompile($pair['value']);
}
$compiler
->raw('], ')
->raw(', ')
->subcompile($this->getNode('arguments'))
->raw(', ')
->repr($this->getTemplateLine())
->raw(', $context, $this->getSourceContext())');
}

View File

@@ -13,6 +13,7 @@
namespace Twig\Node\Expression;
use Twig\Compiler;
use Twig\Node\Expression\Variable\ContextVariable;
class NameExpression extends AbstractExpression
{
@@ -24,6 +25,10 @@ class NameExpression extends AbstractExpression
public function __construct(string $name, int $lineno)
{
if (self::class === static::class) {
trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated, use "%s" instead.', self::class, ContextVariable::class);
}
parent::__construct([], ['name' => $name, 'is_defined_test' => false, 'ignore_strict_check' => false, 'always_defined' => false], $lineno);
}
@@ -34,7 +39,7 @@ class NameExpression extends AbstractExpression
$compiler->addDebugInfo($this);
if ($this->getAttribute('is_defined_test')) {
if ($this->isSpecial()) {
if (isset($this->specialVars[$name])) {
$compiler->repr(true);
} elseif (\PHP_VERSION_ID >= 70400) {
$compiler
@@ -51,7 +56,7 @@ class NameExpression extends AbstractExpression
->raw(', $context))')
;
}
} elseif ($this->isSpecial()) {
} elseif (isset($this->specialVars[$name])) {
$compiler->raw($this->specialVars[$name]);
} elseif ($this->getAttribute('always_defined')) {
$compiler
@@ -85,13 +90,23 @@ class NameExpression extends AbstractExpression
}
}
/**
* @deprecated since Twig 3.11 (to be removed in 4.0)
*/
public function isSpecial()
{
trigger_deprecation('twig/twig', '3.11', 'The "%s()" method is deprecated and will be removed in Twig 4.0.', __METHOD__);
return isset($this->specialVars[$this->getAttribute('name')]);
}
/**
* @deprecated since Twig 3.11 (to be removed in 4.0)
*/
public function isSimple()
{
trigger_deprecation('twig/twig', '3.11', 'The "%s()" method is deprecated and will be removed in Twig 4.0.', __METHOD__);
return !$this->isSpecial() && !$this->getAttribute('is_defined_test');
}
}

View File

@@ -12,22 +12,35 @@
namespace Twig\Node\Expression;
use Twig\Compiler;
use Twig\Node\EmptyNode;
use Twig\Node\Expression\Binary\AndBinary;
use Twig\Node\Expression\Test\DefinedTest;
use Twig\Node\Expression\Test\NullTest;
use Twig\Node\Expression\Unary\NotUnary;
use Twig\Node\Node;
use Twig\TwigTest;
class NullCoalesceExpression extends ConditionalExpression
{
/**
* @param AbstractExpression $left
* @param AbstractExpression $right
*/
public function __construct(Node $left, Node $right, int $lineno)
{
$test = new DefinedTest(clone $left, 'defined', new Node(), $left->getTemplateLine());
if (!$left instanceof AbstractExpression) {
trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "left" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($left));
}
if (!$right instanceof AbstractExpression) {
trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "right" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($right));
}
$test = new DefinedTest(clone $left, new TwigTest('defined'), new EmptyNode(), $left->getTemplateLine());
// for "block()", we don't need the null test as the return value is always a string
if (!$left instanceof BlockReferenceExpression) {
$test = new AndBinary(
$test,
new NotUnary(new NullTest($left, 'null', new Node(), $left->getTemplateLine()), $left->getTemplateLine()),
new NotUnary(new NullTest($left, new TwigTest('null'), new EmptyNode(), $left->getTemplateLine()), $left->getTemplateLine()),
$left->getTemplateLine()
);
}

View File

@@ -21,9 +21,9 @@ use Twig\Compiler;
*/
class ParentExpression extends AbstractExpression
{
public function __construct(string $name, int $lineno, string $tag = null)
public function __construct(string $name, int $lineno)
{
parent::__construct([], ['output' => false, 'name' => $name], $lineno, $tag);
parent::__construct([], ['output' => false, 'name' => $name], $lineno);
}
public function compile(Compiler $compiler): void
@@ -31,7 +31,7 @@ class ParentExpression extends AbstractExpression
if ($this->getAttribute('output')) {
$compiler
->addDebugInfo($this)
->write('$this->displayParentBlock(')
->write('yield from $this->yieldParentBlock(')
->string($this->getAttribute('name'))
->raw(", \$context, \$blocks);\n")
;

View File

@@ -12,20 +12,38 @@
namespace Twig\Node\Expression;
use Twig\Compiler;
use Twig\Error\SyntaxError;
class TempNameExpression extends AbstractExpression
{
public function __construct(string $name, int $lineno)
public const RESERVED_NAMES = ['varargs', 'context', 'macros', 'blocks', 'this'];
public function __construct(string|int|null $name, int $lineno)
{
// All names supported by ExpressionParser::parsePrimaryExpression() should be excluded
if ($name && \in_array(strtolower($name), ['true', 'false', 'none', 'null'])) {
throw new SyntaxError(\sprintf('You cannot assign a value to "%s".', $name), $lineno);
}
if (self::class === static::class) {
trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated.', self::class);
}
if (null !== $name && (is_int($name) || ctype_digit($name))) {
$name = (int) $name;
} elseif (in_array($name, self::RESERVED_NAMES)) {
$name = "\u{035C}".$name;
}
parent::__construct([], ['name' => $name], $lineno);
}
public function compile(Compiler $compiler): void
{
$compiler
->raw('$_')
->raw($this->getAttribute('name'))
->raw('_')
;
if (null === $this->getAttribute('name')) {
$this->setAttribute('name', $compiler->getVarName());
}
$compiler->raw('$'.$this->getAttribute('name'));
}
}

View File

@@ -33,16 +33,16 @@ class ConstantTest extends TestExpression
->raw(' === constant(')
;
if ($this->getNode('arguments')->hasNode(1)) {
if ($this->getNode('arguments')->hasNode('1')) {
$compiler
->raw('get_class(')
->subcompile($this->getNode('arguments')->getNode(1))
->subcompile($this->getNode('arguments')->getNode('1'))
->raw(')."::".')
;
}
$compiler
->subcompile($this->getNode('arguments')->getNode(0))
->subcompile($this->getNode('arguments')->getNode('0'))
->raw('))')
;
}

View File

@@ -11,17 +11,21 @@
namespace Twig\Node\Expression\Test;
use Twig\Attribute\FirstClassTwigCallableReady;
use Twig\Compiler;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\BlockReferenceExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FunctionExpression;
use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\MacroReferenceExpression;
use Twig\Node\Expression\MethodCallExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Node\Expression\TestExpression;
use Twig\Node\Node;
use Twig\TwigTest;
/**
* Checks if a variable is defined in the current context.
@@ -35,8 +39,16 @@ use Twig\Node\Node;
*/
class DefinedTest extends TestExpression
{
public function __construct(Node $node, string $name, ?Node $arguments, int $lineno)
/**
* @param AbstractExpression $node
*/
#[FirstClassTwigCallableReady]
public function __construct(Node $node, TwigTest|string $name, ?Node $arguments, int $lineno)
{
if (!$node instanceof AbstractExpression) {
trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node));
}
if ($node instanceof NameExpression) {
$node->setAttribute('is_defined_test', true);
} elseif ($node instanceof GetAttrExpression) {
@@ -44,6 +56,8 @@ class DefinedTest extends TestExpression
$this->changeIgnoreStrictCheck($node);
} elseif ($node instanceof BlockReferenceExpression) {
$node->setAttribute('is_defined_test', true);
} elseif ($node instanceof MacroReferenceExpression) {
$node->setAttribute('is_defined_test', true);
} elseif ($node instanceof FunctionExpression && 'constant' === $node->getAttribute('name')) {
$node->setAttribute('is_defined_test', true);
} elseif ($node instanceof ConstantExpression || $node instanceof ArrayExpression) {
@@ -54,6 +68,10 @@ class DefinedTest extends TestExpression
throw new SyntaxError('The "defined" test only works with simple variables.', $lineno);
}
if (\is_string($name) && 'defined' !== $name) {
trigger_deprecation('twig/twig', '3.12', 'Creating a "DefinedTest" instance with a test name that is not "defined" is deprecated.');
}
parent::__construct($node, $name, $arguments, $lineno);
}

View File

@@ -29,7 +29,7 @@ class DivisiblebyTest extends TestExpression
->raw('(0 == ')
->subcompile($this->getNode('node'))
->raw(' % ')
->subcompile($this->getNode('arguments')->getNode(0))
->subcompile($this->getNode('arguments')->getNode('0'))
->raw(')')
;
}

View File

@@ -27,7 +27,7 @@ class SameasTest extends TestExpression
->raw('(')
->subcompile($this->getNode('node'))
->raw(' === ')
->subcompile($this->getNode('arguments')->getNode(0))
->subcompile($this->getNode('arguments')->getNode('0'))
->raw(')')
;
}

View File

@@ -11,31 +11,62 @@
namespace Twig\Node\Expression;
use Twig\Attribute\FirstClassTwigCallableReady;
use Twig\Compiler;
use Twig\Node\NameDeprecation;
use Twig\Node\Node;
use Twig\TwigTest;
class TestExpression extends CallExpression
{
public function __construct(Node $node, string $name, ?Node $arguments, int $lineno)
#[FirstClassTwigCallableReady]
/**
* @param AbstractExpression $node
*/
public function __construct(Node $node, string|TwigTest $test, ?Node $arguments, int $lineno)
{
if (!$node instanceof AbstractExpression) {
trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node));
}
$nodes = ['node' => $node];
if (null !== $arguments) {
$nodes['arguments'] = $arguments;
}
parent::__construct($nodes, ['name' => $name], $lineno);
if ($test instanceof TwigTest) {
$name = $test->getName();
} else {
$name = $test;
trigger_deprecation('twig/twig', '3.12', 'Not passing an instance of "TwigTest" when creating a "%s" test of type "%s" is deprecated.', $name, static::class);
}
parent::__construct($nodes, ['name' => $name, 'type' => 'test'], $lineno);
if ($test instanceof TwigTest) {
$this->setAttribute('twig_callable', $test);
}
$this->deprecateAttribute('arguments', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('callable', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('is_variadic', new NameDeprecation('twig/twig', '3.12'));
$this->deprecateAttribute('dynamic_name', new NameDeprecation('twig/twig', '3.12'));
}
public function compile(Compiler $compiler): void
{
$name = $this->getAttribute('name');
$test = $compiler->getEnvironment()->getTest($name);
if ($this->hasAttribute('twig_callable')) {
$name = $this->getAttribute('twig_callable')->getName();
if ($name !== $this->getAttribute('name')) {
trigger_deprecation('twig/twig', '3.12', 'Changing the value of a "test" node in a NodeVisitor class is not supported anymore.');
$this->removeAttribute('twig_callable');
}
}
$this->setAttribute('name', $name);
$this->setAttribute('type', 'test');
$this->setAttribute('arguments', $test->getArguments());
$this->setAttribute('callable', $test->getCallable());
$this->setAttribute('is_variadic', $test->isVariadic());
if (!$this->hasAttribute('twig_callable')) {
$this->setAttribute('twig_callable', $compiler->getEnvironment()->getTest($this->getAttribute('name')));
}
$this->compileCallable($compiler);
}

View File

@@ -18,16 +18,30 @@ use Twig\Node\Node;
abstract class AbstractUnary extends AbstractExpression
{
/**
* @param AbstractExpression $node
*/
public function __construct(Node $node, int $lineno)
{
parent::__construct(['node' => $node], [], $lineno);
if (!$node instanceof AbstractExpression) {
trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance argument to "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, get_class($node));
}
parent::__construct(['node' => $node], ['with_parentheses' => false], $lineno);
}
public function compile(Compiler $compiler): void
{
$compiler->raw(' ');
if ($this->hasExplicitParentheses()) {
$compiler->raw('(');
} else {
$compiler->raw(' ');
}
$this->operator($compiler);
$compiler->subcompile($this->getNode('node'));
if ($this->hasExplicitParentheses()) {
$compiler->raw(')');
}
}
abstract public function operator(Compiler $compiler): Compiler;

View File

@@ -11,6 +11,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
@@ -18,18 +19,22 @@ use Twig\Compiler;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class FlushNode extends Node
{
public function __construct(int $lineno, string $tag)
public function __construct(int $lineno)
{
parent::__construct([], [], $lineno, $tag);
parent::__construct([], [], $lineno);
}
public function compile(Compiler $compiler): void
{
$compiler
->addDebugInfo($this)
->write("flush();\n")
;
$compiler->addDebugInfo($this);
if ($compiler->getEnvironment()->useYield()) {
$compiler->write("yield '';\n");
}
$compiler->write("flush();\n");
}
}

View File

@@ -11,6 +11,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
@@ -18,11 +19,12 @@ use Twig\Compiler;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class ForLoopNode extends Node
{
public function __construct(int $lineno, string $tag = null)
public function __construct(int $lineno)
{
parent::__construct([], ['with_loop' => false, 'ifexpr' => false, 'else' => false], $lineno, $tag);
parent::__construct([], ['with_loop' => false, 'ifexpr' => false, 'else' => false], $lineno);
}
public function compile(Compiler $compiler): void
@@ -36,7 +38,7 @@ class ForLoopNode extends Node
->write("++\$context['loop']['index0'];\n")
->write("++\$context['loop']['index'];\n")
->write("\$context['loop']['first'] = false;\n")
->write("if (isset(\$context['loop']['length'])) {\n")
->write("if (isset(\$context['loop']['revindex0'], \$context['loop']['revindex'])) {\n")
->indent()
->write("--\$context['loop']['revindex0'];\n")
->write("--\$context['loop']['revindex'];\n")

View File

@@ -12,6 +12,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\AssignNameExpression;
@@ -21,20 +22,21 @@ use Twig\Node\Expression\AssignNameExpression;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class ForNode extends Node
{
private $loop;
public function __construct(AssignNameExpression $keyTarget, AssignNameExpression $valueTarget, AbstractExpression $seq, ?Node $ifexpr, Node $body, ?Node $else, int $lineno, string $tag = null)
public function __construct(AssignNameExpression $keyTarget, AssignNameExpression $valueTarget, AbstractExpression $seq, ?Node $ifexpr, Node $body, ?Node $else, int $lineno)
{
$body = new Node([$body, $this->loop = new ForLoopNode($lineno, $tag)]);
$body = new Nodes([$body, $this->loop = new ForLoopNode($lineno)]);
$nodes = ['key_target' => $keyTarget, 'value_target' => $valueTarget, 'seq' => $seq, 'body' => $body];
if (null !== $else) {
$nodes['else'] = $else;
}
parent::__construct($nodes, ['with_loop' => true], $lineno, $tag);
parent::__construct($nodes, ['with_loop' => true], $lineno);
}
public function compile(Compiler $compiler): void
@@ -42,7 +44,7 @@ class ForNode extends Node
$compiler
->addDebugInfo($this)
->write("\$context['_parent'] = \$context;\n")
->write("\$context['_seq'] = twig_ensure_traversable(")
->write("\$context['_seq'] = CoreExtension::ensureTraversable(")
->subcompile($this->getNode('seq'))
->raw(");\n")
;
@@ -99,7 +101,14 @@ class ForNode extends Node
$compiler->write("\$_parent = \$context['_parent'];\n");
// remove some "private" loop variables (needed for nested loops)
$compiler->write('unset($context[\'_seq\'], $context[\'_iterated\'], $context[\''.$this->getNode('key_target')->getAttribute('name').'\'], $context[\''.$this->getNode('value_target')->getAttribute('name').'\'], $context[\'_parent\'], $context[\'loop\']);'."\n");
$compiler->write('unset($context[\'_seq\'], $context[\''.$this->getNode('key_target')->getAttribute('name').'\'], $context[\''.$this->getNode('value_target')->getAttribute('name').'\'], $context[\'_parent\']');
if ($this->hasNode('else')) {
$compiler->raw(', $context[\'_iterated\']');
}
if ($this->getAttribute('with_loop')) {
$compiler->raw(', $context[\'loop\']');
}
$compiler->raw(");\n");
// keep the values set in the inner context for variables defined in the outer context
$compiler->write("\$context = array_intersect_key(\$context, \$_parent) + \$_parent;\n");

View File

@@ -12,6 +12,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
@@ -19,16 +20,17 @@ use Twig\Compiler;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class IfNode extends Node
{
public function __construct(Node $tests, ?Node $else, int $lineno, string $tag = null)
public function __construct(Node $tests, ?Node $else, int $lineno)
{
$nodes = ['tests' => $tests];
if (null !== $else) {
$nodes['else'] = $else;
}
parent::__construct($nodes, [], $lineno, $tag);
parent::__construct($nodes, [], $lineno);
}
public function compile(Compiler $compiler): void
@@ -47,13 +49,13 @@ class IfNode extends Node
}
$compiler
->subcompile($this->getNode('tests')->getNode($i))
->subcompile($this->getNode('tests')->getNode((string) $i))
->raw(") {\n")
->indent()
;
// The node might not exists if the content is empty
if ($this->getNode('tests')->hasNode($i + 1)) {
$compiler->subcompile($this->getNode('tests')->getNode($i + 1));
if ($this->getNode('tests')->hasNode((string) ($i + 1))) {
$compiler->subcompile($this->getNode('tests')->getNode((string) ($i + 1)));
}
}

View File

@@ -11,38 +11,38 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Node\Expression\Variable\AssignTemplateVariable;
/**
* Represents an import node.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class ImportNode extends Node
{
public function __construct(AbstractExpression $expr, AbstractExpression $var, int $lineno, string $tag = null, bool $global = true)
public function __construct(AbstractExpression $expr, AbstractExpression|AssignTemplateVariable $var, int $lineno)
{
parent::__construct(['expr' => $expr, 'var' => $var], ['global' => $global], $lineno, $tag);
if (\func_num_args() > 3) {
trigger_deprecation('twig/twig', '3.15', \sprintf('Passing more than 3 arguments to "%s()" is deprecated.', __METHOD__));
}
if (!$var instanceof AssignTemplateVariable) {
trigger_deprecation('twig/twig', '3.15', \sprintf('Passing a "%s" instance as the second argument of "%s" is deprecated, pass a "%s" instead.', $var::class, __CLASS__, AssignTemplateVariable::class));
$var = new AssignTemplateVariable($var->getAttribute('name'), $lineno);
}
parent::__construct(['expr' => $expr, 'var' => $var], [], $lineno);
}
public function compile(Compiler $compiler): void
{
$compiler
->addDebugInfo($this)
->write('$macros[')
->repr($this->getNode('var')->getAttribute('name'))
->raw('] = ')
;
if ($this->getAttribute('global')) {
$compiler
->raw('$this->macros[')
->repr($this->getNode('var')->getAttribute('name'))
->raw('] = ')
;
}
$compiler->subcompile($this->getNode('var'));
if ($this->getNode('expr') instanceof NameExpression && '_self' === $this->getNode('expr')->getAttribute('name')) {
$compiler->raw('$this');

View File

@@ -12,6 +12,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
@@ -20,16 +21,17 @@ use Twig\Node\Expression\AbstractExpression;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class IncludeNode extends Node implements NodeOutputInterface
{
public function __construct(AbstractExpression $expr, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno, string $tag = null)
public function __construct(AbstractExpression $expr, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno)
{
$nodes = ['expr' => $expr];
if (null !== $variables) {
$nodes['variables'] = $variables;
}
parent::__construct($nodes, ['only' => $only, 'ignore_missing' => $ignoreMissing], $lineno, $tag);
parent::__construct($nodes, ['only' => $only, 'ignore_missing' => $ignoreMissing], $lineno);
}
public function compile(Compiler $compiler): void
@@ -40,13 +42,12 @@ class IncludeNode extends Node implements NodeOutputInterface
$template = $compiler->getVarName();
$compiler
->write(sprintf("$%s = null;\n", $template))
->write("try {\n")
->indent()
->write(sprintf('$%s = ', $template))
->write(\sprintf('$%s = ', $template))
;
$this->addGetTemplate($compiler);
$this->addGetTemplate($compiler, $template);
$compiler
->raw(";\n")
@@ -54,12 +55,14 @@ class IncludeNode extends Node implements NodeOutputInterface
->write("} catch (LoaderError \$e) {\n")
->indent()
->write("// ignore missing template\n")
->write(\sprintf("\$$template = null;\n", $template))
->outdent()
->write("}\n")
->write(sprintf("if ($%s) {\n", $template))
->write(\sprintf("if ($%s) {\n", $template))
->indent()
->write(sprintf('$%s->display(', $template))
->write(\sprintf('yield from $%s->unwrap()->yield(', $template))
;
$this->addTemplateArguments($compiler);
$compiler
->raw(");\n")
@@ -67,17 +70,18 @@ class IncludeNode extends Node implements NodeOutputInterface
->write("}\n")
;
} else {
$compiler->write('yield from ');
$this->addGetTemplate($compiler);
$compiler->raw('->display(');
$compiler->raw('->unwrap()->yield(');
$this->addTemplateArguments($compiler);
$compiler->raw(");\n");
}
}
protected function addGetTemplate(Compiler $compiler)
protected function addGetTemplate(Compiler $compiler/* , string $template = '' */)
{
$compiler
->write('$this->loadTemplate(')
->raw('$this->loadTemplate(')
->subcompile($this->getNode('expr'))
->raw(', ')
->repr($this->getTemplateName())
@@ -93,12 +97,12 @@ class IncludeNode extends Node implements NodeOutputInterface
$compiler->raw(false === $this->getAttribute('only') ? '$context' : '[]');
} elseif (false === $this->getAttribute('only')) {
$compiler
->raw('twig_array_merge($context, ')
->raw('CoreExtension::merge($context, ')
->subcompile($this->getNode('variables'))
->raw(')')
;
} else {
$compiler->raw('twig_to_array(');
$compiler->raw('CoreExtension::toArray(');
$compiler->subcompile($this->getNode('variables'));
$compiler->raw(')');
}

View File

@@ -11,101 +11,109 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\Variable\LocalVariable;
/**
* Represents a macro node.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class MacroNode extends Node
{
public const VARARGS_NAME = 'varargs';
public function __construct(string $name, Node $body, Node $arguments, int $lineno, string $tag = null)
/**
* @param BodyNode $body
* @param ArrayExpression $arguments
*/
public function __construct(string $name, Node $body, Node $arguments, int $lineno)
{
foreach ($arguments as $argumentName => $argument) {
if (self::VARARGS_NAME === $argumentName) {
throw new SyntaxError(sprintf('The argument "%s" in macro "%s" cannot be defined because the variable "%s" is reserved for arbitrary arguments.', self::VARARGS_NAME, $name, self::VARARGS_NAME), $argument->getTemplateLine(), $argument->getSourceContext());
if (!$body instanceof BodyNode) {
trigger_deprecation('twig/twig', '3.12', \sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated ("%s" given).', BodyNode::class, static::class, $body::class));
}
if (!$arguments instanceof ArrayExpression) {
trigger_deprecation('twig/twig', '3.15', \sprintf('Not passing a "%s" instance as the "arguments" argument of the "%s" constructor is deprecated ("%s" given).', ArrayExpression::class, static::class, $arguments::class));
$args = new ArrayExpression([], $arguments->getTemplateLine());
foreach ($arguments as $name => $default) {
$args->addElement($default, new LocalVariable($name, $default->getTemplateLine()));
}
$arguments = $args;
}
foreach ($arguments->getKeyValuePairs() as $pair) {
if ("\u{035C}".self::VARARGS_NAME === $pair['key']->getAttribute('name')) {
throw new SyntaxError(\sprintf('The argument "%s" in macro "%s" cannot be defined because the variable "%s" is reserved for arbitrary arguments.', self::VARARGS_NAME, $name, self::VARARGS_NAME), $pair['value']->getTemplateLine(), $pair['value']->getSourceContext());
}
}
parent::__construct(['body' => $body, 'arguments' => $arguments], ['name' => $name], $lineno, $tag);
parent::__construct(['body' => $body, 'arguments' => $arguments], ['name' => $name], $lineno);
}
public function compile(Compiler $compiler): void
{
$compiler
->addDebugInfo($this)
->write(sprintf('public function macro_%s(', $this->getAttribute('name')))
->write(\sprintf('public function macro_%s(', $this->getAttribute('name')))
;
$count = \count($this->getNode('arguments'));
$pos = 0;
foreach ($this->getNode('arguments') as $name => $default) {
/** @var ArrayExpression $arguments */
$arguments = $this->getNode('arguments');
foreach ($arguments->getKeyValuePairs() as $pair) {
$name = $pair['key'];
$default = $pair['value'];
$compiler
->raw('$__'.$name.'__ = ')
->subcompile($name)
->raw(' = ')
->subcompile($default)
->raw(', ')
;
if (++$pos < $count) {
$compiler->raw(', ');
}
}
if ($count) {
$compiler->raw(', ');
}
$compiler
->raw('...$__varargs__')
->raw(")\n")
->raw('...$varargs')
->raw("): string|Markup\n")
->write("{\n")
->indent()
->write("\$macros = \$this->macros;\n")
->write("\$context = \$this->env->mergeGlobals([\n")
->write("\$context = [\n")
->indent()
;
foreach ($this->getNode('arguments') as $name => $default) {
foreach ($arguments->getKeyValuePairs() as $pair) {
$name = $pair['key'];
$var = $name->getAttribute('name');
if (str_starts_with($var, "\u{035C}")) {
$var = substr($var, \strlen("\u{035C}"));
}
$compiler
->write('')
->string($name)
->raw(' => $__'.$name.'__')
->string($var)
->raw(' => ')
->subcompile($name)
->raw(",\n")
;
}
$node = new CaptureNode($this->getNode('body'), $this->getNode('body')->lineno);
$compiler
->write('')
->string(self::VARARGS_NAME)
->raw(' => ')
;
$compiler
->raw("\$__varargs__,\n")
->raw("\$varargs,\n")
->outdent()
->write("]);\n\n")
->write("] + \$this->env->getGlobals();\n\n")
->write("\$blocks = [];\n\n")
;
if ($compiler->getEnvironment()->isDebug()) {
$compiler->write("ob_start();\n");
} else {
$compiler->write("ob_start(function () { return ''; });\n");
}
$compiler
->write("try {\n")
->indent()
->subcompile($this->getNode('body'))
->write('return ')
->subcompile($node)
->raw("\n")
->write("return ('' === \$tmp = ob_get_contents()) ? '' : new Markup(\$tmp, \$this->env->getCharset());\n")
->outdent()
->write("} finally {\n")
->indent()
->write("ob_end_clean();\n")
->outdent()
->write("}\n")
->outdent()
->write("}\n\n")
;

View File

@@ -12,6 +12,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ConstantExpression;
@@ -20,26 +21,34 @@ use Twig\Source;
/**
* Represents a module node.
*
* Consider this class as being final. If you need to customize the behavior of
* the generated class, consider adding nodes to the following nodes: display_start,
* display_end, constructor_start, constructor_end, and class_end.
* If you need to customize the behavior of the generated class, add nodes to
* the following nodes: display_start, display_end, constructor_start,
* constructor_end, and class_end.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
final class ModuleNode extends Node
{
/**
* @param BodyNode $body
*/
public function __construct(Node $body, ?AbstractExpression $parent, Node $blocks, Node $macros, Node $traits, $embeddedTemplates, Source $source)
{
if (!$body instanceof BodyNode) {
trigger_deprecation('twig/twig', '3.12', \sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated.', BodyNode::class, static::class));
}
$nodes = [
'body' => $body,
'blocks' => $blocks,
'macros' => $macros,
'traits' => $traits,
'display_start' => new Node(),
'display_end' => new Node(),
'constructor_start' => new Node(),
'constructor_end' => new Node(),
'class_end' => new Node(),
'display_start' => new Nodes(),
'display_end' => new Nodes(),
'constructor_start' => new Nodes(),
'constructor_end' => new Nodes(),
'class_end' => new Nodes(),
];
if (null !== $parent) {
$nodes['parent'] = $parent;
@@ -106,7 +115,7 @@ final class ModuleNode extends Node
$parent = $this->getNode('parent');
$compiler
->write("protected function doGetParent(array \$context)\n", "{\n")
->write("protected function doGetParent(array \$context): bool|string|Template|TemplateWrapper\n", "{\n")
->indent()
->addDebugInfo($parent)
->write('return ')
@@ -143,6 +152,7 @@ final class ModuleNode extends Node
->write("use Twig\Environment;\n")
->write("use Twig\Error\LoaderError;\n")
->write("use Twig\Error\RuntimeError;\n")
->write("use Twig\Extension\CoreExtension;\n")
->write("use Twig\Extension\SandboxExtension;\n")
->write("use Twig\Markup;\n")
->write("use Twig\Sandbox\SecurityError;\n")
@@ -150,7 +160,9 @@ final class ModuleNode extends Node
->write("use Twig\Sandbox\SecurityNotAllowedFilterError;\n")
->write("use Twig\Sandbox\SecurityNotAllowedFunctionError;\n")
->write("use Twig\Source;\n")
->write("use Twig\Template;\n\n")
->write("use Twig\Template;\n")
->write("use Twig\TemplateWrapper;\n")
->write("\n")
;
}
$compiler
@@ -160,8 +172,11 @@ final class ModuleNode extends Node
->raw(" extends Template\n")
->write("{\n")
->indent()
->write("private \$source;\n")
->write("private \$macros = [];\n\n")
->write("private Source \$source;\n")
->write("/**\n")
->write(" * @var array<string, Template>\n")
->write(" */\n")
->write("private array \$macros = [];\n\n")
;
}
@@ -188,14 +203,14 @@ final class ModuleNode extends Node
$compiler
->addDebugInfo($node)
->write(sprintf('$_trait_%s = $this->loadTemplate(', $i))
->write(\sprintf('$_trait_%s = $this->loadTemplate(', $i))
->subcompile($node)
->raw(', ')
->repr($node->getTemplateName())
->raw(', ')
->repr($node->getTemplateLine())
->raw(");\n")
->write(sprintf("if (!\$_trait_%s->isTraitable()) {\n", $i))
->write(\sprintf("if (!\$_trait_%s->unwrap()->isTraitable()) {\n", $i))
->indent()
->write("throw new RuntimeError('Template \"'.")
->subcompile($trait->getNode('template'))
@@ -204,12 +219,12 @@ final class ModuleNode extends Node
->raw(", \$this->source);\n")
->outdent()
->write("}\n")
->write(sprintf("\$_trait_%s_blocks = \$_trait_%s->getBlocks();\n\n", $i, $i))
->write(\sprintf("\$_trait_%s_blocks = \$_trait_%s->unwrap()->getBlocks();\n\n", $i, $i))
;
foreach ($trait->getNode('targets') as $key => $value) {
$compiler
->write(sprintf('if (!isset($_trait_%s_blocks[', $i))
->write(\sprintf('if (!isset($_trait_%s_blocks[', $i))
->string($key)
->raw("])) {\n")
->indent()
@@ -223,13 +238,17 @@ final class ModuleNode extends Node
->outdent()
->write("}\n\n")
->write(sprintf('$_trait_%s_blocks[', $i))
->write(\sprintf('$_trait_%s_blocks[', $i))
->subcompile($value)
->raw(sprintf('] = $_trait_%s_blocks[', $i))
->raw(\sprintf('] = $_trait_%s_blocks[', $i))
->string($key)
->raw(sprintf(']; unset($_trait_%s_blocks[', $i))
->raw(\sprintf(']; unset($_trait_%s_blocks[', $i))
->string($key)
->raw("]);\n\n")
->raw("]); \$this->traitAliases[")
->subcompile($value)
->raw("] = ")
->string($key)
->raw(";\n\n")
;
}
}
@@ -242,7 +261,7 @@ final class ModuleNode extends Node
for ($i = 0; $i < $countTraits; ++$i) {
$compiler
->write(sprintf('$_trait_%s_blocks'.($i == $countTraits - 1 ? '' : ',')."\n", $i))
->write(\sprintf('$_trait_%s_blocks'.($i == $countTraits - 1 ? '' : ',')."\n", $i))
;
}
@@ -275,7 +294,7 @@ final class ModuleNode extends Node
foreach ($this->getNode('blocks') as $name => $node) {
$compiler
->write(sprintf("'%s' => [\$this, 'block_%s'],\n", $name, $name))
->write(\sprintf("'%s' => [\$this, 'block_%s'],\n", $name, $name))
;
}
@@ -303,7 +322,7 @@ final class ModuleNode extends Node
protected function compileDisplay(Compiler $compiler)
{
$compiler
->write("protected function doDisplay(array \$context, array \$blocks = [])\n", "{\n")
->write("protected function doDisplay(array \$context, array \$blocks = []): iterable\n", "{\n")
->indent()
->write("\$macros = \$this->macros;\n")
->subcompile($this->getNode('display_start'))
@@ -324,15 +343,24 @@ final class ModuleNode extends Node
->repr($parent->getTemplateLine())
->raw(");\n")
;
$compiler->write('$this->parent');
} else {
$compiler->write('$this->getParent($context)');
}
$compiler->raw("->display(\$context, array_merge(\$this->blocks, \$blocks));\n");
$compiler->write('yield from ');
if ($parent instanceof ConstantExpression) {
$compiler->raw('$this->parent');
} else {
$compiler->raw('$this->getParent($context)');
}
$compiler->raw("->unwrap()->yield(\$context, array_merge(\$this->blocks, \$blocks));\n");
}
$compiler->subcompile($this->getNode('display_end'));
if (!$this->hasNode('parent')) {
$compiler->write("yield from [];\n");
}
$compiler
->subcompile($this->getNode('display_end'))
->outdent()
->write("}\n\n")
;
@@ -358,7 +386,7 @@ final class ModuleNode extends Node
->write("/**\n")
->write(" * @codeCoverageIgnore\n")
->write(" */\n")
->write("public function getTemplateName()\n", "{\n")
->write("public function getTemplateName(): string\n", "{\n")
->indent()
->write('return ')
->repr($this->getSourceContext()->getName())
@@ -380,13 +408,13 @@ final class ModuleNode extends Node
$traitable = !$this->hasNode('parent') && 0 === \count($this->getNode('macros'));
if ($traitable) {
if ($this->getNode('body') instanceof BodyNode) {
$nodes = $this->getNode('body')->getNode(0);
$nodes = $this->getNode('body')->getNode('0');
} else {
$nodes = $this->getNode('body');
}
if (!\count($nodes)) {
$nodes = new Node([$nodes]);
$nodes = new Nodes([$nodes]);
}
foreach ($nodes as $node) {
@@ -394,14 +422,6 @@ final class ModuleNode extends Node
continue;
}
if ($node instanceof TextNode && ctype_space($node->getAttribute('data'))) {
continue;
}
if ($node instanceof BlockReferenceNode) {
continue;
}
$traitable = false;
break;
}
@@ -415,9 +435,9 @@ final class ModuleNode extends Node
->write("/**\n")
->write(" * @codeCoverageIgnore\n")
->write(" */\n")
->write("public function isTraitable()\n", "{\n")
->write("public function isTraitable(): bool\n", "{\n")
->indent()
->write(sprintf("return %s;\n", $traitable ? 'true' : 'false'))
->write("return false;\n")
->outdent()
->write("}\n\n")
;
@@ -429,9 +449,9 @@ final class ModuleNode extends Node
->write("/**\n")
->write(" * @codeCoverageIgnore\n")
->write(" */\n")
->write("public function getDebugInfo()\n", "{\n")
->write("public function getDebugInfo(): array\n", "{\n")
->indent()
->write(sprintf("return %s;\n", str_replace("\n", '', var_export(array_reverse($compiler->getDebugInfo(), true), true))))
->write(\sprintf("return %s;\n", str_replace("\n", '', var_export(array_reverse($compiler->getDebugInfo(), true), true))))
->outdent()
->write("}\n\n")
;
@@ -440,7 +460,7 @@ final class ModuleNode extends Node
protected function compileGetSourceContext(Compiler $compiler)
{
$compiler
->write("public function getSourceContext()\n", "{\n")
->write("public function getSourceContext(): Source\n", "{\n")
->indent()
->write('return new Source(')
->string($compiler->getEnvironment()->isDebug() ? $this->getSourceContext()->getCode() : '')
@@ -458,7 +478,7 @@ final class ModuleNode extends Node
{
if ($node instanceof ConstantExpression) {
$compiler
->write(sprintf('%s = $this->loadTemplate(', $var))
->write(\sprintf('%s = $this->loadTemplate(', $var))
->subcompile($node)
->raw(', ')
->repr($node->getTemplateName())

View File

@@ -12,6 +12,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Source;
@@ -19,61 +20,89 @@ use Twig\Source;
* Represents a node in the AST.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @implements \IteratorAggregate<int|string, Node>
*/
#[YieldReady]
class Node implements \Countable, \IteratorAggregate
{
/**
* @var array<string|int, Node>
*/
protected $nodes;
protected $attributes;
protected $lineno;
protected $tag;
private $sourceContext;
/** @var array<string, NameDeprecation> */
private $nodeNameDeprecations = [];
/** @var array<string, NameDeprecation> */
private $attributeNameDeprecations = [];
/**
* @param array $nodes An array of named nodes
* @param array $attributes An array of attributes (should not be nodes)
* @param int $lineno The line number
* @param string $tag The tag name associated with the Node
* @param array<string|int, Node> $nodes An array of named nodes
* @param array $attributes An array of attributes (should not be nodes)
* @param int $lineno The line number
*/
public function __construct(array $nodes = [], array $attributes = [], int $lineno = 0, string $tag = null)
public function __construct(array $nodes = [], array $attributes = [], int $lineno = 0)
{
if (self::class === static::class) {
trigger_deprecation('twig/twig', '3.15', \sprintf('Instantiating "%s" directly is deprecated; the class will become abstract in 4.0.', self::class));
}
foreach ($nodes as $name => $node) {
if (!$node instanceof self) {
throw new \InvalidArgumentException(sprintf('Using "%s" for the value of node "%s" of "%s" is not supported. You must pass a \Twig\Node\Node instance.', \is_object($node) ? \get_class($node) : (null === $node ? 'null' : \gettype($node)), $name, static::class));
throw new \InvalidArgumentException(\sprintf('Using "%s" for the value of node "%s" of "%s" is not supported. You must pass a \Twig\Node\Node instance.', get_debug_type($node), $name, static::class));
}
}
$this->nodes = $nodes;
$this->attributes = $attributes;
$this->lineno = $lineno;
$this->tag = $tag;
if (\func_num_args() > 3) {
trigger_deprecation('twig/twig', '3.12', \sprintf('The "tag" constructor argument of the "%s" class is deprecated and ignored (check which TokenParser class set it to "%s"), the tag is now automatically set by the Parser when needed.', static::class, func_get_arg(3) ?: 'null'));
}
}
public function __toString()
{
$attributes = [];
foreach ($this->attributes as $name => $value) {
$attributes[] = sprintf('%s: %s', $name, str_replace("\n", '', var_export($value, true)));
$repr = static::class;
if ($this->tag) {
$repr .= \sprintf("\n tag: %s", $this->tag);
}
$repr = [static::class.'('.implode(', ', $attributes)];
$attributes = [];
foreach ($this->attributes as $name => $value) {
if (\is_callable($value)) {
$v = '\Closure';
} elseif ($value instanceof \Stringable) {
$v = (string) $value;
} else {
$v = str_replace("\n", '', var_export($value, true));
}
$attributes[] = \sprintf('%s: %s', $name, $v);
}
if ($attributes) {
$repr .= \sprintf("\n attributes:\n %s", implode("\n ", $attributes));
}
if (\count($this->nodes)) {
$repr .= "\n nodes:";
foreach ($this->nodes as $name => $node) {
$len = \strlen($name) + 4;
$len = \strlen($name) + 6;
$noderepr = [];
foreach (explode("\n", (string) $node) as $line) {
$noderepr[] = str_repeat(' ', $len).$line;
}
$repr[] = sprintf(' %s: %s', $name, ltrim(implode("\n", $noderepr)));
$repr .= \sprintf("\n %s: %s", $name, ltrim(implode("\n", $noderepr)));
}
$repr[] = ')';
} else {
$repr[0] .= ')';
}
return implode("\n", $repr);
return $repr;
}
/**
@@ -82,7 +111,7 @@ class Node implements \Countable, \IteratorAggregate
public function compile(Compiler $compiler)
{
foreach ($this->nodes as $node) {
$node->compile($compiler);
$compiler->subcompile($node);
}
}
@@ -96,6 +125,18 @@ class Node implements \Countable, \IteratorAggregate
return $this->tag;
}
/**
* @internal
*/
public function setNodeTag(string $tag): void
{
if ($this->tag) {
throw new \LogicException('The tag of a node can only be set once.');
}
$this->tag = $tag;
}
public function hasAttribute(string $name): bool
{
return \array_key_exists($name, $this->attributes);
@@ -104,7 +145,17 @@ class Node implements \Countable, \IteratorAggregate
public function getAttribute(string $name)
{
if (!\array_key_exists($name, $this->attributes)) {
throw new \LogicException(sprintf('Attribute "%s" does not exist for Node "%s".', $name, static::class));
throw new \LogicException(\sprintf('Attribute "%s" does not exist for Node "%s".', $name, static::class));
}
$triggerDeprecation = \func_num_args() > 1 ? func_get_arg(1) : true;
if ($triggerDeprecation && isset($this->attributeNameDeprecations[$name])) {
$dep = $this->attributeNameDeprecations[$name];
if ($dep->getNewName()) {
trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting attribute "%s" on a "%s" class is deprecated, get the "%s" attribute instead.', $name, static::class, $dep->getNewName());
} else {
trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting attribute "%s" on a "%s" class is deprecated.', $name, static::class);
}
}
return $this->attributes[$name];
@@ -112,38 +163,96 @@ class Node implements \Countable, \IteratorAggregate
public function setAttribute(string $name, $value): void
{
$triggerDeprecation = \func_num_args() > 2 ? func_get_arg(2) : true;
if ($triggerDeprecation && isset($this->attributeNameDeprecations[$name])) {
$dep = $this->attributeNameDeprecations[$name];
if ($dep->getNewName()) {
trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting attribute "%s" on a "%s" class is deprecated, set the "%s" attribute instead.', $name, static::class, $dep->getNewName());
} else {
trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting attribute "%s" on a "%s" class is deprecated.', $name, static::class);
}
}
$this->attributes[$name] = $value;
}
public function deprecateAttribute(string $name, NameDeprecation $dep): void
{
$this->attributeNameDeprecations[$name] = $dep;
}
public function removeAttribute(string $name): void
{
unset($this->attributes[$name]);
}
/**
* @param string|int $name
*/
public function hasNode(string $name): bool
{
return isset($this->nodes[$name]);
}
/**
* @param string|int $name
*/
public function getNode(string $name): self
{
if (!isset($this->nodes[$name])) {
throw new \LogicException(sprintf('Node "%s" does not exist for Node "%s".', $name, static::class));
throw new \LogicException(\sprintf('Node "%s" does not exist for Node "%s".', $name, static::class));
}
$triggerDeprecation = \func_num_args() > 1 ? func_get_arg(1) : true;
if ($triggerDeprecation && isset($this->nodeNameDeprecations[$name])) {
$dep = $this->nodeNameDeprecations[$name];
if ($dep->getNewName()) {
trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting node "%s" on a "%s" class is deprecated, get the "%s" node instead.', $name, static::class, $dep->getNewName());
} else {
trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting node "%s" on a "%s" class is deprecated.', $name, static::class);
}
}
return $this->nodes[$name];
}
/**
* @param string|int $name
*/
public function setNode(string $name, self $node): void
{
$triggerDeprecation = \func_num_args() > 2 ? func_get_arg(2) : true;
if ($triggerDeprecation && isset($this->nodeNameDeprecations[$name])) {
$dep = $this->nodeNameDeprecations[$name];
if ($dep->getNewName()) {
trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting node "%s" on a "%s" class is deprecated, set the "%s" node instead.', $name, static::class, $dep->getNewName());
} else {
trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting node "%s" on a "%s" class is deprecated.', $name, static::class);
}
}
if (null !== $this->sourceContext) {
$node->setSourceContext($this->sourceContext);
}
$this->nodes[$name] = $node;
}
/**
* @param string|int $name
*/
public function removeNode(string $name): void
{
unset($this->nodes[$name]);
}
/**
* @param string|int $name
*/
public function deprecateNode(string $name, NameDeprecation $dep): void
{
$this->nodeNameDeprecations[$name] = $dep;
}
/**
* @return int
*/

View File

@@ -12,6 +12,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
@@ -20,19 +21,23 @@ use Twig\Node\Expression\AbstractExpression;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class PrintNode extends Node implements NodeOutputInterface
{
public function __construct(AbstractExpression $expr, int $lineno, string $tag = null)
public function __construct(AbstractExpression $expr, int $lineno)
{
parent::__construct(['expr' => $expr], [], $lineno, $tag);
parent::__construct(['expr' => $expr], [], $lineno);
}
public function compile(Compiler $compiler): void
{
/** @var AbstractExpression */
$expr = $this->getNode('expr');
$compiler
->addDebugInfo($this)
->write('echo ')
->subcompile($this->getNode('expr'))
->write($expr->isGenerator() ? 'yield from ' : 'yield ')
->subcompile($expr)
->raw(";\n")
;
}

View File

@@ -11,6 +11,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
@@ -18,11 +19,12 @@ use Twig\Compiler;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class SandboxNode extends Node
{
public function __construct(Node $body, int $lineno, string $tag = null)
public function __construct(Node $body, int $lineno)
{
parent::__construct(['body' => $body], [], $lineno, $tag);
parent::__construct(['body' => $body], [], $lineno);
}
public function compile(Compiler $compiler): void

View File

@@ -11,6 +11,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
use Twig\Node\Expression\ConstantExpression;
@@ -19,26 +20,35 @@ use Twig\Node\Expression\ConstantExpression;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class SetNode extends Node implements NodeCaptureInterface
{
public function __construct(bool $capture, Node $names, Node $values, int $lineno, string $tag = null)
public function __construct(bool $capture, Node $names, Node $values, int $lineno)
{
parent::__construct(['names' => $names, 'values' => $values], ['capture' => $capture, 'safe' => false], $lineno, $tag);
/*
* Optimizes the node when capture is used for a large block of text.
*
* {% set foo %}foo{% endset %} is compiled to $context['foo'] = new Twig\Markup("foo");
*/
if ($this->getAttribute('capture')) {
$this->setAttribute('safe', true);
$values = $this->getNode('values');
if ($values instanceof TextNode) {
$this->setNode('values', new ConstantExpression($values->getAttribute('data'), $values->getTemplateLine()));
$this->setAttribute('capture', false);
$safe = false;
if ($capture) {
$safe = true;
// Node::class === get_class($values) should be removed in Twig 4.0
if (($values instanceof Nodes || Node::class === get_class($values)) && !count($values)) {
$values = new ConstantExpression('', $values->getTemplateLine());
$capture = false;
} elseif ($values instanceof TextNode) {
$values = new ConstantExpression($values->getAttribute('data'), $values->getTemplateLine());
$capture = false;
} elseif ($values instanceof PrintNode && $values->getNode('expr') instanceof ConstantExpression) {
$values = $values->getNode('expr');
$capture = false;
} else {
$values = new CaptureNode($values, $values->getTemplateLine());
}
}
parent::__construct(['names' => $names, 'values' => $values], ['capture' => $capture, 'safe' => $safe], $lineno);
}
public function compile(Compiler $compiler): void
@@ -46,7 +56,7 @@ class SetNode extends Node implements NodeCaptureInterface
$compiler->addDebugInfo($this);
if (\count($this->getNode('names')) > 1) {
$compiler->write('list(');
$compiler->write('[');
foreach ($this->getNode('names') as $idx => $node) {
if ($idx) {
$compiler->raw(', ');
@@ -54,29 +64,15 @@ class SetNode extends Node implements NodeCaptureInterface
$compiler->subcompile($node);
}
$compiler->raw(')');
$compiler->raw(']');
} else {
if ($this->getAttribute('capture')) {
if ($compiler->getEnvironment()->isDebug()) {
$compiler->write("ob_start();\n");
} else {
$compiler->write("ob_start(function () { return ''; });\n");
}
$compiler
->subcompile($this->getNode('values'))
;
}
$compiler->subcompile($this->getNode('names'), false);
if ($this->getAttribute('capture')) {
$compiler->raw(" = ('' === \$tmp = ob_get_clean()) ? '' : new Markup(\$tmp, \$this->env->getCharset())");
}
}
$compiler->raw(' = ');
if (!$this->getAttribute('capture')) {
$compiler->raw(' = ');
if ($this->getAttribute('capture')) {
$compiler->subcompile($this->getNode('values'));
} else {
if (\count($this->getNode('names')) > 1) {
$compiler->write('[');
foreach ($this->getNode('values') as $idx => $value) {
@@ -89,17 +85,31 @@ class SetNode extends Node implements NodeCaptureInterface
$compiler->raw(']');
} else {
if ($this->getAttribute('safe')) {
$compiler
->raw("('' === \$tmp = ")
->subcompile($this->getNode('values'))
->raw(") ? '' : new Markup(\$tmp, \$this->env->getCharset())")
;
if ($this->getNode('values') instanceof ConstantExpression) {
if ('' === $this->getNode('values')->getAttribute('value')) {
$compiler->raw('""');
} else {
$compiler
->raw('new Markup(')
->subcompile($this->getNode('values'))
->raw(', $this->env->getCharset())')
;
}
} else {
$compiler
->raw("('' === \$tmp = ")
->subcompile($this->getNode('values'))
->raw(") ? '' : new Markup(\$tmp, \$this->env->getCharset())")
;
}
} else {
$compiler->subcompile($this->getNode('values'));
}
}
$compiler->raw(';');
}
$compiler->raw(";\n");
$compiler->raw("\n");
}
}

View File

@@ -12,6 +12,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
@@ -19,6 +20,7 @@ use Twig\Compiler;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class TextNode extends Node implements NodeOutputInterface
{
public function __construct(string $data, int $lineno)
@@ -28,9 +30,10 @@ class TextNode extends Node implements NodeOutputInterface
public function compile(Compiler $compiler): void
{
$compiler->addDebugInfo($this);
$compiler
->addDebugInfo($this)
->write('echo ')
->write('yield ')
->string($this->getAttribute('data'))
->raw(";\n")
;

View File

@@ -11,6 +11,7 @@
namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler;
/**
@@ -18,16 +19,17 @@ use Twig\Compiler;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
#[YieldReady]
class WithNode extends Node
{
public function __construct(Node $body, ?Node $variables, bool $only, int $lineno, string $tag = null)
public function __construct(Node $body, ?Node $variables, bool $only, int $lineno)
{
$nodes = ['body' => $body];
if (null !== $variables) {
$nodes['variables'] = $variables;
}
parent::__construct($nodes, ['only' => $only], $lineno, $tag);
parent::__construct($nodes, ['only' => $only], $lineno);
}
public function compile(Compiler $compiler): void
@@ -36,35 +38,35 @@ class WithNode extends Node
$parentContextName = $compiler->getVarName();
$compiler->write(sprintf("\$%s = \$context;\n", $parentContextName));
$compiler->write(\sprintf("\$%s = \$context;\n", $parentContextName));
if ($this->hasNode('variables')) {
$node = $this->getNode('variables');
$varsName = $compiler->getVarName();
$compiler
->write(sprintf('$%s = ', $varsName))
->write(\sprintf('$%s = ', $varsName))
->subcompile($node)
->raw(";\n")
->write(sprintf("if (!is_iterable(\$%s)) {\n", $varsName))
->write(\sprintf("if (!is_iterable(\$%s)) {\n", $varsName))
->indent()
->write("throw new RuntimeError('Variables passed to the \"with\" tag must be a hash.', ")
->write("throw new RuntimeError('Variables passed to the \"with\" tag must be a mapping.', ")
->repr($node->getTemplateLine())
->raw(", \$this->getSourceContext());\n")
->outdent()
->write("}\n")
->write(sprintf("\$%s = twig_to_array(\$%s);\n", $varsName, $varsName))
->write(\sprintf("\$%s = CoreExtension::toArray(\$%s);\n", $varsName, $varsName))
;
if ($this->getAttribute('only')) {
$compiler->write("\$context = [];\n");
}
$compiler->write(sprintf("\$context = \$this->env->mergeGlobals(array_merge(\$context, \$%s));\n", $varsName));
$compiler->write(\sprintf("\$context = \$%s + \$context + \$this->env->getGlobals();\n", $varsName));
}
$compiler
->subcompile($this->getNode('body'))
->write(sprintf("\$context = \$%s;\n", $parentContextName))
->write(\sprintf("\$context = \$%s;\n", $parentContextName))
;
}
}

View File

@@ -17,9 +17,9 @@ use Twig\Node\Node;
/**
* Used to make node visitors compatible with Twig 1.x and 2.x.
*
* To be removed in Twig 3.1.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @deprecated since 3.9 (to be removed in 4.0)
*/
abstract class AbstractNodeVisitor implements NodeVisitorInterface
{

View File

@@ -16,14 +16,14 @@ use Twig\Extension\EscaperExtension;
use Twig\Node\AutoEscapeNode;
use Twig\Node\BlockNode;
use Twig\Node\BlockReferenceNode;
use Twig\Node\DoNode;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ConditionalExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FilterExpression;
use Twig\Node\Expression\InlinePrint;
use Twig\Node\ImportNode;
use Twig\Node\ModuleNode;
use Twig\Node\Node;
use Twig\Node\Nodes;
use Twig\Node\PrintNode;
use Twig\NodeTraverser;
@@ -59,7 +59,7 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
} elseif ($node instanceof BlockNode) {
$this->statusStack[] = $this->blocks[$node->getAttribute('name')] ?? $this->needEscaping();
} elseif ($node instanceof ImportNode) {
$this->safeVars[] = $node->getNode('var')->getAttribute('name');
$this->safeVars[] = $node->getNode('var')->getNode('var')->getAttribute('name');
}
return $node;
@@ -75,11 +75,13 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
return $this->preEscapeFilterNode($node, $env);
} elseif ($node instanceof PrintNode && false !== $type = $this->needEscaping()) {
$expression = $node->getNode('expr');
if ($expression instanceof ConditionalExpression && $this->shouldUnwrapConditional($expression, $env, $type)) {
return new DoNode($this->unwrapConditional($expression, $env, $type), $expression->getTemplateLine());
if ($expression instanceof ConditionalExpression) {
$this->escapeConditional($expression, $env, $type);
} else {
$node->setNode('expr', $this->escapeExpression($expression, $env, $type));
}
return $this->escapePrintNode($node, $env, $type);
return $node;
}
if ($node instanceof AutoEscapeNode || $node instanceof BlockNode) {
@@ -91,85 +93,60 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
return $node;
}
private function shouldUnwrapConditional(ConditionalExpression $expression, Environment $env, string $type): bool
private function escapeConditional(ConditionalExpression $expression, Environment $env, string $type): void
{
$expr2Safe = $this->isSafeFor($type, $expression->getNode('expr2'), $env);
$expr3Safe = $this->isSafeFor($type, $expression->getNode('expr3'), $env);
return $expr2Safe !== $expr3Safe;
}
private function unwrapConditional(ConditionalExpression $expression, Environment $env, string $type): ConditionalExpression
{
// convert "echo a ? b : c" to "a ? echo b : echo c" recursively
/** @var AbstractExpression $expr2 */
$expr2 = $expression->getNode('expr2');
if ($expr2 instanceof ConditionalExpression && $this->shouldUnwrapConditional($expr2, $env, $type)) {
$expr2 = $this->unwrapConditional($expr2, $env, $type);
if ($expr2 instanceof ConditionalExpression) {
$this->escapeConditional($expr2, $env, $type);
} else {
$expr2 = $this->escapeInlinePrintNode(new InlinePrint($expr2, $expr2->getTemplateLine()), $env, $type);
$expression->setNode('expr2', $this->escapeExpression($expr2, $env, $type));
}
/** @var AbstractExpression $expr3 */
$expr3 = $expression->getNode('expr3');
if ($expr3 instanceof ConditionalExpression && $this->shouldUnwrapConditional($expr3, $env, $type)) {
$expr3 = $this->unwrapConditional($expr3, $env, $type);
if ($expr3 instanceof ConditionalExpression) {
$this->escapeConditional($expr3, $env, $type);
} else {
$expr3 = $this->escapeInlinePrintNode(new InlinePrint($expr3, $expr3->getTemplateLine()), $env, $type);
$expression->setNode('expr3', $this->escapeExpression($expr3, $env, $type));
}
return new ConditionalExpression($expression->getNode('expr1'), $expr2, $expr3, $expression->getTemplateLine());
}
private function escapeInlinePrintNode(InlinePrint $node, Environment $env, string $type): Node
private function escapeExpression(AbstractExpression $expression, Environment $env, string $type): AbstractExpression
{
$expression = $node->getNode('node');
if ($this->isSafeFor($type, $expression, $env)) {
return $node;
}
return new InlinePrint($this->getEscaperFilter($type, $expression), $node->getTemplateLine());
}
private function escapePrintNode(PrintNode $node, Environment $env, string $type): Node
{
if (false === $type) {
return $node;
}
$expression = $node->getNode('expr');
if ($this->isSafeFor($type, $expression, $env)) {
return $node;
}
$class = \get_class($node);
return new $class($this->getEscaperFilter($type, $expression), $node->getTemplateLine());
return $this->isSafeFor($type, $expression, $env) ? $expression : $this->getEscaperFilter($env, $type, $expression);
}
private function preEscapeFilterNode(FilterExpression $filter, Environment $env): FilterExpression
{
$name = $filter->getNode('filter')->getAttribute('value');
if ($filter->hasAttribute('twig_callable')) {
$type = $filter->getAttribute('twig_callable')->getPreEscape();
} else {
// legacy
$name = $filter->getNode('filter', false)->getAttribute('value');
$type = $env->getFilter($name)->getPreEscape();
}
$type = $env->getFilter($name)->getPreEscape();
if (null === $type) {
return $filter;
}
/** @var AbstractExpression $node */
$node = $filter->getNode('node');
if ($this->isSafeFor($type, $node, $env)) {
return $filter;
}
$filter->setNode('node', $this->getEscaperFilter($type, $node));
$filter->setNode('node', $this->getEscaperFilter($env, $type, $node));
return $filter;
}
private function isSafeFor(string $type, Node $expression, Environment $env): bool
private function isSafeFor(string $type, AbstractExpression $expression, Environment $env): bool
{
$safe = $this->safeAnalysis->getSafe($expression);
if (null === $safe) {
if (!$safe) {
if (null === $this->traverser) {
$this->traverser = new NodeTraverser($env, [$this->safeAnalysis]);
}
@@ -192,13 +169,13 @@ final class EscaperNodeVisitor implements NodeVisitorInterface
return $this->defaultStrategy ?: false;
}
private function getEscaperFilter(string $type, Node $node): FilterExpression
private function getEscaperFilter(Environment $env, string $type, AbstractExpression $node): FilterExpression
{
$line = $node->getTemplateLine();
$name = new ConstantExpression('escape', $line);
$args = new Node([new ConstantExpression($type, $line), new ConstantExpression(null, $line), new ConstantExpression(true, $line)]);
$filter = $env->getFilter('escape');
$args = new Nodes([new ConstantExpression($type, $line), new ConstantExpression(null, $line), new ConstantExpression(true, $line)]);
return new FilterExpression($node, $name, $args, $line);
return new FilterExpression($node, $filter, $args, $line);
}
public function getPriority(): int

View File

@@ -1,74 +0,0 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\NodeVisitor;
use Twig\Environment;
use Twig\Node\Expression\AssignNameExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\MethodCallExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Node\ImportNode;
use Twig\Node\ModuleNode;
use Twig\Node\Node;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
final class MacroAutoImportNodeVisitor implements NodeVisitorInterface
{
private $inAModule = false;
private $hasMacroCalls = false;
public function enterNode(Node $node, Environment $env): Node
{
if ($node instanceof ModuleNode) {
$this->inAModule = true;
$this->hasMacroCalls = false;
}
return $node;
}
public function leaveNode(Node $node, Environment $env): Node
{
if ($node instanceof ModuleNode) {
$this->inAModule = false;
if ($this->hasMacroCalls) {
$node->getNode('constructor_end')->setNode('_auto_macro_import', new ImportNode(new NameExpression('_self', 0), new AssignNameExpression('_self', 0), 0, 'import', true));
}
} elseif ($this->inAModule) {
if (
$node instanceof GetAttrExpression
&& $node->getNode('node') instanceof NameExpression
&& '_self' === $node->getNode('node')->getAttribute('name')
&& $node->getNode('attribute') instanceof ConstantExpression
) {
$this->hasMacroCalls = true;
$name = $node->getNode('attribute')->getAttribute('value');
$node = new MethodCallExpression($node->getNode('node'), 'macro_'.$name, $node->getNode('arguments'), $node->getTemplateLine());
$node->setAttribute('safe', true);
}
}
return $node;
}
public function getPriority(): int
{
// we must be ran before auto-escaping
return -10;
}
}

View File

@@ -15,7 +15,6 @@ use Twig\Environment;
use Twig\Node\BlockReferenceNode;
use Twig\Node\Expression\BlockReferenceExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FilterExpression;
use Twig\Node\Expression\FunctionExpression;
use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\NameExpression;
@@ -24,6 +23,7 @@ use Twig\Node\ForNode;
use Twig\Node\IncludeNode;
use Twig\Node\Node;
use Twig\Node\PrintNode;
use Twig\Node\TextNode;
/**
* Tries to optimize the AST.
@@ -43,21 +43,28 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface
public const OPTIMIZE_NONE = 0;
public const OPTIMIZE_FOR = 2;
public const OPTIMIZE_RAW_FILTER = 4;
public const OPTIMIZE_TEXT_NODES = 8;
private $loops = [];
private $loopsTargets = [];
private $optimizers;
/**
* @param int $optimizers The optimizer mode
*/
public function __construct(int $optimizers = -1)
{
if ($optimizers > (self::OPTIMIZE_FOR | self::OPTIMIZE_RAW_FILTER)) {
throw new \InvalidArgumentException(sprintf('Optimizer mode "%s" is not valid.', $optimizers));
public function __construct(
private int $optimizers = -1,
) {
if ($optimizers > (self::OPTIMIZE_FOR | self::OPTIMIZE_RAW_FILTER | self::OPTIMIZE_TEXT_NODES)) {
throw new \InvalidArgumentException(\sprintf('Optimizer mode "%s" is not valid.', $optimizers));
}
$this->optimizers = $optimizers;
if (-1 !== $optimizers && self::OPTIMIZE_RAW_FILTER === (self::OPTIMIZE_RAW_FILTER & $optimizers)) {
trigger_deprecation('twig/twig', '3.11', 'The "Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_RAW_FILTER" option is deprecated and does nothing.');
}
if (-1 !== $optimizers && self::OPTIMIZE_TEXT_NODES === (self::OPTIMIZE_TEXT_NODES & $optimizers)) {
trigger_deprecation('twig/twig', '3.12', 'The "Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES" option is deprecated and does nothing.');
}
}
public function enterNode(Node $node, Environment $env): Node
@@ -75,10 +82,6 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface
$this->leaveOptimizeFor($node);
}
if (self::OPTIMIZE_RAW_FILTER === (self::OPTIMIZE_RAW_FILTER & $this->optimizers)) {
$node = $this->optimizeRawFilter($node);
}
$node = $this->optimizePrintNode($node);
return $node;
@@ -98,6 +101,11 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface
}
$exprNode = $node->getNode('expr');
if ($exprNode instanceof ConstantExpression && \is_string($exprNode->getAttribute('value'))) {
return new TextNode($exprNode->getAttribute('value'), $exprNode->getTemplateLine());
}
if (
$exprNode instanceof BlockReferenceExpression
|| $exprNode instanceof ParentExpression
@@ -110,18 +118,6 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface
return $node;
}
/**
* Removes "raw" filters.
*/
private function optimizeRawFilter(Node $node): Node
{
if ($node instanceof FilterExpression && 'raw' == $node->getNode('filter')->getAttribute('value')) {
return $node->getNode('node');
}
return $node;
}
/**
* Optimizes "for" tag by removing the "loop" variable creation whenever possible.
*/

View File

@@ -18,6 +18,7 @@ use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FilterExpression;
use Twig\Node\Expression\FunctionExpression;
use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\MacroReferenceExpression;
use Twig\Node\Expression\MethodCallExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Node\Expression\ParentExpression;
@@ -36,11 +37,14 @@ final class SafeAnalysisNodeVisitor implements NodeVisitorInterface
$this->safeVars = $safeVars;
}
/**
* @return array
*/
public function getSafe(Node $node)
{
$hash = spl_object_hash($node);
if (!isset($this->data[$hash])) {
return;
return [];
}
foreach ($this->data[$hash] as $bucket) {
@@ -54,6 +58,8 @@ final class SafeAnalysisNodeVisitor implements NodeVisitorInterface
return $bucket['value'];
}
return [];
}
private function setSafe(Node $node, array $safe): void
@@ -96,49 +102,58 @@ final class SafeAnalysisNodeVisitor implements NodeVisitorInterface
$this->setSafe($node, $safe);
} elseif ($node instanceof FilterExpression) {
// filter expression is safe when the filter is safe
$name = $node->getNode('filter')->getAttribute('value');
$args = $node->getNode('arguments');
if ($filter = $env->getFilter($name)) {
$safe = $filter->getSafe($args);
if ($node->hasAttribute('twig_callable')) {
$filter = $node->getAttribute('twig_callable');
} else {
// legacy
$filter = $env->getFilter($node->getAttribute('name'));
}
if ($filter) {
$safe = $filter->getSafe($node->getNode('arguments'));
if (null === $safe) {
trigger_deprecation('twig/twig', '3.16', 'The "%s::getSafe()" method should not return "null" anymore, return "[]" instead.', $filter::class);
$safe = [];
}
if (!$safe) {
$safe = $this->intersectSafe($this->getSafe($node->getNode('node')), $filter->getPreservesSafety());
}
$this->setSafe($node, $safe);
} else {
$this->setSafe($node, []);
}
} elseif ($node instanceof FunctionExpression) {
// function expression is safe when the function is safe
$name = $node->getAttribute('name');
$args = $node->getNode('arguments');
if ($function = $env->getFunction($name)) {
$this->setSafe($node, $function->getSafe($args));
if ($node->hasAttribute('twig_callable')) {
$function = $node->getAttribute('twig_callable');
} else {
$this->setSafe($node, []);
// legacy
$function = $env->getFunction($node->getAttribute('name'));
}
} elseif ($node instanceof MethodCallExpression) {
if ($node->getAttribute('safe')) {
$this->setSafe($node, ['all']);
} else {
$this->setSafe($node, []);
if ($function) {
$safe = $function->getSafe($node->getNode('arguments'));
if (null === $safe) {
trigger_deprecation('twig/twig', '3.16', 'The "%s::getSafe()" method should not return "null" anymore, return "[]" instead.', $function::class);
$safe = [];
}
$this->setSafe($node, $safe);
}
} elseif ($node instanceof MethodCallExpression || $node instanceof MacroReferenceExpression) {
// all macro calls are safe
$this->setSafe($node, ['all']);
} elseif ($node instanceof GetAttrExpression && $node->getNode('node') instanceof NameExpression) {
$name = $node->getNode('node')->getAttribute('name');
if (\in_array($name, $this->safeVars)) {
$this->setSafe($node, ['all']);
} else {
$this->setSafe($node, []);
}
} else {
$this->setSafe($node, []);
}
return $node;
}
private function intersectSafe(array $a = null, array $b = null): array
private function intersectSafe(array $a, array $b): array
{
if (null === $a || null === $b) {
if (!$a || !$b) {
return [];
}

View File

@@ -15,14 +15,17 @@ use Twig\Environment;
use Twig\Node\CheckSecurityCallNode;
use Twig\Node\CheckSecurityNode;
use Twig\Node\CheckToStringNode;
use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\Binary\ConcatBinary;
use Twig\Node\Expression\Binary\RangeBinary;
use Twig\Node\Expression\FilterExpression;
use Twig\Node\Expression\FunctionExpression;
use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Node\Expression\Unary\SpreadUnary;
use Twig\Node\ModuleNode;
use Twig\Node\Node;
use Twig\Node\Nodes;
use Twig\Node\PrintNode;
use Twig\Node\SetNode;
@@ -34,8 +37,11 @@ use Twig\Node\SetNode;
final class SandboxNodeVisitor implements NodeVisitorInterface
{
private $inAModule = false;
/** @var array<string, int> */
private $tags;
/** @var array<string, int> */
private $filters;
/** @var array<string, int> */
private $functions;
private $needsToStringWrap = false;
@@ -51,22 +57,22 @@ final class SandboxNodeVisitor implements NodeVisitorInterface
} elseif ($this->inAModule) {
// look for tags
if ($node->getNodeTag() && !isset($this->tags[$node->getNodeTag()])) {
$this->tags[$node->getNodeTag()] = $node;
$this->tags[$node->getNodeTag()] = $node->getTemplateLine();
}
// look for filters
if ($node instanceof FilterExpression && !isset($this->filters[$node->getNode('filter')->getAttribute('value')])) {
$this->filters[$node->getNode('filter')->getAttribute('value')] = $node;
if ($node instanceof FilterExpression && !isset($this->filters[$node->getAttribute('name')])) {
$this->filters[$node->getAttribute('name')] = $node->getTemplateLine();
}
// look for functions
if ($node instanceof FunctionExpression && !isset($this->functions[$node->getAttribute('name')])) {
$this->functions[$node->getAttribute('name')] = $node;
$this->functions[$node->getAttribute('name')] = $node->getTemplateLine();
}
// the .. operator is equivalent to the range() function
if ($node instanceof RangeBinary && !isset($this->functions['range'])) {
$this->functions['range'] = $node;
$this->functions['range'] = $node->getTemplateLine();
}
if ($node instanceof PrintNode) {
@@ -102,8 +108,8 @@ final class SandboxNodeVisitor implements NodeVisitorInterface
if ($node instanceof ModuleNode) {
$this->inAModule = false;
$node->setNode('constructor_end', new Node([new CheckSecurityCallNode(), $node->getNode('constructor_end')]));
$node->setNode('class_end', new Node([new CheckSecurityNode($this->filters, $this->tags, $this->functions), $node->getNode('class_end')]));
$node->setNode('constructor_end', new Nodes([new CheckSecurityCallNode(), $node->getNode('constructor_end')]));
$node->setNode('class_end', new Nodes([new CheckSecurityNode($this->filters, $this->tags, $this->functions), $node->getNode('class_end')]));
} elseif ($this->inAModule) {
if ($node instanceof PrintNode || $node instanceof SetNode) {
$this->needsToStringWrap = false;
@@ -116,8 +122,19 @@ final class SandboxNodeVisitor implements NodeVisitorInterface
private function wrapNode(Node $node, string $name): void
{
$expr = $node->getNode($name);
if ($expr instanceof NameExpression || $expr instanceof GetAttrExpression) {
$node->setNode($name, new CheckToStringNode($expr));
if (($expr instanceof NameExpression || $expr instanceof GetAttrExpression) && !$expr->isGenerator()) {
// Simplify in 4.0 as the spread attribute has been removed there
$new = new CheckToStringNode($expr);
if ($expr->hasAttribute('spread')) {
$new->setAttribute('spread', $expr->getAttribute('spread'));
}
$node->setNode($name, $new);
} elseif ($expr instanceof SpreadUnary) {
$this->wrapNode($expr, 'node');
} elseif ($expr instanceof ArrayExpression) {
foreach ($expr as $name => $_) {
$this->wrapNode($expr, $name);
}
}
}

Some files were not shown because too many files have changed in this diff Show More