/src/serenity/Userland/Libraries/LibWeb/HTML/Navigation.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <LibJS/Heap/Heap.h> |
8 | | #include <LibJS/Runtime/Realm.h> |
9 | | #include <LibJS/Runtime/VM.h> |
10 | | #include <LibWeb/Bindings/ExceptionOrUtils.h> |
11 | | #include <LibWeb/Bindings/Intrinsics.h> |
12 | | #include <LibWeb/Bindings/NavigationPrototype.h> |
13 | | #include <LibWeb/DOM/AbortController.h> |
14 | | #include <LibWeb/DOM/Document.h> |
15 | | #include <LibWeb/HTML/DocumentState.h> |
16 | | #include <LibWeb/HTML/ErrorEvent.h> |
17 | | #include <LibWeb/HTML/History.h> |
18 | | #include <LibWeb/HTML/NavigateEvent.h> |
19 | | #include <LibWeb/HTML/Navigation.h> |
20 | | #include <LibWeb/HTML/NavigationCurrentEntryChangeEvent.h> |
21 | | #include <LibWeb/HTML/NavigationDestination.h> |
22 | | #include <LibWeb/HTML/NavigationHistoryEntry.h> |
23 | | #include <LibWeb/HTML/NavigationTransition.h> |
24 | | #include <LibWeb/HTML/Scripting/ExceptionReporter.h> |
25 | | #include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h> |
26 | | #include <LibWeb/HTML/TraversableNavigable.h> |
27 | | #include <LibWeb/HTML/Window.h> |
28 | | #include <LibWeb/WebIDL/AbstractOperations.h> |
29 | | #include <LibWeb/XHR/FormData.h> |
30 | | |
31 | | namespace Web::HTML { |
32 | | |
33 | | JS_DEFINE_ALLOCATOR(Navigation); |
34 | | JS_DEFINE_ALLOCATOR(NavigationAPIMethodTracker); |
35 | | |
36 | | static NavigationResult navigation_api_method_tracker_derived_result(JS::NonnullGCPtr<NavigationAPIMethodTracker> api_method_tracker); |
37 | | |
38 | | NavigationAPIMethodTracker::NavigationAPIMethodTracker(JS::NonnullGCPtr<Navigation> navigation, |
39 | | Optional<String> key, |
40 | | JS::Value info, |
41 | | Optional<SerializationRecord> serialized_state, |
42 | | JS::GCPtr<NavigationHistoryEntry> commited_to_entry, |
43 | | JS::NonnullGCPtr<WebIDL::Promise> committed_promise, |
44 | | JS::NonnullGCPtr<WebIDL::Promise> finished_promise) |
45 | 0 | : navigation(navigation) |
46 | 0 | , key(move(key)) |
47 | 0 | , info(info) |
48 | 0 | , serialized_state(move(serialized_state)) |
49 | 0 | , commited_to_entry(commited_to_entry) |
50 | 0 | , committed_promise(committed_promise) |
51 | 0 | , finished_promise(finished_promise) |
52 | 0 | { |
53 | 0 | } |
54 | | |
55 | | void NavigationAPIMethodTracker::visit_edges(Cell::Visitor& visitor) |
56 | 0 | { |
57 | 0 | Base::visit_edges(visitor); |
58 | 0 | visitor.visit(navigation); |
59 | 0 | visitor.visit(info); |
60 | 0 | visitor.visit(commited_to_entry); |
61 | 0 | visitor.visit(committed_promise); |
62 | 0 | visitor.visit(finished_promise); |
63 | 0 | } |
64 | | |
65 | | JS::NonnullGCPtr<Navigation> Navigation::create(JS::Realm& realm) |
66 | 0 | { |
67 | 0 | return realm.heap().allocate<Navigation>(realm, realm); |
68 | 0 | } |
69 | | |
70 | | Navigation::Navigation(JS::Realm& realm) |
71 | 0 | : DOM::EventTarget(realm) |
72 | 0 | { |
73 | 0 | } |
74 | | |
75 | 0 | Navigation::~Navigation() = default; |
76 | | |
77 | | void Navigation::initialize(JS::Realm& realm) |
78 | 0 | { |
79 | 0 | Base::initialize(realm); |
80 | 0 | WEB_SET_PROTOTYPE_FOR_INTERFACE(Navigation); |
81 | 0 | } |
82 | | |
83 | | void Navigation::visit_edges(JS::Cell::Visitor& visitor) |
84 | 0 | { |
85 | 0 | Base::visit_edges(visitor); |
86 | 0 | visitor.visit(m_entry_list); |
87 | 0 | visitor.visit(m_transition); |
88 | 0 | visitor.visit(m_ongoing_navigate_event); |
89 | 0 | visitor.visit(m_ongoing_api_method_tracker); |
90 | 0 | visitor.visit(m_upcoming_non_traverse_api_method_tracker); |
91 | 0 | visitor.visit(m_upcoming_traverse_api_method_trackers); |
92 | 0 | } |
93 | | |
94 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-entries |
95 | | Vector<JS::NonnullGCPtr<NavigationHistoryEntry>> Navigation::entries() const |
96 | 0 | { |
97 | | // The entries() method steps are: |
98 | | |
99 | | // 1. If this has entries and events disabled, then return the empty list. |
100 | 0 | if (has_entries_and_events_disabled()) |
101 | 0 | return {}; |
102 | | |
103 | | // 2. Return this's entry list. |
104 | | // NOTE: Recall that because of Web IDL's sequence type conversion rules, |
105 | | // this will create a new JavaScript array object on each call. |
106 | | // That is, navigation.entries() !== navigation.entries(). |
107 | 0 | return m_entry_list; |
108 | 0 | } |
109 | | |
110 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigation-current-entry |
111 | | JS::GCPtr<NavigationHistoryEntry> Navigation::current_entry() const |
112 | 0 | { |
113 | | // The current entry of a Navigation navigation is the result of running the following steps: |
114 | | |
115 | | // 1. If navigation has entries and events disabled, then return null. |
116 | 0 | if (has_entries_and_events_disabled()) |
117 | 0 | return nullptr; |
118 | | |
119 | | // 2. Assert: navigation's current entry index is not −1. |
120 | 0 | VERIFY(m_current_entry_index != -1); |
121 | | |
122 | | // 3. Return navigation's entry list[navigation's current entry index]. |
123 | 0 | return m_entry_list[m_current_entry_index]; |
124 | 0 | } |
125 | | |
126 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-updatecurrententry |
127 | | WebIDL::ExceptionOr<void> Navigation::update_current_entry(NavigationUpdateCurrentEntryOptions options) |
128 | 0 | { |
129 | | // The updateCurrentEntry(options) method steps are: |
130 | | |
131 | | // 1. Let current be the current entry of this. |
132 | 0 | auto current = current_entry(); |
133 | | |
134 | | // 2. If current is null, then throw an "InvalidStateError" DOMException. |
135 | 0 | if (current == nullptr) |
136 | 0 | return WebIDL::InvalidStateError::create(realm(), "Cannot update current NavigationHistoryEntry when there is no current entry"_string); |
137 | | |
138 | | // 3. Let serializedState be StructuredSerializeForStorage(options["state"]), rethrowing any exceptions. |
139 | 0 | auto serialized_state = TRY(structured_serialize_for_storage(vm(), options.state)); |
140 | | |
141 | | // 4. Set current's session history entry's navigation API state to serializedState. |
142 | 0 | current->session_history_entry().set_navigation_api_state(serialized_state); |
143 | | |
144 | | // 5. Fire an event named currententrychange at this using NavigationCurrentEntryChangeEvent, |
145 | | // with its navigationType attribute initialized to null and its from initialized to current. |
146 | 0 | NavigationCurrentEntryChangeEventInit event_init = {}; |
147 | 0 | event_init.navigation_type = {}; |
148 | 0 | event_init.from = current; |
149 | 0 | dispatch_event(HTML::NavigationCurrentEntryChangeEvent::construct_impl(realm(), HTML::EventNames::currententrychange, event_init)); |
150 | |
|
151 | 0 | return {}; |
152 | 0 | } |
153 | | |
154 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-cangoback |
155 | | bool Navigation::can_go_back() const |
156 | 0 | { |
157 | | // The canGoBack getter steps are: |
158 | | |
159 | | // 1. If this has entries and events disabled, then return false. |
160 | 0 | if (has_entries_and_events_disabled()) |
161 | 0 | return false; |
162 | | |
163 | | // 2. Assert: navigation's current entry index is not −1. |
164 | 0 | VERIFY(m_current_entry_index != -1); |
165 | | |
166 | | // 3. If this's current entry index is 0, then return false. |
167 | | // 4. Return true. |
168 | 0 | return (m_current_entry_index != 0); |
169 | 0 | } |
170 | | |
171 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-cangoforward |
172 | | bool Navigation::can_go_forward() const |
173 | 0 | { |
174 | | // The canGoForward getter steps are: |
175 | | |
176 | | // 1. If this has entries and events disabled, then return false. |
177 | 0 | if (has_entries_and_events_disabled()) |
178 | 0 | return false; |
179 | | |
180 | | // 2. Assert: navigation's current entry index is not −1. |
181 | 0 | VERIFY(m_current_entry_index != -1); |
182 | | |
183 | | // 3. If this's current entry index is equal to this's entry list's size, then return false. |
184 | | // 4. Return true. |
185 | 0 | return (m_current_entry_index != static_cast<i64>(m_entry_list.size())); |
186 | 0 | } |
187 | | |
188 | | // https://html.spec.whatwg.org/multipage/browsing-the-web.html#history-handling-behavior |
189 | | HistoryHandlingBehavior to_history_handling_behavior(Bindings::NavigationHistoryBehavior b) |
190 | 0 | { |
191 | | // A history handling behavior is a NavigationHistoryBehavior that is either "push" or "replace", |
192 | | // i.e., that has been resolved away from any initial "auto" value. |
193 | 0 | VERIFY(b != Bindings::NavigationHistoryBehavior::Auto); |
194 | | |
195 | 0 | switch (b) { |
196 | 0 | case Bindings::NavigationHistoryBehavior::Push: |
197 | 0 | return HistoryHandlingBehavior::Push; |
198 | 0 | case Bindings::NavigationHistoryBehavior::Replace: |
199 | 0 | return HistoryHandlingBehavior::Replace; |
200 | 0 | case Bindings::NavigationHistoryBehavior::Auto: |
201 | 0 | VERIFY_NOT_REACHED(); |
202 | 0 | }; |
203 | 0 | VERIFY_NOT_REACHED(); |
204 | 0 | } |
205 | | |
206 | | // https://html.spec.whatwg.org/multipage/browsing-the-web.html#history-handling-behavior |
207 | | Bindings::NavigationHistoryBehavior to_navigation_history_behavior(HistoryHandlingBehavior b) |
208 | 0 | { |
209 | | // A history handling behavior is a NavigationHistoryBehavior that is either "push" or "replace", |
210 | | // i.e., that has been resolved away from any initial "auto" value. |
211 | |
|
212 | 0 | switch (b) { |
213 | 0 | case HistoryHandlingBehavior::Push: |
214 | 0 | return Bindings::NavigationHistoryBehavior::Push; |
215 | 0 | case HistoryHandlingBehavior::Replace: |
216 | 0 | return Bindings::NavigationHistoryBehavior::Replace; |
217 | 0 | } |
218 | 0 | VERIFY_NOT_REACHED(); |
219 | 0 | } |
220 | | |
221 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-navigate |
222 | | WebIDL::ExceptionOr<NavigationResult> Navigation::navigate(String url, NavigationNavigateOptions const& options) |
223 | 0 | { |
224 | 0 | auto& realm = this->realm(); |
225 | 0 | auto& vm = this->vm(); |
226 | | // The navigate(options) method steps are: |
227 | | |
228 | | // 1. Parse url relative to this's relevant settings object. |
229 | | // If that returns failure, then return an early error result for a "SyntaxError" DOMException. |
230 | | // Otherwise, let urlRecord be the resulting URL record. |
231 | 0 | auto url_record = relevant_settings_object(*this).parse_url(url); |
232 | 0 | if (!url_record.is_valid()) |
233 | 0 | return early_error_result(WebIDL::SyntaxError::create(realm, "Cannot navigate to Invalid URL"_string)); |
234 | | |
235 | | // 2. Let document be this's relevant global object's associated Document. |
236 | 0 | auto& document = verify_cast<HTML::Window>(relevant_global_object(*this)).associated_document(); |
237 | | |
238 | | // 3. If options["history"] is "push", and the navigation must be a replace given urlRecord and document, |
239 | | // then return an early error result for a "NotSupportedError" DOMException. |
240 | 0 | if (options.history == Bindings::NavigationHistoryBehavior::Push && navigation_must_be_a_replace(url_record, document)) |
241 | 0 | return early_error_result(WebIDL::NotSupportedError::create(realm, "Navigation must be a replace, but push was requested"_string)); |
242 | | |
243 | | // 4. Let state be options["state"], if it exists; otherwise, undefined. |
244 | 0 | auto state = options.state.value_or(JS::js_undefined()); |
245 | | |
246 | | // 5. Let serializedState be StructuredSerializeForStorage(state). |
247 | | // If this throws an exception, then return an early error result for that exception. |
248 | | // FIXME: Fix this spec grammaro in the note |
249 | | // NOTE: It is importantly to perform this step early, since serialization can invoke web developer code, |
250 | | // which in turn might change various things we check in later steps. |
251 | 0 | auto serialized_state_or_error = structured_serialize_for_storage(vm, state); |
252 | 0 | if (serialized_state_or_error.is_error()) { |
253 | 0 | return early_error_result(serialized_state_or_error.release_error()); |
254 | 0 | } |
255 | | |
256 | 0 | auto serialized_state = serialized_state_or_error.release_value(); |
257 | | |
258 | | // 6. If document is not fully active, then return an early error result for an "InvalidStateError" DOMException. |
259 | 0 | if (!document.is_fully_active()) |
260 | 0 | return early_error_result(WebIDL::InvalidStateError::create(realm, "Document is not fully active"_string)); |
261 | | |
262 | | // 7. If document's unload counter is greater than 0, then return an early error result for an "InvalidStateError" DOMException. |
263 | 0 | if (document.unload_counter() > 0) |
264 | 0 | return early_error_result(WebIDL::InvalidStateError::create(realm, "Document already unloaded"_string)); |
265 | | |
266 | | // 8. Let info be options["info"], if it exists; otherwise, undefined. |
267 | 0 | auto info = options.info.value_or(JS::js_undefined()); |
268 | | |
269 | | // 9. Let apiMethodTracker be the result of maybe setting the upcoming non-traverse API method tracker for this |
270 | | // given info and serializedState. |
271 | 0 | auto api_method_tracker = maybe_set_the_upcoming_non_traverse_api_method_tracker(info, serialized_state); |
272 | | |
273 | | // 10. Navigate document's node navigable to urlRecord using document, |
274 | | // with historyHandling set to options["history"] and navigationAPIState set to serializedState. |
275 | | // FIXME: Fix spec typo here |
276 | | // NOTE: Unlike location.assign() and friends, which are exposed across origin-domain boundaries, |
277 | | // navigation.navigate() can only be accessed by code with direct synchronous access to the |
278 | | // window.navigation property. Thus, we avoid the complications about attributing the source document |
279 | | // of the navigation, and we don't need to deal with the allowed by sandboxing to navigate check and its |
280 | | // acccompanying exceptionsEnabled flag. We just treat all navigations as if they come from the Document |
281 | | // corresponding to this Navigation object itself (i.e., document). |
282 | 0 | TRY(document.navigable()->navigate({ .url = url_record, .source_document = document, .history_handling = options.history, .navigation_api_state = move(serialized_state) })); |
283 | | |
284 | | // 11. If this's upcoming non-traverse API method tracker is apiMethodTracker, then: |
285 | | // NOTE: If the upcoming non-traverse API method tracker is still apiMethodTracker, this means that the navigate |
286 | | // algorithm bailed out before ever getting to the inner navigate event firing algorithm which would promote |
287 | | // that upcoming API method tracker to ongoing. |
288 | 0 | if (m_upcoming_non_traverse_api_method_tracker == api_method_tracker) { |
289 | 0 | m_upcoming_non_traverse_api_method_tracker = nullptr; |
290 | 0 | return early_error_result(WebIDL::AbortError::create(realm, "Navigation aborted"_string)); |
291 | 0 | } |
292 | | |
293 | | // 12. Return a navigation API method tracker-derived result for apiMethodTracker. |
294 | 0 | return navigation_api_method_tracker_derived_result(api_method_tracker); |
295 | 0 | } |
296 | | |
297 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-reload |
298 | | WebIDL::ExceptionOr<NavigationResult> Navigation::reload(NavigationReloadOptions const& options) |
299 | 0 | { |
300 | 0 | auto& realm = this->realm(); |
301 | 0 | auto& vm = this->vm(); |
302 | | // The reload(options) method steps are: |
303 | | |
304 | | // 1. Let document be this's relevant global object's associated Document. |
305 | 0 | auto& document = verify_cast<HTML::Window>(relevant_global_object(*this)).associated_document(); |
306 | | |
307 | | // 2. Let serializedState be StructuredSerializeForStorage(undefined). |
308 | 0 | auto serialized_state = MUST(structured_serialize_for_storage(vm, JS::js_undefined())); |
309 | | |
310 | | // 3. If options["state"] exists, then set serializedState to StructuredSerializeForStorage(options["state"]). |
311 | | // If this throws an exception, then return an early error result for that exception. |
312 | | // NOTE: It is importantly to perform this step early, since serialization can invoke web developer |
313 | | // code, which in turn might change various things we check in later steps. |
314 | 0 | if (options.state.has_value()) { |
315 | 0 | auto serialized_state_or_error = structured_serialize_for_storage(vm, options.state.value()); |
316 | 0 | if (serialized_state_or_error.is_error()) |
317 | 0 | return early_error_result(serialized_state_or_error.release_error()); |
318 | 0 | serialized_state = serialized_state_or_error.release_value(); |
319 | 0 | } |
320 | | |
321 | | // 4. Otherwise: |
322 | 0 | else { |
323 | | // 1. Let current be the current entry of this. |
324 | 0 | auto current = current_entry(); |
325 | | |
326 | | // 2. If current is not null, then set serializedState to current's session history entry's navigation API state. |
327 | 0 | if (current != nullptr) |
328 | 0 | serialized_state = current->session_history_entry().navigation_api_state(); |
329 | 0 | } |
330 | | |
331 | | // 5. If document is not fully active, then return an early error result for an "InvalidStateError" DOMException. |
332 | 0 | if (!document.is_fully_active()) |
333 | 0 | return early_error_result(WebIDL::InvalidStateError::create(realm, "Document is not fully active"_string)); |
334 | | |
335 | | // 6. If document's unload counter is greater than 0, then return an early error result for an "InvalidStateError" DOMException. |
336 | 0 | if (document.unload_counter() > 0) |
337 | 0 | return early_error_result(WebIDL::InvalidStateError::create(realm, "Document already unloaded"_string)); |
338 | | |
339 | | // 7. Let info be options["info"], if it exists; otherwise, undefined. |
340 | 0 | auto info = options.info.value_or(JS::js_undefined()); |
341 | | |
342 | | // 8. Let apiMethodTracker be the result of maybe setting the upcoming non-traverse API method tracker for this given info and serializedState. |
343 | 0 | auto api_method_tracker = maybe_set_the_upcoming_non_traverse_api_method_tracker(info, serialized_state); |
344 | | |
345 | | // 9. Reload document's node navigable with navigationAPIState set to serializedState. |
346 | | // FIXME: Pass serialized_state to reload |
347 | 0 | document.navigable()->reload(); |
348 | |
|
349 | 0 | return navigation_api_method_tracker_derived_result(api_method_tracker); |
350 | 0 | } |
351 | | |
352 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-traverseto |
353 | | WebIDL::ExceptionOr<NavigationResult> Navigation::traverse_to(String key, NavigationOptions const& options) |
354 | 0 | { |
355 | 0 | auto& realm = this->realm(); |
356 | | // The traverseTo(key, options) method steps are: |
357 | | |
358 | | // 1. If this's current entry index is −1, then return an early error result for an "InvalidStateError" DOMException. |
359 | 0 | if (m_current_entry_index == -1) |
360 | 0 | return early_error_result(WebIDL::InvalidStateError::create(realm, "Cannot traverseTo: no current session history entry"_string)); |
361 | | |
362 | | // 2. If this's entry list does not contain a NavigationHistoryEntry whose session history entry's navigation API key equals key, |
363 | | // then return an early error result for an "InvalidStateError" DOMException. |
364 | 0 | auto it = m_entry_list.find_if([&key](auto const& entry) { |
365 | 0 | return entry->session_history_entry().navigation_api_key() == key; |
366 | 0 | }); |
367 | 0 | if (it == m_entry_list.end()) |
368 | 0 | return early_error_result(WebIDL::InvalidStateError::create(realm, "Cannot traverseTo: key not found in session history list"_string)); |
369 | | |
370 | | // 3. Return the result of performing a navigation API traversal given this, key, and options. |
371 | 0 | return perform_a_navigation_api_traversal(key, options); |
372 | 0 | } |
373 | | |
374 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#performing-a-navigation-api-traversal |
375 | | WebIDL::ExceptionOr<NavigationResult> Navigation::back(NavigationOptions const& options) |
376 | 0 | { |
377 | 0 | auto& realm = this->realm(); |
378 | | // The back(options) method steps are: |
379 | | |
380 | | // 1. If this's current entry index is −1 or 0, then return an early error result for an "InvalidStateError" DOMException. |
381 | 0 | if (m_current_entry_index == -1 || m_current_entry_index == 0) |
382 | 0 | return early_error_result(WebIDL::InvalidStateError::create(realm, "Cannot navigate back: no previous session history entry"_string)); |
383 | | |
384 | | // 2. Let key be this's entry list[this's current entry index − 1]'s session history entry's navigation API key. |
385 | 0 | auto key = m_entry_list[m_current_entry_index - 1]->session_history_entry().navigation_api_key(); |
386 | | |
387 | | // 3. Return the result of performing a navigation API traversal given this, key, and options. |
388 | 0 | return perform_a_navigation_api_traversal(key, options); |
389 | 0 | } |
390 | | |
391 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-forward |
392 | | WebIDL::ExceptionOr<NavigationResult> Navigation::forward(NavigationOptions const& options) |
393 | 0 | { |
394 | 0 | auto& realm = this->realm(); |
395 | | // The forward(options) method steps are: |
396 | | |
397 | | // 1. If this's current entry index is −1 or is equal to this's entry list's size − 1, |
398 | | // then return an early error result for an "InvalidStateError" DOMException. |
399 | 0 | if (m_current_entry_index == -1 || m_current_entry_index == static_cast<i64>(m_entry_list.size() - 1)) |
400 | 0 | return early_error_result(WebIDL::InvalidStateError::create(realm, "Cannot navigate forward: no next session history entry"_string)); |
401 | | |
402 | | // 2. Let key be this's entry list[this's current entry index + 1]'s session history entry's navigation API key. |
403 | 0 | auto key = m_entry_list[m_current_entry_index + 1]->session_history_entry().navigation_api_key(); |
404 | | |
405 | | // 3. Return the result of performing a navigation API traversal given this, key, and options. |
406 | 0 | return perform_a_navigation_api_traversal(key, options); |
407 | 0 | } |
408 | | |
409 | | void Navigation::set_onnavigate(WebIDL::CallbackType* event_handler) |
410 | 0 | { |
411 | 0 | set_event_handler_attribute(HTML::EventNames::navigate, event_handler); |
412 | 0 | } |
413 | | |
414 | | WebIDL::CallbackType* Navigation::onnavigate() |
415 | 0 | { |
416 | 0 | return event_handler_attribute(HTML::EventNames::navigate); |
417 | 0 | } |
418 | | |
419 | | void Navigation::set_onnavigatesuccess(WebIDL::CallbackType* event_handler) |
420 | 0 | { |
421 | 0 | set_event_handler_attribute(HTML::EventNames::navigatesuccess, event_handler); |
422 | 0 | } |
423 | | |
424 | | WebIDL::CallbackType* Navigation::onnavigatesuccess() |
425 | 0 | { |
426 | 0 | return event_handler_attribute(HTML::EventNames::navigatesuccess); |
427 | 0 | } |
428 | | |
429 | | void Navigation::set_onnavigateerror(WebIDL::CallbackType* event_handler) |
430 | 0 | { |
431 | 0 | set_event_handler_attribute(HTML::EventNames::navigateerror, event_handler); |
432 | 0 | } |
433 | | |
434 | | WebIDL::CallbackType* Navigation::onnavigateerror() |
435 | 0 | { |
436 | 0 | return event_handler_attribute(HTML::EventNames::navigateerror); |
437 | 0 | } |
438 | | |
439 | | void Navigation::set_oncurrententrychange(WebIDL::CallbackType* event_handler) |
440 | 0 | { |
441 | 0 | set_event_handler_attribute(HTML::EventNames::currententrychange, event_handler); |
442 | 0 | } |
443 | | |
444 | | WebIDL::CallbackType* Navigation::oncurrententrychange() |
445 | 0 | { |
446 | 0 | return event_handler_attribute(HTML::EventNames::currententrychange); |
447 | 0 | } |
448 | | |
449 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#has-entries-and-events-disabled |
450 | | bool Navigation::has_entries_and_events_disabled() const |
451 | 0 | { |
452 | | // A Navigation navigation has entries and events disabled if the following steps return true: |
453 | | |
454 | | // 1. Let document be navigation's relevant global object's associated Document. |
455 | 0 | auto const& document = verify_cast<HTML::Window>(relevant_global_object(*this)).associated_document(); |
456 | | |
457 | | // 2. If document is not fully active, then return true. |
458 | 0 | if (!document.is_fully_active()) |
459 | 0 | return true; |
460 | | |
461 | | // 3. If document's is initial about:blank is true, then return true. |
462 | 0 | if (document.is_initial_about_blank()) |
463 | 0 | return true; |
464 | | |
465 | | // 4. If document's origin is opaque, then return true. |
466 | 0 | if (document.origin().is_opaque()) |
467 | 0 | return true; |
468 | | |
469 | | // 5. Return false. |
470 | 0 | return false; |
471 | 0 | } |
472 | | |
473 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#getting-the-navigation-api-entry-index |
474 | | i64 Navigation::get_the_navigation_api_entry_index(SessionHistoryEntry const& she) const |
475 | 0 | { |
476 | | // To get the navigation API entry index of a session history entry she within a Navigation navigation: |
477 | | |
478 | | // 1. Let index be 0. |
479 | 0 | i64 index = 0; |
480 | | |
481 | | // 2. For each nhe of navigation's entry list: |
482 | 0 | for (auto const& nhe : m_entry_list) { |
483 | | // 1. If nhe's session history entry is equal to she, then return index. |
484 | 0 | if (&nhe->session_history_entry() == &she) |
485 | 0 | return index; |
486 | | |
487 | | // 2. Increment index by 1. |
488 | 0 | ++index; |
489 | 0 | } |
490 | | |
491 | | // 3. Return −1. |
492 | 0 | return -1; |
493 | 0 | } |
494 | | |
495 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigation-api-early-error-result |
496 | | NavigationResult Navigation::early_error_result(AnyException e) |
497 | 0 | { |
498 | 0 | auto& vm = this->vm(); |
499 | | |
500 | | // An early error result for an exception e is a NavigationResult dictionary instance given by |
501 | | // «[ "committed" → a promise rejected with e, "finished" → a promise rejected with e ]». |
502 | 0 | auto throw_completion = Bindings::dom_exception_to_throw_completion(vm, e); |
503 | 0 | return { |
504 | 0 | .committed = WebIDL::create_rejected_promise(realm(), *throw_completion.value())->promise(), |
505 | 0 | .finished = WebIDL::create_rejected_promise(realm(), *throw_completion.value())->promise(), |
506 | 0 | }; |
507 | 0 | } |
508 | | |
509 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigation-api-method-tracker-derived-result |
510 | | NavigationResult navigation_api_method_tracker_derived_result(JS::NonnullGCPtr<NavigationAPIMethodTracker> api_method_tracker) |
511 | 0 | { |
512 | | // A navigation API method tracker-derived result for a navigation API method tracker is a NavigationResult |
513 | | /// dictionary instance given by «[ "committed" apiMethodTracker's committed promise, "finished" → apiMethodTracker's finished promise ]». |
514 | 0 | return { |
515 | 0 | api_method_tracker->committed_promise->promise(), |
516 | 0 | api_method_tracker->finished_promise->promise(), |
517 | 0 | }; |
518 | 0 | } |
519 | | |
520 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#upcoming-non-traverse-api-method-tracker |
521 | | JS::NonnullGCPtr<NavigationAPIMethodTracker> Navigation::maybe_set_the_upcoming_non_traverse_api_method_tracker(JS::Value info, Optional<SerializationRecord> serialized_state) |
522 | 0 | { |
523 | 0 | auto& realm = relevant_realm(*this); |
524 | 0 | auto& vm = this->vm(); |
525 | | // To maybe set the upcoming non-traverse API method tracker given a Navigation navigation, |
526 | | // a JavaScript value info, and a serialized state-or-null serializedState: |
527 | | |
528 | | // 1. Let committedPromise and finishedPromise be new promises created in navigation's relevant realm. |
529 | 0 | auto committed_promise = WebIDL::create_promise(realm); |
530 | 0 | auto finished_promise = WebIDL::create_promise(realm); |
531 | | |
532 | | // 2. Mark as handled finishedPromise. |
533 | | // NOTE: The web developer doesn’t necessarily care about finishedPromise being rejected: |
534 | | // - They might only care about committedPromise. |
535 | | // - They could be doing multiple synchronous navigations within the same task, |
536 | | // in which case all but the last will be aborted (causing their finishedPromise to reject). |
537 | | // This could be an application bug, but also could just be an emergent feature of disparate |
538 | | // parts of the application overriding each others' actions. |
539 | | // - They might prefer to listen to other transition-failure signals instead of finishedPromise, e.g., |
540 | | // the navigateerror event, or the navigation.transition.finished promise. |
541 | | // As such, we mark it as handled to ensure that it never triggers unhandledrejection events. |
542 | 0 | WebIDL::mark_promise_as_handled(finished_promise); |
543 | | |
544 | | // 3. Let apiMethodTracker be a new navigation API method tracker with: |
545 | | // navigation object: navigation |
546 | | // key: null |
547 | | // info: info |
548 | | // serialized state: serializedState |
549 | | // comitted-to entry: null |
550 | | // comitted promise: committedPromise |
551 | | // finished promise: finishedPromise |
552 | 0 | auto api_method_tracker = vm.heap().allocate_without_realm<NavigationAPIMethodTracker>( |
553 | 0 | /* .navigation = */ *this, |
554 | 0 | /* .key = */ OptionalNone {}, |
555 | 0 | /* .info = */ info, |
556 | 0 | /* .serialized_state = */ move(serialized_state), |
557 | 0 | /* .commited_to_entry = */ nullptr, |
558 | 0 | /* .committed_promise = */ committed_promise, |
559 | 0 | /* .finished_promise = */ finished_promise); |
560 | | |
561 | | // 4. Assert: navigation's upcoming non-traverse API method tracker is null. |
562 | 0 | VERIFY(m_upcoming_non_traverse_api_method_tracker == nullptr); |
563 | | |
564 | | // 5. If navigation does not have entries and events disabled, |
565 | | // then set navigation's upcoming non-traverse API method tracker to apiMethodTracker. |
566 | | // NOTE: If navigation has entries and events disabled, then committedPromise and finishedPromise will never fulfill |
567 | | // (since we never create a NavigationHistoryEntry object for such Documents, and so we have nothing to resolve them with); |
568 | | // there is no NavigationHistoryEntry to apply serializedState to; and there is no navigate event to include info with. |
569 | | // So, we don't need to track this API method call after all. |
570 | 0 | if (!has_entries_and_events_disabled()) |
571 | 0 | m_upcoming_non_traverse_api_method_tracker = api_method_tracker; |
572 | | |
573 | | // 6. Return apiMethodTracker. |
574 | 0 | return api_method_tracker; |
575 | 0 | } |
576 | | |
577 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#add-an-upcoming-traverse-api-method-tracker |
578 | | JS::NonnullGCPtr<NavigationAPIMethodTracker> Navigation::add_an_upcoming_traverse_api_method_tracker(String destination_key, JS::Value info) |
579 | 0 | { |
580 | 0 | auto& vm = this->vm(); |
581 | 0 | auto& realm = relevant_realm(*this); |
582 | | // To add an upcoming traverse API method tracker given a Navigation navigation, a string destinationKey, and a JavaScript value info: |
583 | | |
584 | | // 1. Let committedPromise and finishedPromise be new promises created in navigation's relevant realm. |
585 | 0 | auto committed_promise = WebIDL::create_promise(realm); |
586 | 0 | auto finished_promise = WebIDL::create_promise(realm); |
587 | | |
588 | | // 2. Mark as handled finishedPromise. |
589 | | // NOTE: See the previous discussion about why this is done |
590 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#note-mark-as-handled-navigation-api-finished |
591 | 0 | WebIDL::mark_promise_as_handled(*finished_promise); |
592 | | |
593 | | // 3. Let apiMethodTracker be a new navigation API method tracker with: |
594 | | // navigation object: navigation |
595 | | // key: destinationKey |
596 | | // info: info |
597 | | // serialized state: null |
598 | | // comitted-to entry: null |
599 | | // comitted promise: committedPromise |
600 | | // finished promise: finishedPromise |
601 | 0 | auto api_method_tracker = vm.heap().allocate_without_realm<NavigationAPIMethodTracker>( |
602 | 0 | /* .navigation = */ *this, |
603 | 0 | /* .key = */ destination_key, |
604 | 0 | /* .info = */ info, |
605 | 0 | /* .serialized_state = */ OptionalNone {}, |
606 | 0 | /* .commited_to_entry = */ nullptr, |
607 | 0 | /* .committed_promise = */ committed_promise, |
608 | 0 | /* .finished_promise = */ finished_promise); |
609 | | |
610 | | // 4. Set navigation's upcoming traverse API method trackers[key] to apiMethodTracker. |
611 | | // FIXME: Fix spec typo key --> destinationKey |
612 | 0 | m_upcoming_traverse_api_method_trackers.set(destination_key, api_method_tracker); |
613 | | |
614 | | // 5. Return apiMethodTracker. |
615 | 0 | return api_method_tracker; |
616 | 0 | } |
617 | | |
618 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#performing-a-navigation-api-traversal |
619 | | WebIDL::ExceptionOr<NavigationResult> Navigation::perform_a_navigation_api_traversal(String key, NavigationOptions const& options) |
620 | 0 | { |
621 | 0 | auto& realm = this->realm(); |
622 | | // To perform a navigation API traversal given a Navigation navigation, a string key, and a NavigationOptions options: |
623 | | |
624 | | // 1. Let document be this's relevant global object's associated Document. |
625 | 0 | auto& document = verify_cast<HTML::Window>(relevant_global_object(*this)).associated_document(); |
626 | | |
627 | | // 2. If document is not fully active, then return an early error result for an "InvalidStateError" DOMException. |
628 | 0 | if (!document.is_fully_active()) |
629 | 0 | return early_error_result(WebIDL::InvalidStateError::create(realm, "Document is not fully active"_string)); |
630 | | |
631 | | // 3. If document's unload counter is greater than 0, then return an early error result for an "InvalidStateError" DOMException. |
632 | 0 | if (document.unload_counter() > 0) |
633 | 0 | return early_error_result(WebIDL::InvalidStateError::create(realm, "Document already unloaded"_string)); |
634 | | |
635 | | // 4. Let current be the current entry of navigation. |
636 | 0 | auto current = current_entry(); |
637 | | |
638 | | // 5. If key equals current's session history entry's navigation API key, then return |
639 | | // «[ "committed" → a promise resolved with current, "finished" → a promise resolved with current ]». |
640 | 0 | if (key == current->session_history_entry().navigation_api_key()) { |
641 | 0 | return NavigationResult { |
642 | 0 | .committed = WebIDL::create_resolved_promise(realm, current)->promise(), |
643 | 0 | .finished = WebIDL::create_resolved_promise(realm, current)->promise() |
644 | 0 | }; |
645 | 0 | } |
646 | | |
647 | | // 6. If navigation's upcoming traverse API method trackers[key] exists, |
648 | | // then return a navigation API method tracker-derived result for navigation's upcoming traverse API method trackers[key]. |
649 | 0 | if (auto maybe_tracker = m_upcoming_traverse_api_method_trackers.get(key); maybe_tracker.has_value()) |
650 | 0 | return navigation_api_method_tracker_derived_result(maybe_tracker.value()); |
651 | | |
652 | | // 7. Let info be options["info"], if it exists; otherwise, undefined. |
653 | 0 | auto info = options.info.value_or(JS::js_undefined()); |
654 | | |
655 | | // 8. Let apiMethodTracker be the result of adding an upcoming traverse API method tracker for navigation given key and info. |
656 | 0 | auto api_method_tracker = add_an_upcoming_traverse_api_method_tracker(key, info); |
657 | | |
658 | | // 9. Let navigable be document's node navigable. |
659 | 0 | auto navigable = document.navigable(); |
660 | | |
661 | | // 10. Let traversable be navigable's traversable navigable. |
662 | 0 | auto traversable = navigable->traversable_navigable(); |
663 | | |
664 | | // 11. Let sourceSnapshotParams be the result of snapshotting source snapshot params given document. |
665 | 0 | auto source_snapshot_params = document.snapshot_source_snapshot_params(); |
666 | | |
667 | | // 12. Append the following session history traversal steps to traversable: |
668 | 0 | traversable->append_session_history_traversal_steps(JS::create_heap_function(heap(), [key, api_method_tracker, navigable, source_snapshot_params, traversable, this] { |
669 | | // 1. Let navigableSHEs be the result of getting session history entries given navigable. |
670 | 0 | auto navigable_shes = navigable->get_session_history_entries(); |
671 | | |
672 | | // 2. Let targetSHE be the session history entry in navigableSHEs whose navigation API key is key. If no such entry exists, then: |
673 | 0 | auto it = navigable_shes.find_if([&key](auto const& entry) { |
674 | 0 | return entry->navigation_api_key() == key; |
675 | 0 | }); |
676 | 0 | if (it == navigable_shes.end()) { |
677 | | // NOTE: This path is taken if navigation's entry list was outdated compared to navigableSHEs, |
678 | | // which can occur for brief periods while all the relevant threads and processes are being synchronized in reaction to a history change. |
679 | | |
680 | | // 1. Queue a global task on the navigation and traversal task source given navigation's relevant global object |
681 | | // to reject the finished promise for apiMethodTracker with an "InvalidStateError" DOMException. |
682 | 0 | queue_global_task(HTML::Task::Source::NavigationAndTraversal, relevant_global_object(*this), JS::create_heap_function(heap(), [this, api_method_tracker] { |
683 | 0 | auto& reject_realm = relevant_realm(*this); |
684 | 0 | TemporaryExecutionContext execution_context { relevant_settings_object(*this) }; |
685 | 0 | WebIDL::reject_promise(reject_realm, api_method_tracker->finished_promise, |
686 | 0 | WebIDL::InvalidStateError::create(reject_realm, "Cannot traverse with stale session history entry"_string)); |
687 | 0 | })); |
688 | | |
689 | | // 2. Abort these steps. |
690 | 0 | return; |
691 | 0 | } |
692 | 0 | auto target_she = *it; |
693 | | |
694 | | // 3. If targetSHE is navigable's active session history entry, then abort these steps. |
695 | | // NOTE: This can occur if a previously queued traversal already took us to this session history entry. |
696 | | // In that case the previous traversal will have dealt with apiMethodTracker already. |
697 | 0 | if (target_she == navigable->active_session_history_entry()) |
698 | 0 | return; |
699 | | |
700 | | // 4. Let result be the result of applying the traverse history step given by targetSHE's step to traversable, |
701 | | // given sourceSnapshotParams, navigable, and "none". |
702 | 0 | auto result = traversable->apply_the_traverse_history_step(target_she->step().get<int>(), source_snapshot_params, navigable, UserNavigationInvolvement::None); |
703 | | |
704 | | // NOTE: When result is "canceled-by-beforeunload" or "initiator-disallowed", the navigate event was never fired, |
705 | | // aborting the ongoing navigation would not be correct; it would result in a navigateerror event without a |
706 | | // preceding navigate event. In the "canceled-by-navigate" case, navigate is fired, but the inner navigate event |
707 | | // firing algorithm will take care of aborting the ongoing navigation. |
708 | | |
709 | | // 5. If result is "canceled-by-beforeunload", then queue a global task on the navigation and traversal task source |
710 | | // given navigation's relevant global object to reject the finished promise for apiMethodTracker with a |
711 | | // new "AbortError" DOMException created in navigation's relevant realm. |
712 | 0 | auto& realm = relevant_realm(*this); |
713 | 0 | auto& global = relevant_global_object(*this); |
714 | 0 | if (result == TraversableNavigable::HistoryStepResult::CanceledByBeforeUnload) { |
715 | 0 | queue_global_task(Task::Source::NavigationAndTraversal, global, JS::create_heap_function(heap(), [this, api_method_tracker, &realm] { |
716 | 0 | TemporaryExecutionContext execution_context { relevant_settings_object(*this) }; |
717 | 0 | reject_the_finished_promise(api_method_tracker, WebIDL::AbortError::create(realm, "Navigation cancelled by beforeunload"_string)); |
718 | 0 | })); |
719 | 0 | } |
720 | | |
721 | | // 6. If result is "initiator-disallowed", then queue a global task on the navigation and traversal task source |
722 | | // given navigation's relevant global object to reject the finished promise for apiMethodTracker with a |
723 | | // new "SecurityError" DOMException created in navigation's relevant realm. |
724 | 0 | if (result == TraversableNavigable::HistoryStepResult::InitiatorDisallowed) { |
725 | 0 | queue_global_task(Task::Source::NavigationAndTraversal, global, JS::create_heap_function(heap(), [this, api_method_tracker, &realm] { |
726 | 0 | TemporaryExecutionContext execution_context { relevant_settings_object(*this) }; |
727 | 0 | reject_the_finished_promise(api_method_tracker, WebIDL::SecurityError::create(realm, "Navigation disallowed from this origin"_string)); |
728 | 0 | })); |
729 | 0 | } |
730 | 0 | })); |
731 | | |
732 | | // 13. Return a navigation API method tracker-derived result for apiMethodTracker. |
733 | 0 | return navigation_api_method_tracker_derived_result(api_method_tracker); |
734 | 0 | } |
735 | | |
736 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#abort-the-ongoing-navigation |
737 | | void Navigation::abort_the_ongoing_navigation(JS::GCPtr<WebIDL::DOMException> error) |
738 | 0 | { |
739 | 0 | auto& realm = relevant_realm(*this); |
740 | | |
741 | | // To abort the ongoing navigation given a Navigation navigation and an optional DOMException error: |
742 | | |
743 | | // 1. Let event be navigation's ongoing navigate event. |
744 | 0 | auto event = ongoing_navigate_event(); |
745 | | |
746 | | // 2. Assert: event is not null. |
747 | 0 | VERIFY(event != nullptr); |
748 | | |
749 | | // 3. Set navigation's focus changed during ongoing navigation to false. |
750 | 0 | m_focus_changed_during_ongoing_navigation = false; |
751 | | |
752 | | // 4. Set navigation's suppress normal scroll restoration during ongoing navigation to false. |
753 | 0 | m_suppress_scroll_restoration_during_ongoing_navigation = false; |
754 | | |
755 | | // 5. If error was not given, then let error be a new "AbortError" DOMException created in navigation's relevant realm. |
756 | 0 | if (!error) |
757 | 0 | error = WebIDL::AbortError::create(realm, "Navigation aborted"_string); |
758 | |
|
759 | 0 | VERIFY(error); |
760 | | |
761 | | // 6. If event's dispatch flag is set, then set event's canceled flag to true. |
762 | 0 | if (event->dispatched()) |
763 | 0 | event->set_cancelled(true); |
764 | | |
765 | | // 7. Signal abort on event's abort controller given error. |
766 | 0 | event->abort_controller()->abort(error); |
767 | | |
768 | | // 8. Set navigation's ongoing navigate event to null. |
769 | 0 | m_ongoing_navigate_event = nullptr; |
770 | | |
771 | | // 9. Fire an event named navigateerror at navigation using ErrorEvent, with error initialized to error, |
772 | | // and message, filename, lineno, and colno initialized to appropriate values that can be extracted |
773 | | // from error and the current JavaScript stack in the same underspecified way that the report the exception algorithm does. |
774 | 0 | ErrorEventInit event_init = {}; |
775 | 0 | event_init.error = error; |
776 | | // FIXME: Extract information from the exception and the JS context in the wishy-washy way the spec says here. |
777 | 0 | event_init.filename = String {}; |
778 | 0 | event_init.colno = 0; |
779 | 0 | event_init.lineno = 0; |
780 | 0 | event_init.message = String {}; |
781 | |
|
782 | 0 | dispatch_event(ErrorEvent::create(realm, EventNames::navigateerror, event_init)); |
783 | | |
784 | | // 10. If navigation's ongoing API method tracker is non-null, then reject the finished promise for apiMethodTracker with error. |
785 | 0 | if (m_ongoing_api_method_tracker != nullptr) |
786 | 0 | WebIDL::reject_promise(realm, m_ongoing_api_method_tracker->finished_promise, error); |
787 | | |
788 | | // 11. If navigation's transition is not null, then: |
789 | 0 | if (m_transition != nullptr) { |
790 | | // 1. Reject navigation's transition's finished promise with error. |
791 | 0 | m_transition->finished()->reject(error); |
792 | | |
793 | | // 2. Set navigation's transition to null. |
794 | 0 | m_transition = nullptr; |
795 | 0 | } |
796 | 0 | } |
797 | | |
798 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#promote-an-upcoming-api-method-tracker-to-ongoing |
799 | | void Navigation::promote_an_upcoming_api_method_tracker_to_ongoing(Optional<String> destination_key) |
800 | 0 | { |
801 | | // 1. Assert: navigation's ongoing API method tracker is null. |
802 | 0 | VERIFY(m_ongoing_api_method_tracker == nullptr); |
803 | | |
804 | | // 2. If destinationKey is not null, then: |
805 | 0 | if (destination_key.has_value()) { |
806 | | // 1. Assert: navigation's upcoming non-traverse API method tracker is null. |
807 | 0 | VERIFY(m_upcoming_non_traverse_api_method_tracker == nullptr); |
808 | | |
809 | | // 2. If navigation's upcoming traverse API method trackers[destinationKey] exists, then: |
810 | 0 | if (auto tracker = m_upcoming_traverse_api_method_trackers.get(destination_key.value()); tracker.has_value()) { |
811 | | // 1. Set navigation's ongoing API method tracker to navigation's upcoming traverse API method trackers[destinationKey]. |
812 | 0 | m_ongoing_api_method_tracker = tracker.value(); |
813 | | |
814 | | // 2. Remove navigation's upcoming traverse API method trackers[destinationKey]. |
815 | 0 | m_upcoming_traverse_api_method_trackers.remove(destination_key.value()); |
816 | 0 | } |
817 | 0 | } |
818 | | |
819 | | // 3. Otherwise: |
820 | 0 | else { |
821 | | // 1. Set navigation's ongoing API method tracker to navigation's upcoming non-traverse API method tracker. |
822 | 0 | m_ongoing_api_method_tracker = m_upcoming_non_traverse_api_method_tracker; |
823 | | |
824 | | // 2. Set navigation's upcoming non-traverse API method tracker to null. |
825 | 0 | m_upcoming_non_traverse_api_method_tracker = nullptr; |
826 | 0 | } |
827 | 0 | } |
828 | | |
829 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigation-api-method-tracker-clean-up |
830 | | void Navigation::clean_up(JS::NonnullGCPtr<NavigationAPIMethodTracker> api_method_tracker) |
831 | 0 | { |
832 | | // 1. Let navigation be apiMethodTracker's navigation object. |
833 | 0 | VERIFY(api_method_tracker->navigation == this); |
834 | | |
835 | | // 2. If navigation's ongoing API method tracker is apiMethodTracker, then set navigation's ongoing API method tracker to null. |
836 | 0 | if (m_ongoing_api_method_tracker == api_method_tracker) { |
837 | 0 | m_ongoing_api_method_tracker = nullptr; |
838 | 0 | } |
839 | | // 3. Otherwise: |
840 | 0 | else { |
841 | | // 1. Let key be apiMethodTracker's key. |
842 | 0 | auto& key = api_method_tracker->key; |
843 | | |
844 | | // 2. Assert: key is not null. |
845 | 0 | VERIFY(key.has_value()); |
846 | | |
847 | | // 3. Assert: navigation's upcoming traverse API method trackers[key] exists. |
848 | 0 | VERIFY(m_upcoming_traverse_api_method_trackers.contains(*key)); |
849 | | |
850 | | // 4. Remove navigation's upcoming traverse API method trackers[key]. |
851 | 0 | m_upcoming_traverse_api_method_trackers.remove(*key); |
852 | 0 | } |
853 | 0 | } |
854 | | |
855 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#resolve-the-finished-promise |
856 | | void Navigation::resolve_the_finished_promise(JS::NonnullGCPtr<NavigationAPIMethodTracker> api_method_tracker) |
857 | 0 | { |
858 | 0 | auto& realm = this->realm(); |
859 | | |
860 | | // 1. Resolve apiMethodTracker's committed promise with its committed-to entry. |
861 | | // NOTE: Usually, notify about the committed-to entry has previously been called on apiMethodTracker, |
862 | | // and so this will do nothing. However, in some cases resolve the finished promise is called |
863 | | // directly, in which case this step is necessary. |
864 | 0 | WebIDL::resolve_promise(realm, api_method_tracker->committed_promise, api_method_tracker->commited_to_entry); |
865 | | |
866 | | // 2. Resolve apiMethodTracker's finished promise with its committed-to entry. |
867 | 0 | WebIDL::resolve_promise(realm, api_method_tracker->finished_promise, api_method_tracker->commited_to_entry); |
868 | | |
869 | | // 3. Clean up apiMethodTracker. |
870 | 0 | clean_up(api_method_tracker); |
871 | 0 | } |
872 | | |
873 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#reject-the-finished-promise |
874 | | void Navigation::reject_the_finished_promise(JS::NonnullGCPtr<NavigationAPIMethodTracker> api_method_tracker, JS::Value exception) |
875 | 0 | { |
876 | 0 | auto& realm = this->realm(); |
877 | | |
878 | | // 1. Reject apiMethodTracker's committed promise with exception. |
879 | | // NOTE: This will do nothing if apiMethodTracker's committed promise was previously resolved |
880 | | // via notify about the committed-to entry. |
881 | 0 | WebIDL::reject_promise(realm, api_method_tracker->committed_promise, exception); |
882 | | |
883 | | // 2. Reject apiMethodTracker's finished promise with exception. |
884 | 0 | WebIDL::reject_promise(realm, api_method_tracker->finished_promise, exception); |
885 | | |
886 | | // 3. Clean up apiMethodTracker. |
887 | 0 | clean_up(api_method_tracker); |
888 | 0 | } |
889 | | |
890 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#notify-about-the-committed-to-entry |
891 | | void Navigation::notify_about_the_committed_to_entry(JS::NonnullGCPtr<NavigationAPIMethodTracker> api_method_tracker, JS::NonnullGCPtr<NavigationHistoryEntry> nhe) |
892 | 0 | { |
893 | 0 | auto& realm = this->realm(); |
894 | | |
895 | | // 1. Set apiMethodTracker's committed-to entry to nhe. |
896 | 0 | api_method_tracker->commited_to_entry = nhe; |
897 | | |
898 | | // 2. If apiMethodTracker's serialized state is not null, then set nhe's session history entry's navigation API state to apiMethodTracker's serialized state.' |
899 | | // NOTE: If it's null, then we're traversing to nhe via navigation.traverseTo(), which does not allow changing the state. |
900 | 0 | if (api_method_tracker->serialized_state.has_value()) { |
901 | | // NOTE: At this point, apiMethodTracker's serialized state is no longer needed. |
902 | | // Implementations might want to clear it out to avoid keeping it alive for the lifetime of the navigation API method tracker. |
903 | 0 | nhe->session_history_entry().set_navigation_api_state(*api_method_tracker->serialized_state); |
904 | 0 | api_method_tracker->serialized_state = {}; |
905 | 0 | } |
906 | | |
907 | | // 3. Resolve apiMethodTracker's committed promise with nhe. |
908 | 0 | TemporaryExecutionContext execution_context { relevant_settings_object(*this) }; |
909 | 0 | WebIDL::resolve_promise(realm, api_method_tracker->committed_promise, nhe); |
910 | 0 | } |
911 | | |
912 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#inner-navigate-event-firing-algorithm |
913 | | bool Navigation::inner_navigate_event_firing_algorithm( |
914 | | Bindings::NavigationType navigation_type, |
915 | | JS::NonnullGCPtr<NavigationDestination> destination, |
916 | | UserNavigationInvolvement user_involvement, |
917 | | Optional<Vector<XHR::FormDataEntry>&> form_data_entry_list, |
918 | | Optional<String> download_request_filename, |
919 | | Optional<SerializationRecord> classic_history_api_state) |
920 | 0 | { |
921 | | // NOTE: Specification assumes that ongoing navigation event is cancelled before dispatching next navigation event. |
922 | 0 | if (m_ongoing_navigate_event) |
923 | 0 | abort_the_ongoing_navigation(); |
924 | |
|
925 | 0 | auto& realm = relevant_realm(*this); |
926 | | |
927 | | // 1. If navigation has entries and events disabled, then: |
928 | | // NOTE: These assertions holds because traverseTo(), back(), and forward() will immediately fail when entries and events are disabled |
929 | | // (since there are no entries to traverse to), and if our starting point is instead navigate() or reload(), |
930 | | // then we avoided setting the upcoming non-traverse API method tracker in the first place. |
931 | 0 | if (has_entries_and_events_disabled()) { |
932 | | // 1. Assert: navigation's ongoing API method tracker is null. |
933 | 0 | VERIFY(m_ongoing_api_method_tracker == nullptr); |
934 | | |
935 | | // 2. Assert: navigation's upcoming non-traverse API method tracker is null. |
936 | 0 | VERIFY(m_upcoming_non_traverse_api_method_tracker == nullptr); |
937 | | |
938 | | // 3. Assert: navigation's upcoming traverse API method trackers is empty. |
939 | 0 | VERIFY(m_upcoming_traverse_api_method_trackers.is_empty()); |
940 | | |
941 | | // 4. Return true. |
942 | 0 | return true; |
943 | 0 | } |
944 | | |
945 | | // 2. Let destinationKey be null. |
946 | 0 | Optional<String> destination_key = {}; |
947 | | |
948 | | // 3. If destination's entry is non-null, then set destinationKey to destination's entry's key. |
949 | 0 | if (destination->navigation_history_entry() != nullptr) |
950 | 0 | destination_key = destination->navigation_history_entry()->key(); |
951 | | |
952 | | // 4. Assert: destinationKey is not the empty string. |
953 | 0 | VERIFY(destination_key != ""sv); |
954 | | |
955 | | // 5. Promote an upcoming API method tracker to ongoing given navigation and destinationKey. |
956 | 0 | promote_an_upcoming_api_method_tracker_to_ongoing(destination_key); |
957 | | |
958 | | // 6. Let apiMethodTracker be navigation's ongoing API method tracker. |
959 | 0 | auto api_method_tracker = m_ongoing_api_method_tracker; |
960 | | |
961 | | // 7. Let navigable be navigation's relevant global object's navigable. |
962 | 0 | auto& relevant_global_object = verify_cast<HTML::Window>(Web::HTML::relevant_global_object(*this)); |
963 | 0 | auto navigable = relevant_global_object.navigable(); |
964 | | |
965 | | // 8. Let document be navigation's relevant global object's associated Document. |
966 | 0 | auto& document = relevant_global_object.associated_document(); |
967 | | |
968 | | // Note: We create the Event in this algorithm instead of passing it in, |
969 | | // and have all the following "initialize" steps set up the event init |
970 | 0 | NavigateEventInit event_init = {}; |
971 | | |
972 | | // 9. If document can have its URL rewritten to destination's URL, |
973 | | // and either destination's is same document is true or navigationType is not "traverse", |
974 | | // then initialize event's canIntercept to true. Otherwise, initialize it to false. |
975 | 0 | event_init.can_intercept = can_have_its_url_rewritten(document, destination->raw_url()) && (destination->same_document() || navigation_type != Bindings::NavigationType::Traverse); |
976 | | |
977 | | // 10. Let traverseCanBeCanceled be true if all of the following are true: |
978 | | // - navigable is a top-level traversable; |
979 | | // - destination's is same document is true; and |
980 | | // - either userInvolvement is not "browser UI", or navigation's relevant global object has history-action activation. |
981 | | // Otherwise, let it be false. |
982 | 0 | bool const traverse_can_be_canceled = navigable->is_top_level_traversable() |
983 | 0 | && destination->same_document() |
984 | 0 | && (user_involvement != UserNavigationInvolvement::BrowserUI || relevant_global_object.has_history_action_activation()); |
985 | | |
986 | | // 11. If either: |
987 | | // - navigationType is not "traverse"; or |
988 | | // - traverseCanBeCanceled is true |
989 | | // then initialize event's cancelable to true. Otherwise, initialize it to false. |
990 | 0 | event_init.cancelable = (navigation_type != Bindings::NavigationType::Traverse) || traverse_can_be_canceled; |
991 | | |
992 | | // 12. Initialize event's type to "navigate". |
993 | | // AD-HOC: Happens later, when calling the factory function |
994 | | |
995 | | // 13. Initialize event's navigationType to navigationType. |
996 | 0 | event_init.navigation_type = navigation_type; |
997 | | |
998 | | // 14. Initialize event's destination to destination. |
999 | 0 | event_init.destination = destination; |
1000 | | |
1001 | | // 15. Initialize event's downloadRequest to downloadRequestFilename. |
1002 | 0 | event_init.download_request = move(download_request_filename); |
1003 | | |
1004 | | // 16. If apiMethodTracker is not null, then initialize event's info to apiMethodTracker's info. Otherwise, initialize it to undefined. |
1005 | | // NOTE: At this point apiMethodTracker's info is no longer needed and can be nulled out instead of keeping it alive for the lifetime of the navigation API method tracker. |
1006 | 0 | if (api_method_tracker) { |
1007 | 0 | event_init.info = api_method_tracker->info; |
1008 | 0 | api_method_tracker->info = JS::js_undefined(); |
1009 | 0 | } else { |
1010 | 0 | event_init.info = JS::js_undefined(); |
1011 | 0 | } |
1012 | | |
1013 | | // FIXME: 17: Initialize event's hasUAVisualTransition to true if a visual transition, to display a cached rendered state |
1014 | | // of the document's latest entry, was done by the user agent. Otherwise, initialize it to false. |
1015 | 0 | event_init.has_ua_visual_transition = false; |
1016 | | |
1017 | | // 18. Set event's abort controller to a new AbortController created in navigation's relevant realm. |
1018 | | // AD-HOC: Set on the NavigateEvent later after construction |
1019 | 0 | auto abort_controller = MUST(DOM::AbortController::construct_impl(realm)); |
1020 | | |
1021 | | // 19. Initialize event's signal to event's abort controller's signal. |
1022 | 0 | event_init.signal = abort_controller->signal(); |
1023 | | |
1024 | | // 20. Let currentURL be document's URL. |
1025 | 0 | auto current_url = document.url(); |
1026 | | |
1027 | | // 21. If all of the following are true: |
1028 | | // - event's classic history API state is null; |
1029 | | // - destination's is same document is true; |
1030 | | // - destination's URL equals currentURL with exclude fragments set to true; and |
1031 | | // - destination's URL's fragment is not identical to currentURL's fragment, |
1032 | | // then initialize event's hashChange to true. Otherwise, initialize it to false. |
1033 | 0 | event_init.hash_change = (!classic_history_api_state.has_value() |
1034 | 0 | && destination->same_document() |
1035 | 0 | && destination->raw_url().equals(current_url, URL::ExcludeFragment::Yes) |
1036 | 0 | && destination->raw_url().fragment() != current_url.fragment()); |
1037 | | |
1038 | | // 22. If userInvolvement is not "none", then initialize event's userInitiated to true. Otherwise, initialize it to false. |
1039 | 0 | event_init.user_initiated = user_involvement != UserNavigationInvolvement::None; |
1040 | | |
1041 | | // 23. If formDataEntryList is not null, then initialize event's formData to a new FormData created in navigation's relevant realm, |
1042 | | // associated to formDataEntryList. Otherwise, initialize it to null. |
1043 | 0 | if (form_data_entry_list.has_value()) { |
1044 | 0 | event_init.form_data = MUST(XHR::FormData::construct_impl(realm, form_data_entry_list.release_value())); |
1045 | 0 | } else { |
1046 | 0 | event_init.form_data = nullptr; |
1047 | 0 | } |
1048 | | |
1049 | | // AD-HOC: *Now* we have all the info required to create the event |
1050 | 0 | auto event = NavigateEvent::construct_impl(realm, EventNames::navigate, event_init); |
1051 | 0 | event->set_abort_controller(abort_controller); |
1052 | | |
1053 | | // AD-HOC: This is supposed to be set in "fire a <type> navigate event", and is only non-null when |
1054 | | // we're doing a push or replace. We set it here because we create the event here |
1055 | 0 | event->set_classic_history_api_state(move(classic_history_api_state)); |
1056 | | |
1057 | | // 24. Assert: navigation's ongoing navigate event is null. |
1058 | 0 | VERIFY(m_ongoing_navigate_event == nullptr); |
1059 | | |
1060 | | // 25. Set navigation's ongoing navigate event to event. |
1061 | 0 | m_ongoing_navigate_event = event; |
1062 | | |
1063 | | // 26. Set navigation's focus changed during ongoing navigation to false. |
1064 | 0 | m_focus_changed_during_ongoing_navigation = false; |
1065 | | |
1066 | | // 27. Set navigation's suppress normal scroll restoration during ongoing navigation to false. |
1067 | 0 | m_suppress_scroll_restoration_during_ongoing_navigation = false; |
1068 | | |
1069 | | // 28. Let dispatchResult be the result of dispatching event at navigation. |
1070 | 0 | auto dispatch_result = dispatch_event(*event); |
1071 | | |
1072 | | // 29. If dispatchResult is false: |
1073 | 0 | if (!dispatch_result) { |
1074 | | // 1. If navigationType is "traverse", then consume history-action user activation given navigation's relevant global object. |
1075 | 0 | if (navigation_type == Bindings::NavigationType::Traverse) |
1076 | 0 | relevant_global_object.consume_history_action_user_activation(); |
1077 | | |
1078 | | // 2. If event's abort controller's signal is not aborted, then abort the ongoing navigation given navigation. |
1079 | 0 | if (!event->abort_controller()->signal()->aborted()) |
1080 | 0 | abort_the_ongoing_navigation(); |
1081 | | |
1082 | | // 3. Return false. |
1083 | 0 | return false; |
1084 | 0 | } |
1085 | | |
1086 | | // 30. Let endResultIsSameDocument be true if event's interception state |
1087 | | // is not "none" or event's destination's is same document is true. |
1088 | 0 | bool const end_result_is_same_document = (event->interception_state() != NavigateEvent::InterceptionState::None) || event->destination()->same_document(); |
1089 | | |
1090 | | // 31. Prepare to run script given navigation's relevant settings object. |
1091 | | // NOTE: There's a massive spec note here |
1092 | 0 | TemporaryExecutionContext execution_context { relevant_settings_object(*this), TemporaryExecutionContext::CallbacksEnabled::Yes }; |
1093 | | |
1094 | | // 32. If event's interception state is not "none": |
1095 | 0 | if (event->interception_state() != NavigateEvent::InterceptionState::None) { |
1096 | | // 1. Set event's interception state to "committed". |
1097 | 0 | event->set_interception_state(NavigateEvent::InterceptionState::Committed); |
1098 | | |
1099 | | // 2. Let fromNHE be the current entry of navigation. |
1100 | 0 | auto from_nhe = current_entry(); |
1101 | | |
1102 | | // 3. Assert: fromNHE is not null. |
1103 | 0 | VERIFY(from_nhe != nullptr); |
1104 | | |
1105 | | // 4. Set navigation's transition to a new NavigationTransition created in navigation's relevant realm, |
1106 | | // whose navigation type is navigationType, from entry is fromNHE, and whose finished promise is a new promise |
1107 | | // created in navigation's relevant realm. |
1108 | 0 | m_transition = NavigationTransition::create(realm, navigation_type, *from_nhe, JS::Promise::create(realm)); |
1109 | | |
1110 | | // 5. Mark as handled navigation's transition's finished promise. |
1111 | 0 | m_transition->finished()->set_is_handled(); |
1112 | | |
1113 | | // 6. If navigationType is "traverse", then set navigation's suppress normal scroll restoration during ongoing navigation to true. |
1114 | | // NOTE: If event's scroll behavior was set to "after-transition", then scroll restoration will happen as part of finishing |
1115 | | // the relevant NavigateEvent. Otherwise, there will be no scroll restoration. That is, no navigation which is intercepted |
1116 | | // by intercept() goes through the normal scroll restoration process; scroll restoration for such navigations |
1117 | | // is either done manually, by the web developer, or is done after the transition. |
1118 | 0 | if (navigation_type == Bindings::NavigationType::Traverse) |
1119 | 0 | m_suppress_scroll_restoration_during_ongoing_navigation = true; |
1120 | | |
1121 | | // FIXME: Fix spec typo "serialied" |
1122 | | // 7. If navigationType is "push" or "replace", then run the URL and history update steps given document and |
1123 | | // event's destination's URL, with serialiedData set to event's classic history API state and historyHandling |
1124 | | // set to navigationType. |
1125 | 0 | if (navigation_type == Bindings::NavigationType::Push || navigation_type == Bindings::NavigationType::Replace) { |
1126 | 0 | auto history_handling = navigation_type == Bindings::NavigationType::Push ? HistoryHandlingBehavior::Push : HistoryHandlingBehavior::Replace; |
1127 | 0 | perform_url_and_history_update_steps(document, event->destination()->raw_url(), event->classic_history_api_state(), history_handling); |
1128 | 0 | } |
1129 | | // Big spec note about reload here |
1130 | 0 | } |
1131 | | |
1132 | | // 33. If endResultIsSameDocument is true: |
1133 | 0 | if (end_result_is_same_document) { |
1134 | | // 1. Let promisesList be an empty list. |
1135 | 0 | JS::MarkedVector<JS::NonnullGCPtr<WebIDL::Promise>> promises_list(realm.heap()); |
1136 | | |
1137 | | // 2. For each handler of event's navigation handler list: |
1138 | 0 | for (auto const& handler : event->navigation_handler_list()) { |
1139 | | // 1. Append the result of invoking handler with an empty arguments list to promisesList. |
1140 | 0 | auto result = WebIDL::invoke_callback(handler, {}); |
1141 | | // This *should* be equivalent to converting a promise to a promise capability |
1142 | 0 | promises_list.append(WebIDL::create_resolved_promise(realm, result.value().value())); |
1143 | 0 | } |
1144 | | |
1145 | | // 3. If promisesList's size is 0, then set promisesList to « a promise resolved with undefined ». |
1146 | | // NOTE: There is a subtle timing difference between how waiting for all schedules its success and failure |
1147 | | // steps when given zero promises versus ≥1 promises. For most uses of waiting for all, this does not matter. |
1148 | | // However, with this API, there are so many events and promise handlers which could fire around the same time |
1149 | | // that the difference is pretty easily observable: it can cause the event/promise handler sequence to vary. |
1150 | | // (Some of the events and promises involved include: navigatesuccess / navigateerror, currententrychange, |
1151 | | // dispose, apiMethodTracker's promises, and the navigation.transition.finished promise.) |
1152 | 0 | if (promises_list.size() == 0) { |
1153 | 0 | promises_list.append(WebIDL::create_resolved_promise(realm, JS::js_undefined())); |
1154 | 0 | } |
1155 | | |
1156 | | // 4. Wait for all of promisesList, with the following success steps: |
1157 | 0 | WebIDL::wait_for_all( |
1158 | 0 | realm, promises_list, [event, this, api_method_tracker](auto const&) -> void { |
1159 | | |
1160 | | // FIXME: Spec issue: Event's relevant global objects' *associated document* |
1161 | | // 1. If event's relevant global object is not fully active, then abort these steps. |
1162 | 0 | auto& relevant_global_object = verify_cast<HTML::Window>(HTML::relevant_global_object(*event)); |
1163 | 0 | auto& realm = event->realm(); |
1164 | 0 | if (!relevant_global_object.associated_document().is_fully_active()) |
1165 | 0 | return; |
1166 | | |
1167 | | // 2. If event's abort controller's signal is aborted, then abort these steps. |
1168 | 0 | if (event->abort_controller()->signal()->aborted()) |
1169 | 0 | return; |
1170 | | |
1171 | | // 3. Assert: event equals navigation's ongoing navigate event. |
1172 | 0 | VERIFY(event == m_ongoing_navigate_event); |
1173 | | |
1174 | | // 4. Set navigation's ongoing navigate event to null. |
1175 | 0 | m_ongoing_navigate_event = nullptr; |
1176 | | |
1177 | | // 5. Finish event given true. |
1178 | 0 | event->finish(true); |
1179 | | |
1180 | | // FIXME: Implement https://dom.spec.whatwg.org/#concept-event-fire somewhere |
1181 | | // 6. Fire an event named navigatesuccess at navigation. |
1182 | 0 | dispatch_event(DOM::Event::create(realm, EventNames::navigatesuccess)); |
1183 | | |
1184 | | // 7. If apiMethodTracker is non-null, then resolve the finished promise for apiMethodTracker. |
1185 | 0 | if (api_method_tracker != nullptr) |
1186 | 0 | resolve_the_finished_promise(*api_method_tracker); |
1187 | | |
1188 | | // 8. If navigation's transition is not null, then resolve navigation's transition's finished promise with undefined. |
1189 | 0 | if (m_transition != nullptr) |
1190 | 0 | m_transition->finished()->fulfill(JS::js_undefined()); |
1191 | | |
1192 | | // 9. Set navigation's transition to null. |
1193 | 0 | m_transition = nullptr; }, |
1194 | | // and the following failure steps given reason rejectionReason: |
1195 | 0 | [event, this, api_method_tracker](JS::Value rejection_reason) -> void { |
1196 | | // FIXME: Spec issue: Event's relevant global objects' *associated document* |
1197 | | // 1. If event's relevant global object is not fully active, then abort these steps. |
1198 | 0 | auto& relevant_global_object = verify_cast<HTML::Window>(HTML::relevant_global_object(*event)); |
1199 | 0 | auto& realm = event->realm(); |
1200 | 0 | if (!relevant_global_object.associated_document().is_fully_active()) |
1201 | 0 | return; |
1202 | | |
1203 | | // 2. If event's abort controller's signal is aborted, then abort these steps. |
1204 | 0 | if (event->abort_controller()->signal()->aborted()) |
1205 | 0 | return; |
1206 | | |
1207 | | // 3. Assert: event equals navigation's ongoing navigate event. |
1208 | 0 | VERIFY(event == m_ongoing_navigate_event); |
1209 | | |
1210 | | // 4. Set navigation's ongoing navigate event to null. |
1211 | 0 | m_ongoing_navigate_event = nullptr; |
1212 | | |
1213 | | // 5. Finish event given false. |
1214 | 0 | event->finish(false); |
1215 | | |
1216 | | // 6. Let errorInfo be the result of extracting error information from rejectionReason. |
1217 | 0 | ErrorEventInit event_init = {}; |
1218 | 0 | event_init.error = rejection_reason; |
1219 | | // FIXME: Extract information from the exception and the JS context in the wishy-washy way the spec says here. |
1220 | 0 | event_init.filename = String {}; |
1221 | 0 | event_init.colno = 0; |
1222 | 0 | event_init.lineno = 0; |
1223 | 0 | event_init.message = String {}; |
1224 | | |
1225 | | // 7. Fire an event named navigateerror at navigation using ErrorEvent,with additional attributes initialized according to errorInfo. |
1226 | 0 | dispatch_event(ErrorEvent::create(realm, EventNames::navigateerror, event_init)); |
1227 | | |
1228 | | // 8. If apiMethodTracker is non-null, then reject the finished promise for apiMethodTracker with rejectionReason. |
1229 | 0 | if (api_method_tracker != nullptr) |
1230 | 0 | reject_the_finished_promise(*api_method_tracker, rejection_reason); |
1231 | | |
1232 | | // 9. If navigation's transition is not null, then reject navigation's transition's finished promise with rejectionReason. |
1233 | 0 | if (m_transition) |
1234 | 0 | m_transition->finished()->reject(rejection_reason); |
1235 | | |
1236 | | // 10. Set navigation's transition to null. |
1237 | 0 | m_transition = nullptr; |
1238 | 0 | }); |
1239 | 0 | } |
1240 | | |
1241 | | // 34. Otherwise, if apiMethodTracker is non-null, then clean up apiMethodTracker. |
1242 | 0 | else if (api_method_tracker != nullptr) { |
1243 | 0 | clean_up(*api_method_tracker); |
1244 | 0 | } |
1245 | | |
1246 | | // 35. Clean up after running script given navigation's relevant settings object. |
1247 | | // Handled by TemporaryExecutionContext destructor from step 31 |
1248 | | |
1249 | | // 36. If event's interception state is "none", then return true. |
1250 | | // 37. Return false. |
1251 | 0 | return event->interception_state() == NavigateEvent::InterceptionState::None; |
1252 | 0 | } |
1253 | | |
1254 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#fire-a-traverse-navigate-event |
1255 | | bool Navigation::fire_a_traverse_navigate_event(JS::NonnullGCPtr<SessionHistoryEntry> destination_she, UserNavigationInvolvement user_involvement) |
1256 | 0 | { |
1257 | 0 | auto& realm = relevant_realm(*this); |
1258 | 0 | auto& vm = this->vm(); |
1259 | | |
1260 | | // 1. Let event be the result of creating an event given NavigateEvent, in navigation's relevant realm. |
1261 | | // 2. Set event's classic history API state to null. |
1262 | | // AD-HOC: These are handled in the inner algorithm |
1263 | | |
1264 | | // 3. Let destination be a new NavigationDestination created in navigation's relevant realm. |
1265 | 0 | auto destination = NavigationDestination::create(realm); |
1266 | | |
1267 | | // 4. Set destination's URL to destinationSHE's URL. |
1268 | 0 | destination->set_url(destination_she->url()); |
1269 | | |
1270 | | // 5. Let destinationNHE be the NavigationHistoryEntry in navigation's entry list whose session history entry is destinationSHE, |
1271 | | // or null if no such NavigationHistoryEntry exists. |
1272 | 0 | auto destination_nhe = m_entry_list.find_if([destination_she](auto& nhe) { |
1273 | 0 | return &nhe->session_history_entry() == destination_she; |
1274 | 0 | }); |
1275 | | |
1276 | | // 6. If destinationNHE is non-null, then: |
1277 | 0 | if (destination_nhe != m_entry_list.end()) { |
1278 | | // 1. Set destination's entry to destinationNHE. |
1279 | 0 | destination->set_entry(*destination_nhe); |
1280 | | |
1281 | | // 2. Set destination's state to destinationSHE's navigation API state. |
1282 | 0 | destination->set_state(destination_she->navigation_api_state()); |
1283 | 0 | } |
1284 | | |
1285 | | // 7. Otherwise: |
1286 | 0 | else { |
1287 | | // 1. Set destination's entry to null. |
1288 | 0 | destination->set_entry(nullptr); |
1289 | | |
1290 | | // 2. Set destination's state to StructuredSerializeForStorage(null). |
1291 | 0 | destination->set_state(MUST(structured_serialize_for_storage(vm, JS::js_null()))); |
1292 | 0 | } |
1293 | | |
1294 | | // 8. Set destination's is same document to true if destinationSHE's document is equal to |
1295 | | // navigation's relevant global object's associated Document; otherwise false. |
1296 | 0 | destination->set_is_same_document(destination_she->document() == &verify_cast<Window>(relevant_global_object(*this)).associated_document()); |
1297 | | |
1298 | | // 9. Return the result of performing the inner navigate event firing algorithm given navigation, "traverse", event, destination, userInvolvement, null, and null. |
1299 | | // AD-HOC: We don't pass the event, but we do pass the classic_history_api state at the end to be set later |
1300 | 0 | return inner_navigate_event_firing_algorithm(Bindings::NavigationType::Traverse, destination, user_involvement, {}, {}, {}); |
1301 | 0 | } |
1302 | | |
1303 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#fire-a-push/replace/reload-navigate-event |
1304 | | bool Navigation::fire_a_push_replace_reload_navigate_event( |
1305 | | Bindings::NavigationType navigation_type, |
1306 | | URL::URL destination_url, |
1307 | | bool is_same_document, |
1308 | | UserNavigationInvolvement user_involvement, |
1309 | | Optional<Vector<XHR::FormDataEntry>&> form_data_entry_list, |
1310 | | Optional<SerializationRecord> navigation_api_state, |
1311 | | Optional<SerializationRecord> classic_history_api_state) |
1312 | 0 | { |
1313 | 0 | auto& realm = relevant_realm(*this); |
1314 | 0 | auto& vm = this->vm(); |
1315 | | |
1316 | | // This fulfills the entry requirement: an optional serialized state navigationAPIState (default StructuredSerializeForStorage(null)) |
1317 | 0 | if (!navigation_api_state.has_value()) |
1318 | 0 | navigation_api_state = MUST(structured_serialize_for_storage(vm, JS::js_null())); |
1319 | | |
1320 | | // 1. Let event be the result of creating an event given NavigateEvent, in navigation's relevant realm. |
1321 | | // 2. Set event's classic history API state to classicHistoryAPIState. |
1322 | | // AD-HOC: These are handled in the inner algorithm |
1323 | | |
1324 | | // 3. Let destination be a new NavigationDestination created in navigation's relevant realm. |
1325 | 0 | auto destination = NavigationDestination::create(realm); |
1326 | | |
1327 | | // 4. Set destination's URL to destinationURL. |
1328 | 0 | destination->set_url(destination_url); |
1329 | | |
1330 | | // 5. Set destination's entry to null. |
1331 | 0 | destination->set_entry(nullptr); |
1332 | | |
1333 | | // 6. Set destination's state to navigationAPIState. |
1334 | 0 | destination->set_state(*navigation_api_state); |
1335 | | |
1336 | | // 7. Set destination's is same document to isSameDocument. |
1337 | 0 | destination->set_is_same_document(is_same_document); |
1338 | | |
1339 | | // 8. Return the result of performing the inner navigate event firing algorithm given navigation, |
1340 | | // navigationType, event, destination, userInvolvement, formDataEntryList, and null. |
1341 | | // AD-HOC: We don't pass the event, but we do pass the classic_history_api state at the end to be set later |
1342 | 0 | return inner_navigate_event_firing_algorithm(navigation_type, destination, user_involvement, move(form_data_entry_list), {}, move(classic_history_api_state)); |
1343 | 0 | } |
1344 | | |
1345 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#fire-a-download-request-navigate-event |
1346 | | bool Navigation::fire_a_download_request_navigate_event(URL::URL destination_url, UserNavigationInvolvement user_involvement, String filename) |
1347 | 0 | { |
1348 | 0 | auto& realm = relevant_realm(*this); |
1349 | 0 | auto& vm = this->vm(); |
1350 | | |
1351 | | // 1. Let event be the result of creating an event given NavigateEvent, in navigation's relevant realm. |
1352 | | // 2. Set event's classic history API state to classicHistoryAPIState. |
1353 | | // AD-HOC: These are handled in the inner algorithm |
1354 | | |
1355 | | // 3. Let destination be a new NavigationDestination created in navigation's relevant realm. |
1356 | 0 | auto destination = NavigationDestination::create(realm); |
1357 | | |
1358 | | // 4. Set destination's URL to destinationURL. |
1359 | 0 | destination->set_url(destination_url); |
1360 | | |
1361 | | // 5. Set destination's entry to null. |
1362 | 0 | destination->set_entry(nullptr); |
1363 | | |
1364 | | // 6. Set destination's state to StructuredSerializeForStorage(null). |
1365 | 0 | destination->set_state(MUST(structured_serialize_for_storage(vm, JS::js_null()))); |
1366 | | |
1367 | | // 7. Set destination's is same document to false. |
1368 | 0 | destination->set_is_same_document(false); |
1369 | | |
1370 | | // 8. Return the result of performing the inner navigate event firing algorithm given navigation, |
1371 | | // "push", event, destination, userInvolvement, null, and filename. |
1372 | | // AD-HOC: We don't pass the event, but we do pass the classic_history_api state at the end to be set later |
1373 | 0 | return inner_navigate_event_firing_algorithm(Bindings::NavigationType::Push, destination, user_involvement, {}, move(filename), {}); |
1374 | 0 | } |
1375 | | |
1376 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#initialize-the-navigation-api-entries-for-a-new-document |
1377 | | void Navigation::initialize_the_navigation_api_entries_for_a_new_document(Vector<JS::NonnullGCPtr<SessionHistoryEntry>> const& new_shes, JS::NonnullGCPtr<SessionHistoryEntry> initial_she) |
1378 | 0 | { |
1379 | 0 | auto& realm = relevant_realm(*this); |
1380 | | |
1381 | | // 1. Assert: navigation's entry list is empty. |
1382 | 0 | VERIFY(m_entry_list.is_empty()); |
1383 | | |
1384 | | // 2. Assert: navigation's current entry index is −1. |
1385 | 0 | VERIFY(m_current_entry_index == -1); |
1386 | | |
1387 | | // 3. If navigation has entries and events disabled, then return. |
1388 | 0 | if (has_entries_and_events_disabled()) |
1389 | 0 | return; |
1390 | | |
1391 | | // 4. For each newSHE of newSHEs: |
1392 | 0 | for (auto const& new_she : new_shes) { |
1393 | | // 1. Let newNHE be a new NavigationHistoryEntry created in the relevant realm of navigation. |
1394 | | // 2. Set newNHE's session history entry to newSHE. |
1395 | 0 | auto new_nhe = NavigationHistoryEntry::create(realm, new_she); |
1396 | | |
1397 | | // 3. Append newNHE to navigation's entry list. |
1398 | 0 | m_entry_list.append(new_nhe); |
1399 | 0 | } |
1400 | | |
1401 | | // 5. Set navigation's current entry index to the result of getting the navigation API entry index of initialSHE within navigation. |
1402 | 0 | m_current_entry_index = get_the_navigation_api_entry_index(*initial_she); |
1403 | 0 | } |
1404 | | |
1405 | | // https://html.spec.whatwg.org/multipage/nav-history-apis.html#update-the-navigation-api-entries-for-a-same-document-navigation |
1406 | | void Navigation::update_the_navigation_api_entries_for_a_same_document_navigation(JS::NonnullGCPtr<SessionHistoryEntry> destination_she, Bindings::NavigationType navigation_type) |
1407 | 0 | { |
1408 | 0 | auto& realm = relevant_realm(*this); |
1409 | | |
1410 | | // 1. If navigation has entries and events disabled, then return. |
1411 | 0 | if (has_entries_and_events_disabled()) |
1412 | 0 | return; |
1413 | | |
1414 | | // 2. Let oldCurrentNHE be the current entry of navigation. |
1415 | 0 | auto old_current_nhe = current_entry(); |
1416 | | |
1417 | | // 3. Let disposedNHEs be a new empty list. |
1418 | 0 | Vector<JS::NonnullGCPtr<NavigationHistoryEntry>> disposed_nhes; |
1419 | | |
1420 | | // 4. If navigationType is "traverse", then: |
1421 | 0 | if (navigation_type == Bindings::NavigationType::Traverse) { |
1422 | | // 1. Set navigation's current entry index to the result of getting the navigation API entry index of destinationSHE within navigation. |
1423 | 0 | m_current_entry_index = get_the_navigation_api_entry_index(destination_she); |
1424 | | |
1425 | | // 2. Assert: navigation's current entry index is not −1. |
1426 | | // NOTE: This algorithm is only called for same-document traversals. |
1427 | | // Cross-document traversals will instead call either initialize the navigation API entries for a new document |
1428 | | // or update the navigation API entries for reactivation |
1429 | 0 | VERIFY(m_current_entry_index != -1); |
1430 | 0 | } |
1431 | | |
1432 | | // 5. Otherwise, if navigationType is "push", then: |
1433 | 0 | else if (navigation_type == Bindings::NavigationType::Push) { |
1434 | | // 1. Set navigation's current entry index to navigation's current entry index + 1. |
1435 | 0 | m_current_entry_index++; |
1436 | | |
1437 | | // 2. Let i be navigation's current entry index. |
1438 | 0 | auto i = m_current_entry_index; |
1439 | | |
1440 | | // 3. While i < navigation's entry list's size: |
1441 | 0 | while (i < static_cast<i64>(m_entry_list.size())) { |
1442 | | // 1. Append navigation's entry list[i] to disposedNHEs. |
1443 | 0 | disposed_nhes.append(m_entry_list[i]); |
1444 | | |
1445 | | // 2. Set i to i + 1. |
1446 | 0 | ++i; |
1447 | 0 | } |
1448 | | |
1449 | | // 4. Remove all items in disposedNHEs from navigation's entry list. |
1450 | 0 | m_entry_list.remove(m_current_entry_index, m_entry_list.size() - m_current_entry_index); |
1451 | 0 | } |
1452 | | |
1453 | | // 6. Otherwise, if navigationType is "replace", then: |
1454 | 0 | else if (navigation_type == Bindings::NavigationType::Replace) { |
1455 | 0 | VERIFY(old_current_nhe != nullptr); |
1456 | | |
1457 | | // 1. Append oldCurrentNHE to disposedNHEs. |
1458 | 0 | disposed_nhes.append(*old_current_nhe); |
1459 | 0 | } |
1460 | | |
1461 | | // 7. If navigationType is "push" or "replace", then: |
1462 | 0 | if (navigation_type == Bindings::NavigationType::Push || navigation_type == Bindings::NavigationType::Replace) { |
1463 | | // 1. Let newNHE be a new NavigationHistoryEntry created in the relevant realm of navigation. |
1464 | | // 2. Set newNHE's session history entry to destinationSHE. |
1465 | 0 | auto new_nhe = NavigationHistoryEntry::create(realm, destination_she); |
1466 | |
|
1467 | 0 | VERIFY(m_current_entry_index != -1); |
1468 | | |
1469 | | // 3. Set navigation's entry list[navigation's current entry index] to newNHE. |
1470 | 0 | if (m_current_entry_index < static_cast<i64>(m_entry_list.size())) |
1471 | 0 | m_entry_list[m_current_entry_index] = new_nhe; |
1472 | 0 | else { |
1473 | 0 | VERIFY(m_current_entry_index == static_cast<i64>(m_entry_list.size())); |
1474 | 0 | m_entry_list.append(new_nhe); |
1475 | 0 | } |
1476 | 0 | } |
1477 | | |
1478 | | // 8. If navigation's ongoing API method tracker is non-null, then notify about the committed-to entry |
1479 | | // given navigation's ongoing API method tracker and the current entry of navigation. |
1480 | | // NOTE: It is important to do this before firing the dispose or currententrychange events, |
1481 | | // since event handlers could start another navigation, or otherwise change the value of |
1482 | | // navigation's ongoing API method tracker. |
1483 | 0 | if (m_ongoing_api_method_tracker != nullptr) |
1484 | 0 | notify_about_the_committed_to_entry(*m_ongoing_api_method_tracker, *current_entry()); |
1485 | | |
1486 | | // 9. Prepare to run script given navigation's relevant settings object. |
1487 | 0 | relevant_settings_object(*this).prepare_to_run_script(); |
1488 | | |
1489 | | // 10. Fire an event named currententrychange at navigation using NavigationCurrentEntryChangeEvent, |
1490 | | // with its navigationType attribute initialized to navigationType and its from initialized to oldCurrentNHE. |
1491 | 0 | NavigationCurrentEntryChangeEventInit event_init = {}; |
1492 | 0 | event_init.navigation_type = navigation_type; |
1493 | 0 | event_init.from = old_current_nhe; |
1494 | 0 | dispatch_event(NavigationCurrentEntryChangeEvent::construct_impl(realm, EventNames::currententrychange, event_init)); |
1495 | | |
1496 | | // 11. For each disposedNHE of disposedNHEs: |
1497 | 0 | for (auto& disposed_nhe : disposed_nhes) { |
1498 | | // 1. Fire an event named dispose at disposedNHE. |
1499 | 0 | disposed_nhe->dispatch_event(DOM::Event::create(realm, EventNames::dispose, {})); |
1500 | 0 | } |
1501 | | |
1502 | | // 12. Clean up after running script given navigation's relevant settings object. |
1503 | 0 | relevant_settings_object(*this).clean_up_after_running_script(); |
1504 | 0 | } |
1505 | | |
1506 | | } |