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 servo_base::generic_channel::GenericSend;
16use servo_base::id::HistoryStateId;
17use servo_constellation_traits::{
18    ScriptToConstellationMessage, StructuredSerializedData, TraversalDirection,
19};
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;
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(None));
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        cx: &mut JSContext,
91        state_id: Option<HistoryStateId>,
92        url: ServoUrl,
93    ) {
94        // Steps 5
95        let document = self.window.Document();
96        let old_url = document.url();
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.scroll_to_the_fragment(cx, 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) =
113                    generic_channel::channel(self.global().time_profiler_chan().clone()).unwrap();
114                let _ = self
115                    .window
116                    .as_global_scope()
117                    .resource_threads()
118                    .send(CoreResourceMsg::GetHistoryState(state_id, tx));
119                rx.recv().unwrap()
120            },
121            None => None,
122        };
123
124        match serialized_data {
125            Some(data) => {
126                let data = StructuredSerializedData {
127                    serialized: data,
128                    ..Default::default()
129                };
130                rooted!(in(*GlobalScope::get_cx()) let mut state = UndefinedValue());
131                if structuredclone::read(
132                    self.window.as_global_scope(),
133                    data,
134                    state.handle_mut(),
135                    CanGc::from_cx(cx),
136                )
137                .is_err()
138                {
139                    warn!("Error reading structuredclone data");
140                }
141                self.state.set(state.get());
142            },
143            None => {
144                self.state.set(NullValue());
145            },
146        }
147
148        // TODO: Queue events on DOM Manipulation task source if non-blocking flag is set.
149        // Step 16.1
150        if state_changed {
151            PopStateEvent::dispatch_jsval(
152                self.window.upcast::<EventTarget>(),
153                &self.window,
154                self.state.as_handle_value(),
155                CanGc::from_cx(cx),
156            );
157        }
158
159        // Step 16.3
160        if hash_changed {
161            let event = HashChangeEvent::new(
162                &self.window,
163                atom!("hashchange"),
164                false,
165                false,
166                old_url.into_string(),
167                url.into_string(),
168                CanGc::from_cx(cx),
169            );
170            event
171                .upcast::<Event>()
172                .fire(self.window.upcast::<EventTarget>(), CanGc::from_cx(cx));
173        }
174    }
175
176    pub(crate) fn remove_states(&self, states: Vec<HistoryStateId>) {
177        let _ = self
178            .window
179            .as_global_scope()
180            .resource_threads()
181            .send(CoreResourceMsg::RemoveHistoryStates(states));
182    }
183
184    /// <https://html.spec.whatwg.org/multipage/#dom-history-pushstate>
185    /// <https://html.spec.whatwg.org/multipage/#dom-history-replacestate>
186    fn push_or_replace_state(
187        &self,
188        cx: &mut JSContext,
189        data: HandleValue,
190        _title: DOMString,
191        url: Option<USVString>,
192        push_or_replace: PushOrReplace,
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(None));
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.into(), 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(None));
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(None));
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            CanGc::from_cx(cx),
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: &mut JSContext, mut retval: MutableHandleValue) -> Fallible<()> {
337        if !self.window.Document().is_fully_active() {
338            return Err(Error::Security(None));
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(None));
348        }
349
350        let Some((sender, recv)) =
351            generic_channel::channel(self.global().time_profiler_chan().clone())
352        else {
353            return Err(Error::InvalidState(None));
354        };
355
356        let msg = ScriptToConstellationMessage::JointSessionHistoryLength(sender);
357
358        self.window
359            .as_global_scope()
360            .script_to_constellation_chan()
361            .send(msg)
362            .map_err(|_| Error::InvalidState(None))?;
363
364        recv.recv().map_err(|_| Error::InvalidState(None))
365    }
366
367    /// <https://html.spec.whatwg.org/multipage/#dom-history-go>
368    fn Go(&self, cx: &mut JSContext, delta: i32) -> ErrorResult {
369        let direction = match delta.cmp(&0) {
370            Ordering::Greater => TraversalDirection::Forward(delta as usize),
371            Ordering::Less => TraversalDirection::Back(-delta as usize),
372            Ordering::Equal => return self.window.Location(cx).Reload(cx),
373        };
374
375        self.traverse_history(direction)
376    }
377
378    /// <https://html.spec.whatwg.org/multipage/#dom-history-back>
379    fn Back(&self) -> ErrorResult {
380        self.traverse_history(TraversalDirection::Back(1))
381    }
382
383    /// <https://html.spec.whatwg.org/multipage/#dom-history-forward>
384    fn Forward(&self) -> ErrorResult {
385        self.traverse_history(TraversalDirection::Forward(1))
386    }
387
388    /// <https://html.spec.whatwg.org/multipage/#dom-history-pushstate>
389    fn PushState(
390        &self,
391        cx: &mut JSContext,
392        data: HandleValue,
393        title: DOMString,
394        url: Option<USVString>,
395    ) -> ErrorResult {
396        self.push_or_replace_state(cx, data, title, url, PushOrReplace::Push)
397    }
398
399    /// <https://html.spec.whatwg.org/multipage/#dom-history-replacestate>
400    fn ReplaceState(
401        &self,
402        cx: &mut JSContext,
403        data: HandleValue,
404        title: DOMString,
405        url: Option<USVString>,
406    ) -> ErrorResult {
407        self.push_or_replace_state(cx, data, title, url, PushOrReplace::Replace)
408    }
409}