Skip to content

Commit 3989cb1

Browse files
samdarkdevanychvjik
authored
Encode tag content by default
Co-authored-by: devanych <mail@devanych.ru> Co-authored-by: Sergey Predvoditelev <sergey.predvoditelev@gmail.com>
1 parent a7cd1cb commit 3989cb1

2 files changed

Lines changed: 76 additions & 43 deletions

File tree

src/Html.php

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@
3131
* id?: string|null,
3232
* class?: string[]|string|null,
3333
* style?: array<string, string>|string|null,
34+
* encode?: bool|null,
3435
* }
3536
* @psalm-type ListHtmlOptions = HtmlOptions&array{
3637
* tag?: string,
37-
* encode?: bool,
3838
* item?: Closure(string, array-key):string|null,
3939
* separator?: string,
40-
* itemOptions: HtmlOptions,
40+
* itemOptions?: HtmlOptions,
4141
* }
4242
* @psalm-type InputHtmlOptions = HtmlOptions&array {
4343
* value?: string|int|float|\Stringable|bool|null,
@@ -52,7 +52,6 @@
5252
* }
5353
* @psalm-type SelectHtmlOptions = HtmlOptions&array{
5454
* encodeSpaces?: bool,
55-
* encode?: bool,
5655
* prompt?: array{
5756
* text: string,
5857
* options: HtmlOptions,
@@ -277,11 +276,13 @@ public static function escapeJavaScriptStringValue($value): string
277276
*
278277
* @param bool|string|null $name The tag name. If $name is `null` or `false`, the corresponding content will be
279278
* rendered without any tag.
280-
* @param string $content The content to be enclosed between the start and end tags. It will not be HTML-encoded.
281-
* If this is coming from end users, you should consider {@see encode()} it to prevent XSS attacks.
279+
* @param string $content The content to be enclosed between the start and end tags. It will be HTML-encoded by
280+
* default to prevent XSS attacks. In order to turn it off, set `encode` option to `false`.
282281
* @param array $options The HTML tag attributes (HTML options) in terms of name-value pairs. These will be
283282
* rendered as the attributes of the resulting tag. The values will be HTML-encoded using
284283
* {@see encodeAttribute()}. If a value is null, the corresponding attribute will not be rendered.
284+
* The `encode` option is specially handled. If it is `false`, content will be rendered as is. Else it will be
285+
* HTML-encoded with {@see encode()}.
285286
*
286287
* For example when using `['class' => 'my-class', 'target' => '_blank', 'value' => null]` it will result in the
287288
* HTML attributes rendered like this: `class="my-class" target="_blank"`.
@@ -299,6 +300,10 @@ public static function escapeJavaScriptStringValue($value): string
299300
*/
300301
public static function tag($name, string $content = '', array $options = []): string
301302
{
303+
if (ArrayHelper::remove($options, 'encode', true)) {
304+
$content = self::encode($content);
305+
}
306+
302307
if ($name === null || is_bool($name)) {
303308
return $content;
304309
}
@@ -364,12 +369,16 @@ public static function endTag($name): string
364369
* corresponding attribute will not be rendered. See {@see renderTagAttributes()} for details on how attributes
365370
* are being rendered.
366371
*
372+
* @psalm-param HtmlOptions|array<empty, empty> $options
373+
*
367374
* @throws JsonException
368375
*
369376
* @return string The generated style tag.
370377
*/
371378
public static function style(string $content, array $options = []): string
372379
{
380+
/** @psalm-var HtmlOptions $options */
381+
$options['encode'] ??= false;
373382
return self::tag('style', $content, $options);
374383
}
375384

@@ -382,12 +391,16 @@ public static function style(string $content, array $options = []): string
382391
* the corresponding attribute will not be rendered. See {@see renderTagAttributes()} for details on how attributes
383392
* are being rendered.
384393
*
394+
* @psalm-param HtmlOptions|array<empty, empty> $options
395+
*
385396
* @throws JsonException
386397
*
387398
* @return string The generated script tag.
388399
*/
389400
public static function script(string $content, array $options = []): string
390401
{
402+
/** @psalm-var HtmlOptions $options */
403+
$options['encode'] ??= false;
391404
return self::tag('script', $content, $options);
392405
}
393406

@@ -848,10 +861,12 @@ public static function textarea(string $name, ?string $value = '', array $option
848861
{
849862
$options['name'] = $name;
850863

851-
/** @var bool $doubleEncode */
852-
$doubleEncode = ArrayHelper::remove($options, 'doubleEncode', true);
864+
if (isset($options['doubleEncode']) && $options['doubleEncode'] === false) {
865+
$value = self::encode($value, false);
866+
$options['encode'] = false;
867+
}
853868

854-
return self::tag('textarea', self::encode($value, $doubleEncode), $options);
869+
return self::tag('textarea', (string)$value, $options);
855870
}
856871

857872
/**
@@ -928,7 +943,9 @@ private static function booleanInput(string $type, string $name, bool $checked,
928943
{
929944
$options['checked'] = $checked;
930945
$value = array_key_exists('value', $options) ? $options['value'] : '1';
946+
$encode = (bool) ArrayHelper::remove($options, 'encode', true);
931947

948+
/** @var BooleanInputHtmlOptions $options */
932949
if (isset($options['uncheck'])) {
933950
// Add a hidden field so that if the checkbox is not selected, it still submits a value.
934951
$hiddenOptions = [];
@@ -955,6 +972,9 @@ private static function booleanInput(string $type, string $name, bool $checked,
955972
return $hidden . self::input($type, $name, $value, $options);
956973
}
957974

975+
$label = $encode ? self::encode($label) : $label;
976+
$labelOptions['encode'] = false;
977+
958978
if ($wrapInput) {
959979
$input = self::input($type, $name, $value, $options);
960980
return $hidden . self::label($input . ' ' . $label, null, $labelOptions);
@@ -1032,6 +1052,8 @@ public static function dropDownList(string $name, $selection = null, array $item
10321052

10331053
$selectOptions = self::renderSelectOptionTags($selection, $items, $options);
10341054

1055+
$options['encode'] = false;
1056+
10351057
return self::tag('select', "\n" . $selectOptions . "\n", $options);
10361058
}
10371059

@@ -1118,6 +1140,8 @@ public static function listBox(string $name, $selection = null, array $items = [
11181140
/** @var SelectHtmlOptions $options */
11191141
$selectOptions = self::renderSelectOptionTags($selection, $items, $options);
11201142

1143+
$options['encode'] = false;
1144+
11211145
return $hidden . self::tag('select', "\n" . $selectOptions . "\n", $options);
11221146
}
11231147

@@ -1161,7 +1185,6 @@ public static function listBox(string $name, $selection = null, array $items = [
11611185
* @psalm-param InputHtmlOptions&array{
11621186
* item?: Closure(int, string, string, bool, mixed):string|null,
11631187
* itemOptions?: HtmlOptions|null,
1164-
* encode?: bool,
11651188
* separator?: string|null,
11661189
* tag?: string|null,
11671190
* unselect?: string|int|float|\Stringable|bool|null,
@@ -1187,9 +1210,6 @@ public static function checkboxList(string $name, $selection = null, array $item
11871210
/** @psalm-var HtmlOptions $itemOptions */
11881211
$itemOptions = ArrayHelper::remove($options, 'itemOptions', []);
11891212

1190-
/** @var bool $encode */
1191-
$encode = ArrayHelper::remove($options, 'encode', true);
1192-
11931213
/** @var string $separator */
11941214
$separator = ArrayHelper::remove($options, 'separator', "\n");
11951215

@@ -1209,7 +1229,7 @@ public static function checkboxList(string $name, $selection = null, array $item
12091229
} else {
12101230
$lines[] = self::checkbox($name, $checked, array_merge([
12111231
'value' => $value,
1212-
'label' => $encode ? self::encode($label) : $label,
1232+
'label' => $label,
12131233
], $itemOptions));
12141234
}
12151235
$index++;
@@ -1229,6 +1249,7 @@ public static function checkboxList(string $name, $selection = null, array $item
12291249
$hidden = '';
12301250
}
12311251

1252+
$options['encode'] = false;
12321253
return $hidden . self::tag($tag, implode($separator, $lines), $options);
12331254
}
12341255

@@ -1271,7 +1292,6 @@ public static function checkboxList(string $name, $selection = null, array $item
12711292
* @psalm-param InputHtmlOptions&array{
12721293
* item?: Closure(int, string, string, bool, mixed):string|null,
12731294
* itemOptions?: HtmlOptions|null,
1274-
* encode?: bool,
12751295
* separator?: string|null,
12761296
* tag?: string|null,
12771297
* unselect?: string|int|float|\Stringable|bool|null,
@@ -1295,9 +1315,6 @@ public static function radioList(string $name, $selection = null, array $items =
12951315
/** @psalm-var HtmlOptions $itemOptions */
12961316
$itemOptions = ArrayHelper::remove($options, 'itemOptions', []);
12971317

1298-
/** @var bool $encode */
1299-
$encode = ArrayHelper::remove($options, 'encode', true);
1300-
13011318
/** @var string $separator */
13021319
$separator = ArrayHelper::remove($options, 'separator', "\n");
13031320

@@ -1329,13 +1346,14 @@ public static function radioList(string $name, $selection = null, array $items =
13291346
} else {
13301347
$lines[] = self::radio($name, $checked, array_merge([
13311348
'value' => $value,
1332-
'label' => $encode ? self::encode($label) : $label,
1349+
'label' => $label,
13331350
], $itemOptions));
13341351
}
13351352
$index++;
13361353
}
13371354
$visibleContent = implode($separator, $lines);
13381355

1356+
$options['encode'] = false;
13391357
return $hidden . self::tag($tag, $visibleContent, $options);
13401358
}
13411359

@@ -1415,7 +1433,7 @@ public static function p(string $content = '', array $options = []): string
14151433
*
14161434
* See {@see renderTagAttributes()} for details on how attributes are being rendered.
14171435
*
1418-
* @psalm-param array<array-key, string> $items
1436+
* @psalm-param array<array-key, mixed> $items
14191437
* @psalm-param ListHtmlOptions $options
14201438
*
14211439
* @throws JsonException
@@ -1427,9 +1445,6 @@ public static function ul($items, array $options = []): string
14271445
/** @var string $tag */
14281446
$tag = ArrayHelper::remove($options, 'tag', 'ul');
14291447

1430-
/** @var bool $encode */
1431-
$encode = ArrayHelper::remove($options, 'encode', true);
1432-
14331448
/** @var Closure(string, array-key):string|null $formatter */
14341449
$formatter = ArrayHelper::remove($options, 'item');
14351450

@@ -1445,14 +1460,19 @@ public static function ul($items, array $options = []): string
14451460
}
14461461

14471462
$results = [];
1463+
/** @var mixed $item */
14481464
foreach ($items as $index => $item) {
1465+
$item = (string)$item;
14491466
if ($formatter !== null) {
14501467
$results[] = $formatter($item, $index);
14511468
} else {
1452-
$results[] = self::tag('li', $encode ? self::encode($item) : $item, $itemOptions);
1469+
$results[] = self::tag('li', $item, $itemOptions);
14531470
}
14541471
}
14551472

1473+
$separator = self::encode($separator);
1474+
$options['encode'] = false;
1475+
14561476
return self::tag(
14571477
$tag,
14581478
$separator . implode($separator, $results) . $separator,
@@ -1558,7 +1578,7 @@ private static function renderSelectOptionTags($selection, array $items, array &
15581578

15591579
$lines = [];
15601580
if (isset($tagOptions['prompt'])) {
1561-
$promptOptions = ['value' => ''];
1581+
$promptOptions = ['value' => '', 'encode' => false];
15621582
if (is_string($tagOptions['prompt'])) {
15631583
$promptText = $tagOptions['prompt'];
15641584
} else {
@@ -1583,6 +1603,7 @@ private static function renderSelectOptionTags($selection, array $items, array &
15831603
foreach ($items as $key => $value) {
15841604
if (is_array($value)) {
15851605
$groupAttrs = $groups[$key] ?? [];
1606+
$groupAttrs['encode'] = false;
15861607
if (!isset($groupAttrs['label'])) {
15871608
$groupAttrs['label'] = $key;
15881609
}
@@ -1607,6 +1628,7 @@ private static function renderSelectOptionTags($selection, array $items, array &
16071628
if ($encodeSpaces) {
16081629
$text = str_replace(' ', '&nbsp;', $text);
16091630
}
1631+
$attrs['encode'] = false;
16101632
$lines[] = self::tag('option', $text, $attrs);
16111633
}
16121634
}

0 commit comments

Comments
 (0)