use std::cell::Cell;
use std::cmp::Ordering;
use base::id::HistoryStateId;
use dom_struct::dom_struct;
use js::jsapi::Heap;
use js::jsval::{JSVal, NullValue, UndefinedValue};
use js::rust::{HandleValue, MutableHandleValue};
use net_traits::{CoreResourceMsg, IpcSend};
use profile_traits::ipc;
use profile_traits::ipc::channel;
use script_traits::{ScriptMsg, StructuredSerializedData, TraversalDirection};
use servo_url::ServoUrl;
use crate::dom::bindings::codegen::Bindings::HistoryBinding::HistoryMethods;
use crate::dom::bindings::codegen::Bindings::LocationBinding::Location_Binding::LocationMethods;
use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::reflector::{reflect_dom_object, DomObject, Reflector};
use crate::dom::bindings::root::{Dom, DomRoot};
use crate::dom::bindings::str::{DOMString, USVString};
use crate::dom::bindings::structuredclone;
use crate::dom::event::Event;
use crate::dom::eventtarget::EventTarget;
use crate::dom::globalscope::GlobalScope;
use crate::dom::hashchangeevent::HashChangeEvent;
use crate::dom::popstateevent::PopStateEvent;
use crate::dom::window::Window;
use crate::script_runtime::{CanGc, JSContext};
enum PushOrReplace {
Push,
Replace,
}
#[dom_struct]
pub struct History {
reflector_: Reflector,
window: Dom<Window>,
#[ignore_malloc_size_of = "mozjs"]
state: Heap<JSVal>,
#[no_trace]
state_id: Cell<Option<HistoryStateId>>,
}
impl History {
pub fn new_inherited(window: &Window) -> History {
let state = Heap::default();
state.set(NullValue());
History {
reflector_: Reflector::new(),
window: Dom::from_ref(window),
state,
state_id: Cell::new(None),
}
}
pub fn new(window: &Window) -> DomRoot<History> {
reflect_dom_object(
Box::new(History::new_inherited(window)),
window,
CanGc::note(),
)
}
}
impl History {
fn traverse_history(&self, direction: TraversalDirection) -> ErrorResult {
if !self.window.Document().is_fully_active() {
return Err(Error::Security);
}
let msg = ScriptMsg::TraverseHistory(direction);
let _ = self
.window
.upcast::<GlobalScope>()
.script_to_constellation_chan()
.send(msg);
Ok(())
}
#[allow(unsafe_code)]
pub fn activate_state(&self, state_id: Option<HistoryStateId>, url: ServoUrl, can_gc: CanGc) {
let document = self.window.Document();
let old_url = document.url().clone();
document.set_url(url.clone());
let hash_changed = old_url.fragment() != url.fragment();
if let Some(fragment) = url.fragment() {
document.check_and_scroll_fragment(fragment, can_gc);
}
let state_changed = state_id != self.state_id.get();
self.state_id.set(state_id);
let serialized_data = match state_id {
Some(state_id) => {
let (tx, rx) = ipc::channel(self.global().time_profiler_chan().clone()).unwrap();
let _ = self
.window
.upcast::<GlobalScope>()
.resource_threads()
.send(CoreResourceMsg::GetHistoryState(state_id, tx));
rx.recv().unwrap()
},
None => None,
};
match serialized_data {
Some(data) => {
let data = StructuredSerializedData {
serialized: data,
ports: None,
blobs: None,
};
let global_scope = self.window.upcast::<GlobalScope>();
rooted!(in(*GlobalScope::get_cx()) let mut state = UndefinedValue());
if structuredclone::read(global_scope, data, state.handle_mut()).is_err() {
warn!("Error reading structuredclone data");
}
self.state.set(state.get());
},
None => {
self.state.set(NullValue());
},
}
if state_changed {
PopStateEvent::dispatch_jsval(
self.window.upcast::<EventTarget>(),
&self.window,
unsafe { HandleValue::from_raw(self.state.handle()) },
can_gc,
);
}
if hash_changed {
let event = HashChangeEvent::new(
&self.window,
atom!("hashchange"),
false,
false,
old_url.into_string(),
url.into_string(),
can_gc,
);
event
.upcast::<Event>()
.fire(self.window.upcast::<EventTarget>(), can_gc);
}
}
pub fn remove_states(&self, states: Vec<HistoryStateId>) {
let _ = self
.window
.upcast::<GlobalScope>()
.resource_threads()
.send(CoreResourceMsg::RemoveHistoryStates(states));
}
fn push_or_replace_state(
&self,
cx: JSContext,
data: HandleValue,
_title: DOMString,
url: Option<USVString>,
push_or_replace: PushOrReplace,
) -> ErrorResult {
let document = self.window.Document();
if !document.is_fully_active() {
return Err(Error::Security);
}
let serialized_data = structuredclone::write(cx, data, None)?;
let new_url: ServoUrl = match url {
Some(urlstring) => {
let document_url = document.url();
let new_url = match ServoUrl::parse_with_base(Some(&document_url), &urlstring.0) {
Ok(parsed_url) => parsed_url,
Err(_) => return Err(Error::Security),
};
if new_url.scheme() != document_url.scheme() ||
new_url.host() != document_url.host() ||
new_url.port() != document_url.port() ||
new_url.username() != document_url.username() ||
new_url.password() != document_url.password()
{
return Err(Error::Security);
}
if new_url.origin() != document_url.origin() {
return Err(Error::Security);
}
new_url
},
None => document.url(),
};
let state_id = match push_or_replace {
PushOrReplace::Push => {
let state_id = HistoryStateId::new();
self.state_id.set(Some(state_id));
let msg = ScriptMsg::PushHistoryState(state_id, new_url.clone());
let _ = self
.window
.upcast::<GlobalScope>()
.script_to_constellation_chan()
.send(msg);
state_id
},
PushOrReplace::Replace => {
let state_id = match self.state_id.get() {
Some(state_id) => state_id,
None => {
let state_id = HistoryStateId::new();
self.state_id.set(Some(state_id));
state_id
},
};
let msg = ScriptMsg::ReplaceHistoryState(state_id, new_url.clone());
let _ = self
.window
.upcast::<GlobalScope>()
.script_to_constellation_chan()
.send(msg);
state_id
},
};
let _ = self.window.upcast::<GlobalScope>().resource_threads().send(
CoreResourceMsg::SetHistoryState(state_id, serialized_data.serialized.clone()),
);
document.set_url(new_url);
let global_scope = self.window.upcast::<GlobalScope>();
rooted!(in(*cx) let mut state = UndefinedValue());
if structuredclone::read(global_scope, serialized_data, state.handle_mut()).is_err() {
warn!("Error reading structuredclone data");
}
self.state.set(state.get());
Ok(())
}
}
impl HistoryMethods<crate::DomTypeHolder> for History {
fn GetState(&self, _cx: JSContext, mut retval: MutableHandleValue) -> Fallible<()> {
if !self.window.Document().is_fully_active() {
return Err(Error::Security);
}
retval.set(self.state.get());
Ok(())
}
fn GetLength(&self) -> Fallible<u32> {
if !self.window.Document().is_fully_active() {
return Err(Error::Security);
}
let (sender, recv) = channel(self.global().time_profiler_chan().clone())
.expect("Failed to create channel to send jsh length.");
let msg = ScriptMsg::JointSessionHistoryLength(sender);
let _ = self
.window
.upcast::<GlobalScope>()
.script_to_constellation_chan()
.send(msg);
Ok(recv.recv().unwrap())
}
fn Go(&self, delta: i32, can_gc: CanGc) -> ErrorResult {
let direction = match delta.cmp(&0) {
Ordering::Greater => TraversalDirection::Forward(delta as usize),
Ordering::Less => TraversalDirection::Back(-delta as usize),
Ordering::Equal => return self.window.Location().Reload(can_gc),
};
self.traverse_history(direction)
}
fn Back(&self) -> ErrorResult {
self.traverse_history(TraversalDirection::Back(1))
}
fn Forward(&self) -> ErrorResult {
self.traverse_history(TraversalDirection::Forward(1))
}
fn PushState(
&self,
cx: JSContext,
data: HandleValue,
title: DOMString,
url: Option<USVString>,
) -> ErrorResult {
self.push_or_replace_state(cx, data, title, url, PushOrReplace::Push)
}
fn ReplaceState(
&self,
cx: JSContext,
data: HandleValue,
title: DOMString,
url: Option<USVString>,
) -> ErrorResult {
self.push_or_replace_state(cx, data, title, url, PushOrReplace::Replace)
}
}