Skip to content
Snippets Groups Projects
Unverified Commit f7d93687 authored by Lauri Timmanee's avatar Lauri Timmanee
Browse files

Issue #3274648 by nod_, Wim Leers: HTMLRestrictions::merge() and...

Issue #3274648 by nod_, Wim Leers: HTMLRestrictions::merge() and ::toGeneralHtmlSupportConfig() fail on allowed attribute values that can be interpreted as integers

(cherry picked from commit 94b5d575)
parent 0b4483b7
No related branches found
No related tags found
42 merge requests!85673265330-fix-missing-hyphens: Create patch to MR and fix remaining words,!8394[warning] array_flip(): Can only flip STRING and INTEGER values, when saving a non-revisionable custom content entity,!7780issue 3443822: fix for 'No route found for the specified format html. Supported formats: json, xml.',!7416Simplify the HTML of field.html.twig,!7150Revert "Issue #3137119 by munish.kumar, johnwebdev, Jaypan, jungle, xjm,...,!6445Issue #3034692: Renamed the getHandler function which return the configuration of a handler instance on given display,!5013Issue #3071143: Table Render Array Example Is Incorrect,!4848Issue #1566662: Update module should send notifications on Thursdays,!4792Issue #2230689: Remove redundant "Italic" style,!4782Issue #2662898: "Links" field not displaying on custom view modes,!4220Issue #3368223: Link field > Access to internal links is not checked on display.,!4173Issue #2123543: Add string context and location filters to the translate interface,!3884Issue #3356842,!3870Issue #3087868,!3812Draft: Issue #3339373 by alexpott, andypost, mondrake:...,!3736Issue #3294005: Refactor Claro's form--password-confirm stylesheet,!3686Issue #3219967 against 9.5.x,!3683Issue #2939397: Clearing AliasManager cache with root path raises warning,!3543Issue #3344259: Allow ajax dialog to have focus configurable,!3437Issue #3106205: Length of menu_tree.url and menu_tree.route_param_key are too short (255 characters),!3356Issue #3209129: Scrolling problems when adding a block via layout builder,!2982Issue #3301562: Translate the default settings for this plugin (TimestampAgoFormatter),!2921Issue #1383696: Allow a custom HTML element to be selected for a grouping field,!2920Issue #3260175: Saving media entity without an owner crashes,!2857Issue #3314541: Remove unnecessary fill from SVG icon for the "Media Library" CKEditor 5 button — enabling dark mode support in contrib,!2841Resolve #3296811 "Resourceresponsetrait needs a",!2803Issue #3041402: Add option absolute url in formatter URL to image,!2733Issue #3293855: Update the outdated user_help text for user.admin_permissions and the description of the select box on the role settings page,!2527Issue #3298714: Undefined #options and Count Warning in Radios.php,!2447Issue #3293135: shouldUpdateThumbnail does not update thumbnail is source field changed,!2428Issue #3032078: Multiple webheads can cause infinite growth of Twig cache,!2280Issue #3280415: Metapackage Generator Breaks Under Composer --no-dev,!2205Quote all names in the regions section.,!2050Issue #3272969: Remove UnqiueField constraint.,!1956Issue #3268872: hook_views_invalidate_cache not called when a view is deleted,!1893Issue #3217260: Add a way to make media captions not editable in CKEditor,!1690fixing include_source documentation at SubProcess.php,!1520Issue #2815221: Add ability to use Quick Edit to the latest_revision route,!1459Issue #3087632: menu_name max length is too long,!878Issue #3221534: throw an exception when IDs passed to loadMultiple() are badly formed,!866Issue #2845319: The highlighting of the 'Home' menu-link does not respect query strings and fragment identifiers,!204Issue #3040556: It is not possible to react to an entity being duplicated
......@@ -693,6 +693,67 @@ public function doIntersect(HTMLRestrictions $other): HTMLRestrictions {
return new self($intersection);
}
/**
* Merge arrays of allowed elements according to HTMLRestrictions rules.
*
* @param array $array1
* The first array of allowed elements.
* @param array $array2
* The second array of allowed elements.
*
* @return array
* Merged array of allowed elements.
*/
private static function mergeAllowedElementsLevel(array $array1, array $array2): array {
$union = [];
$array1_keys = array_keys($array1);
$array2_keys = array_keys($array2);
$common_keys = array_intersect($array1_keys, $array2_keys);
if (count($common_keys) === 0) {
// There are no keys in common, simply append the arrays.
$union = $array1 + $array2;
}
else {
// For all the distinct keys, append them to the result.
$filter_keys = array_flip($common_keys);
// Add all unique keys from $array1.
$union += array_diff_key($array1, $filter_keys);
// Add all unique keys from $array2.
$union += array_diff_key($array2, $filter_keys);
// There are some keys in common that need to be merged.
foreach ($common_keys as $key) {
$value1 = $array1[$key];
$value2 = $array2[$key];
$value1_is_bool = is_bool($value1);
$value2_is_bool = is_bool($value2);
// When both values are boolean, combine the two.
if ($value1_is_bool && $value2_is_bool) {
$union[$key] = $value1 || $value2;
}
// When only one value is a boolean, take the most permissive result:
// - when the value it TRUE, keep TRUE as it is the most permissive
// - when the value is FALSE, take the other value.
elseif ($value1_is_bool) {
$union[$key] = $value1 ?: $value2;
}
elseif ($value2_is_bool) {
$union[$key] = $value2 ?: $value1;
}
// Process nested arrays, in this case it correspond to tag attributes
// configuration.
elseif (is_array($value1) && is_array($value2)) {
$union[$key] = self::mergeAllowedElementsLevel($value1, $value2);
}
}
}
// Make sure the order of the union array matches the order of the keys in
// the arrays provided.
$keys_order = array_merge($array1_keys, $array2_keys);
return array_merge(array_flip($keys_order), $union);
}
/**
* Computes set union of two HTML restrictions, with wildcard support.
*
......@@ -704,103 +765,7 @@ public function doIntersect(HTMLRestrictions $other): HTMLRestrictions {
* are either allowed in $this or in $other.
*/
public function merge(HTMLRestrictions $other): HTMLRestrictions {
$union = array_merge_recursive($this->elements, $other->elements);
// When recursively merging elements arrays, unkeyed boolean values can
// appear in attribute config arrays. This removes them.
foreach ($union as $tag => $tag_config) {
if (is_array($tag_config)) {
// If the HTML tag restrictions for both operands were both booleans,
// then the result of array_merge_recursive() is an array containing two
// booleans (because it is designed for arrays, not for also merging
// booleans) under the first two numeric keys: 0 and 1. This does not
// match the structure expected of HTML restrictions. Combine the two
// booleans.
if (array_key_exists(0, $tag_config) && array_key_exists(1, $tag_config) && is_bool($tag_config[0]) && is_bool($tag_config[1])) {
// Twice FALSE.
if ($tag_config === [FALSE, FALSE]) {
$union[$tag] = FALSE;
}
// Once or twice TRUE.
else {
$union[$tag] = TRUE;
}
continue;
}
// If the HTML tag restrictions for only one of the two operands was a
// boolean, then the result of array_merge_recursive() is an array
// containing the complete contents of the non-boolean operand plus an
// additional key-value pair with the first numeric key: 0.
if (array_key_exists(0, $tag_config)) {
// If the boolean was FALSE (meaning: "no attributes allowed"), then
// the other operand's values should be used in an union: this yields
// the most permissive result.
if ($tag_config[0] === FALSE) {
unset($union[$tag][0]);
}
// If the boolean was TRUE (meaning: "all attributes allowed"), then
// the other operand's values should be ignored in an union: this
// yields the most permissive result.
elseif ($tag_config[0] === TRUE) {
$union[$tag] = TRUE;
}
continue;
}
// If the HTML tag restrictions are arrays for both operands, similar
// logic needs to be applied to the attribute-level restrictions.
foreach ($tag_config as $html_tag_attribute_name => $html_tag_attribute_restrictions) {
if (is_bool($html_tag_attribute_restrictions)) {
continue;
}
if (array_key_exists(0, $html_tag_attribute_restrictions)) {
// Special case: the global attribute `*` HTML tag.
// @see https://html.spec.whatwg.org/multipage/dom.html#global-attributes
// @see validateAllowedRestrictionsPhase2()
// @see validateAllowedRestrictionsPhase4()
if ($tag === '*') {
assert(is_bool($html_tag_attribute_restrictions[0]) || is_bool($html_tag_attribute_restrictions[1]));
// When both are boolean, pick the most permissive value.
if (is_bool($html_tag_attribute_restrictions[0]) && isset($html_tag_attribute_restrictions[1]) && is_bool($html_tag_attribute_restrictions[1])) {
$value = $html_tag_attribute_restrictions[0] || $html_tag_attribute_restrictions[1];
}
else {
$value = is_bool($html_tag_attribute_restrictions[0])
? $html_tag_attribute_restrictions[0]
: $html_tag_attribute_restrictions[1];
}
$union[$tag][$html_tag_attribute_name] = $value;
continue;
}
// The "twice FALSE" case cannot occur for attributes, because
// attribute restrictions either have "TRUE" (to indicate any value
// is allowed for the attribute) or a list of allowed attribute
// values. If there is a numeric key, then one of the two operands
// must allow all attribute values (the "TRUE" case). Otherwise, an
// array merge would have happened, and no numeric key would exist.
// Therefore, this is always once or twice TRUE.
// e.g.: <foo bar> and <foo bar>, or <foo bar> and <foo bar="baz">
assert($html_tag_attribute_restrictions[0] === TRUE || $html_tag_attribute_restrictions[1] === TRUE);
$union[$tag][$html_tag_attribute_name] = TRUE;
}
else {
// Finally, when both operands list the same allowed attribute
// values, then the result provided by array_merge_recursive() for
// those allowed attribute values is an array containing two times
// `TRUE` (because it is designed for arrays, not for also merging
// booleans) under the first two numeric keys: 0 and 1.
// e.g.: <foo bar="baz qux"> merged with <foo bar="baz quux">.
foreach ($html_tag_attribute_restrictions as $allowed_attribute_value => $merged_result) {
if ($merged_result === [0 => TRUE, 1 => TRUE]) {
$union[$tag][$html_tag_attribute_name][$allowed_attribute_value] = TRUE;
}
}
}
}
}
}
$union = self::mergeAllowedElementsLevel($this->elements, $other->elements);
// Special case: wildcard attributes, and the ability to define restrictions
// for all concrete attributes matching them using:
......@@ -1109,7 +1074,10 @@ public function toGeneralHtmlSupportConfig(): array {
// that this class expects to the `['en', 'fr']` structure that the
// GHS functionality in CKEditor 5 expects.
if (is_array($value)) {
$value = array_keys($value);
// Ensure that all values are strings, this is necessary since PHP
// transforms the "1" string into 1 the number when it is used as
// an array key.
$value = array_map('strval', array_keys($value));
}
// Drupal never allows style attributes due to security concerns.
// @see \Drupal\Component\Utility\Xss
......
......@@ -294,6 +294,14 @@ public function providerConvenienceConstructors(): \Generator {
'<a target class>',
['a' => ['target' => TRUE, 'class' => TRUE]],
];
yield 'tag with allowed attribute value that happen to be numbers' => [
'<ol type="1 A I">',
['ol' => ['type' => [1 => TRUE, 'A' => TRUE, 'I' => TRUE]]],
];
yield 'tag with allowed attribute value that happen to be numbers (reversed)' => [
'<ol type="I A 1">',
['ol' => ['type' => ['I' => TRUE, 'A' => TRUE, 1 => TRUE]]],
];
// Multiple tag cases.
yield 'two tags' => [
......@@ -682,6 +690,27 @@ public function providerRepresentations(): \Generator {
],
],
];
yield '<ol type="1 A">' => [
new HTMLRestrictions(['ol' => ['type' => ['1' => TRUE, 'A' => TRUE]]]),
['<ol type="1 A">'],
'<ol type="1 A">',
[
[
'name' => 'ol',
'attributes' => [
[
'key' => 'type',
'value' => [
'regexp' => [
'pattern' => '/^(1|A)$/',
],
],
],
],
],
],
];
}
/**
......@@ -906,6 +935,34 @@ public function providerOperands(): \Generator {
'intersection' => 'a',
'union' => 'b',
];
yield 'attribute restrictions are different: <ol type=*> vs <ol type="A">' => [
'a' => new HTMLRestrictions(['ol' => ['type' => TRUE]]),
'b' => new HTMLRestrictions(['ol' => ['type' => ['A' => TRUE]]]),
'diff' => 'a',
'intersection' => 'b',
'union' => 'a',
];
yield 'attribute restrictions are different: <ol type=*> vs <ol type="A"> — vice versa' => [
'b' => new HTMLRestrictions(['ol' => ['type' => ['A' => TRUE]]]),
'a' => new HTMLRestrictions(['ol' => ['type' => TRUE]]),
'diff' => HTMLRestrictions::emptySet(),
'intersection' => 'a',
'union' => 'b',
];
yield 'attribute restrictions are different: <ol type=*> vs <ol type="1">' => [
'a' => new HTMLRestrictions(['ol' => ['type' => TRUE]]),
'b' => new HTMLRestrictions(['ol' => ['type' => ['1' => TRUE]]]),
'diff' => 'a',
'intersection' => 'b',
'union' => 'a',
];
yield 'attribute restrictions are different: <ol type=*> vs <ol type="1"> — vice versa' => [
'b' => new HTMLRestrictions(['ol' => ['type' => ['1' => TRUE]]]),
'a' => new HTMLRestrictions(['ol' => ['type' => TRUE]]),
'diff' => HTMLRestrictions::emptySet(),
'intersection' => 'a',
'union' => 'b',
];
// Complex cases.
yield 'attribute restrictions are different: <a hreflang="en"> vs <strong>' => [
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment