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 servo_base::generic_channel::GenericSend;
16use servo_base::id::HistoryStateId;
17use servo_constellation_traits::{
18 ScriptToConstellationMessage, StructuredSerializedData, TraversalDirection,
19};
20use servo_url::ServoUrl;
21
22use crate::dom::bindings::codegen::Bindings::HistoryBinding::HistoryMethods;
23use crate::dom::bindings::codegen::Bindings::LocationBinding::Location_Binding::LocationMethods;
24use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
25use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
26use crate::dom::bindings::inheritance::Castable;
27use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object};
28use crate::dom::bindings::root::{AsHandleValue, Dom, DomRoot};
29use crate::dom::bindings::str::{DOMString, USVString};
30use crate::dom::bindings::structuredclone;
31use crate::dom::event::Event;
32use crate::dom::eventtarget::EventTarget;
33use crate::dom::globalscope::GlobalScope;
34use crate::dom::hashchangeevent::HashChangeEvent;
35use crate::dom::popstateevent::PopStateEvent;
36use crate::dom::window::Window;
37use crate::script_runtime::CanGc;
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(window: &Window, can_gc: CanGc) -> DomRoot<History> {
66 let dom_root = reflect_dom_object(Box::new(History::new_inherited(window)), window, can_gc);
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(None));
76 }
77 let msg = ScriptToConstellationMessage::TraverseHistory(direction);
78 let _ = self
79 .window
80 .as_global_scope()
81 .script_to_constellation_chan()
82 .send(msg);
83 Ok(())
84 }
85
86 pub(crate) fn activate_state(
89 &self,
90 cx: &mut JSContext,
91 state_id: Option<HistoryStateId>,
92 url: ServoUrl,
93 ) {
94 let document = self.window.Document();
96 let old_url = document.url();
97 document.set_url(url.clone());
98
99 let hash_changed = old_url.fragment() != url.fragment();
101
102 if let Some(fragment) = url.fragment() {
104 document.scroll_to_the_fragment(cx, fragment);
105 }
106
107 let state_changed = state_id != self.state_id.get();
109 self.state_id.set(state_id);
110 let serialized_data = match state_id {
111 Some(state_id) => {
112 let (tx, rx) =
113 generic_channel::channel(self.global().time_profiler_chan().clone()).unwrap();
114 let _ = self
115 .window
116 .as_global_scope()
117 .resource_threads()
118 .send(CoreResourceMsg::GetHistoryState(state_id, tx));
119 rx.recv().unwrap()
120 },
121 None => None,
122 };
123
124 match serialized_data {
125 Some(data) => {
126 let data = StructuredSerializedData {
127 serialized: data,
128 ..Default::default()
129 };
130 rooted!(in(*GlobalScope::get_cx()) let mut state = UndefinedValue());
131 if structuredclone::read(
132 self.window.as_global_scope(),
133 data,
134 state.handle_mut(),
135 CanGc::from_cx(cx),
136 )
137 .is_err()
138 {
139 warn!("Error reading structuredclone data");
140 }
141 self.state.set(state.get());
142 },
143 None => {
144 self.state.set(NullValue());
145 },
146 }
147
148 if state_changed {
151 PopStateEvent::dispatch_jsval(
152 self.window.upcast::<EventTarget>(),
153 &self.window,
154 self.state.as_handle_value(),
155 CanGc::from_cx(cx),
156 );
157 }
158
159 if hash_changed {
161 let event = HashChangeEvent::new(
162 &self.window,
163 atom!("hashchange"),
164 false,
165 false,
166 old_url.into_string(),
167 url.into_string(),
168 CanGc::from_cx(cx),
169 );
170 event
171 .upcast::<Event>()
172 .fire(self.window.upcast::<EventTarget>(), CanGc::from_cx(cx));
173 }
174 }
175
176 pub(crate) fn remove_states(&self, states: Vec<HistoryStateId>) {
177 let _ = self
178 .window
179 .as_global_scope()
180 .resource_threads()
181 .send(CoreResourceMsg::RemoveHistoryStates(states));
182 }
183
184 fn push_or_replace_state(
187 &self,
188 cx: &mut JSContext,
189 data: HandleValue,
190 _title: DOMString,
191 url: Option<USVString>,
192 push_or_replace: PushOrReplace,
193 ) -> ErrorResult {
194 let document = self.window.Document();
196
197 if !document.is_fully_active() {
199 return Err(Error::Security(None));
200 }
201
202 let serialized_data = structuredclone::write(cx.into(), data, None)?;
207
208 let new_url: ServoUrl = match url {
210 Some(urlstring) => {
212 let document_url = document.url();
213
214 let Ok(url) = ServoUrl::parse_with_base(Some(&document_url), &urlstring.0) else {
217 return Err(Error::Security(None));
219 };
220
221 if !Self::can_have_url_rewritten(&document_url, &url) {
224 return Err(Error::Security(None));
225 }
226
227 url
228 },
229 None => document.url(),
230 };
231
232 let state_id = match push_or_replace {
234 PushOrReplace::Push => {
235 let state_id = HistoryStateId::new();
236 self.state_id.set(Some(state_id));
237 let msg = ScriptToConstellationMessage::PushHistoryState(state_id, new_url.clone());
238 let _ = self
239 .window
240 .as_global_scope()
241 .script_to_constellation_chan()
242 .send(msg);
243 state_id
244 },
245 PushOrReplace::Replace => {
246 let state_id = match self.state_id.get() {
247 Some(state_id) => state_id,
248 None => {
249 let state_id = HistoryStateId::new();
250 self.state_id.set(Some(state_id));
251 state_id
252 },
253 };
254 let msg =
255 ScriptToConstellationMessage::ReplaceHistoryState(state_id, new_url.clone());
256 let _ = self
257 .window
258 .as_global_scope()
259 .script_to_constellation_chan()
260 .send(msg);
261 state_id
262 },
263 };
264
265 let _ = self.window.as_global_scope().resource_threads().send(
266 CoreResourceMsg::SetHistoryState(state_id, serialized_data.serialized.clone()),
267 );
268
269 document.set_url(new_url);
274
275 rooted!(&in(cx) let mut state = UndefinedValue());
277 if structuredclone::read(
278 self.window.as_global_scope(),
279 serialized_data,
280 state.handle_mut(),
281 CanGc::from_cx(cx),
282 )
283 .is_err()
284 {
285 warn!("Error reading structuredclone data");
286 }
287
288 self.state.set(state.get());
290
291 Ok(())
295 }
296
297 fn can_have_url_rewritten(document_url: &ServoUrl, target_url: &ServoUrl) -> bool {
300 if target_url.scheme() != document_url.scheme() ||
303 target_url.username() != document_url.username() ||
304 target_url.password() != document_url.password() ||
305 target_url.host() != document_url.host() ||
306 target_url.port() != document_url.port()
307 {
308 return false;
309 }
310
311 if target_url.scheme() == "http" || target_url.scheme() == "https" {
313 return true;
314 }
315
316 if target_url.scheme() == "file" {
318 return target_url.path() == document_url.path();
321 }
322
323 if target_url.path() != document_url.path() || target_url.query() != document_url.query() {
326 return false;
327 }
328
329 true
331 }
332}
333
334impl HistoryMethods<crate::DomTypeHolder> for History {
335 fn GetState(&self, _cx: &mut JSContext, mut retval: MutableHandleValue) -> Fallible<()> {
337 if !self.window.Document().is_fully_active() {
338 return Err(Error::Security(None));
339 }
340 retval.set(self.state.get());
341 Ok(())
342 }
343
344 fn GetLength(&self) -> Fallible<u32> {
346 if !self.window.Document().is_fully_active() {
347 return Err(Error::Security(None));
348 }
349
350 let Some((sender, recv)) =
351 generic_channel::channel(self.global().time_profiler_chan().clone())
352 else {
353 return Err(Error::InvalidState(None));
354 };
355
356 let msg = ScriptToConstellationMessage::JointSessionHistoryLength(sender);
357
358 self.window
359 .as_global_scope()
360 .script_to_constellation_chan()
361 .send(msg)
362 .map_err(|_| Error::InvalidState(None))?;
363
364 recv.recv().map_err(|_| Error::InvalidState(None))
365 }
366
367 fn Go(&self, cx: &mut JSContext, delta: i32) -> ErrorResult {
369 let direction = match delta.cmp(&0) {
370 Ordering::Greater => TraversalDirection::Forward(delta as usize),
371 Ordering::Less => TraversalDirection::Back(-delta as usize),
372 Ordering::Equal => return self.window.Location(cx).Reload(cx),
373 };
374
375 self.traverse_history(direction)
376 }
377
378 fn Back(&self) -> ErrorResult {
380 self.traverse_history(TraversalDirection::Back(1))
381 }
382
383 fn Forward(&self) -> ErrorResult {
385 self.traverse_history(TraversalDirection::Forward(1))
386 }
387
388 fn PushState(
390 &self,
391 cx: &mut JSContext,
392 data: HandleValue,
393 title: DOMString,
394 url: Option<USVString>,
395 ) -> ErrorResult {
396 self.push_or_replace_state(cx, data, title, url, PushOrReplace::Push)
397 }
398
399 fn ReplaceState(
401 &self,
402 cx: &mut JSContext,
403 data: HandleValue,
404 title: DOMString,
405 url: Option<USVString>,
406 ) -> ErrorResult {
407 self.push_or_replace_state(cx, data, title, url, PushOrReplace::Replace)
408 }
409}