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::context::JSContext;
15use js::jsapi::Heap;
16use js::jsval::{JSVal, NullValue, UndefinedValue};
17use js::rust::{HandleValue, MutableHandleValue};
18use net_traits::CoreResourceMsg;
19use profile_traits::{generic_channel, ipc};
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/// <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(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    /// <https://html.spec.whatwg.org/multipage/#history-traversal>
87    /// Steps 5-16
88    pub(crate) fn activate_state(
89        &self,
90        state_id: Option<HistoryStateId>,
91        url: ServoUrl,
92        can_gc: CanGc,
93    ) {
94        // Steps 5
95        let document = self.window.Document();
96        let old_url = document.url();
97        document.set_url(url.clone());
98
99        // Step 6
100        let hash_changed = old_url.fragment() != url.fragment();
101
102        // Step 8
103        if let Some(fragment) = url.fragment() {
104            document.scroll_to_the_fragment(fragment);
105        }
106
107        // Step 11
108        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) = ipc::channel(self.global().time_profiler_chan().clone()).unwrap();
113                let _ = self
114                    .window
115                    .as_global_scope()
116                    .resource_threads()
117                    .send(CoreResourceMsg::GetHistoryState(state_id, tx));
118                rx.recv().unwrap()
119            },
120            None => None,
121        };
122
123        match serialized_data {
124            Some(data) => {
125                let data = StructuredSerializedData {
126                    serialized: data,
127                    ..Default::default()
128                };
129                rooted!(in(*GlobalScope::get_cx()) let mut state = UndefinedValue());
130                if structuredclone::read(
131                    self.window.as_global_scope(),
132                    data,
133                    state.handle_mut(),
134                    can_gc,
135                )
136                .is_err()
137                {
138                    warn!("Error reading structuredclone data");
139                }
140                self.state.set(state.get());
141            },
142            None => {
143                self.state.set(NullValue());
144            },
145        }
146
147        // TODO: Queue events on DOM Manipulation task source if non-blocking flag is set.
148        // Step 16.1
149        if state_changed {
150            PopStateEvent::dispatch_jsval(
151                self.window.upcast::<EventTarget>(),
152                &self.window,
153                self.state.as_handle_value(),
154                can_gc,
155            );
156        }
157
158        // Step 16.3
159        if hash_changed {
160            let event = HashChangeEvent::new(
161                &self.window,
162                atom!("hashchange"),
163                false,
164                false,
165                old_url.into_string(),
166                url.into_string(),
167                can_gc,
168            );
169            event
170                .upcast::<Event>()
171                .fire(self.window.upcast::<EventTarget>(), can_gc);
172        }
173    }
174
175    pub(crate) fn remove_states(&self, states: Vec<HistoryStateId>) {
176        let _ = self
177            .window
178            .as_global_scope()
179            .resource_threads()
180            .send(CoreResourceMsg::RemoveHistoryStates(states));
181    }
182
183    /// <https://html.spec.whatwg.org/multipage/#dom-history-pushstate>
184    /// <https://html.spec.whatwg.org/multipage/#dom-history-replacestate>
185    fn push_or_replace_state(
186        &self,
187        cx: &mut JSContext,
188        data: HandleValue,
189        _title: DOMString,
190        url: Option<USVString>,
191        push_or_replace: PushOrReplace,
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.into(), 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            CanGc::from_cx(cx),
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: &mut 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, cx: &mut JSContext, delta: i32) -> 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(CanGc::from_cx(cx)),
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: &mut JSContext,
391        data: HandleValue,
392        title: DOMString,
393        url: Option<USVString>,
394    ) -> ErrorResult {
395        self.push_or_replace_state(cx, data, title, url, PushOrReplace::Push)
396    }
397
398    /// <https://html.spec.whatwg.org/multipage/#dom-history-replacestate>
399    fn ReplaceState(
400        &self,
401        cx: &mut JSContext,
402        data: HandleValue,
403        title: DOMString,
404        url: Option<USVString>,
405    ) -> ErrorResult {
406        self.push_or_replace_state(cx, data, title, url, PushOrReplace::Replace)
407    }
408}