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