Skip to content

Commit 601209b

Browse files
committed
[BUGFIX] Refactored unwrapping of USER_INT
- simplified handling - improved regex to be faster & more reliable trying not hitting limits and return nulls - add fallback if that happens - leverage new PHP power if ext:headless running on newer PHP version
1 parent a27ff50 commit 601209b

2 files changed

Lines changed: 73 additions & 48 deletions

File tree

Classes/Utility/HeadlessUserInt.php

Lines changed: 70 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,38 @@
1313

1414
use function json_decode;
1515
use function json_encode;
16+
use function json_last_error;
1617
use function preg_quote;
1718
use function preg_replace;
1819
use function preg_replace_callback;
1920
use function sprintf;
2021
use function str_contains;
2122
use function substr;
2223

24+
use function trim;
25+
26+
use const JSON_ERROR_NONE;
27+
use const PHP_VERSION_ID;
28+
2329
class HeadlessUserInt
2430
{
2531
public const STANDARD = 'HEADLESS_INT';
2632
public const NESTED = 'NESTED_HEADLESS_INT';
2733
public const STANDARD_NULLABLE = 'HEADLESS_INT_NULL';
2834
public const NESTED_NULLABLE = 'NESTED_HEADLESS_INT_NULL';
29-
private const REGEX = '/("|)%s_START<<(.*?)>>%s_END("|)/s';
35+
36+
private const REGEX = '/(?P<quote>\\\\"|")?(?P<type>%s|%s)_START<<(?P<content>(?:[^>]|>(?!>(?P=type)_END))*+)>>(?P=type)_END(?P=quote)?/sS';
37+
38+
/** @var array<string, string> */
39+
private static array $regexPatterns = [];
3040

3141
public function wrap(string $content, string $type = self::STANDARD): string
3242
{
3343
return preg_replace(
3444
'/(' . preg_quote('<!--INT_SCRIPT.', '/') . '[0-9a-z]{32}' . preg_quote('-->', '/') . ')/',
3545
sprintf('%s_START<<\1>>%s_END', $type, $type),
3646
$content
37-
);
47+
) ?? $content;
3848
}
3949

4050
public function hasNonCacheableContent(string $content): bool
@@ -46,67 +56,80 @@ public function unwrap(string $content): string
4656
{
4757
if (str_contains($content, self::NESTED)) {
4858
$content = preg_replace_callback(
49-
sprintf(self::REGEX, self::NESTED, self::NESTED),
50-
[$this, 'replace'],
51-
$content
52-
);
53-
}
54-
55-
if (str_contains($content, self::NESTED_NULLABLE)) {
56-
$content = preg_replace_callback(
57-
sprintf(self::REGEX, self::NESTED_NULLABLE, self::NESTED_NULLABLE),
58-
function (array $content) {
59-
return $this->replace($content, true);
60-
},
59+
$this->buildPattern(self::NESTED, self::NESTED_NULLABLE),
60+
fn(array $m) => $this->replace($m, $m['type'] === self::NESTED_NULLABLE),
6161
$content
62-
);
63-
}
64-
65-
if (str_contains($content, self::STANDARD_NULLABLE)) {
66-
$content = preg_replace_callback(
67-
sprintf(self::REGEX, self::STANDARD_NULLABLE, self::STANDARD_NULLABLE),
68-
function (array $content) {
69-
return $this->replace($content, true);
70-
},
71-
$content
72-
);
62+
) ?? $content;
7363
}
7464

7565
return preg_replace_callback(
76-
sprintf(self::REGEX, self::STANDARD, self::STANDARD),
77-
[$this, 'replace'],
66+
$this->buildPattern(self::STANDARD, self::STANDARD_NULLABLE),
67+
fn(array $m) => $this->replace($m, $m['type'] === self::STANDARD_NULLABLE),
7868
$content
69+
) ?? $content;
70+
}
71+
72+
protected function buildPattern(string $primary, string $nullable): string
73+
{
74+
return self::$regexPatterns[$primary] ??= sprintf(
75+
self::REGEX,
76+
preg_quote($nullable, '/'),
77+
preg_quote($primary, '/')
7978
);
8079
}
8180

82-
/**
83-
* for use in preg_replace_callback
84-
* to unwrap all HEADLESS_INT<<>>HEADLESS_INT blocks
85-
*
86-
* @param array<int, string> $input
87-
*/
88-
private function replace(array $input, bool $returnNull = false): ?string
81+
protected function replace(array $m, bool $isNullable): string
8982
{
90-
$content = $input[2];
91-
if ($input[1] === $input[3] && $input[1] === '"') {
92-
// have a look inside if it might be json already
93-
$decoded = json_decode($content);
83+
$hasQuotes = $m['quote'] !== '';
84+
$rawContent = (string)$m['content'];
9485

95-
if (empty($decoded) && $returnNull) {
96-
return json_encode(null);
86+
if ($hasQuotes) {
87+
if ($this->isJson($rawContent)) {
88+
return $rawContent;
89+
}
90+
91+
$decoded = json_decode($rawContent);
92+
93+
if (empty($decoded) && $isNullable) {
94+
return 'null';
9795
}
9896

9997
if ($decoded !== null) {
100-
return $content;
98+
return $rawContent;
10199
}
102-
return json_encode($content);
100+
101+
return json_encode($rawContent);
102+
}
103+
104+
$jsonEncoded = json_encode($rawContent);
105+
106+
if ($jsonEncoded !== false && $jsonEncoded[0] === '"') {
107+
return substr($jsonEncoded, 1, -1);
108+
}
109+
110+
return $jsonEncoded ?: '';
111+
}
112+
113+
protected function isJson(string $string): bool
114+
{
115+
$string = trim($string);
116+
117+
if ($string === '') {
118+
return false;
103119
}
104120

105-
// trim one occurrence of double quotes at both ends
106-
$jsonEncoded = json_encode($content);
107-
if ($jsonEncoded[0] === '"' && $jsonEncoded[-1] === '"') {
108-
$jsonEncoded = substr($jsonEncoded, 1, -1);
121+
$first = $string[0];
122+
$last = $string[-1];
123+
124+
if (!(($first === '{' && $last === '}') || ($first === '[' && $last === ']'))) {
125+
return false;
126+
}
127+
128+
if (PHP_VERSION_ID >= 80300) {
129+
return json_validate($string);
109130
}
110-
return $jsonEncoded;
131+
132+
json_decode($string);
133+
return json_last_error() === JSON_ERROR_NONE;
111134
}
112135
}

Tests/Unit/ViewHelpers/Format/Json/DecodeViewHelperTest.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ public function testRender(): void
2121
$GLOBALS['TYPO3_CONF_VARS']['FE']['debug'] = true;
2222
$decodeViewHelper = new DecodeViewHelper();
2323
$decodeViewHelper->setArguments(['json' => null]);
24-
$decodeViewHelper->setRenderChildrenClosure(function () { return "\n \n"; });
24+
$decodeViewHelper->setRenderChildrenClosure(function () {
25+
return "\n \n";
26+
});
2527
$result = $decodeViewHelper->render();
2628
self::assertNull($result);
2729
}

0 commit comments

Comments
 (0)