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 HistoryTraversalSource, ScriptToConstellationMessage, SessionHistoryTraversalRequest,
20 StructuredSerializedData, TraversalDirection,
21};
22use servo_url::ServoUrl;
23
24use crate::dom::bindings::codegen::Bindings::HistoryBinding::HistoryMethods;
25use crate::dom::bindings::codegen::Bindings::LocationBinding::Location_Binding::LocationMethods;
26use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
27use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
28use crate::dom::bindings::inheritance::Castable;
29use crate::dom::bindings::reflector::DomGlobal;
30use crate::dom::bindings::root::{AsHandleValue, Dom, DomRoot};
31use crate::dom::bindings::str::{DOMString, USVString};
32use crate::dom::bindings::structuredclone;
33use crate::dom::event::Event;
34use crate::dom::eventtarget::EventTarget;
35use crate::dom::hashchangeevent::HashChangeEvent;
36use crate::dom::popstateevent::PopStateEvent;
37use crate::dom::window::Window;
38
39enum PushOrReplace {
40 Push,
41 Replace,
42}
43
44#[dom_struct]
46pub(crate) struct History {
47 reflector_: Reflector,
48 window: Dom<Window>,
49 #[ignore_malloc_size_of = "mozjs"]
50 state: Heap<JSVal>,
51 #[no_trace]
52 state_id: Cell<Option<HistoryStateId>>,
53}
54
55impl History {
56 pub(crate) fn new_inherited(window: &Window) -> History {
57 History {
58 reflector_: Reflector::new(),
59 window: Dom::from_ref(window),
60 state: Heap::default(),
61 state_id: Cell::new(None),
62 }
63 }
64
65 pub(crate) fn new(cx: &mut JSContext, window: &Window) -> DomRoot<History> {
66 let dom_root = reflect_dom_object(cx, Box::new(History::new_inherited(window)), window);
67 dom_root.state.set(NullValue());
68 dom_root
69 }
70}
71
72impl History {
73 fn traverse_history(&self, direction: TraversalDirection) -> ErrorResult {
74 if !self.window.Document().is_fully_active() {
75 return Err(Error::Security(Some("Document is not fully active".into())));
76 }
77 let _ = self
78 .window
79 .as_global_scope()
80 .script_to_constellation_chan()
81 .send(ScriptToConstellationMessage::TraverseHistory(
82 SessionHistoryTraversalRequest::new(
83 self.window.webview_id(),
84 direction,
85 HistoryTraversalSource::Script,
86 ),
87 ));
88 Ok(())
89 }
90
91 pub(crate) fn activate_state(
94 &self,
95 cx: &mut JSContext,
96 state_id: Option<HistoryStateId>,
97 url: ServoUrl,
98 ) {
99 let document = self.window.Document();
101 let old_url = document.url();
102 document.set_url(url.clone());
103
104 let hash_changed = old_url.fragment() != url.fragment();
106
107 if let Some(fragment) = url.fragment() {
109 document.scroll_to_the_fragment(cx, fragment);
110 }
111
112 let state_changed = state_id != self.state_id.get();
114 self.state_id.set(state_id);
115 let serialized_data = match state_id {
116 Some(state_id) => {
117 let (tx, rx) =
118 generic_channel::channel(self.global().time_profiler_chan().clone()).unwrap();
119 let _ = self
120 .window
121 .as_global_scope()
122 .resource_threads()
123 .send(CoreResourceMsg::GetHistoryState(state_id, tx));
124 rx.recv().unwrap()
125 },
126 None => None,
127 };
128
129 match serialized_data {
130 Some(data) => {
131 let data = StructuredSerializedData {
132 serialized: data,
133 ..Default::default()
134 };
135 rooted!(&in(cx) let mut state = UndefinedValue());
136 if structuredclone::read(
137 cx,
138 self.window.as_global_scope(),
139 data,
140 state.handle_mut(),
141 )
142 .is_err()
143 {
144 warn!("Error reading structuredclone data");
145 }
146 self.state.set(state.get());
147 },
148 None => {
149 self.state.set(NullValue());
150 },
151 }
152
153 if state_changed {
156 PopStateEvent::dispatch_jsval(
157 cx,
158 self.window.upcast::<EventTarget>(),
159 &self.window,
160 self.state.as_handle_value(),
161 );
162 }
163
164 if hash_changed {
166 let event = HashChangeEvent::new(
167 cx,
168 &self.window,
169 atom!("hashchange"),
170 false,
171 false,
172 old_url.into_string(),
173 url.into_string(),
174 );
175 event
176 .upcast::<Event>()
177 .fire(cx, self.window.upcast::<EventTarget>());
178 }
179 }
180
181 pub(crate) fn remove_states(&self, states: Vec<HistoryStateId>) {
182 let _ = self
183 .window
184 .as_global_scope()
185 .resource_threads()
186 .send(CoreResourceMsg::RemoveHistoryStates(states));
187 }
188
189 fn push_or_replace_state(
192 &self,
193 cx: &mut JSContext,
194 data: HandleValue,
195 _title: DOMString,
196 url: Option<USVString>,
197 push_or_replace: PushOrReplace,
198 ) -> ErrorResult {
199 let document = self.window.Document();
201
202 if !document.is_fully_active() {
204 return Err(Error::Security(Some("Document is not fully active".into())));
205 }
206
207 let serialized_data = structuredclone::write(cx, data, None)?;
212
213 let new_url: ServoUrl = match url {
215 Some(urlstring) => {
217 let document_url = document.url();
218
219 let Ok(url) = ServoUrl::parse_with_base(Some(&document_url), &urlstring.0) else {
222 return Err(Error::Security(Some("Invalid target URL".into())));
224 };
225
226 if !Self::can_have_url_rewritten(&document_url, &url) {
229 return Err(Error::Security(Some(
230 "Document cannot have URL rewritten".into(),
231 )));
232 }
233
234 url
235 },
236 None => document.url(),
237 };
238
239 let state_id = match push_or_replace {
241 PushOrReplace::Push => {
242 let state_id = HistoryStateId::new();
243 self.state_id.set(Some(state_id));
244 let msg = ScriptToConstellationMessage::PushHistoryState(state_id, new_url.clone());
245 let _ = self
246 .window
247 .as_global_scope()
248 .script_to_constellation_chan()
249 .send(msg);
250 state_id
251 },
252 PushOrReplace::Replace => {
253 let state_id = match self.state_id.get() {
254 Some(state_id) => state_id,
255 None => {
256 let state_id = HistoryStateId::new();
257 self.state_id.set(Some(state_id));
258 state_id
259 },
260 };
261 let msg =
262 ScriptToConstellationMessage::ReplaceHistoryState(state_id, new_url.clone());
263 let _ = self
264 .window
265 .as_global_scope()
266 .script_to_constellation_chan()
267 .send(msg);
268 state_id
269 },
270 };
271
272 let _ = self.window.as_global_scope().resource_threads().send(
273 CoreResourceMsg::SetHistoryState(state_id, serialized_data.serialized.clone()),
274 );
275
276 document.set_url(new_url);
281
282 rooted!(&in(cx) let mut state = UndefinedValue());
284 if structuredclone::read(
285 cx,
286 self.window.as_global_scope(),
287 serialized_data,
288 state.handle_mut(),
289 )
290 .is_err()
291 {
292 warn!("Error reading structuredclone data");
293 }
294
295 self.state.set(state.get());
297
298 Ok(())
302 }
303
304 fn can_have_url_rewritten(document_url: &ServoUrl, target_url: &ServoUrl) -> bool {
307 if target_url.scheme() != document_url.scheme() ||
310 target_url.username() != document_url.username() ||
311 target_url.password() != document_url.password() ||
312 target_url.host() != document_url.host() ||
313 target_url.port() != document_url.port()
314 {
315 return false;
316 }
317
318 if target_url.scheme() == "http" || target_url.scheme() == "https" {
320 return true;
321 }
322
323 if target_url.scheme() == "file" {
325 return target_url.path() == document_url.path();
328 }
329
330 if target_url.path() != document_url.path() || target_url.query() != document_url.query() {
333 return false;
334 }
335
336 true
338 }
339}
340
341impl HistoryMethods<crate::DomTypeHolder> for History {
342 fn GetState(&self, _cx: &mut JSContext, mut retval: MutableHandleValue) -> Fallible<()> {
344 if !self.window.Document().is_fully_active() {
345 return Err(Error::Security(Some("Document is not fully active".into())));
346 }
347 retval.set(self.state.get());
348 Ok(())
349 }
350
351 fn GetLength(&self) -> Fallible<u32> {
353 if !self.window.Document().is_fully_active() {
354 return Err(Error::Security(Some("Document is not fully active".into())));
355 }
356
357 let Some((sender, recv)) =
358 generic_channel::channel(self.global().time_profiler_chan().clone())
359 else {
360 return Err(Error::InvalidState(Some(
361 "Failed to create channel to get session history length".into(),
362 )));
363 };
364
365 let msg = ScriptToConstellationMessage::JointSessionHistoryLength(sender);
366
367 self.window
368 .as_global_scope()
369 .script_to_constellation_chan()
370 .send(msg)
371 .map_err(|_| {
372 Error::InvalidState(Some("Failed to send session history length request".into()))
373 })?;
374
375 recv.recv().map_err(|_| {
376 Error::InvalidState(Some("Failed to receive session history length".into()))
377 })
378 }
379
380 fn Go(&self, cx: &mut JSContext, delta: i32) -> ErrorResult {
382 let direction = match delta.cmp(&0) {
383 Ordering::Greater => TraversalDirection::Forward(delta as usize),
384 Ordering::Less => TraversalDirection::Back(-delta as usize),
385 Ordering::Equal => return self.window.Location(cx).Reload(cx),
386 };
387
388 self.traverse_history(direction)
389 }
390
391 fn Back(&self) -> ErrorResult {
393 self.traverse_history(TraversalDirection::Back(1))
394 }
395
396 fn Forward(&self) -> ErrorResult {
398 self.traverse_history(TraversalDirection::Forward(1))
399 }
400
401 fn PushState(
403 &self,
404 cx: &mut JSContext,
405 data: HandleValue,
406 title: DOMString,
407 url: Option<USVString>,
408 ) -> ErrorResult {
409 self.push_or_replace_state(cx, data, title, url, PushOrReplace::Push)
410 }
411
412 fn ReplaceState(
414 &self,
415 cx: &mut JSContext,
416 data: HandleValue,
417 title: DOMString,
418 url: Option<USVString>,
419 ) -> ErrorResult {
420 self.push_or_replace_state(cx, data, title, url, PushOrReplace::Replace)
421 }
422}