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