Skip to main content

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