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::IpcSend;
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::ipc;
19use profile_traits::ipc::channel;
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, JSContext};
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);
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().clone();
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.check_and_scroll_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: JSContext,
188        data: HandleValue,
189        _title: DOMString,
190        url: Option<USVString>,
191        push_or_replace: PushOrReplace,
192        can_gc: CanGc,
193    ) -> ErrorResult {
194        // Step 1
195        let document = self.window.Document();
196
197        // Step 2
198        if !document.is_fully_active() {
199            return Err(Error::Security);
200        }
201
202        // TODO: Step 3 Optionally abort these steps
203        // https://github.com/servo/servo/issues/19159
204
205        // Step 4. Let serializedData be StructuredSerializeForStorage(data). Rethrow any exceptions.
206        let serialized_data = structuredclone::write(cx, data, None)?;
207
208        // Step 5. Let newURL be document's URL.
209        let new_url: ServoUrl = match url {
210            // Step 6. If url is not null or the empty string, then:
211            Some(urlstring) => {
212                let document_url = document.url();
213
214                // Step 6.1 Set newURL to the result of encoding-parsing a URL given url,
215                // relative to the relevant settings object of history.
216                let Ok(url) = ServoUrl::parse_with_base(Some(&document_url), &urlstring.0) else {
217                    // Step 6.2 If newURL is failure, then throw a "SecurityError" DOMException.
218                    return Err(Error::Security);
219                };
220
221                // Step 6.3 If document cannot have its URL rewritten to newURL,
222                // then throw a "SecurityError" DOMException.
223                if !Self::can_have_url_rewritten(&document_url, &url) {
224                    return Err(Error::Security);
225                }
226
227                url
228            },
229            None => document.url(),
230        };
231
232        // Step 8
233        let state_id = match push_or_replace {
234            PushOrReplace::Push => {
235                let state_id = HistoryStateId::new();
236                self.state_id.set(Some(state_id));
237                let msg = ScriptToConstellationMessage::PushHistoryState(state_id, new_url.clone());
238                let _ = self
239                    .window
240                    .as_global_scope()
241                    .script_to_constellation_chan()
242                    .send(msg);
243                state_id
244            },
245            PushOrReplace::Replace => {
246                let state_id = match self.state_id.get() {
247                    Some(state_id) => state_id,
248                    None => {
249                        let state_id = HistoryStateId::new();
250                        self.state_id.set(Some(state_id));
251                        state_id
252                    },
253                };
254                let msg =
255                    ScriptToConstellationMessage::ReplaceHistoryState(state_id, new_url.clone());
256                let _ = self
257                    .window
258                    .as_global_scope()
259                    .script_to_constellation_chan()
260                    .send(msg);
261                state_id
262            },
263        };
264
265        let _ = self.window.as_global_scope().resource_threads().send(
266            CoreResourceMsg::SetHistoryState(state_id, serialized_data.serialized.clone()),
267        );
268
269        // TODO: Step 9 Update current entry to represent a GET request
270        // https://github.com/servo/servo/issues/19156
271
272        // Step 10
273        document.set_url(new_url);
274
275        // Step 11
276        rooted!(in(*cx) let mut state = UndefinedValue());
277        if structuredclone::read(
278            self.window.as_global_scope(),
279            serialized_data,
280            state.handle_mut(),
281            can_gc,
282        )
283        .is_err()
284        {
285            warn!("Error reading structuredclone data");
286        }
287
288        // Step 12
289        self.state.set(state.get());
290
291        // TODO: Step 13 Update Document's latest entry to current entry
292        // https://github.com/servo/servo/issues/19158
293
294        Ok(())
295    }
296
297    /// <https://html.spec.whatwg.org/multipage/#can-have-its-url-rewritten>
298    /// Step 2-6
299    fn can_have_url_rewritten(document_url: &ServoUrl, target_url: &ServoUrl) -> bool {
300        // Step 2. If targetURL and documentURL differ in their scheme, username,
301        // password, host, or port components, then return false.
302        if target_url.scheme() != document_url.scheme() ||
303            target_url.username() != document_url.username() ||
304            target_url.password() != document_url.password() ||
305            target_url.host() != document_url.host() ||
306            target_url.port() != document_url.port()
307        {
308            return false;
309        }
310
311        // Step 3. If targetURL's scheme is an HTTP(S) scheme, then return true.
312        if target_url.scheme() == "http" || target_url.scheme() == "https" {
313            return true;
314        }
315
316        // Step 4. If targetURL's scheme is "file", then:
317        if target_url.scheme() == "file" {
318            // Step 4.1 If targetURL and documentURL differ in their path component, then return false.
319            // Step 4.2 Return true.
320            return target_url.path() == document_url.path();
321        }
322
323        // Step 5. If targetURL and documentURL differ in their path component
324        // or query components, then return false.
325        if target_url.path() != document_url.path() || target_url.query() != document_url.query() {
326            return false;
327        }
328
329        // Step 6. Return true.
330        true
331    }
332}
333
334impl HistoryMethods<crate::DomTypeHolder> for History {
335    /// <https://html.spec.whatwg.org/multipage/#dom-history-state>
336    fn GetState(&self, _cx: JSContext, mut retval: MutableHandleValue) -> Fallible<()> {
337        if !self.window.Document().is_fully_active() {
338            return Err(Error::Security);
339        }
340        retval.set(self.state.get());
341        Ok(())
342    }
343
344    /// <https://html.spec.whatwg.org/multipage/#dom-history-length>
345    fn GetLength(&self) -> Fallible<u32> {
346        if !self.window.Document().is_fully_active() {
347            return Err(Error::Security);
348        }
349        let (sender, recv) = channel(self.global().time_profiler_chan().clone())
350            .expect("Failed to create channel to send jsh length.");
351        let msg = ScriptToConstellationMessage::JointSessionHistoryLength(sender);
352        let _ = self
353            .window
354            .as_global_scope()
355            .script_to_constellation_chan()
356            .send(msg);
357        Ok(recv.recv().unwrap())
358    }
359
360    /// <https://html.spec.whatwg.org/multipage/#dom-history-go>
361    fn Go(&self, delta: i32, can_gc: CanGc) -> ErrorResult {
362        let direction = match delta.cmp(&0) {
363            Ordering::Greater => TraversalDirection::Forward(delta as usize),
364            Ordering::Less => TraversalDirection::Back(-delta as usize),
365            Ordering::Equal => return self.window.Location().Reload(can_gc),
366        };
367
368        self.traverse_history(direction)
369    }
370
371    /// <https://html.spec.whatwg.org/multipage/#dom-history-back>
372    fn Back(&self) -> ErrorResult {
373        self.traverse_history(TraversalDirection::Back(1))
374    }
375
376    /// <https://html.spec.whatwg.org/multipage/#dom-history-forward>
377    fn Forward(&self) -> ErrorResult {
378        self.traverse_history(TraversalDirection::Forward(1))
379    }
380
381    /// <https://html.spec.whatwg.org/multipage/#dom-history-pushstate>
382    fn PushState(
383        &self,
384        cx: JSContext,
385        data: HandleValue,
386        title: DOMString,
387        url: Option<USVString>,
388        can_gc: CanGc,
389    ) -> ErrorResult {
390        self.push_or_replace_state(cx, data, title, url, PushOrReplace::Push, can_gc)
391    }
392
393    /// <https://html.spec.whatwg.org/multipage/#dom-history-replacestate>
394    fn ReplaceState(
395        &self,
396        cx: JSContext,
397        data: HandleValue,
398        title: DOMString,
399        url: Option<USVString>,
400        can_gc: CanGc,
401    ) -> ErrorResult {
402        self.push_or_replace_state(cx, data, title, url, PushOrReplace::Replace, can_gc)
403    }
404}