mirror of
https://github.com/lexmount/moli.git
synced 2026-11-04 16:06:44 +00:00
fix(svg): implement anchor hyperlink attributes
Expose SVG anchor target as a live owner-realm SVGAnimatedString, reflect download/ping/rel/hreflang/type/referrerPolicy through existing DOMString and USVString bindings, and share native hyperlink component callbacks under generated SVGAElement receiver checks. Apply SVG2 href/xlink namespace rules when reading and updating URL components, preserve token-list PutForwards UTF-16, and cover live/windowless documents, both realms, reentrant conversion and registered native Proxies.
This commit is contained in:
@@ -2187,6 +2187,13 @@ macro_rules! define_svg_uri_reference_accessors {
|
||||
}
|
||||
|
||||
define_svg_uri_reference_accessors!(SvgAUriReferenceAccessorsDeclaration, SVGAElement);
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::SVGAElement, enumerable, receiver)]
|
||||
struct SvgAElementTargetAccessorsDeclaration {
|
||||
#[webapi(accessor_property = "target", getter = svg_a_target_getter)]
|
||||
target: (),
|
||||
}
|
||||
define_svg_uri_reference_accessors!(SvgImageUriReferenceAccessorsDeclaration, SVGImageElement);
|
||||
define_svg_uri_reference_accessors!(SvgUseUriReferenceAccessorsDeclaration, SVGUseElement);
|
||||
define_svg_uri_reference_accessors!(
|
||||
@@ -2765,6 +2772,7 @@ pub(super) fn install_svg_element_accessor_bindings<'s>(
|
||||
}
|
||||
"SVGAElement" => {
|
||||
SvgAUriReferenceAccessorsDeclaration::initialize_prototype_template(scope, prototype);
|
||||
SvgAElementTargetAccessorsDeclaration::initialize_prototype_template(scope, prototype);
|
||||
}
|
||||
"SVGScriptElement" => {
|
||||
SvgScriptUriReferenceAccessorsDeclaration::initialize_prototype_template(
|
||||
|
||||
@@ -357,6 +357,20 @@ pub(super) fn svg_uri_href_getter<'s>(
|
||||
svg_animated_string_attribute_getter(scope, owner, rv, SVG_URI_HREF_SLOT, "href");
|
||||
}
|
||||
|
||||
pub(super) fn svg_a_target_getter<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let owner = moli_webapi_declare::web_api_object_target(scope, args.this())
|
||||
.expect("SVG anchor receiver validated by WebIDL");
|
||||
let Some(context) = owner.get_creation_context(scope) else {
|
||||
return;
|
||||
};
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
svg_animated_string_attribute_getter(scope, owner, rv, "__moliSvgAAnimatedTarget", "target");
|
||||
}
|
||||
|
||||
pub(super) fn svg_fe_convolve_matrix_preserve_alpha_getter<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
|
||||
@@ -1689,7 +1689,10 @@ pub(crate) fn install_element_template_bindings<'s>(
|
||||
"HTMLScriptElement" => install!(HtmlScriptElementPrototypeDeclaration),
|
||||
"SVGScriptElement" => install!(SvgScriptElementPrototypeDeclaration),
|
||||
"SVGImageElement" => install!(SvgImageElementPrototypeDeclaration),
|
||||
"SVGAElement" => install!(SvgAElementRelListPrototypeDeclaration),
|
||||
"SVGAElement" => install!(
|
||||
SvgAElementPrototypeDeclaration,
|
||||
SvgAElementUrlPrototypeDeclaration
|
||||
),
|
||||
"HTMLStyleElement" => install!(HtmlStyleElementPrototypeDeclaration),
|
||||
"SVGStyleElement" => install!(SvgStyleElementPrototypeDeclaration),
|
||||
"HTMLTableElement" => install!(HtmlTableElementPrototypeDeclaration),
|
||||
|
||||
@@ -3,7 +3,7 @@ use super::super::{
|
||||
node::{node_is_element, node_runtime_and_handle_from_object_or_detached},
|
||||
throw_dom_exception, validate_class_list_token, validate_class_list_token_pair,
|
||||
};
|
||||
use super::{property_dom_string_value, reflected_attribute, set_reflected_attribute};
|
||||
use super::{reflected_attribute, set_reflected_attribute};
|
||||
use crate::{document_runtime::DomHandle, util::v8_string};
|
||||
|
||||
mod accessors;
|
||||
|
||||
@@ -52,10 +52,18 @@ fn set_token_list_for_receiver<'s>(
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(value) = property_dom_string_value(scope, value, owner, property) else {
|
||||
let Some(units) =
|
||||
super::super::reflection::property_dom_string_utf16_value(scope, value, owner, property)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
set_reflected_attribute(scope, runtime_ptr, handle, attribute, &value);
|
||||
super::super::reflection::set_reflected_attribute_utf16(
|
||||
scope,
|
||||
runtime_ptr,
|
||||
handle,
|
||||
attribute,
|
||||
units,
|
||||
);
|
||||
}
|
||||
|
||||
pub(in crate::native_bridge) fn html_rel_list_getter_function<'s>(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use super::{
|
||||
identity::class_list_runtime_handle_and_kind_from_object,
|
||||
tokens::{class_list_tokens, token_list_attribute_name},
|
||||
*,
|
||||
};
|
||||
|
||||
pub(super) fn class_list_length_getter_callback(
|
||||
@@ -30,13 +29,11 @@ pub(super) fn class_list_value_getter_callback(
|
||||
rv.set_null();
|
||||
return;
|
||||
};
|
||||
let value = reflected_attribute(
|
||||
unsafe { &*runtime_ptr },
|
||||
handle,
|
||||
token_list_attribute_name(kind),
|
||||
)
|
||||
.unwrap_or_default();
|
||||
let Some(value) = v8_string(scope, &value) else {
|
||||
let units = unsafe { &*runtime_ptr }
|
||||
.dom_host()
|
||||
.get_attribute_ns_utf16_units(handle, None, token_list_attribute_name(kind))
|
||||
.unwrap_or_default();
|
||||
let Some(value) = crate::util::v8_string_from_utf16_units(scope, &units) else {
|
||||
rv.set_null();
|
||||
return;
|
||||
};
|
||||
@@ -54,16 +51,21 @@ pub(super) fn class_list_value_setter_callback<'s>(
|
||||
rv.set_undefined();
|
||||
return;
|
||||
};
|
||||
let Some(value) = property_dom_string_value(scope, args.get(0), "DOMTokenList", "value") else {
|
||||
let Some(units) = super::super::reflection::property_dom_string_utf16_value(
|
||||
scope,
|
||||
args.get(0),
|
||||
"DOMTokenList",
|
||||
"value",
|
||||
) else {
|
||||
rv.set_undefined();
|
||||
return;
|
||||
};
|
||||
set_reflected_attribute(
|
||||
super::super::reflection::set_reflected_attribute_utf16(
|
||||
scope,
|
||||
runtime_ptr,
|
||||
handle,
|
||||
token_list_attribute_name(kind),
|
||||
&value,
|
||||
units,
|
||||
);
|
||||
rv.set_undefined();
|
||||
}
|
||||
|
||||
@@ -268,6 +268,11 @@ pub(super) enum DomStringReflection {
|
||||
TableWidth,
|
||||
TrackLabel,
|
||||
UlType,
|
||||
SvgAnchorDownload,
|
||||
SvgAnchorRel,
|
||||
SvgAnchorHreflang,
|
||||
SvgAnchorType,
|
||||
SvgAnchorReferrerPolicy,
|
||||
Count,
|
||||
}
|
||||
|
||||
@@ -831,6 +836,26 @@ const DOM_STRING_REFLECTION_DESCRIPTORS: &[(DomStringReflection, DomStringReflec
|
||||
DomStringReflection::UlType,
|
||||
DomStringReflectionDescriptor::new("HTMLUListElement", "type", "type"),
|
||||
),
|
||||
(
|
||||
DomStringReflection::SvgAnchorDownload,
|
||||
DomStringReflectionDescriptor::new("SVGAElement", "download", "download"),
|
||||
),
|
||||
(
|
||||
DomStringReflection::SvgAnchorRel,
|
||||
DomStringReflectionDescriptor::new("SVGAElement", "rel", "rel"),
|
||||
),
|
||||
(
|
||||
DomStringReflection::SvgAnchorHreflang,
|
||||
DomStringReflectionDescriptor::new("SVGAElement", "hreflang", "hreflang"),
|
||||
),
|
||||
(
|
||||
DomStringReflection::SvgAnchorType,
|
||||
DomStringReflectionDescriptor::new("SVGAElement", "type", "type"),
|
||||
),
|
||||
(
|
||||
DomStringReflection::SvgAnchorReferrerPolicy,
|
||||
DomStringReflectionDescriptor::new("SVGAElement", "referrerpolicy", "referrerPolicy"),
|
||||
),
|
||||
];
|
||||
|
||||
const _: () = {
|
||||
@@ -869,6 +894,7 @@ pub(super) enum UsvStringReflection {
|
||||
ObjectData,
|
||||
QuoteCite,
|
||||
SourceSrcset,
|
||||
SvgAnchorPing,
|
||||
Count,
|
||||
}
|
||||
|
||||
@@ -917,6 +943,10 @@ const USV_STRING_REFLECTION_DESCRIPTORS: &[(UsvStringReflection, ReflectedAttrib
|
||||
UsvStringReflection::SourceSrcset,
|
||||
ReflectedAttributeDescriptor::new("HTMLSourceElement", "srcset", "srcset"),
|
||||
),
|
||||
(
|
||||
UsvStringReflection::SvgAnchorPing,
|
||||
ReflectedAttributeDescriptor::new("SVGAElement", "ping", "ping"),
|
||||
),
|
||||
];
|
||||
|
||||
const _: () = {
|
||||
@@ -1441,6 +1471,10 @@ pub(super) fn element_reflection_receiver_or_throw<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
) -> Option<(*mut super::super::JsContextHost, DomHandle)> {
|
||||
let Some(object) = moli_webapi_declare::web_api_object_target(scope, object) else {
|
||||
webidl::throw_type_error(scope, "Illegal invocation");
|
||||
return None;
|
||||
};
|
||||
let Ok((runtime_ptr, handle)) = node_runtime_and_handle_from_object_or_detached(scope, object)
|
||||
else {
|
||||
webidl::throw_type_error(scope, "Illegal invocation");
|
||||
|
||||
@@ -7,6 +7,10 @@ fn anchor_url_string_function_getter<'s>(
|
||||
project: impl FnOnce(&url::Url) -> String,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let Some(receiver) = moli_webapi_declare::web_api_object_target(scope, receiver) else {
|
||||
rv.set_empty_string();
|
||||
return;
|
||||
};
|
||||
let Ok((runtime_ptr, handle)) =
|
||||
node_runtime_and_handle_from_object_or_detached(scope, receiver)
|
||||
else {
|
||||
@@ -96,8 +100,11 @@ pub(super) fn html_referrer_policy_getter_function<'s>(
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let Some(receiver) = moli_webapi_declare::web_api_object_target(scope, args.this()) else {
|
||||
return;
|
||||
};
|
||||
let Ok((runtime_ptr, handle)) =
|
||||
node_runtime_and_handle_from_object_or_detached(scope, args.this())
|
||||
node_runtime_and_handle_from_object_or_detached(scope, receiver)
|
||||
else {
|
||||
rv.set_null();
|
||||
return;
|
||||
@@ -301,13 +308,14 @@ fn hyperlink_url_setter_input<'s>(
|
||||
args: &v8::FunctionCallbackArguments<'s>,
|
||||
property: &'static str,
|
||||
) -> Option<(*mut JsContextHost, DomHandle, url::Url, String)> {
|
||||
node_runtime_and_handle_from_object_or_detached(scope, args.this()).ok()?;
|
||||
let receiver = moli_webapi_declare::web_api_object_target(scope, args.this())?;
|
||||
node_runtime_and_handle_from_object_or_detached(scope, receiver).ok()?;
|
||||
let value =
|
||||
property_usv_string_value(scope, args.get(0), "HTMLHyperlinkElementUtils", property)?;
|
||||
// Conversion can change href, the document's base, or the node's owner.
|
||||
// Resolve the current node and URL only after those script side effects.
|
||||
let (runtime_ptr, handle) =
|
||||
node_runtime_and_handle_from_object_or_detached(scope, args.this()).ok()?;
|
||||
node_runtime_and_handle_from_object_or_detached(scope, receiver).ok()?;
|
||||
let url = parsed_url_like_attribute(unsafe { &*runtime_ptr }, handle, "href")?;
|
||||
Some((runtime_ptr, handle, url, value))
|
||||
}
|
||||
@@ -2629,7 +2637,50 @@ pub(super) const HTML_NAME_REFLECTION_INTERFACES: &[ElementReflectionInterface]
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::SVGAElement, enumerable, receiver)]
|
||||
pub(super) struct SvgAElementRelListPrototypeDeclaration {
|
||||
pub(super) struct SvgAElementPrototypeDeclaration {
|
||||
#[webapi(
|
||||
accessor_property = "download",
|
||||
getter = dom_string_reflection_getter_function,
|
||||
setter = dom_string_reflection_setter_function,
|
||||
data = DomStringReflection::SvgAnchorDownload
|
||||
)]
|
||||
download: (),
|
||||
#[webapi(
|
||||
accessor_property = "rel",
|
||||
getter = dom_string_reflection_getter_function,
|
||||
setter = dom_string_reflection_setter_function,
|
||||
data = DomStringReflection::SvgAnchorRel
|
||||
)]
|
||||
rel: (),
|
||||
#[webapi(
|
||||
accessor_property = "hreflang",
|
||||
getter = dom_string_reflection_getter_function,
|
||||
setter = dom_string_reflection_setter_function,
|
||||
data = DomStringReflection::SvgAnchorHreflang
|
||||
)]
|
||||
hreflang: (),
|
||||
#[webapi(
|
||||
accessor_property = "type",
|
||||
getter = dom_string_reflection_getter_function,
|
||||
setter = dom_string_reflection_setter_function,
|
||||
data = DomStringReflection::SvgAnchorType
|
||||
)]
|
||||
r#type: (),
|
||||
#[webapi(
|
||||
accessor_property = "referrerPolicy",
|
||||
getter = html_referrer_policy_getter_function,
|
||||
setter = dom_string_reflection_setter_function,
|
||||
setter_data = DomStringReflection::SvgAnchorReferrerPolicy
|
||||
)]
|
||||
referrer_policy: (),
|
||||
#[webapi(
|
||||
accessor_property,
|
||||
getter = usv_string_reflection_getter_function,
|
||||
setter = usv_string_reflection_setter_function,
|
||||
data = UsvStringReflection::SvgAnchorPing
|
||||
)]
|
||||
ping: (),
|
||||
|
||||
#[webapi(
|
||||
accessor_property = "relList",
|
||||
getter = html_rel_list_getter_function,
|
||||
@@ -2637,3 +2688,64 @@ pub(super) struct SvgAElementRelListPrototypeDeclaration {
|
||||
)]
|
||||
rel_list: (),
|
||||
}
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::SVGAElement, enumerable, receiver)]
|
||||
pub(super) struct SvgAElementUrlPrototypeDeclaration {
|
||||
#[webapi(
|
||||
accessor_property,
|
||||
getter = anchor_host_getter_function,
|
||||
setter = anchor_host_setter_function
|
||||
)]
|
||||
host: (),
|
||||
#[webapi(
|
||||
accessor_property,
|
||||
getter = anchor_hostname_getter_function,
|
||||
setter = anchor_hostname_setter_function
|
||||
)]
|
||||
hostname: (),
|
||||
#[webapi(
|
||||
accessor_property,
|
||||
getter = anchor_port_getter_function,
|
||||
setter = anchor_port_setter_function
|
||||
)]
|
||||
port: (),
|
||||
#[webapi(
|
||||
accessor_property,
|
||||
getter = anchor_pathname_getter_function,
|
||||
setter = anchor_pathname_setter_function
|
||||
)]
|
||||
pathname: (),
|
||||
#[webapi(
|
||||
accessor_property,
|
||||
getter = anchor_search_getter_function,
|
||||
setter = anchor_search_setter_function
|
||||
)]
|
||||
search: (),
|
||||
#[webapi(
|
||||
accessor_property,
|
||||
getter = anchor_hash_getter_function,
|
||||
setter = anchor_hash_setter_function
|
||||
)]
|
||||
hash: (),
|
||||
#[webapi(accessor_property, getter = anchor_origin_getter_function)]
|
||||
origin: (),
|
||||
#[webapi(
|
||||
accessor_property,
|
||||
getter = anchor_protocol_getter_function,
|
||||
setter = anchor_protocol_setter_function
|
||||
)]
|
||||
protocol: (),
|
||||
#[webapi(
|
||||
accessor_property,
|
||||
getter = anchor_username_getter_function,
|
||||
setter = anchor_username_setter_function
|
||||
)]
|
||||
username: (),
|
||||
#[webapi(
|
||||
accessor_property,
|
||||
getter = anchor_password_getter_function,
|
||||
setter = anchor_password_setter_function
|
||||
)]
|
||||
password: (),
|
||||
}
|
||||
|
||||
@@ -42,7 +42,16 @@ pub(in crate::native_bridge::element) fn parsed_url_like_attribute(
|
||||
handle: DomHandle,
|
||||
name: &str,
|
||||
) -> Option<Url> {
|
||||
let value = reflected_attribute(runtime, handle, name)?;
|
||||
let value = reflected_attribute(runtime, handle, name).or_else(|| {
|
||||
if name != "href" || !is_svg_anchor(runtime, handle) {
|
||||
return None;
|
||||
}
|
||||
runtime.dom_host().get_attribute_ns(
|
||||
handle,
|
||||
Some(crate::native_bridge::document::XLINK_NS),
|
||||
name,
|
||||
)
|
||||
})?;
|
||||
let base = url_base_for_handle(runtime, handle);
|
||||
parse_url_with_document_query_encoding(runtime, handle, &base, &value).ok()
|
||||
}
|
||||
@@ -119,9 +128,41 @@ pub(in crate::native_bridge::element) fn set_resolved_url_attribute(
|
||||
name: &str,
|
||||
url: &Url,
|
||||
) {
|
||||
// SVG2 HyperlinkElementUtils reads href first, but update-href writes the
|
||||
// namespaced attribute whenever xlink:href is present, even if both exist.
|
||||
let runtime = unsafe { &*runtime_ptr };
|
||||
if name == "href"
|
||||
&& is_svg_anchor(runtime, handle)
|
||||
&& runtime
|
||||
.dom_host()
|
||||
.get_attribute_ns(handle, Some(crate::native_bridge::document::XLINK_NS), name)
|
||||
.is_some()
|
||||
{
|
||||
crate::custom_elements::with_custom_element_reaction_scope(scope, runtime_ptr, |scope| {
|
||||
let _ = super::super::set_live_element_attribute_ns_appending_to_current_reaction_queue(
|
||||
scope,
|
||||
runtime_ptr,
|
||||
handle,
|
||||
Some(crate::native_bridge::document::XLINK_NS),
|
||||
Some("xlink"),
|
||||
name,
|
||||
"xlink:href",
|
||||
url.as_ref(),
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
set_reflected_attribute(scope, runtime_ptr, handle, name, url.as_ref());
|
||||
}
|
||||
|
||||
fn is_svg_anchor(runtime: &JsContextHost, handle: DomHandle) -> bool {
|
||||
runtime
|
||||
.dom_host()
|
||||
.node(handle)
|
||||
.and_then(Node::as_element)
|
||||
.is_some_and(|element| element.is_svg_element("a"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -2367,5 +2367,6 @@ mod window_scroll_methods;
|
||||
mod window_sync_method_receivers;
|
||||
|
||||
mod rectangle_receivers;
|
||||
mod svg_hyperlinks;
|
||||
mod svg_transform_lists;
|
||||
mod svg_value_lists;
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
(() => {
|
||||
const checks = [];
|
||||
const check = (name, run) => {
|
||||
try { checks.push({name, passed: run() === true}); }
|
||||
catch (error) { checks.push({name, passed: false, error: String(error)}); }
|
||||
};
|
||||
const SVG = 'http://www.w3.org/2000/svg', XLINK = 'http://www.w3.org/1999/xlink';
|
||||
const child = document.querySelector('iframe').contentWindow;
|
||||
const components = ['origin','protocol','username','password','host','hostname','port','pathname','search','hash'];
|
||||
const strings = ['download','rel','hreflang','type','referrerPolicy','ping'];
|
||||
for (const [realmName, realm] of [['top',globalThis],['child',child]]) {
|
||||
const proto = realm.SVGAElement.prototype;
|
||||
for (const property of ['target', ...strings, ...components]) {
|
||||
check(`${realmName} descriptor ${property}`, () => {
|
||||
const d = Object.getOwnPropertyDescriptor(proto, property);
|
||||
return d.enumerable && d.configurable && typeof d.get === 'function' &&
|
||||
(['target','origin'].includes(property) ? d.set === undefined : typeof d.set === 'function');
|
||||
});
|
||||
const wrong = realm.document.createElement('a');
|
||||
const genuine = realm.document.createElementNS(SVG,'a');
|
||||
const revoked = realm.Proxy.revocable(genuine,{}); revoked.revoke();
|
||||
let traps = 0;
|
||||
const proxy = new realm.Proxy(genuine,{get(){traps++;throw Error('trap');}});
|
||||
for (const [kind,receiver] of [['null',null],['undefined',undefined],['plain',{}],['forged',Object.create(proto)],['inherited',Object.create(genuine)],['HTML',wrong],['other SVG',realm.document.createElementNS(SVG,'rect')],['proxy',proxy],['revoked',revoked.proxy]]) {
|
||||
check(`${realmName} ${property} rejects ${kind} getter`, () => {
|
||||
const d = Object.getOwnPropertyDescriptor(proto,property); let error;
|
||||
try { d.get.call(receiver); } catch (e) { error=e; }
|
||||
return error instanceof realm.TypeError && traps===0;
|
||||
});
|
||||
if (!['target','origin'].includes(property)) check(`${realmName} ${property} rejects ${kind} before conversion`, () => {
|
||||
let converted=0,error; const d=Object.getOwnPropertyDescriptor(proto,property);
|
||||
try { d.set.call(receiver,{toString(){converted++;return 'changed';}}); } catch(e){error=e;}
|
||||
return error instanceof realm.TypeError && converted===0 && traps===0;
|
||||
});
|
||||
}
|
||||
}
|
||||
const documents = [['live',realm.document], ['windowless HTML',realm.document.implementation.createHTMLDocument('')], ['XML',new realm.DOMParser().parseFromString(`<svg xmlns="${SVG}"/>`,'image/svg+xml')]];
|
||||
for (const [documentName,doc] of documents) {
|
||||
const prefix=`${realmName} ${documentName}`;
|
||||
const anchor=doc.createElementNS(SVG,'a');
|
||||
for (const property of ['download','rel','hreflang','type']) {
|
||||
const attr=property;
|
||||
for(const value of ['', ' MiXeD value ', '\ud800x\udfff', 'null']) check(`${prefix} ${property} DOMString ${JSON.stringify(value)}`, () => {
|
||||
Object.getOwnPropertyDescriptor(proto,property).set.call(anchor,value);
|
||||
return anchor.getAttribute(attr)===value && Object.getOwnPropertyDescriptor(proto,property).get.call(anchor)===value;
|
||||
});
|
||||
check(`${prefix} ${property} removal`,()=>{anchor.removeAttribute(attr);return anchor[property]==='';});
|
||||
check(`${prefix} ${property} original conversion exception`,()=>{
|
||||
const marker=new realm.Error('conversion'); let error;
|
||||
try{Object.getOwnPropertyDescriptor(proto,property).set.call(anchor,{toString(){throw marker;}});}catch(e){error=e;}
|
||||
return error===marker && !anchor.hasAttribute(attr);
|
||||
});
|
||||
}
|
||||
check(`${prefix} ping USVString`,()=>{anchor.ping='\ud800 /two';return anchor.getAttribute('ping')==='\ufffd /two' && anchor.ping==='\ufffd /two';});
|
||||
check(`${prefix} ping attribute USVString`,()=>{anchor.setAttribute('ping','\ud800');return anchor.ping==='\ufffd' && anchor.getAttribute('ping')==='\ud800';});
|
||||
for(const policy of ['no-referrer','no-referrer-when-downgrade','same-origin','origin','strict-origin','origin-when-cross-origin','strict-origin-when-cross-origin','unsafe-url','invalid',' origin ','\ud800']) check(`${prefix} referrerPolicy ${JSON.stringify(policy)}`,()=>{
|
||||
anchor.referrerPolicy=policy.toUpperCase();
|
||||
return anchor.getAttribute('referrerpolicy')===policy.toUpperCase() && anchor.referrerPolicy===(['invalid',' origin ','\ud800'].includes(policy)?'':policy);
|
||||
});
|
||||
check(`${prefix} target native realm and identity`,()=>{
|
||||
const target=Object.getOwnPropertyDescriptor(proto,'target').get.call(anchor);
|
||||
return target===anchor.target && target instanceof realm.SVGAnimatedString && target.baseVal==='' && target.animVal==='';
|
||||
});
|
||||
check(`${prefix} target live UTF16`,()=>{
|
||||
const target=anchor.target; anchor.setAttribute('target','\ud800frame');
|
||||
if(target.baseVal!=='\ud800frame'||target.animVal!=='\ud800frame')return false;
|
||||
target.baseVal='_blank';
|
||||
return anchor.getAttribute('target')==='_blank' && target.animVal==='_blank' && target===anchor.target;
|
||||
});
|
||||
check(`${prefix} target removal`,()=>{const saved=anchor.target;anchor.removeAttribute('target');return saved.baseVal===''&&saved.animVal==='';});
|
||||
check(`${prefix} relList SameObject and token updates`,()=>{
|
||||
anchor.rel='one one two';const list=anchor.relList;
|
||||
if(list.length!==2||list[0]!=='one'||list[1]!=='two')return false;
|
||||
list.add('three');list.remove('one');list.toggle('four',true);list.replace('two','five');
|
||||
return anchor.rel==='five three four' && anchor.relList===list;
|
||||
});
|
||||
check(`${prefix} relList PutForwards UTF16`,()=>{anchor.relList='\ud800 token';return anchor.rel==='\ud800 token'&&anchor.relList.value==='\ud800 token';});
|
||||
check(`${prefix} relList supports`,()=>['noopener','noreferrer','opener'].every(t=>anchor.relList.supports(t)) && !anchor.relList.supports('stylesheet'));
|
||||
check(`${prefix} relList token exception`,()=>{
|
||||
anchor.rel='one';let error;try{anchor.relList.add('two','has space');}catch(e){error=e;}
|
||||
return error instanceof realm.DOMException && error.name==='InvalidCharacterError' && anchor.rel==='one';
|
||||
});
|
||||
for(const [kind,href,xlink] of [['missing',null,null],['href','https://user:pass@example.test:8443/a/b?q=1#part',null],['xlink',null,'https://legacy.test:8080/old?q=2#p'],['both','https://primary.test/a','https://legacy.test/old'],['empty','', 'https://legacy.test/old'],['invalid','http://[invalid',null],['opaque','mailto:user@example.test',null]]) {
|
||||
anchor.removeAttribute('href');anchor.removeAttributeNS(XLINK,'href');
|
||||
if(href!==null)anchor.setAttribute('href',href);
|
||||
if(xlink!==null)anchor.setAttributeNS(XLINK,'xlink:href',xlink);
|
||||
let expected=null;const raw=href===null?xlink:href;
|
||||
if(raw!==null)try{expected=new URL(raw,doc.baseURI);}catch{}
|
||||
for(const property of components) check(`${prefix} ${kind} ${property}`,()=>
|
||||
Object.getOwnPropertyDescriptor(proto,property).get.call(anchor)===(expected?expected[property]:property==='protocol'?':':''));
|
||||
check(`${prefix} ${kind} href stays SVGAnimatedString`,()=>anchor.href instanceof realm.SVGAnimatedString && anchor.href.baseVal===(raw===null?'':raw));
|
||||
}
|
||||
const initial='https://user:pass@example.test:8443/a/b?q=1#part';
|
||||
for(const [property,value] of [['protocol','http'],['host','other.test:9876'],['hostname','[::1]'],['port','443'],['port','invalid'],['username','other user'],['password','secret?'],['pathname','/new path'],['search','?q=two three'],['hash','#next part'],['protocol','invalid space:']]) {
|
||||
for(const mode of ['href','xlink','both'])check(`${prefix} ${mode} ${property} setter ${value}`,()=>{
|
||||
anchor.removeAttribute('href');anchor.removeAttributeNS(XLINK,'href');
|
||||
if(mode!=='xlink')anchor.setAttribute('href',initial);
|
||||
if(mode!=='href')anchor.setAttributeNS(XLINK,'xlink:href',mode==='both'?'https://ignored.test/':initial);
|
||||
const expected=new URL(initial);expected[property]=value;
|
||||
Object.getOwnPropertyDescriptor(proto,property).set.call(anchor,value);
|
||||
// SVG2 update-href writes xlink:href whenever that attribute is present.
|
||||
return (mode==='href'?anchor.getAttribute('href'):anchor.getAttributeNS(XLINK,'href'))===expected.href &&
|
||||
(mode!=='both'||anchor.getAttribute('href')===initial);
|
||||
});
|
||||
}
|
||||
for(const property of components.filter(p=>p!=='origin')) {
|
||||
check(`${prefix} ${property} propagates conversion exception`,()=>{
|
||||
anchor.setAttribute('href',initial);const marker=new realm.Error('convert');let error;
|
||||
try{Object.getOwnPropertyDescriptor(proto,property).set.call(anchor,{toString(){throw marker;}});}catch(e){error=e;}
|
||||
return error===marker && anchor.getAttribute('href')===initial;
|
||||
});
|
||||
check(`${prefix} ${property} converts absent URL`,()=>{
|
||||
anchor.removeAttribute('href');anchor.removeAttributeNS(XLINK,'href');let converted=0;
|
||||
Object.getOwnPropertyDescriptor(proto,property).set.call(anchor,{toString(){converted++;return 'value';}});
|
||||
return converted===1 && !anchor.hasAttribute('href');
|
||||
});
|
||||
}
|
||||
check(`${prefix} reentrant href mutation before URL parse`,()=>{
|
||||
anchor.removeAttributeNS(XLINK,'href');anchor.setAttribute('href',initial);
|
||||
anchor.pathname={toString(){anchor.setAttribute('href','https://changed.test/root');return '/converted';}};
|
||||
return anchor.getAttribute('href')==='https://changed.test/converted';
|
||||
});
|
||||
check(`${prefix} adoption during conversion`,()=>{
|
||||
const destination=realm.document.implementation.createHTMLDocument('');
|
||||
anchor.setAttribute('href','https://old.test/');anchor.removeAttributeNS(XLINK,'href');
|
||||
anchor.hash={toString(){destination.adoptNode(anchor);anchor.setAttribute('href','https://adopted.test/');return '#ok';}};
|
||||
return anchor.ownerDocument===destination && anchor.getAttribute('href')==='https://adopted.test/#ok';
|
||||
});
|
||||
}
|
||||
check(`${realmName} document base update`,()=>{
|
||||
const doc=realm.document,base=doc.createElement('base'),a=doc.createElementNS(SVG,'a');
|
||||
base.href='https://first.test/dir/';doc.head.prepend(base);a.setAttribute('href','item');
|
||||
try{
|
||||
if(a.host!=='first.test'||a.pathname!=='/dir/item')return false;
|
||||
base.href='https://second.test/next/';
|
||||
if(a.host!=='second.test'||a.pathname!=='/next/item')return false;
|
||||
a.hash={toString(){base.href='https://third.test/final/';return '#done';}};
|
||||
return a.getAttribute('href')==='https://third.test/final/item#done';
|
||||
}finally{base.remove();}
|
||||
});
|
||||
}
|
||||
globalThis.__uiEventResults={checks,complete:true,total:checks.length,passed:checks.filter(c=>c.passed).length};
|
||||
})();
|
||||
@@ -0,0 +1,98 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn svg_hyperlinks_reflect_attributes_and_preserve_conversion_order() {
|
||||
let mut vm = new_storage_page_task_executor_test_vm("https://svg-hyperlinks.test/");
|
||||
vm.eval("document.body.innerHTML='<iframe></iframe>'")
|
||||
.unwrap();
|
||||
vm.eval(include_str!("svg_hyperlinks.js")).unwrap();
|
||||
assert_eq!(vm.eval("__uiEventResults.complete").unwrap(), "true");
|
||||
assert_eq!(vm.eval("__uiEventResults.total").unwrap(), "1656");
|
||||
assert_eq!(
|
||||
vm.eval("JSON.stringify(__uiEventResults.checks.filter(row=>!row.passed).slice(0,20))")
|
||||
.unwrap(),
|
||||
"[]"
|
||||
);
|
||||
assert_eq!(vm.eval("__uiEventResults.passed").unwrap(), "1656");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn svg_hyperlink_registered_proxies_share_native_state() {
|
||||
let mut vm = new_storage_page_task_executor_test_vm("https://svg-link-proxies.test/");
|
||||
vm.eval(
|
||||
r#"
|
||||
document.body.innerHTML='<iframe></iframe>';
|
||||
const child=document.querySelector('iframe').contentWindow;
|
||||
const detached=child.document.implementation.createHTMLDocument('');
|
||||
const anchor=detached.createElementNS('http://www.w3.org/2000/svg','a');
|
||||
anchor.setAttribute('href','https://example.test/old');
|
||||
globalThis.__linkTargets={anchor,target:anchor.target,href:anchor.href};
|
||||
globalThis.__linkProxies={};globalThis.__linkTraps=0;
|
||||
globalThis.__linkTrap=function(){__linkTraps++;throw Error('proxy trap');};
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let context_ptr = &vm.page_default_context as *const _;
|
||||
vm.with_context_scope_by_ptr_and_checkpoint_for_test(context_ptr, |scope, _| {
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let targets_key = crate::util::v8str(scope, "__linkTargets");
|
||||
let targets = global.get(scope, targets_key.into()).unwrap();
|
||||
let targets = v8::Local::<v8::Object>::try_from(targets).unwrap();
|
||||
let proxies_key = crate::util::v8str(scope, "__linkProxies");
|
||||
let proxies = global.get(scope, proxies_key.into()).unwrap();
|
||||
let trap_key = crate::util::v8str(scope, "__linkTrap");
|
||||
let trap = global.get(scope, trap_key.into()).unwrap();
|
||||
let proxies = v8::Local::<v8::Object>::try_from(proxies).unwrap();
|
||||
for name in ["anchor", "target", "href"] {
|
||||
let key = crate::util::v8str(scope, name);
|
||||
let target = targets.get(scope, key.into()).unwrap();
|
||||
let target = v8::Local::<v8::Object>::try_from(target).unwrap();
|
||||
let handler = crate::util::new_null_prototype_object(scope);
|
||||
let get_key = crate::util::v8str(scope, "get");
|
||||
assert_eq!(
|
||||
handler.create_data_property(scope, get_key.into(), trap),
|
||||
Some(true)
|
||||
);
|
||||
let proxy = v8::Proxy::new(scope, target, handler).unwrap();
|
||||
moli_webapi_declare::register_web_api_proxy(scope, proxy).unwrap();
|
||||
assert_eq!(
|
||||
proxies.create_data_property(scope, key.into(), proxy.into()),
|
||||
Some(true)
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
r#"(() => {
|
||||
const own=(realm,kind,key)=>Object.getOwnPropertyDescriptor(realm[kind].prototype,key);
|
||||
const proxy=__linkProxies.anchor,real=__linkTargets.anchor;
|
||||
for(const realm of [globalThis,child]){
|
||||
for(const [property,value,attr] of [['download','\ud800name','download'],['rel','noopener','rel'],['hreflang','en','hreflang'],['type','text/html','type'],['referrerPolicy','ORIGIN','referrerpolicy'],['ping','\ud800path','ping']]){
|
||||
const d=own(realm,'SVGAElement',property);d.set.call(proxy,value);
|
||||
const expected=property==='ping'?'\ufffdpath':value;
|
||||
if(real.getAttribute(attr)!==expected||d.get.call(proxy)!==(property==='referrerPolicy'?'origin':expected))return property;
|
||||
}
|
||||
for(const property of ['target','href'])if(own(realm,'SVGAElement',property).get.call(proxy)!==__linkTargets[property])return property+' identity';
|
||||
const list=own(realm,'SVGAElement','relList').get.call(proxy);
|
||||
if(Object.getPrototypeOf(list)!==child.DOMTokenList.prototype)return 'list realm';
|
||||
own(realm,'SVGAElement','relList').set.call(proxy,'\ud800 token');
|
||||
if(real.rel!=='\ud800 token')return 'list value';
|
||||
list.value='\udfff value';
|
||||
if(real.rel!=='\udfff value'||list.value!=='\udfff value')return 'DOMTokenList UTF16';
|
||||
own(realm,'SVGAElement','pathname').set.call(proxy,'/new');
|
||||
if(own(realm,'SVGAElement','host').get.call(proxy)!=='example.test'||real.href.baseVal!=='https://example.test/new')return 'URL';
|
||||
own(realm,'SVGAnimatedString','baseVal').set.call(__linkProxies.target,'_blank');
|
||||
if(real.getAttribute('target')!=='_blank')return 'target mutation';
|
||||
const wrapped=new Proxy(proxy,{get:__linkTrap});let converted=0,error;
|
||||
try{own(realm,'SVGAElement','download').set.call(wrapped,{toString(){converted++;return 'bad';}});}catch(e){error=e;}
|
||||
if(!(error instanceof realm.TypeError)||converted!==0)return 'author wrapper';
|
||||
}
|
||||
return __linkTraps===0?'ok':'proxy trap';
|
||||
})()"#,
|
||||
)
|
||||
.unwrap(),
|
||||
"ok"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user