From 1b2ff23dff279bf447359eaaa8ec4dbaa05b3504 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Sat, 13 Jun 2026 10:25:25 -0300 Subject: [PATCH] Guard expression resolver against malformed-UTF-8 json_encode resolveVariable() declares a string return type, but json_encode returns false on malformed UTF-8 (plausible for scraped feed/HTTP payloads), which throws a TypeError under strict_types and fails the node. Encode with JSON_PARTIAL_OUTPUT_ON_ERROR and fall back to an empty string. --- app/Services/Automation/ExpressionResolver.php | 4 +++- tests/Unit/Automation/ExpressionResolverTest.php | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/Services/Automation/ExpressionResolver.php b/app/Services/Automation/ExpressionResolver.php index 492ed53d..f86b1557 100644 --- a/app/Services/Automation/ExpressionResolver.php +++ b/app/Services/Automation/ExpressionResolver.php @@ -58,6 +58,8 @@ private function resolveVariable(string $path, array $context): string return (string) $value; } - return json_encode($value); + // json_encode returns false on malformed UTF-8 (plausible for scraped + // feed/HTTP payloads); the method must still return a string. + return json_encode($value, JSON_PARTIAL_OUTPUT_ON_ERROR) ?: ''; } } diff --git a/tests/Unit/Automation/ExpressionResolverTest.php b/tests/Unit/Automation/ExpressionResolverTest.php index cbdb2f4a..bed7c6b6 100644 --- a/tests/Unit/Automation/ExpressionResolverTest.php +++ b/tests/Unit/Automation/ExpressionResolverTest.php @@ -50,3 +50,17 @@ it('passes through templates with no variables', function () { expect($this->resolver->resolve('plain text', []))->toBe('plain text'); }); + +it('json-encodes array values', function () { + $context = ['fetched' => ['tags' => ['a', 'b']]]; + + expect($this->resolver->resolve('{{ fetched.tags }}', $context))->toBe('["a","b"]'); +}); + +it('does not throw when an array value contains malformed UTF-8', function () { + // Scraped feed/HTTP payloads can carry invalid UTF-8; json_encode returns + // false for it, but the resolver must still yield a string. + $context = ['fetched' => ['body' => ["bad\xB1utf8"]]]; + + expect($this->resolver->resolve('{{ fetched.body }}', $context))->toBeString(); +});