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_with_cx};
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 =
67 reflect_dom_object_with_cx(Box::new(History::new_inherited(window)), window, cx);
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 _ = self
79 .window
80 .as_global_scope()
81 .script_to_constellation_chan()
82 .send(ScriptToConstellationMessage::TraverseHistory(
83 SessionHistoryTraversalRequest::new(
84 self.window.webview_id(),
85 direction,
86 HistoryTraversalSource::Script,
87 ),
88 ));
89 Ok(())
90 }
91
92 pub(crate) fn activate_state(
95 &self,
96 cx: &mut JSContext,
97 state_id: Option<HistoryStateId>,
98 url: ServoUrl,
99 ) {
100 let document = self.window.Document();
102 let old_url = document.url();
103 document.set_url(url.clone());
104
105 let hash_changed = old_url.fragment() != url.fragment();
107
108 if let Some(fragment) = url.fragment() {
110 document.scroll_to_the_fragment(cx, fragment);
111 }
112
113 let state_changed = state_id != self.state_id.get();
115 self.state_id.set(state_id);
116 let serialized_data = match state_id {
117 Some(state_id) => {
118 let (tx, rx) =
119 generic_channel::channel(self.global().time_profiler_chan().clone()).unwrap();
120 let _ = self
121 .window
122 .as_global_scope()
123 .resource_threads()
124 .send(CoreResourceMsg::GetHistoryState(state_id, tx));
125 rx.recv().unwrap()
126 },
127 None => None,
128 };
129
130 match serialized_data {
131 Some(data) => {
132 let data = StructuredSerializedData {
133 serialized: data,
134 ..Default::default()
135 };
136 rooted!(&in(cx) let mut state = UndefinedValue());
137 if structuredclone::read(
138 cx,
139 self.window.as_global_scope(),
140 data,
141 state.handle_mut(),
142 )
143 .is_err()
144 {
145 warn!("Error reading structuredclone data");
146 }
147 self.state.set(state.get());
148 },
149 None => {
150 self.state.set(NullValue());
151 },
152 }
153
154 if state_changed {
157 PopStateEvent::dispatch_jsval(
158 cx,
159 self.window.upcast::<EventTarget>(),
160 &self.window,
161 self.state.as_handle_value(),
162 );
163 }
164
165 if hash_changed {
167 let event = HashChangeEvent::new(
168 cx,
169 &self.window,
170 atom!("hashchange"),
171 false,
172 false,
173 old_url.into_string(),
174 url.into_string(),
175 );
176 event
177 .upcast::<Event>()
178 .fire(cx, self.window.upcast::<EventTarget>());
179 }
180 }
181
182 pub(crate) fn remove_states(&self, states: Vec<HistoryStateId>) {
183 let _ = self
184 .window
185 .as_global_scope()
186 .resource_threads()
187 .send(CoreResourceMsg::RemoveHistoryStates(states));
188 }
189
190 fn push_or_replace_state(
193 &self,
194 cx: &mut JSContext,
195 data: HandleValue,
196 _title: DOMString,
197 url: Option<USVString>,
198 push_or_replace: PushOrReplace,
199 ) -> ErrorResult {
200 let document = self.window.Document();
202
203 if !document.is_fully_active() {
205 return Err(Error::Security(None));
206 }
207
208 let serialized_data = structuredclone::write(cx, data, None)?;
213
214 let new_url: ServoUrl = match url {
216 Some(urlstring) => {
218 let document_url = document.url();
219
220 let Ok(url) = ServoUrl::parse_with_base(Some(&document_url), &urlstring.0) else {
223 return Err(Error::Security(None));
225 };
226
227 if !Self::can_have_url_rewritten(&document_url, &url) {
230 return Err(Error::Security(None));
231 }
232
233 url
234 },
235 None => document.url(),
236 };
237
238 let state_id = match push_or_replace {
240 PushOrReplace::Push => {
241 let state_id = HistoryStateId::new();
242 self.state_id.set(Some(state_id));
243 let msg = ScriptToConstellationMessage::PushHistoryState(state_id, new_url.clone());
244 let _ = self
245 .window
246 .as_global_scope()
247 .script_to_constellation_chan()
248 .send(msg);
249 state_id
250 },
251 PushOrReplace::Replace => {
252 let state_id = match self.state_id.get() {
253 Some(state_id) => state_id,
254 None => {
255 let state_id = HistoryStateId::new();
256 self.state_id.set(Some(state_id));
257 state_id
258 },
259 };
260 let msg =
261 ScriptToConstellationMessage::ReplaceHistoryState(state_id, new_url.clone());
262 let _ = self
263 .window
264 .as_global_scope()
265 .script_to_constellation_chan()
266 .send(msg);
267 state_id
268 },
269 };
270
271 let _ = self.window.as_global_scope().resource_threads().send(
272 CoreResourceMsg::SetHistoryState(state_id, serialized_data.serialized.clone()),
273 );
274
275 document.set_url(new_url);
280
281 rooted!(&in(cx) let mut state = UndefinedValue());
283 if structuredclone::read(
284 cx,
285 self.window.as_global_scope(),
286 serialized_data,
287 state.handle_mut(),
288 )
289 .is_err()
290 {
291 warn!("Error reading structuredclone data");
292 }
293
294 self.state.set(state.get());
296
297 Ok(())
301 }
302
303 fn can_have_url_rewritten(document_url: &ServoUrl, target_url: &ServoUrl) -> bool {
306 if target_url.scheme() != document_url.scheme() ||
309 target_url.username() != document_url.username() ||
310 target_url.password() != document_url.password() ||
311 target_url.host() != document_url.host() ||
312 target_url.port() != document_url.port()
313 {
314 return false;
315 }
316
317 if target_url.scheme() == "http" || target_url.scheme() == "https" {
319 return true;
320 }
321
322 if target_url.scheme() == "file" {
324 return target_url.path() == document_url.path();
327 }
328
329 if target_url.path() != document_url.path() || target_url.query() != document_url.query() {
332 return false;
333 }
334
335 true
337 }
338}
339
340impl HistoryMethods<crate::DomTypeHolder> for History {
341 fn GetState(&self, _cx: &mut JSContext, mut retval: MutableHandleValue) -> Fallible<()> {
343 if !self.window.Document().is_fully_active() {
344 return Err(Error::Security(None));
345 }
346 retval.set(self.state.get());
347 Ok(())
348 }
349
350 fn GetLength(&self) -> Fallible<u32> {
352 if !self.window.Document().is_fully_active() {
353 return Err(Error::Security(None));
354 }
355
356 let Some((sender, recv)) =
357 generic_channel::channel(self.global().time_profiler_chan().clone())
358 else {
359 return Err(Error::InvalidState(None));
360 };
361
362 let msg = ScriptToConstellationMessage::JointSessionHistoryLength(sender);
363
364 self.window
365 .as_global_scope()
366 .script_to_constellation_chan()
367 .send(msg)
368 .map_err(|_| Error::InvalidState(None))?;
369
370 recv.recv().map_err(|_| Error::InvalidState(None))
371 }
372
373 fn Go(&self, cx: &mut JSContext, delta: i32) -> ErrorResult {
375 let direction = match delta.cmp(&0) {
376 Ordering::Greater => TraversalDirection::Forward(delta as usize),
377 Ordering::Less => TraversalDirection::Back(-delta as usize),
378 Ordering::Equal => return self.window.Location(cx).Reload(cx),
379 };
380
381 self.traverse_history(direction)
382 }
383
384 fn Back(&self) -> ErrorResult {
386 self.traverse_history(TraversalDirection::Back(1))
387 }
388
389 fn Forward(&self) -> ErrorResult {
391 self.traverse_history(TraversalDirection::Forward(1))
392 }
393
394 fn PushState(
396 &self,
397 cx: &mut JSContext,
398 data: HandleValue,
399 title: DOMString,
400 url: Option<USVString>,
401 ) -> ErrorResult {
402 self.push_or_replace_state(cx, data, title, url, PushOrReplace::Push)
403 }
404
405 fn ReplaceState(
407 &self,
408 cx: &mut JSContext,
409 data: HandleValue,
410 title: DOMString,
411 url: Option<USVString>,
412 ) -> ErrorResult {
413 self.push_or_replace_state(cx, data, title, url, PushOrReplace::Replace)
414 }
415}