html: Count <image> attributes state changes as the relevant mutations (#39483)

Follow the HTML specification and take into account that state changes
of the <image> 'crossorigin' and 'referrerpolicy' content attributes
(not 'crossOrigin' and 'referrerPolicy' IDL attributes) should be
counted as relevant mutations.

See https://html.spec.whatwg.org/multipage/#relevant-mutations

Testing: Improvements in the following tests
- html/dom/reflection-embedded.html
-
html/semantics/embedded-content/the-img-element/relevant-mutations.html

Signed-off-by: Andrei Volykhin <andrei.volykhin@gmail.com>
This commit is contained in:
Andrei Volykhin 2025-09-29 12:00:51 +03:00 committed by GitHub
parent aaa7f83176
commit af74db4e92
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 96 additions and 245 deletions

View file

@ -5428,17 +5428,18 @@ impl TaskOnce for ElementPerformFullscreenExit {
} }
} }
/// <https://html.spec.whatwg.org/multipage/#cors-settings-attribute>
pub(crate) fn reflect_cross_origin_attribute(element: &Element) -> Option<DOMString> { pub(crate) fn reflect_cross_origin_attribute(element: &Element) -> Option<DOMString> {
let attr = element.get_attribute(&ns!(), &local_name!("crossorigin")); element
.get_attribute(&ns!(), &local_name!("crossorigin"))
if let Some(mut val) = attr.map(|v| v.Value()) { .map(|attribute| {
val.make_ascii_lowercase(); let value = attribute.value().to_ascii_lowercase();
if val == "anonymous" || val == "use-credentials" { if value == "anonymous" || value == "use-credentials" {
return Some(val); DOMString::from(value)
} else {
DOMString::from("anonymous")
} }
return Some(DOMString::from("anonymous")); })
}
None
} }
pub(crate) fn set_cross_origin_attribute( pub(crate) fn set_cross_origin_attribute(
@ -5454,38 +5455,38 @@ pub(crate) fn set_cross_origin_attribute(
} }
} }
/// <https://html.spec.whatwg.org/multipage/#referrer-policy-attribute>
pub(crate) fn reflect_referrer_policy_attribute(element: &Element) -> DOMString { pub(crate) fn reflect_referrer_policy_attribute(element: &Element) -> DOMString {
let attr = element
element.get_attribute_by_name(DOMString::from_string(String::from("referrerpolicy"))); .get_attribute(&ns!(), &local_name!("referrerpolicy"))
.map(|attribute| {
if let Some(mut val) = attr.map(|v| v.Value()) { let value = attribute.value().to_ascii_lowercase();
val.make_ascii_lowercase(); if value == "no-referrer" ||
if val == "no-referrer" || value == "no-referrer-when-downgrade" ||
val == "no-referrer-when-downgrade" || value == "same-origin" ||
val == "same-origin" || value == "origin" ||
val == "origin" || value == "strict-origin" ||
val == "strict-origin" || value == "origin-when-cross-origin" ||
val == "origin-when-cross-origin" || value == "strict-origin-when-cross-origin" ||
val == "strict-origin-when-cross-origin" || value == "unsafe-url"
val == "unsafe-url"
{ {
return val; DOMString::from(value)
} } else {
}
DOMString::new() DOMString::new()
} }
})
.unwrap_or_default()
}
pub(crate) fn referrer_policy_for_element(element: &Element) -> ReferrerPolicy { pub(crate) fn referrer_policy_for_element(element: &Element) -> ReferrerPolicy {
element element
.get_attribute_by_name(DOMString::from_string(String::from("referrerpolicy"))) .get_attribute(&ns!(), &local_name!("referrerpolicy"))
.map(|attribute: DomRoot<Attr>| determine_policy_for_token(attribute.Value().str())) .map(|attribute| determine_policy_for_token(&attribute.value()))
.unwrap_or(element.owner_document().get_referrer_policy()) .unwrap_or(element.owner_document().get_referrer_policy())
} }
pub(crate) fn cors_setting_for_element(element: &Element) -> Option<CorsSettings> { pub(crate) fn cors_setting_for_element(element: &Element) -> Option<CorsSettings> {
reflect_cross_origin_attribute(element).and_then(|attr| match attr.str() { element
"anonymous" => Some(CorsSettings::Anonymous), .get_attribute(&ns!(), &local_name!("crossorigin"))
"use-credentials" => Some(CorsSettings::UseCredentials), .map(|attribute| CorsSettings::from_enumerated_attribute(&attribute.value()))
_ => unreachable!(),
})
} }

