1use std::cell::Cell;
6use std::cmp::Ordering;
7
8use dom_struct::dom_struct;
9use js::context::JSContext;
10use js::jsapi::Heap;
11use js::jsval::{JSVal, NullValue, UndefinedValue};
12use js::rust::{HandleValue, MutableHandleValue};
13use net_traits::CoreResourceMsg;
14use profile_traits::generic_channel;
15use script_bindings::reflector::{Reflector, reflect_dom_object};
16use servo_base::generic_channel::GenericSend;
17use servo_base::id::HistoryStateId;
18use servo_constellation_traits::{
19 ScriptToConstellationMessage, StructuredSerializedData, TraversalDirection,
20};
21use servo_url::ServoUrl;
22
23use crate::dom::bindings::codegen::Bindings::HistoryBinding::HistoryMethods;
24use crate::dom::bindings::codegen::Bindings::LocationBinding::Location_Binding::LocationMethods;
25use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
26use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
27use crate::dom::bindings::inheritance::Castable;
28use crate::dom::bindings::reflector::DomGlobal;
29use crate::dom::bindings::root::{AsHandleValue, Dom, DomRoot};
30use crate::dom::bindings::str::{DOMString, USVString};
31use crate::dom::bindings::structuredclone;
32use crate::dom::event::Event;
33use crate::dom::eventtarget::EventTarget;
34use crate::dom::globalscope::GlobalScope;
35use crate::dom::hashchangeevent::HashChangeEvent;
36use crate::dom::popstateevent::PopStateEvent;
37use crate::dom::window::Window;
38use crate::script_runtime::CanGc;
39
40enum PushOrReplace {
41 Push,
42 Replace,
43}
44
45#[dom_struct]
47pub(crate) struct History {
48 reflector_: Reflector,
49 window: Dom<Window>,
50 #[ignore_malloc_size_of = "mozjs"]
51 state: Heap<JSVal>,
52 #[no_trace]
53 state_id: Cell<Option<HistoryStateId>>,
54}
55
56impl History {
57 pub(crate) fn new_inherited(window: &Window) -> History {
58 History {
59 reflector_: Reflector::new(),
60 window: Dom::from_ref(window),
61 state: Heap::default(),
62 state_id: Cell::new(None),
63 }
64 }
65
66 pub(crate) fn new(window: &Window, can_gc: CanGc) -> DomRoot<History> {
67 let dom_root = reflect_dom_object(Box::new(History::new_inherited(window)), window, can_gc);
68 dom_root.state.set(NullValue());
69 dom_root
70 }
71}
72
73impl History {
74 fn traverse_history(&self, direction: TraversalDirection) -> ErrorResult {
75 if !self.window.Document().is_fully_active() {
76 return Err(Error::Security(None));
77 }
78 let msg = ScriptToConstellationMessage::TraverseHistory(direction);
79 let _ = self
80 .window
81 .as_global_scope()
82 .script_to_constellation_chan()
83 .send(msg);
84 Ok(())
85 }
86
87 pub(crate) fn activate_state(
90 &self,
91 cx: &mut JSContext,
92 state_id: Option<HistoryStateId>,
93 url: ServoUrl,
94 ) {
95 let document = self.window.Document();
97 let old_url = document.url();
98 document.set_url(url.clone());
99
100 let hash_changed = old_url.fragment() != url.fragment();
102
103 if let Some(fragment) = url.fragment() {
105 document.scroll_to_the_fragment(cx, fragment);
106 }
107
108 let state_changed = state_id != self.state_id.get();
110 self.state_id.set(state_id);
111 let serialized_data = match state_id {
112 Some(state_id) => {
113 let (tx, rx) =
114 generic_channel::channel(self.global().time_profiler_chan().clone()).unwrap();
115 let _ = self
116 .window
117 .as_global_scope()
118 .resource_threads()
119 .send(CoreResourceMsg::GetHistoryState(state_id, tx));
120 rx.recv().unwrap()
121 },
122 None => None,
123 };
124
125 match serialized_data {
126 Some(data) => {
127 let data = StructuredSerializedData {
128 serialized: data,
129 ..Default::default()
130 };
131 rooted!(in(*GlobalScope::get_cx()) let mut state = UndefinedValue());
132 if structuredclone::read(
133 self.window.as_global_scope(),
134 data,
135 state.handle_mut(),
136 CanGc::from_cx(cx),
137 )
138 .is_err()
139 {
140 warn!("Error reading structuredclone data");
141 }
142 self.state.set(state.get());
143 },
144 None => {
145 self.state.set(NullValue());
146 },
147 }
148
149 if state_changed {
152 PopStateEvent::dispatch_jsval(
153 self.window.upcast::<EventTarget>(),
154 &self.window,
155 self.state.as_handle_value(),
156 CanGc::from_cx(cx),
157 );
158 }
159
160 if hash_changed {
162 let event = HashChangeEvent::new(
163 &self.window,
164 atom!("hashchange"),
165 false,
166 false,
167 old_url.into_string(),
168 url.into_string(),
169 CanGc::from_cx(cx),
170 );
171 event
172 .upcast::<Event>()
173 .fire(self.window.upcast::<EventTarget>(), CanGc::from_cx(cx));
174 }
175 }
176
177 pub(crate) fn remove_states(&self, states: Vec<HistoryStateId>) {
178 let _ = self
179 .window
180 .as_global_scope()
181 .resource_threads()
182 .send(CoreResourceMsg::RemoveHistoryStates(states));
183 }
184
185 fn push_or_replace_state(
188 &self,
189 cx: &mut JSContext,
190 data: HandleValue,
191 _title: DOMString,
192 url: Option<USVString>,
193 push_or_replace: PushOrReplace,
194 ) -> ErrorResult {
195 let document = self.window.Document();
197
198 if !document.is_fully_active() {
200 return Err(Error::Security(None));
201 }
202
203 let serialized_data = structuredclone::write(cx.into(), data, None)?;
208
209 let new_url: ServoUrl = match url {
211 Some(urlstring) => {
213 let document_url = document.url();
214
215 let Ok(url) = ServoUrl::parse_with_base(Some(&document_url), &urlstring.0) else {
218 return Err(Error::Security(None));
220 };
221
222 if !Self::can_have_url_rewritten(&document_url, &url) {
225 return Err(Error::Security(None));
226 }
227
228 url
229 },
230 None => document.url(),
231 };
232
233 let state_id = match push_or_replace {
235 PushOrReplace::Push => {
236 let state_id = HistoryStateId::new();
237 self.state_id.set(Some(state_id));
238 let msg = ScriptToConstellationMessage::PushHistoryState(state_id, new_url.clone());
239 let _ = self
240 .window
241 .as_global_scope()
242 .script_to_constellation_chan()
243 .send(msg);
244 state_id
245 },
246 PushOrReplace::Replace => {
247 let state_id = match self.state_id.get() {
248 Some(state_id) => state_id,
249 None => {
250 let state_id = HistoryStateId::new();
251 self.state_id.set(Some(state_id));
252 state_id
253 },
254 };
255 let msg =
256 ScriptToConstellationMessage::ReplaceHistoryState(state_id, new_url.clone());
257 let _ = self
258 .window
259 .as_global_scope()
260 .script_to_constellation_chan()
261 .send(msg);
262 state_id
263 },
264 };
265
266 let _ = self.window.as_global_scope().resource_threads().send(
267 CoreResourceMsg::SetHistoryState(state_id, serialized_data.serialized.clone()),
268 );
269
270 document.set_url(new_url);
275
276 rooted!(&in(cx) let mut state = UndefinedValue());
278 if structuredclone::read(
279 self.window.as_global_scope(),
280 serialized_data,
281 state.handle_mut(),
282 CanGc::from_cx(cx),
283 )
284 .is_err()
285 {
286 warn!("Error reading structuredclone data");
287 }
288
289 self.state.set(state.get());
291
292 Ok(())
296 }
297
298 fn can_have_url_rewritten(document_url: &ServoUrl, target_url: &ServoUrl) -> bool {
301 if target_url.scheme() != document_url.scheme() ||
304 target_url.username() != document_url.username() ||
305 target_url.password() != document_url.password() ||
306 target_url.host() != document_url.host() ||
307 target_url.port() != document_url.port()
308 {
309 return false;
310 }
311
312 if target_url.scheme() == "http" || target_url.scheme() == "https" {
314 return true;
315 }
316
317 if target_url.scheme() == "file" {
319 return target_url.path() == document_url.path();
322 }
323
324 if target_url.path() != document_url.path() || target_url.query() != document_url.query() {
327 return false;
328 }
329
330 true
332 }
333}
334
335impl HistoryMethods<crate::DomTypeHolder> for History {
336 fn GetState(&self, _cx: &mut JSContext, mut retval: MutableHandleValue) -> Fallible<()> {
338 if !self.window.Document().is_fully_active() {
339 return Err(Error::Security(None));
340 }
341 retval.set(self.state.get());
342 Ok(())
343 }
344
345 fn GetLength(&self) -> Fallible<u32> {
347 if !self.window.Document().is_fully_active() {
348 return Err(Error::Security(None));
349 }
350
351 let Some((sender, recv)) =
352 generic_channel::channel(self.global().time_profiler_chan().clone())
353 else {
354 return Err(Error::InvalidState(None));
355 };
356
357 let msg = ScriptToConstellationMessage::JointSessionHistoryLength(sender);
358
359 self.window
360 .as_global_scope()
361 .script_to_constellation_chan()
362 .send(msg)
363 .map_err(|_| Error::InvalidState(None))?;
364
365 recv.recv().map_err(|_| Error::InvalidState(None))
366 }
367
368 fn Go(&self, cx: &mut JSContext, delta: i32) -> ErrorResult {
370 let direction = match delta.cmp(&0) {
371 Ordering::Greater => TraversalDirection::Forward(delta as usize),
372 Ordering::Less => TraversalDirection::Back(-delta as usize),
373 Ordering::Equal => return self.window.Location(cx).Reload(cx),
374 };
375
376 self.traverse_history(direction)
377 }
378
379 fn Back(&self) -> ErrorResult {
381 self.traverse_history(TraversalDirection::Back(1))
382 }
383
384 fn Forward(&self) -> ErrorResult {
386 self.traverse_history(TraversalDirection::Forward(1))
387 }
388
389 fn PushState(
391 &self,
392 cx: &mut JSContext,
393 data: HandleValue,
394 title: DOMString,
395 url: Option<USVString>,
396 ) -> ErrorResult {
397 self.push_or_replace_state(cx, data, title, url, PushOrReplace::Push)
398 }
399
400 fn ReplaceState(
402 &self,
403 cx: &mut JSContext,
404 data: HandleValue,
405 title: DOMString,
406 url: Option<USVString>,
407 ) -> ErrorResult {
408 self.push_or_replace_state(cx, data, title, url, PushOrReplace::Replace)
409 }
410}