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