From feb902317291731fb215edff305212dcf2f15397 Mon Sep 17 00:00:00 2001 From: John Koster Date: Wed, 25 Feb 2026 23:46:13 -0600 Subject: [PATCH 1/2] Disables PHP by default when calling Antlers::parse --- src/Facades/Antlers.php | 2 +- src/Facades/Endpoint/Parse.php | 2 +- src/Providers/ViewServiceProvider.php | 1 + src/View/Antlers/Antlers.php | 12 ++++- .../Language/Runtime/GlobalRuntimeState.php | 8 +++ .../Language/Runtime/NodeProcessor.php | 14 ++++- .../Language/Runtime/RuntimeConfiguration.php | 7 +++ .../Language/Runtime/RuntimeParser.php | 1 + tests/Antlers/Runtime/PhpDisabledTest.php | 51 +++++++++++++++++++ 9 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 tests/Antlers/Runtime/PhpDisabledTest.php diff --git a/src/Facades/Antlers.php b/src/Facades/Antlers.php index e29d69889bd..eeeed41ced1 100644 --- a/src/Facades/Antlers.php +++ b/src/Facades/Antlers.php @@ -9,7 +9,7 @@ /** * @method static Parser parser() * @method static mixed usingParser(Parser $parser, \Closure $callback) - * @method static AntlersString parse(string $str, array $variables = []) + * @method static AntlersString parse(string $str, array $variables = [], bool $php = false) * @method static AntlersString parseUserContent(string $str, array $variables = []) * @method static string parseLoop(string $content, array $data, bool $supplement = true, array $context = []) * @method static array identifiers(string $content) diff --git a/src/Facades/Endpoint/Parse.php b/src/Facades/Endpoint/Parse.php index 6f447d96541..ebd59ffab63 100644 --- a/src/Facades/Endpoint/Parse.php +++ b/src/Facades/Endpoint/Parse.php @@ -22,7 +22,7 @@ class Parse */ public function template($str, $variables = [], $context = [], $php = false) { - return Antlers::parse($str, $variables, $context, $php); + return Antlers::parse($str, array_merge($variables, $context), $php); } /** diff --git a/src/Providers/ViewServiceProvider.php b/src/Providers/ViewServiceProvider.php index b16050a7f36..ed2ab54bbba 100644 --- a/src/Providers/ViewServiceProvider.php +++ b/src/Providers/ViewServiceProvider.php @@ -102,6 +102,7 @@ private function registerAntlers() $runtimeConfig->guardedContentVariablePatterns = config('statamic.antlers.guardedContentVariables', []); $runtimeConfig->guardedContentTagPatterns = config('statamic.antlers.guardedContentTags', []); $runtimeConfig->guardedContentModifiers = config('statamic.antlers.guardedContentModifiers', []); + $runtimeConfig->isPhpEnabled = config('statamic.antlers.allowPhp', true); $runtimeConfig->allowPhpInUserContent = config('statamic.antlers.allowPhpInContent', false); $runtimeConfig->allowMethodsInUserContent = config('statamic.antlers.allowMethodsInContent', false); diff --git a/src/View/Antlers/Antlers.php b/src/View/Antlers/Antlers.php index b1dc611cfbc..0a6829ff3c5 100644 --- a/src/View/Antlers/Antlers.php +++ b/src/View/Antlers/Antlers.php @@ -27,9 +27,17 @@ public function usingParser(Parser $parser, Closure $callback) return $contents; } - public function parse($str, $variables = []) + public function parse($str, $variables = [], $php = false) { - return $this->parser()->parse($str, $variables); + $parser = $this->parser(); + $previousState = GlobalRuntimeState::$isPhpEnabled; + GlobalRuntimeState::$isPhpEnabled = $php; + + try { + return $parser->parse($str, $variables); + } finally { + GlobalRuntimeState::$isPhpEnabled = $previousState; + } } public function parseUserContent($str, $variables = []) diff --git a/src/View/Antlers/Language/Runtime/GlobalRuntimeState.php b/src/View/Antlers/Language/Runtime/GlobalRuntimeState.php index a62a6a11dfa..09760f80485 100644 --- a/src/View/Antlers/Language/Runtime/GlobalRuntimeState.php +++ b/src/View/Antlers/Language/Runtime/GlobalRuntimeState.php @@ -184,6 +184,13 @@ public static function mergeTagRuntimeAssignments($assignments) */ public static $allowPhpInContent = false; + /** + * Controls whether PHP execution is globally enabled. + * + * @var bool + */ + public static $isPhpEnabled = true; + /** * Controls if method invocations are evaluated in user content. * @@ -273,6 +280,7 @@ public static function resetGlobalState() self::$abandonedNodes = []; self::$isEvaluatingUserData = false; self::$isEvaluatingData = false; + self::$isPhpEnabled = true; self::$userContentEvalState = null; StackReplacementManager::clearStackState(); diff --git a/src/View/Antlers/Language/Runtime/NodeProcessor.php b/src/View/Antlers/Language/Runtime/NodeProcessor.php index 901a5603a6c..e1f4b5f121b 100644 --- a/src/View/Antlers/Language/Runtime/NodeProcessor.php +++ b/src/View/Antlers/Language/Runtime/NodeProcessor.php @@ -1213,6 +1213,10 @@ public function reduce($processNodes) } if ($node instanceof PhpExecutionNode) { + if (! GlobalRuntimeState::$isPhpEnabled) { + continue; + } + if (GlobalRuntimeState::$isEvaluatingUserData && ! GlobalRuntimeState::$allowPhpInContent) { if (GlobalRuntimeState::$throwErrorOnAccessViolation) { throw ErrorFactory::makeRuntimeError( @@ -2453,7 +2457,7 @@ public function reduce($processNodes) // one last time to make sure we didn't miss anything. $this->stopMeasuringTag(); - if ($this->allowPhp) { + if ($this->allowPhp && GlobalRuntimeState::$isPhpEnabled) { $buffer = $this->evaluatePhp($buffer); } @@ -2468,6 +2472,10 @@ public function reduce($processNodes) */ protected function evaluatePhp($buffer) { + if (! GlobalRuntimeState::$isPhpEnabled) { + return is_array($buffer) ? $buffer : StringUtilities::sanitizePhp($buffer); + } + if (is_array($buffer) || $this->isLoopable($buffer)) { return $buffer; } @@ -2527,6 +2535,10 @@ protected function evaluateDirective(DirectiveNode $directive) protected function evaluateAntlersPhpNode(PhpExecutionNode $node) { + if (! GlobalRuntimeState::$isPhpEnabled) { + return ''; + } + if (! GlobalRuntimeState::$allowPhpInContent && GlobalRuntimeState::$isEvaluatingUserData) { return StringUtilities::sanitizePhp($node->content); } diff --git a/src/View/Antlers/Language/Runtime/RuntimeConfiguration.php b/src/View/Antlers/Language/Runtime/RuntimeConfiguration.php index 548be3452f0..8cfefc5670d 100644 --- a/src/View/Antlers/Language/Runtime/RuntimeConfiguration.php +++ b/src/View/Antlers/Language/Runtime/RuntimeConfiguration.php @@ -98,6 +98,13 @@ class RuntimeConfiguration */ public $guardedContentModifiers = []; + /** + * Controls whether PHP execution is globally enabled. + * + * @var bool + */ + public $isPhpEnabled = true; + /** * Indicates if PHP Code should be evaluated in user content. * diff --git a/src/View/Antlers/Language/Runtime/RuntimeParser.php b/src/View/Antlers/Language/Runtime/RuntimeParser.php index 10c424dd150..9f95dae0683 100644 --- a/src/View/Antlers/Language/Runtime/RuntimeParser.php +++ b/src/View/Antlers/Language/Runtime/RuntimeParser.php @@ -138,6 +138,7 @@ public function __construct(DocumentParser $documentParser, NodeProcessor $nodeP */ public function setRuntimeConfiguration(RuntimeConfiguration $configuration) { + GlobalRuntimeState::$isPhpEnabled = $configuration->isPhpEnabled; GlobalRuntimeState::$allowPhpInContent = $configuration->allowPhpInUserContent; GlobalRuntimeState::$allowMethodsInContent = $configuration->allowMethodsInUserContent; GlobalRuntimeState::$throwErrorOnAccessViolation = $configuration->throwErrorOnAccessViolation; diff --git a/tests/Antlers/Runtime/PhpDisabledTest.php b/tests/Antlers/Runtime/PhpDisabledTest.php new file mode 100644 index 00000000000..88a0bc4c1e1 --- /dev/null +++ b/tests/Antlers/Runtime/PhpDisabledTest.php @@ -0,0 +1,51 @@ +assertSame('Before After', $result); + } + + public function test_it_ignores_inline_echo_blocks_when_disabled() + { + $result = (string) Antlers::parse('Before {{$ "hello" $}} After', []); + + $this->assertSame('Before After', $result); + } + + public function test_php_disabled_is_the_default() + { + $result = (string) Antlers::parse('Before {{? echo "hello"; ?}} After', []); + + $this->assertSame('Before After', $result); + } + + public function test_inline_php_tags_disabled_is_the_default() + { + $result = (string) Antlers::parse('Before After', []); + + $this->assertSame('Before <?php echo "hello"; ?> After', $result); + } + + public function test_it_allows_inline_echo_blocks_when_enabled() + { + $result = (string) Antlers::parse('Before {{$ "hello" $}} After', [], true); + + $this->assertSame('Before hello After', $result); + } + + public function test_it_allow_inline_php_blocks_when_enabled() + { + $result = (string) Antlers::parse('Before {{? echo "hello"; ?}} After', [], true); + + $this->assertSame('Before hello After', $result); + } +} From 92d378eaaa4098b9332f2023b8a0971313492305 Mon Sep 17 00:00:00 2001 From: John Koster Date: Wed, 25 Feb 2026 23:53:59 -0600 Subject: [PATCH 2/2] Update PhpDisabledTest.php --- tests/Antlers/Runtime/PhpDisabledTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Antlers/Runtime/PhpDisabledTest.php b/tests/Antlers/Runtime/PhpDisabledTest.php index 88a0bc4c1e1..3e19a4ef690 100644 --- a/tests/Antlers/Runtime/PhpDisabledTest.php +++ b/tests/Antlers/Runtime/PhpDisabledTest.php @@ -39,7 +39,7 @@ public function test_it_allows_inline_echo_blocks_when_enabled() { $result = (string) Antlers::parse('Before {{$ "hello" $}} After', [], true); - $this->assertSame('Before hello After', $result); + $this->assertSame('Before hello After', $result); } public function test_it_allow_inline_php_blocks_when_enabled()