script/dom/
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 base::generic_channel::GenericSend;
9use base::id::HistoryStateId;
10use constellation_traits::{
11    ScriptToConstellationMessage, StructuredSerializedData, TraversalDirection,
12};
13use dom_struct::dom_struct;
14use js::jsapi::Heap;
15use js::jsval::{JSVal, NullValue, UndefinedValue};
16use js::rust::{HandleValue, MutableHandleValue};
17use net_traits::CoreResourceMsg;
18use profile_traits::{generic_channel, ipc};
19use servo_url::ServoUrl;
20
21use crate::dom::bindings::codegen::Bindings::HistoryBinding::HistoryMethods;
22use crate::dom::bindings::codegen::Bindings::LocationBinding::Location_Binding::LocationMethods;
23use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
24use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
25use crate::dom::bindings::inheritance::Castable;
26use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object};
27use crate::dom::bindings::root::{AsHandleValue, Dom, DomRoot};
28use crate::dom::bindings::str::{DOMString, USVString};
29use crate::dom::bindings::structuredclone;
30use crate::dom::event::Event;
31use crate::dom::eventtarget::EventTarget;
32use crate::dom::globalscope::GlobalScope;
33use crate::dom::hashchangeevent::HashChangeEvent;
34use crate::dom::popstateevent::PopStateEvent;
35use crate::dom::window::Window;
36use crate::script_runtime::{CanGc, JSContext};
37
38enum PushOrReplace {
39    Push,
40    Replace,
41}
42
43/// <https://html.spec.whatwg.org/multipage/#the-history-interface>
44#[dom_struct]
45pub(crate) struct History {
46    reflector_: Reflector,
47    window: Dom<Window>,
48    #[ignore_malloc_size_of = "mozjs"]
49    state: Heap<JSVal>,
50    #[no_trace]
51    state_id: Cell<Option<HistoryStateId>>,
52}
53
54impl History {
55    pub(crate) fn new_inherited(window: &Window) -> History {
56        History {
57            reflector_: Reflector::new(),
58            window: Dom::from_ref(window),
59            state: Heap::default(),
60            state_id: Cell::new(None),
61        }
62    }
63
64    pub(crate) fn new(window: &Window, can_gc: CanGc) -> DomRoot<History> {
65        let dom_root = reflect_dom_object(Box::new(History::new_inherited(window)), window, can_gc);
66        dom_root.state.set(NullValue());
67        dom_root
68    }
69}
70
71impl History {
72    fn traverse_history(&self, direction: TraversalDirection) -> ErrorResult {
73        if !self.window.Document().is_fully_active() {
74            return Err(Error::Security(None));
75        }
76        let msg = ScriptToConstellationMessage::TraverseHistory(direction);
77        let _ = self
78            .window
79            .as_global_scope()
80            .script_to_constellation_chan()
81            .send(msg);
82        Ok(())
83    }
84
85    /// <https://html.spec.whatwg.org/multipage/#history-traversal>
86    /// Steps 5-16
87    pub(crate) fn activate_state(
88        &self,
89        state_id: Option<HistoryStateId>,
90        url: ServoUrl,
91        can_gc: CanGc,
92    ) {
93        // Steps 5
94        let document = self.window.Document();
95        let old_url = document.url().clone();
96        document.set_url(url.clone());
97
98        // Step 6
99        let hash_changed = old_url.fragment() != url.fragment();
100
101        // Step 8
102        if let Some(fragment) = url.fragment() {
103            document.scroll_to_the_fragment(fragment);
104        }
105
106        // Step 11
107        let state_changed = state_id != self.state_id.get();
108        self.state_id.set(state_id);
109        let serialized_data = match state_id {
110            Some(state_id) => {
111                let (tx, rx) = ipc::channel(self.global().time_profiler_chan().clone()).unwrap();
112                let _ = self
113                    .window
114                    .as_global_scope()
115                    .resource_threads()
116                    .send(CoreResourceMsg::GetHistoryState(state_id, tx));
117                rx.recv().unwrap()
118            },
119            None => None,
120        };
121
122        match serialized_data {
123            Some(data) => {
124                let data = StructuredSerializedData {
125                    serialized: data,
126                    ..Default::default()
127                };
128                rooted!(in(*GlobalScope::get_cx()) let mut state = UndefinedValue());
129                if structuredclone::read(
130                    self.window.as_global_scope(),
131                    data,
132                    state.handle_mut(),
133                    can_gc,
134                )
135                .is_err()
136                {
137                    warn!("Error reading structuredclone data");
138                }
139                self.state.set(state.get());
140            },
141            None => {
142                self.state.set(NullValue());
143            },
144        }
145
146        // TODO: Queue events on DOM Manipulation task source if non-blocking flag is set.
147        // Step 16.1
148        if state_changed {
149            PopStateEvent::dispatch_jsval(
150                self.window.upcast::<EventTarget>(),
151                &self.window,
152                self.state.as_handle_value(),
153                can_gc,
154            );
155        }
156
157        // Step 16.3
158        if hash_changed {
159            let event = HashChangeEvent::new(
160                &self.window,
161                atom!("hashchange"),
162                false,
163                false,
164                old_url.into_string(),
165                url.into_string(),
166                can_gc,
167            );
168            event
169                .upcast::<Event>()
170                .fire(self.window.upcast::<EventTarget>(), can_gc);
171        }
172    }
173
174    pub(crate) fn remove_states(&self, states: Vec<HistoryStateId>) {
175        let _ = self
176            .window
177            .as_global_scope()
178            .resource_threads()
179            .send(CoreResourceMsg::RemoveHistoryStates(states));
180    }
181
182    /// <https://html.spec.whatwg.org/multipage/#dom-history-pushstate>
183    /// <https://html.spec.whatwg.org/multipage/#dom-history-replacestate>
184    fn push_or_replace_state(
185        &self,
186        cx: JSContext,
187        data: HandleValue,
188        _title: DOMString,
189        url: Option<USVString>,
190        push_or_replace: PushOrReplace,
191        can_gc: CanGc,
192    ) -> ErrorResult {
193        // Step 1
194        let document = self.window.Document();
195
196        // Step 2
197        if !document.is_fully_active() {
198            return Err(Error::Security(None));
199        }
200
201        // TODO: Step 3 Optionally abort these steps
202        // https://github.com/servo/servo/issues/19159
203
204        // Step 4. Let serializedData be StructuredSerializeForStorage(data). Rethrow any exceptions.
205        let serialized_data = structuredclone::write(cx, data, None)?;
206
207        // Step 5. Let newURL be document's URL.
208        let new_url: ServoUrl = match url {
209            // Step 6. If url is not null or the empty string, then:
210            Some(urlstring) => {
211                let document_url = document.url();
212
213                // Step 6.1 Set newURL to the result of encoding-parsing a URL given url,
214                // relative to the relevant settings object of history.
215                let Ok(url) = ServoUrl::parse_with_base(Some(&document_url), &urlstring.0) else {
216                    // Step 6.2 If newURL is failure, then throw a "SecurityError" DOMException.
217                    return Err(Error::Security(None));
218                };
219
220                // Step 6.3 If document cannot have its URL rewritten to newURL,
221                // then throw a "SecurityError" DOMException.
222                if !Self::can_have_url_rewritten(&document_url, &url) {
223                    return Err(Error::Security(None));
224                }
225
226                url
227            },
228            None => document.url(),
229        };
230
231        // Step 8
232        let state_id = match push_or_replace {
233            PushOrReplace::Push => {
234                let state_id = HistoryStateId::new();
235                self.state_id.set(Some(state_id));
236                let msg = ScriptToConstellationMessage::PushHistoryState(state_id, new_url.clone());
237                let _ = self
238                    .window
239                    .as_global_scope()
240                    .script_to_constellation_chan()
241                    .send(msg);
242                state_id
243            },
244            PushOrReplace::Replace => {
245                let state_id = match self.state_id.get() {
246                    Some(state_id) => state_id,
247                    None => {
248                        let state_id = HistoryStateId::new();
249                        self.state_id.set(Some(state_id));
250                        state_id
251                    },
252                };
253                let msg =
254                    ScriptToConstellationMessage::ReplaceHistoryState(state_id, new_url.clone());
255                let _ = self
256                    .window
257                    .as_global_scope()
258                    .script_to_constellation_chan()
259                    .send(msg);
260                state_id
261            },
262        };
263
264        let _ = self.window.as_global_scope().resource_threads().send(
265            CoreResourceMsg::SetHistoryState(state_id, serialized_data.serialized.clone()),
266        );
267
268        // TODO: Step 9 Update current entry to represent a GET request
269        // https://github.com/servo/servo/issues/19156
270
271        // Step 10
272        document.set_url(new_url);
273
274        // Step 11
275        rooted!(in(*cx) let mut state = UndefinedValue());
276        if structuredclone::read(
277            self.window.as_global_scope(),
278            serialized_data,
279            state.handle_mut(),
280            can_gc,
281        )
282        .is_err()
283        {
284            warn!("Error reading structuredclone data");
285        }
286
287        // Step 12
288        self.state.set(state.get());
289
290        // TODO: Step 13 Update Document's latest entry to current entry
291        // https://github.com/servo/servo/issues/19158
292
293        Ok(())
294    }
295
296    /// <https://html.spec.whatwg.org/multipage/#can-have-its-url-rewritten>
297    /// Step 2-6
298    fn can_have_url_rewritten(document_url: &ServoUrl, target_url: &ServoUrl) -> bool {
299        // Step 2. If targetURL and documentURL differ in their scheme, username,
300        // password, host, or port components, then return false.
301        if target_url.scheme() != document_url.scheme() ||
302            target_url.username() != document_url.username() ||
303            target_url.password() != document_url.password() ||
304            target_url.host() != document_url.host() ||
305            target_url.port() != document_url.port()
306        {
307            return false;
308        }
309
310        // Step 3. If targetURL's scheme is an HTTP(S) scheme, then return true.
311        if target_url.scheme() == "http" || target_url.scheme() == "https" {
312            return true;
313        }
314
315        // Step 4. If targetURL's scheme is "file", then:
316        if target_url.scheme() == "file" {
317            // Step 4.1 If targetURL and documentURL differ in their path component, then return false.
318            // Step 4.2 Return true.
319            return target_url.path() == document_url.path();
320        }
321
322        // Step 5. If targetURL and documentURL differ in their path component
323        // or query components, then return false.
324        if target_url.path() != document_url.path() || target_url.query() != document_url.query() {
325            return false;
326        }
327
328        // Step 6. Return true.
329        true
330    }
331}
332
333impl HistoryMethods<crate::DomTypeHolder> for History {
334    /// <https://html.spec.whatwg.org/multipage/#dom-history-state>
335    fn GetState(&self, _cx: JSContext, mut retval: MutableHandleValue) -> Fallible<()> {
336        if !self.window.Document().is_fully_active() {
337            return Err(Error::Security(None));
338        }
339        retval.set(self.state.get());
340        Ok(())
341    }
342
343    /// <https://html.spec.whatwg.org/multipage/#dom-history-length>
344    fn GetLength(&self) -> Fallible<u32> {
345        if !self.window.Document().is_fully_active() {
346            return Err(Error::Security(None));
347        }
348
349        let Some((sender, recv)) =
350            generic_channel::channel(self.global().time_profiler_chan().clone())
351        else {
352            return Err(Error::InvalidState(None));
353        };
354
355        let msg = ScriptToConstellationMessage::JointSessionHistoryLength(sender);
356
357        self.window
358            .as_global_scope()
359            .script_to_constellation_chan()
360            .send(msg)
361            .map_err(|_| Error::InvalidState(None))?;
362
363        recv.recv().map_err(|_| Error::InvalidState(None))
364    }
365
366    /// <https://html.spec.whatwg.org/multipage/#dom-history-go>
367    fn Go(&self, delta: i32, can_gc: CanGc) -> ErrorResult {
368        let direction = match delta.cmp(&0) {
369            Ordering::Greater => TraversalDirection::Forward(delta as usize),
370            Ordering::Less => TraversalDirection::Back(-delta as usize),
371            Ordering::Equal => return self.window.Location().Reload(can_gc),
372        };
373
374        self.traverse_history(direction)
375    }
376
377    /// <https://html.spec.whatwg.org/multipage/#dom-history-back>
378    fn Back(&self) -> ErrorResult {
379        self.traverse_history(TraversalDirection::Back(1))
380    }
381
382    /// <https://html.spec.whatwg.org/multipage/#dom-history-forward>
383    fn Forward(&self) -> ErrorResult {
384        self.traverse_history(TraversalDirection::Forward(1))
385    }
386
387    /// <https://html.spec.whatwg.org/multipage/#dom-history-pushstate>
388    fn PushState(
389        &self,
390        cx: JSContext,
391        data: HandleValue,
392        title: DOMString,
393        url: Option<USVString>,
394        can_gc: CanGc,
395    ) -> ErrorResult {
396        self.push_or_replace_state(cx, data, title, url, PushOrReplace::Push, can_gc)
397    }
398
399    /// <https://html.spec.whatwg.org/multipage/#dom-history-replacestate>
400    fn ReplaceState(
401        &self,
402        cx: JSContext,
403        data: HandleValue,
404        title: DOMString,
405        url: Option<USVString>,
406        can_gc: CanGc,
407    ) -> ErrorResult {
408        self.push_or_replace_state(cx, data, title, url, PushOrReplace::Replace, can_gc)
409    }
410}