View file

@ -22,7 +22,7 @@ use net_traits::image_cache::{
Image, ImageCache, ImageCacheResult, ImageLoadListener, ImageOrMetadataAvailable, Image, ImageCache, ImageCacheResult, ImageLoadListener, ImageOrMetadataAvailable,
ImageResponse, PendingImageId, UsePlaceholder, ImageResponse, PendingImageId, UsePlaceholder,
}; };
use net_traits::request::{Destination, Initiator, RequestId}; use net_traits::request::{CorsSettings, Destination, Initiator, RequestId};
use net_traits::{ use net_traits::{
FetchMetadata, FetchResponseListener, FetchResponseMsg, NetworkError, ReferrerPolicy, FetchMetadata, FetchResponseListener, FetchResponseMsg, NetworkError, ReferrerPolicy,
ResourceFetchTiming, ResourceTimingType, ResourceFetchTiming, ResourceTimingType,
@ -1603,22 +1603,6 @@ fn parse_a_sizes_attribute(value: &str) -> SourceSizeList {
SourceSizeList::parse(&context, &mut parser) SourceSizeList::parse(&context, &mut parser)
} }
fn get_correct_referrerpolicy_from_raw_token(token: &DOMString) -> DOMString {
if token.is_empty() {
// Empty token is treated as the default referrer policy inside determine_policy_for_token,
// so it should remain unchanged.
DOMString::new()
} else {
let policy = determine_policy_for_token(token.str());
if policy == ReferrerPolicy::EmptyString {
return DOMString::new();
}
DOMString::from_string(policy.to_string())
}
}
#[allow(non_snake_case)] #[allow(non_snake_case)]
impl HTMLImageElementMethods<crate::DomTypeHolder> for HTMLImageElement { impl HTMLImageElementMethods<crate::DomTypeHolder> for HTMLImageElement {
// https://html.spec.whatwg.org/multipage/#dom-image // https://html.spec.whatwg.org/multipage/#dom-image
@ -1784,24 +1768,8 @@ impl HTMLImageElementMethods<crate::DomTypeHolder> for HTMLImageElement {
reflect_referrer_policy_attribute(self.upcast::<Element>()) reflect_referrer_policy_attribute(self.upcast::<Element>())
} }
// https://html.spec.whatwg.org/multipage/#dom-img-referrerpolicy // <https://html.spec.whatwg.org/multipage/#dom-img-referrerpolicy>
fn SetReferrerPolicy(&self, value: DOMString, can_gc: CanGc) { make_setter!(SetReferrerPolicy, "referrerpolicy");
let referrerpolicy_attr_name = local_name!("referrerpolicy");
let element = self.upcast::<Element>();
let previous_correct_attribute_value = get_correct_referrerpolicy_from_raw_token(
&element.get_string_attribute(&referrerpolicy_attr_name),
);
let correct_value_or_empty_string = get_correct_referrerpolicy_from_raw_token(&value);
if previous_correct_attribute_value != correct_value_or_empty_string {
// Setting the attribute to the same value will update the image.
// We don't want to start an update if referrerpolicy is set to the same value.
element.set_string_attribute(
&referrerpolicy_attr_name,
correct_value_or_empty_string,
can_gc,
);
}
}
/// <https://html.spec.whatwg.org/multipage/#dom-img-decode> /// <https://html.spec.whatwg.org/multipage/#dom-img-decode>
fn Decode(&self, can_gc: CanGc) -> Rc<Promise> { fn Decode(&self, can_gc: CanGc) -> Rc<Promise> {
@ -1874,9 +1842,51 @@ impl VirtualMethods for HTMLImageElement {
&local_name!("src") | &local_name!("src") |
&local_name!("srcset") | &local_name!("srcset") |
&local_name!("width") | &local_name!("width") |
&local_name!("crossorigin") | &local_name!("sizes") => {
&local_name!("sizes") | // <https://html.spec.whatwg.org/multipage/#reacting-to-dom-mutations>
&local_name!("referrerpolicy") => self.update_the_image_data(can_gc), // The element's src, srcset, width, or sizes attributes are set, changed, or
// removed.
self.update_the_image_data(can_gc);
},
&local_name!("crossorigin") => {
// <https://html.spec.whatwg.org/multipage/#reacting-to-dom-mutations>
// The element's crossorigin attribute's state is changed.
let cross_origin_state_changed = match mutation {
AttributeMutation::Removed | AttributeMutation::Set(None) => true,
AttributeMutation::Set(Some(old_value)) => {
let new_cors_setting =
CorsSettings::from_enumerated_attribute(&attr.value());
let old_cors_setting = CorsSettings::from_enumerated_attribute(old_value);
new_cors_setting != old_cors_setting
},
};
if cross_origin_state_changed {
self.update_the_image_data(can_gc);
}
},
&local_name!("referrerpolicy") => {
// <https://html.spec.whatwg.org/multipage/#reacting-to-dom-mutations>
// The element's referrerpolicy attribute's state is changed.
let referrer_policy_state_changed = match mutation {
AttributeMutation::Removed | AttributeMutation::Set(None) => {
let referrer_policy = determine_policy_for_token(&attr.value());
referrer_policy != ReferrerPolicy::EmptyString
},
AttributeMutation::Set(Some(old_value)) => {
let new_referrer_policy = determine_policy_for_token(&attr.value());
let old_referrer_policy = determine_policy_for_token(old_value);
new_referrer_policy != old_referrer_policy
},
};
if referrer_policy_state_changed {
self.update_the_image_data(can_gc);
}
},
_ => {}, _ => {},
} }
} }

View file

@ -394,7 +394,7 @@ DOMInterfaces = {
}, },
'HTMLImageElement': { 'HTMLImageElement': {
'canGc': ['RequestSubmit', 'ReportValidity', 'Reset','SetRel', 'Decode', 'SetCrossOrigin', 'SetWidth', 'SetHeight', 'SetReferrerPolicy'], 'canGc': ['RequestSubmit', 'ReportValidity', 'Reset','SetRel', 'Decode', 'SetCrossOrigin', 'SetWidth', 'SetHeight'],
}, },
'HTMLInputElement': { 'HTMLInputElement': {

View file

@ -137,6 +137,17 @@ pub enum CorsSettings {
UseCredentials, UseCredentials,
} }
impl CorsSettings {
/// <https://html.spec.whatwg.org/multipage/#cors-settings-attribute>
pub fn from_enumerated_attribute(value: &str) -> CorsSettings {
match value.to_ascii_lowercase().as_str() {
"anonymous" => CorsSettings::Anonymous,
"use-credentials" => CorsSettings::UseCredentials,
_ => CorsSettings::Anonymous,
}
}
}
/// [Parser Metadata](https://fetch.spec.whatwg.org/#concept-request-parser-metadata) /// [Parser Metadata](https://fetch.spec.whatwg.org/#concept-request-parser-metadata)
#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)] #[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)]
pub enum ParserMetadata { pub enum ParserMetadata {

View file

@ -503,168 +503,6 @@
[img.srcset: IDL set to object "test-valueOf"] [img.srcset: IDL set to object "test-valueOf"]
expected: FAIL expected: FAIL
[img.referrerPolicy: IDL set to ""]
expected: FAIL
[img.referrerPolicy: IDL set to " \\0\\x01\\x02\\x03\\x04\\x05\\x06\\x07 \\b\\t\\n\\v\\f\\r\\x0e\\x0f \\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17 \\x18\\x19\\x1a\\x1b\\x1c\\x1d\\x1e\\x1f foo "]
expected: FAIL
[img.referrerPolicy: IDL set to undefined]
expected: FAIL
[img.referrerPolicy: IDL set to 7]
expected: FAIL
[img.referrerPolicy: IDL set to 1.5]
expected: FAIL
[img.referrerPolicy: IDL set to "5%"]
expected: FAIL
[img.referrerPolicy: IDL set to "+100"]
expected: FAIL
[img.referrerPolicy: IDL set to ".5"]
expected: FAIL
[img.referrerPolicy: IDL set to true]
expected: FAIL
[img.referrerPolicy: IDL set to false]
expected: FAIL
[img.referrerPolicy: IDL set to object "[object Object\]"]
expected: FAIL
[img.referrerPolicy: IDL set to NaN]
expected: FAIL
[img.referrerPolicy: IDL set to Infinity]
expected: FAIL
[img.referrerPolicy: IDL set to -Infinity]
expected: FAIL
[img.referrerPolicy: IDL set to "\\0"]
expected: FAIL
[img.referrerPolicy: IDL set to object "test-toString"]
expected: FAIL
[img.referrerPolicy: IDL set to object "test-valueOf"]
expected: FAIL
[img.referrerPolicy: IDL set to "xno-referrer"]
expected: FAIL
[img.referrerPolicy: IDL set to "no-referrer\\0"]
expected: FAIL
[img.referrerPolicy: IDL set to "o-referrer"]
expected: FAIL
[img.referrerPolicy: IDL set to "NO-REFERRER"]
expected: FAIL
[img.referrerPolicy: IDL set to "xno-referrer-when-downgrade"]
expected: FAIL
[img.referrerPolicy: IDL set to "no-referrer-when-downgrade\\0"]
expected: FAIL
[img.referrerPolicy: IDL set to "o-referrer-when-downgrade"]
expected: FAIL
[img.referrerPolicy: IDL set to "NO-REFERRER-WHEN-DOWNGRADE"]
expected: FAIL
[img.referrerPolicy: IDL set to "xsame-origin"]
expected: FAIL
[img.referrerPolicy: IDL set to "same-origin\\0"]
expected: FAIL
[img.referrerPolicy: IDL set to "ame-origin"]
expected: FAIL
[img.referrerPolicy: IDL set to "SAME-ORIGIN"]
expected: FAIL
[img.referrerPolicy: IDL set to "ſame-origin"]
expected: FAIL
[img.referrerPolicy: IDL set to "xorigin"]
expected: FAIL
[img.referrerPolicy: IDL set to "origin\\0"]
expected: FAIL
[img.referrerPolicy: IDL set to "rigin"]
expected: FAIL
[img.referrerPolicy: IDL set to "ORIGIN"]
expected: FAIL
[img.referrerPolicy: IDL set to "xstrict-origin"]
expected: FAIL
[img.referrerPolicy: IDL set to "strict-origin\\0"]
expected: FAIL
[img.referrerPolicy: IDL set to "trict-origin"]
expected: FAIL
[img.referrerPolicy: IDL set to "STRICT-ORIGIN"]
expected: FAIL
[img.referrerPolicy: IDL set to "ſtrict-origin"]
expected: FAIL
[img.referrerPolicy: IDL set to "xorigin-when-cross-origin"]
expected: FAIL
[img.referrerPolicy: IDL set to "origin-when-cross-origin\\0"]
expected: FAIL
[img.referrerPolicy: IDL set to "rigin-when-cross-origin"]
expected: FAIL
[img.referrerPolicy: IDL set to "ORIGIN-WHEN-CROSS-ORIGIN"]
expected: FAIL
[img.referrerPolicy: IDL set to "origin-when-croſſ-origin"]
expected: FAIL
[img.referrerPolicy: IDL set to "xstrict-origin-when-cross-origin"]
expected: FAIL
[img.referrerPolicy: IDL set to "strict-origin-when-cross-origin\\0"]
expected: FAIL
[img.referrerPolicy: IDL set to "trict-origin-when-cross-origin"]
expected: FAIL
[img.referrerPolicy: IDL set to "STRICT-ORIGIN-WHEN-CROSS-ORIGIN"]
expected: FAIL
[img.referrerPolicy: IDL set to "ſtrict-origin-when-croſſ-origin"]
expected: FAIL
[img.referrerPolicy: IDL set to "xunsafe-url"]
expected: FAIL
[img.referrerPolicy: IDL set to "unsafe-url\\0"]
expected: FAIL
[img.referrerPolicy: IDL set to "nsafe-url"]
expected: FAIL
[img.referrerPolicy: IDL set to "UNSAFE-URL"]
expected: FAIL
[img.referrerPolicy: IDL set to "unſafe-url"]
expected: FAIL
[img.decoding: typeof IDL attribute] [img.decoding: typeof IDL attribute]
expected: FAIL expected: FAIL

View file

@ -1,9 +0,0 @@
[relevant-mutations.html]
[crossorigin state not changed: empty to anonymous]
expected: FAIL
[crossorigin state not changed: use-credentials to USE-CREDENTIALS]
expected: FAIL
[crossorigin state not changed: anonymous to foobar]
expected: FAIL