Skip to main content

script/dom/window/
history.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use 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/// <https://html.spec.whatwg.org/multipage/#the-history-interface>
45#[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    /// <https://html.spec.whatwg.org/multipage/#history-traversal>
93    /// Steps 5-16
94    pub(crate) fn activate_state(
95        &self,
96        cx: &mut JSContext,
97        state_id: Option<HistoryStateId>,
98        url: ServoUrl,
99    ) {
100        // Steps 5
101        let document = self.window.Document();
102        let old_url = document.url();
103        document.set_url(url.clone());
104
105        // Step 6
106        let hash_changed = old_url.fragment() != url.fragment();
107
108        // Step 8
109        if let Some(fragment) = url.fragment() {
110            document.scroll_to_the_fragment(cx, fragment);
111        }
112
113        // Step 11
114        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        // TODO: Queue events on DOM Manipulation task source if non-blocking flag is set.
155        // Step 16.1
156        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        // Step 16.3
166        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    /// <https://html.spec.whatwg.org/multipage/#dom-history-pushstate>
191    /// <https://html.spec.whatwg.org/multipage/#dom-history-replacestate>
192    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        // Step 1
201        let document = self.window.Document();
202
203        // Step 2
204        if !document.is_fully_active() {
205            return Err(Error::Security(None));
206        }
207
208        // TODO: Step 3 Optionally abort these steps
209        // https://github.com/servo/servo/issues/19159
210
211        // Step 4. Let serializedData be StructuredSerializeForStorage(data). Rethrow any exceptions.
212        let serialized_data = structuredclone::write(cx, data, None)?;
213
214        // Step 5. Let newURL be document's URL.
215        let new_url: ServoUrl = match url {
216            // Step 6. If url is not null or the empty string, then:
217            Some(urlstring) => {
218                let document_url = document.url();
219
220                // Step 6.1 Set newURL to the result of encoding-parsing a URL given url,
221                // relative to the relevant settings object of history.
222                let Ok(url) = ServoUrl::parse_with_base(Some(&document_url), &urlstring.0) else {
223                    // Step 6.2 If newURL is failure, then throw a "SecurityError" DOMException.
224                    return Err(Error::Security(None));
225                };
226
227                // Step 6.3 If document cannot have its URL rewritten to newURL,
228                // then throw a "SecurityError" DOMException.
229                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        // Step 8
239        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        // TODO: Step 9 Update current entry to represent a GET request
276        // https://github.com/servo/servo/issues/19156
277
278        // Step 10
279        document.set_url(new_url);
280
281        // Step 11
282        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        // Step 12
295        self.state.set(state.get());
296
297        // TODO: Step 13 Update Document's latest entry to current entry
298        // https://github.com/servo/servo/issues/19158
299
300        Ok(())
301    }
302
303    /// <https://html.spec.whatwg.org/multipage/#can-have-its-url-rewritten>
304    /// Step 2-6
305    fn can_have_url_rewritten(document_url: &ServoUrl, target_url: &ServoUrl) -> bool {
306        // Step 2. If targetURL and documentURL differ in their scheme, username,
307        // password, host, or port components, then return false.
308        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        // Step 3. If targetURL's scheme is an HTTP(S) scheme, then return true.
318        if target_url.scheme() == "http" || target_url.scheme() == "https" {
319            return true;
320        }
321
322        // Step 4. If targetURL's scheme is "file", then:
323        if target_url.scheme() == "file" {
324            // Step 4.1 If targetURL and documentURL differ in their path component, then return false.
325            // Step 4.2 Return true.
326            return target_url.path() == document_url.path();
327        }
328
329        // Step 5. If targetURL and documentURL differ in their path component
330        // or query components, then return false.
331        if target_url.path() != document_url.path() || target_url.query() != document_url.query() {
332            return false;
333        }
334
335        // Step 6. Return true.
336        true
337    }
338}
339
340impl HistoryMethods<crate::DomTypeHolder> for History {
341    /// <https://html.spec.whatwg.org/multipage/#dom-history-state>
342    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    /// <https://html.spec.whatwg.org/multipage/#dom-history-length>
351    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    /// <https://html.spec.whatwg.org/multipage/#dom-history-go>
374    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    /// <https://html.spec.whatwg.org/multipage/#dom-history-back>
385    fn Back(&self) -> ErrorResult {
386        self.traverse_history(TraversalDirection::Back(1))
387    }
388
389    /// <https://html.spec.whatwg.org/multipage/#dom-history-forward>
390    fn Forward(&self) -> ErrorResult {
391        self.traverse_history(TraversalDirection::Forward(1))
392    }
393
394    /// <https://html.spec.whatwg.org/multipage/#dom-history-pushstate>
395    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    /// <https://html.spec.whatwg.org/multipage/#dom-history-replacestate>
406    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}