/src/serenity/Userland/Libraries/LibWeb/HTML/History.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2021, Luke Wilde <lukew@serenityos.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <LibWeb/Bindings/HistoryPrototype.h> |
8 | | #include <LibWeb/Bindings/Intrinsics.h> |
9 | | #include <LibWeb/DOM/Document.h> |
10 | | #include <LibWeb/HTML/History.h> |
11 | | #include <LibWeb/HTML/Navigation.h> |
12 | | #include <LibWeb/HTML/StructuredSerialize.h> |
13 | | #include <LibWeb/HTML/TraversableNavigable.h> |
14 | | #include <LibWeb/HTML/Window.h> |
15 | | |
16 | | namespace Web::HTML { |
17 | | |
18 | | JS_DEFINE_ALLOCATOR(History); |
19 | | |
20 | | JS::NonnullGCPtr<History> History::create(JS::Realm& realm, DOM::Document& document) |
21 | 0 | { |
22 | 0 | return realm.heap().allocate<History>(realm, realm, document); |
23 | 0 | } |
24 | | |
25 | | History::History(JS::Realm& realm, DOM::Document& document) |
26 | 0 | : PlatformObject(realm) |
27 | 0 | , m_associated_document(document) |
28 | 0 | { |
29 | 0 | } |
30 | | |
31 | 0 | History::~History() = default; |
32 | | |
33 | | void History::initialize(JS::Realm& realm) |
34 | 0 | { |
35 | 0 | Base::initialize(realm); |
36 | 0 | WEB_SET_PROTOTYPE_FOR_INTERFACE(History); |
37 | 0 | } |
38 | | |
39 | | void History::visit_edges(Cell::Visitor& visitor) |
40 | 0 | { |
41 | 0 | Base::visit_edges(visitor); |
42 | 0 | visitor.visit(m_associated_document); |
43 | 0 | visitor.visit(m_state); |
44 | 0 | } |
45 | | |
46 | | // https://html.spec.whatwg.org/multipage/history.html#dom-history-pushstate |
47 | | // The pushState(data, unused, url) method steps are to run the shared history push/replace state steps given this, data, url, and "push". |
48 | | WebIDL::ExceptionOr<void> History::push_state(JS::Value data, String const&, Optional<String> const& url) |
49 | 0 | { |
50 | 0 | return shared_history_push_replace_state(data, url, HistoryHandlingBehavior::Push); |
51 | 0 | } |
52 | | |
53 | | // https://html.spec.whatwg.org/multipage/history.html#dom-history-replacestate |
54 | | // The replaceState(data, unused, url) method steps are to run the shared history push/replace state steps given this, data, url, and "replace". |
55 | | WebIDL::ExceptionOr<void> History::replace_state(JS::Value data, String const&, Optional<String> const& url) |
56 | 0 | { |
57 | 0 | return shared_history_push_replace_state(data, url, HistoryHandlingBehavior::Replace); |
58 | 0 | } |
59 | | |
60 | | // https://html.spec.whatwg.org/multipage/history.html#dom-history-length |
61 | | WebIDL::ExceptionOr<u64> History::length() const |
62 | 0 | { |
63 | | // 1. If this's relevant global object's associated Document is not fully active, then throw a "SecurityError" DOMException. |
64 | 0 | if (!m_associated_document->is_fully_active()) |
65 | 0 | return WebIDL::SecurityError::create(realm(), "Cannot perform length on a document that isn't fully active."_string); |
66 | | |
67 | | // 2. Return this's length. |
68 | 0 | return m_length; |
69 | 0 | } |
70 | | |
71 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-history-state |
72 | | WebIDL::ExceptionOr<JS::Value> History::state() const |
73 | 0 | { |
74 | | // 1. If this's relevant global object's associated Document is not fully active, then throw a "SecurityError" DOMException. |
75 | 0 | if (!m_associated_document->is_fully_active()) |
76 | 0 | return WebIDL::SecurityError::create(realm(), "Cannot perform state on a document that isn't fully active."_string); |
77 | | |
78 | | // 2. Return this's state. |
79 | 0 | return m_state; |
80 | 0 | } |
81 | | |
82 | | JS::Value History::unsafe_state() const |
83 | 0 | { |
84 | 0 | return m_state; |
85 | 0 | } |
86 | | |
87 | | // https://html.spec.whatwg.org/multipage/history.html#dom-history-go |
88 | | WebIDL::ExceptionOr<void> History::go(WebIDL::Long delta = 0) |
89 | 0 | { |
90 | | // 1. Let document be this's associated Document. |
91 | | |
92 | | // 2. If document is not fully active, then throw a "SecurityError" DOMException. |
93 | 0 | if (!m_associated_document->is_fully_active()) |
94 | 0 | return WebIDL::SecurityError::create(realm(), "Cannot perform go on a document that isn't fully active."_string); |
95 | | |
96 | 0 | VERIFY(m_associated_document->navigable()); |
97 | | |
98 | | // 3. If delta is 0, then reload document's node navigable. |
99 | 0 | if (delta == 0) |
100 | 0 | m_associated_document->navigable()->reload(); |
101 | | |
102 | | // 4. Traverse the history by a delta given document's node navigable's traversable navigable, delta, and with sourceDocument set to document. |
103 | 0 | auto traversable = m_associated_document->navigable()->traversable_navigable(); |
104 | 0 | traversable->traverse_the_history_by_delta(delta); |
105 | |
|
106 | 0 | return {}; |
107 | 0 | } |
108 | | |
109 | | // https://html.spec.whatwg.org/multipage/history.html#dom-history-back |
110 | | WebIDL::ExceptionOr<void> History::back() |
111 | 0 | { |
112 | | // 1. Let document be this's associated Document. |
113 | | // 2. If document is not fully active, then throw a "SecurityError" DOMException. |
114 | | // NOTE: We already did this check in `go` method, so skip the fully active check here. |
115 | | |
116 | | // 3. Traverse the history by a delta with −1 and document's browsing context. |
117 | 0 | return go(-1); |
118 | 0 | } |
119 | | |
120 | | // https://html.spec.whatwg.org/multipage/history.html#dom-history-forward |
121 | | WebIDL::ExceptionOr<void> History::forward() |
122 | 0 | { |
123 | | // 1. Let document be this's associated Document. |
124 | | // 2. If document is not fully active, then throw a "SecurityError" DOMException. |
125 | | // NOTE: We already did this check in `go` method, so skip the fully active check here. |
126 | | |
127 | | // 3. Traverse the history by a delta with +1 and document's browsing context. |
128 | 0 | return go(1); |
129 | 0 | } |
130 | | |
131 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#can-have-its-url-rewritten |
132 | | bool can_have_its_url_rewritten(DOM::Document const& document, URL::URL const& target_url) |
133 | 0 | { |
134 | | // 1. Let documentURL be document's URL. |
135 | 0 | auto document_url = document.url(); |
136 | | |
137 | | // 2. If targetURL and documentURL differ in their scheme, username, password, host, or port components, |
138 | | // then return false. |
139 | 0 | if (target_url.scheme() != document_url.scheme() |
140 | 0 | || target_url.username() != document_url.username() |
141 | 0 | || target_url.password() != document_url.password() |
142 | 0 | || target_url.host() != document_url.host() |
143 | 0 | || target_url.port() != document_url.port()) |
144 | 0 | return false; |
145 | | |
146 | | // 3. If targetURL's scheme is an HTTP(S) scheme, then return true. |
147 | | // (Differences in path, query, and fragment are allowed for http: and https: URLs.) |
148 | 0 | if (target_url.scheme() == "http"sv || target_url.scheme() == "https"sv) |
149 | 0 | return true; |
150 | | |
151 | | // 4. If targetURL's scheme is "file", then: |
152 | | // (Differences in query and fragment are allowed for file: URLs.) |
153 | 0 | if (target_url.scheme() == "file"sv) { |
154 | | // 1. If targetURL and documentURL differ in their path component, then return false. |
155 | 0 | if (target_url.paths() != document_url.paths()) |
156 | 0 | return false; |
157 | | |
158 | | // 2. Return true. |
159 | 0 | return true; |
160 | 0 | } |
161 | | |
162 | | // 5. If targetURL and documentURL differ in their path component or query components, then return false. |
163 | | // (Only differences in fragment are allowed for other types of URLs.) |
164 | 0 | if (target_url.paths() != document_url.paths() || target_url.query() != document_url.query()) |
165 | 0 | return false; |
166 | | |
167 | | // 6. Return true. |
168 | 0 | return true; |
169 | 0 | } |
170 | | |
171 | | // https://html.spec.whatwg.org/multipage/history.html#shared-history-push/replace-state-steps |
172 | | WebIDL::ExceptionOr<void> History::shared_history_push_replace_state(JS::Value data, Optional<String> const& url, HistoryHandlingBehavior history_handling) |
173 | 0 | { |
174 | 0 | auto& vm = this->vm(); |
175 | | |
176 | | // 1. Let document be history's associated Document. |
177 | 0 | auto& document = m_associated_document; |
178 | | |
179 | | // 2. If document is not fully active, then throw a "SecurityError" DOMException. |
180 | 0 | if (!document->is_fully_active()) |
181 | 0 | return WebIDL::SecurityError::create(realm(), "Cannot perform pushState or replaceState on a document that isn't fully active."_string); |
182 | | |
183 | | // 3. Optionally, return. (For example, the user agent might disallow calls to these methods that are invoked on a timer, |
184 | | // or from event listeners that are not triggered in response to a clear user action, or that are invoked in rapid succession.) |
185 | | |
186 | | // 4. Let serializedData be StructuredSerializeForStorage(data). Rethrow any exceptions. |
187 | | // FIXME: Actually rethrow exceptions here once we start using the serialized data. |
188 | | // Throwing here on data types we don't yet serialize will regress sites that use push/replaceState. |
189 | 0 | auto serialized_data_or_error = structured_serialize_for_storage(vm, data); |
190 | 0 | auto serialized_data = serialized_data_or_error.is_error() ? MUST(structured_serialize_for_storage(vm, JS::js_null())) : serialized_data_or_error.release_value(); |
191 | | |
192 | | // 5. Let newURL be document's URL. |
193 | 0 | auto new_url = document->url(); |
194 | | |
195 | | // 6. If url is not null or the empty string, then: |
196 | 0 | if (url.has_value() && !url->is_empty()) { |
197 | | |
198 | | // 1. Parse url, relative to the relevant settings object of history. |
199 | 0 | auto parsed_url = relevant_settings_object(*this).parse_url(url->to_byte_string()); |
200 | | |
201 | | // 2. If that fails, then throw a "SecurityError" DOMException. |
202 | 0 | if (!parsed_url.is_valid()) |
203 | 0 | return WebIDL::SecurityError::create(realm(), "Cannot pushState or replaceState to incompatible URL"_string); |
204 | | |
205 | | // 3. Set newURL to the resulting URL record. |
206 | 0 | new_url = parsed_url; |
207 | | |
208 | | // 4. If document cannot have its URL rewritten to newURL, then throw a "SecurityError" DOMException. |
209 | 0 | if (!can_have_its_url_rewritten(document, new_url)) |
210 | 0 | return WebIDL::SecurityError::create(realm(), "Cannot pushState or replaceState to incompatible URL"_string); |
211 | 0 | } |
212 | | |
213 | | // 7. Let navigation be history's relevant global object's navigation API. |
214 | 0 | auto navigation = verify_cast<Window>(relevant_global_object(*this)).navigation(); |
215 | | |
216 | | // 8. Let continue be the result of firing a push/replace/reload navigate event at navigation |
217 | | // with navigationType set to historyHandling, isSameDocument set to true, destinationURL set to newURL, |
218 | | // and classicHistoryAPIState set to serializedData. |
219 | 0 | auto navigation_type = history_handling == HistoryHandlingBehavior::Push ? Bindings::NavigationType::Push : Bindings::NavigationType::Replace; |
220 | 0 | auto continue_ = navigation->fire_a_push_replace_reload_navigate_event(navigation_type, new_url, true, UserNavigationInvolvement::None, {}, {}, serialized_data); |
221 | | // 9. If continue is false, then return. |
222 | 0 | if (!continue_) |
223 | 0 | return {}; |
224 | | |
225 | | // 10. Run the URL and history update steps given document and newURL, with serializedData set to |
226 | | // serializedData and historyHandling set to historyHandling. |
227 | 0 | perform_url_and_history_update_steps(document, new_url, serialized_data, history_handling); |
228 | |
|
229 | 0 | return {}; |
230 | 0 | } |
231 | | |
232 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-history-scroll-restoration |
233 | | WebIDL::ExceptionOr<Bindings::ScrollRestoration> History::scroll_restoration() const |
234 | 0 | { |
235 | | // 1. If this's relevant global object's associated Document is not fully active, then throw a "SecurityError" DOMException. |
236 | 0 | if (!m_associated_document->is_fully_active()) |
237 | 0 | return WebIDL::SecurityError::create(realm(), "Cannot obtain scroll restoration mode for a document that isn't fully active."_string); |
238 | | |
239 | | // 2. Return this's node navigable's active session history entry's scroll restoration mode. |
240 | 0 | auto scroll_restoration_mode = m_associated_document->navigable()->active_session_history_entry()->scroll_restoration_mode(); |
241 | 0 | switch (scroll_restoration_mode) { |
242 | 0 | case ScrollRestorationMode::Auto: |
243 | 0 | return Bindings::ScrollRestoration::Auto; |
244 | 0 | case ScrollRestorationMode::Manual: |
245 | 0 | return Bindings::ScrollRestoration::Manual; |
246 | 0 | } |
247 | 0 | VERIFY_NOT_REACHED(); |
248 | 0 | } |
249 | | |
250 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-history-scroll-restoration |
251 | | WebIDL::ExceptionOr<void> History::set_scroll_restoration(Bindings::ScrollRestoration scroll_restoration) |
252 | 0 | { |
253 | | // 1. If this's relevant global object's associated Document is not fully active, then throw a "SecurityError" DOMException. |
254 | 0 | if (!m_associated_document->is_fully_active()) |
255 | 0 | return WebIDL::SecurityError::create(realm(), "Cannot set scroll restoration mode for a document that isn't fully active."_string); |
256 | | |
257 | | // 2. Set this's node navigable's active session history entry's scroll restoration mode to the given value. |
258 | 0 | auto active_session_history_entry = m_associated_document->navigable()->active_session_history_entry(); |
259 | 0 | switch (scroll_restoration) { |
260 | 0 | case Bindings::ScrollRestoration::Auto: |
261 | 0 | active_session_history_entry->set_scroll_restoration_mode(ScrollRestorationMode::Auto); |
262 | 0 | break; |
263 | 0 | case Bindings::ScrollRestoration::Manual: |
264 | 0 | active_session_history_entry->set_scroll_restoration_mode(ScrollRestorationMode::Manual); |
265 | 0 | break; |
266 | 0 | } |
267 | | |
268 | 0 | return {}; |
269 | 0 | } |
270 | | |
271 | | } |