mirror of
https://github.com/lexmount/moli.git
synced 2026-09-28 08:01:37 +00:00
refactor(history): centralize fully-active admission
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use super::history_runtime::require_fully_active_history_owner;
|
||||
use super::location_runtime::{location_href_slot, sync_location_object};
|
||||
use super::navigation_activation::bind_navigation_entry_runtime_owner;
|
||||
use super::navigation_callbacks::cancel_active_intercepted_same_document_navigation;
|
||||
@@ -23,8 +24,7 @@ use super::navigation_result::{
|
||||
};
|
||||
use super::navigation_serialize::sync_child_navigation_entry_seed_from_owner;
|
||||
use super::navigation_window::{
|
||||
runtime_window_is_global, runtime_window_owner, window_location_for_holder,
|
||||
window_navigation_for_holder,
|
||||
runtime_window_is_global, window_location_for_holder, window_navigation_for_holder,
|
||||
};
|
||||
use super::*;
|
||||
use crate::webidl;
|
||||
@@ -104,11 +104,13 @@ fn mutate_history_object<'s>(
|
||||
return;
|
||||
};
|
||||
let _ = &parsed.unused;
|
||||
let Some(owner) = require_fully_active_history_owner(scope, history) else {
|
||||
return;
|
||||
};
|
||||
let Some(state) = structured_clone_value_for_storage(scope, parsed.state) else {
|
||||
return;
|
||||
};
|
||||
let state_json = stringify_history_state(scope, state);
|
||||
let owner = runtime_window_owner(scope, history);
|
||||
let Some(location) = window_location_for_holder(scope, owner) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
mod admission;
|
||||
mod apply;
|
||||
mod length;
|
||||
mod results;
|
||||
mod traversal;
|
||||
|
||||
pub(super) use self::admission::require_fully_active_history_owner;
|
||||
pub(super) use self::apply::apply_history_entry;
|
||||
pub(crate) use self::length::{
|
||||
increment_top_level_history_length_for_runtime_owner,
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
use super::super::navigation_window::{navigation_document_is_active, runtime_window_owner};
|
||||
use super::super::{context_host_ptr_from_global_bridge, throw_dom_exception_value};
|
||||
|
||||
/// History API algorithms call this after WebIDL argument conversion.
|
||||
pub(in crate::context_bootstrap) fn require_fully_active_history_owner<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
history: v8::Local<'s, v8::Object>,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
let owner = runtime_window_owner(scope, history);
|
||||
let is_fully_active = if let Some(popup_id) =
|
||||
crate::native_bridge::lightweight_popup_id_from_window(scope, owner)
|
||||
{
|
||||
context_host_ptr_from_global_bridge(scope)
|
||||
.is_some_and(|host_ptr| unsafe { &*host_ptr }.lightweight_popup_is_open(popup_id))
|
||||
} else {
|
||||
navigation_document_is_active(scope, owner)
|
||||
};
|
||||
if is_fully_active {
|
||||
return Some(owner);
|
||||
}
|
||||
throw_dom_exception_value(
|
||||
scope,
|
||||
"The associated Document is not fully active.",
|
||||
"SecurityError",
|
||||
);
|
||||
None
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::super::history_runtime::require_fully_active_history_owner;
|
||||
use super::super::navigation_activation::{
|
||||
navigation_activation_value, navigation_current_entry_value, navigation_transition_value,
|
||||
};
|
||||
@@ -151,6 +152,9 @@ fn history_length_getter_function<'s>(
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
if require_fully_active_history_owner(scope, args.this()).is_none() {
|
||||
return;
|
||||
}
|
||||
let value = history_length_value(scope, args.this())
|
||||
.unwrap_or_else(|| v8::Number::new(scope, 0.0).into());
|
||||
rv.set(value);
|
||||
@@ -161,6 +165,9 @@ fn history_state_getter_function<'s>(
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
if require_fully_active_history_owner(scope, args.this()).is_none() {
|
||||
return;
|
||||
}
|
||||
let value = history_state_value(scope, args.this());
|
||||
rv.set(value);
|
||||
}
|
||||
@@ -212,6 +219,9 @@ fn history_scroll_restoration_getter_function<'s>(
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
if require_fully_active_history_owner(scope, args.this()).is_none() {
|
||||
return;
|
||||
}
|
||||
let value = history_scroll_restoration_value(scope, args.this())
|
||||
.unwrap_or_else(|| v8str(scope, "auto").into());
|
||||
rv.set(value);
|
||||
@@ -232,6 +242,9 @@ fn history_scroll_restoration_setter_function<'s>(
|
||||
let Some(next_value) = ScrollRestoration::parse(&next_value) else {
|
||||
return;
|
||||
};
|
||||
if require_fully_active_history_owner(scope, args.this()).is_none() {
|
||||
return;
|
||||
}
|
||||
set_history_scroll_restoration(scope, args.this(), next_value.label());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::history_runtime::{
|
||||
cancel_pending_precommit_history_traversal, pending_history_traversal_target_index,
|
||||
require_fully_active_history_owner,
|
||||
};
|
||||
use super::location_navigation::{
|
||||
LocationNavigationKind, navigate_location_object,
|
||||
@@ -69,14 +70,43 @@ pub(super) fn history_go_callback<'s>(
|
||||
);
|
||||
return;
|
||||
}
|
||||
let delta = if args.length() == 0 {
|
||||
let action = if args.length() == 0 {
|
||||
Some(HistoryGoAction::Reload)
|
||||
} else {
|
||||
coerce_history_go_delta(scope, args.get(0))
|
||||
};
|
||||
match delta {
|
||||
Some(HistoryGoAction::Reload) => {
|
||||
let owner = runtime_window_owner(scope, args.this());
|
||||
let Some(action) = action else {
|
||||
return;
|
||||
};
|
||||
history_go_with_action(scope, args.this(), action);
|
||||
}
|
||||
|
||||
pub(super) fn history_back_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
_rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
history_go_with_action(scope, args.this(), HistoryGoAction::Traverse(-1));
|
||||
}
|
||||
|
||||
pub(super) fn history_forward_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
_rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
history_go_with_action(scope, args.this(), HistoryGoAction::Traverse(1));
|
||||
}
|
||||
|
||||
fn history_go_with_action<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
history: v8::Local<'s, v8::Object>,
|
||||
action: HistoryGoAction,
|
||||
) {
|
||||
let Some(owner) = require_fully_active_history_owner(scope, history) else {
|
||||
return;
|
||||
};
|
||||
match action {
|
||||
HistoryGoAction::Reload => {
|
||||
let Some(location) = window_location_for_holder(scope, owner) else {
|
||||
return;
|
||||
};
|
||||
@@ -87,28 +117,11 @@ pub(super) fn history_go_callback<'s>(
|
||||
None,
|
||||
);
|
||||
}
|
||||
Some(HistoryGoAction::Traverse(delta)) => history_traverse(scope, args.this(), delta),
|
||||
Some(HistoryGoAction::Noop) => {}
|
||||
None => {}
|
||||
HistoryGoAction::Traverse(delta) => history_traverse(scope, history, delta),
|
||||
HistoryGoAction::Noop => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn history_back_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
_rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
history_traverse(scope, args.this(), -1);
|
||||
}
|
||||
|
||||
pub(super) fn history_forward_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
_rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
history_traverse(scope, args.this(), 1);
|
||||
}
|
||||
|
||||
pub(super) fn navigation_back_callback<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
|
||||
@@ -1449,6 +1449,155 @@ fn history_navigation_arguments_use_webidl_conversion() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_operations_reject_a_removed_child_document() {
|
||||
let mut vm = new_storage_test_vm("https://example.com/page.html");
|
||||
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const root = document.documentElement ||
|
||||
document.appendChild(document.createElement("html"));
|
||||
const body = document.body || root.appendChild(document.createElement("body"));
|
||||
const frame = document.createElement("iframe");
|
||||
body.appendChild(frame);
|
||||
const childHistory = frame.contentWindow.history;
|
||||
const ChildDOMException = frame.contentWindow.DOMException;
|
||||
frame.remove();
|
||||
|
||||
const probe = callback => {
|
||||
try {
|
||||
callback();
|
||||
return "no throw";
|
||||
} catch (error) {
|
||||
return `${error.name}:${error instanceof ChildDOMException}`;
|
||||
}
|
||||
};
|
||||
return [
|
||||
probe(() => childHistory.length),
|
||||
probe(() => childHistory.scrollRestoration),
|
||||
probe(() => childHistory.state),
|
||||
probe(() => { childHistory.scrollRestoration = "manual"; }),
|
||||
probe(() => childHistory.go()),
|
||||
probe(() => childHistory.go(0)),
|
||||
probe(() => childHistory.go(-1)),
|
||||
probe(() => childHistory.go(1)),
|
||||
probe(() => childHistory.go(Infinity)),
|
||||
probe(() => childHistory.go(-Infinity)),
|
||||
probe(() => childHistory.back()),
|
||||
probe(() => childHistory.forward()),
|
||||
probe(() => childHistory.pushState(1, "", "?x=1")),
|
||||
probe(() => childHistory.replaceState(2, "", "?x=2"))
|
||||
].join("|");
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("removed child History operations should be rejected");
|
||||
|
||||
assert_eq!(result, ["SecurityError:true"; 14].join("|"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_argument_errors_precede_inactive_document_errors() {
|
||||
let mut vm = new_storage_test_vm("https://example.com/page.html");
|
||||
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const root = document.documentElement ||
|
||||
document.appendChild(document.createElement("html"));
|
||||
const body = document.body || root.appendChild(document.createElement("body"));
|
||||
const frame = document.createElement("iframe");
|
||||
body.appendChild(frame);
|
||||
const childHistory = frame.contentWindow.history;
|
||||
frame.remove();
|
||||
|
||||
const conversionError = new RangeError("conversion");
|
||||
const rejected = {
|
||||
[Symbol.toPrimitive]() { throw conversionError; }
|
||||
};
|
||||
const probe = callback => {
|
||||
try {
|
||||
callback();
|
||||
return "no throw";
|
||||
} catch (error) {
|
||||
return error === conversionError ? "conversion" : error.name;
|
||||
}
|
||||
};
|
||||
return [
|
||||
probe(() => childHistory.go(Symbol())),
|
||||
probe(() => childHistory.go(1n)),
|
||||
probe(() => childHistory.go(rejected)),
|
||||
probe(() => { childHistory.scrollRestoration = Symbol(); }),
|
||||
probe(() => { childHistory.scrollRestoration = rejected; }),
|
||||
probe(() => { childHistory.scrollRestoration = "invalid"; }),
|
||||
probe(() => childHistory.pushState()),
|
||||
probe(() => childHistory.pushState(null, Symbol())),
|
||||
probe(() => childHistory.replaceState(null, "", Symbol())),
|
||||
probe(() => childHistory.pushState(null, rejected)),
|
||||
probe(() => childHistory.replaceState(null, "", rejected)),
|
||||
probe(() => childHistory.pushState(() => {}, "")),
|
||||
probe(() => childHistory.replaceState(() => {}, ""))
|
||||
].join("|");
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("inactive History should preserve conversion and serialization error ordering");
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
"TypeError|TypeError|conversion|TypeError|conversion|no throw|TypeError|TypeError|TypeError|conversion|conversion|SecurityError|SecurityError"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_activity_is_checked_after_argument_conversion() {
|
||||
let mut vm = new_storage_test_vm("https://example.com/page.html");
|
||||
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const root = document.documentElement ||
|
||||
document.appendChild(document.createElement("html"));
|
||||
const body = document.body || root.appendChild(document.createElement("body"));
|
||||
const operations = [
|
||||
(history, convert) => history.go(convert(0)),
|
||||
(history, convert) => history.go(convert(-1)),
|
||||
(history, convert) => history.go(convert(Infinity)),
|
||||
(history, convert) => { history.scrollRestoration = convert("manual"); },
|
||||
(history, convert) => history.pushState(() => {}, convert("")),
|
||||
(history, convert) => history.replaceState(() => {}, "", convert("?x=2"))
|
||||
];
|
||||
return operations.map(operation => {
|
||||
const frame = document.createElement("iframe");
|
||||
body.appendChild(frame);
|
||||
const childHistory = frame.contentWindow.history;
|
||||
let conversions = 0;
|
||||
const convert = value => ({
|
||||
[Symbol.toPrimitive]() {
|
||||
conversions++;
|
||||
frame.remove();
|
||||
return value;
|
||||
}
|
||||
});
|
||||
try {
|
||||
operation(childHistory, convert);
|
||||
return "no throw";
|
||||
} catch (error) {
|
||||
return `${error.name}:${conversions}`;
|
||||
}
|
||||
}).join("|");
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("History should reject a document removed during argument conversion");
|
||||
|
||||
assert_eq!(result, ["SecurityError:1"; 6].join("|"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_state_preserves_structured_clone_values_not_representable_as_json() {
|
||||
let mut vm = new_storage_test_vm("https://example.com/base");
|
||||
|
||||
Reference in New Issue
Block a user