Serenity Operating System
1/*
2 * Copyright (c) 2018-2022, Andreas Kling <kling@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <LibWeb/Bindings/MainThreadVM.h>
8#include <LibWeb/DOM/Document.h>
9#include <LibWeb/DOM/Event.h>
10#include <LibWeb/DOM/HTMLCollection.h>
11#include <LibWeb/DOM/Range.h>
12#include <LibWeb/Fetch/Infrastructure/HTTP/Requests.h>
13#include <LibWeb/HTML/BrowsingContext.h>
14#include <LibWeb/HTML/BrowsingContextContainer.h>
15#include <LibWeb/HTML/BrowsingContextGroup.h>
16#include <LibWeb/HTML/CrossOrigin/CrossOriginOpenerPolicy.h>
17#include <LibWeb/HTML/EventLoop/EventLoop.h>
18#include <LibWeb/HTML/HTMLAnchorElement.h>
19#include <LibWeb/HTML/HTMLInputElement.h>
20#include <LibWeb/HTML/SandboxingFlagSet.h>
21#include <LibWeb/HTML/Scripting/WindowEnvironmentSettingsObject.h>
22#include <LibWeb/HTML/Window.h>
23#include <LibWeb/HTML/WindowProxy.h>
24#include <LibWeb/HighResolutionTime/TimeOrigin.h>
25#include <LibWeb/Infra/Strings.h>
26#include <LibWeb/Layout/BreakNode.h>
27#include <LibWeb/Layout/TextNode.h>
28#include <LibWeb/Layout/Viewport.h>
29#include <LibWeb/Page/Page.h>
30#include <LibWeb/URL/URL.h>
31
32namespace Web::HTML {
33
34// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#matches-about:blank
35static bool url_matches_about_blank(AK::URL const& url)
36{
37 // A URL matches about:blank if its scheme is "about", its path contains a single string "blank", its username and password are the empty string, and its host is null.
38 return url.scheme() == "about"sv
39 && url.path() == "blank"sv
40 && url.username().is_empty()
41 && url.password().is_empty()
42 && url.host().is_null();
43}
44
45// https://html.spec.whatwg.org/multipage/browsers.html#determining-the-origin
46HTML::Origin determine_the_origin(BrowsingContext const& browsing_context, Optional<AK::URL> url, SandboxingFlagSet sandbox_flags, Optional<HTML::Origin> invocation_origin)
47{
48 // 1. If sandboxFlags has its sandboxed origin browsing context flag set, then return a new opaque origin.
49 if (sandbox_flags.flags & SandboxingFlagSet::SandboxedOrigin) {
50 return HTML::Origin {};
51 }
52
53 // 2. If url is null, then return a new opaque origin.
54 if (!url.has_value()) {
55 return HTML::Origin {};
56 }
57
58 // 3. If invocationOrigin is non-null and url matches about:blank, then return invocationOrigin.
59 if (invocation_origin.has_value() && url_matches_about_blank(*url)) {
60 return invocation_origin.value();
61 }
62
63 // 4. If url is about:srcdoc, then return the origin of browsingContext's container document.
64 if (url == AK::URL("about:srcdoc")) {
65 VERIFY(browsing_context.container_document());
66 return browsing_context.container_document()->origin();
67 }
68
69 // 5. Return url's origin.
70 return URL::url_origin(*url);
71}
72
73// https://html.spec.whatwg.org/multipage/browsers.html#creating-a-new-top-level-browsing-context
74JS::NonnullGCPtr<BrowsingContext> BrowsingContext::create_a_new_top_level_browsing_context(Web::Page& page)
75{
76 // 1. Let group be the result of creating a new browsing context group.
77 auto group = BrowsingContextGroup::create_a_new_browsing_context_group(page);
78
79 // 2. Return group's browsing context set[0].
80 return *(*group->browsing_context_set().begin());
81}
82
83// https://html.spec.whatwg.org/multipage/browsers.html#creating-a-new-browsing-context
84JS::NonnullGCPtr<BrowsingContext> BrowsingContext::create_a_new_browsing_context(Page& page, JS::GCPtr<DOM::Document> creator, JS::GCPtr<DOM::Element> embedder, BrowsingContextGroup&)
85{
86 // 1. Let browsingContext be a new browsing context.
87 BrowsingContextContainer* container = (embedder && is<BrowsingContextContainer>(*embedder)) ? static_cast<BrowsingContextContainer*>(embedder.ptr()) : nullptr;
88 auto browsing_context = Bindings::main_thread_vm().heap().allocate_without_realm<BrowsingContext>(page, container);
89
90 // 2. Let unsafeContextCreationTime be the unsafe shared current time.
91 [[maybe_unused]] auto unsafe_context_creation_time = HighResolutionTime::unsafe_shared_current_time();
92
93 // 3. If creator is non-null, then set browsingContext's creator origin to return creator's origin,
94 // browsingContext's creator URL to return creator's URL,
95 // browsingContext's creator base URL to return creator's base URL,
96 // FIXME: and browsingContext's virtual browsing context group ID to creator's top-level browsing context's virtual browsing context group ID.
97 if (creator) {
98 browsing_context->m_creator_origin = creator->origin();
99 browsing_context->m_creator_url = creator->url();
100 browsing_context->m_creator_base_url = creator->base_url();
101 }
102
103 // FIXME: 4. Let sandboxFlags be the result of determining the creation sandboxing flags given browsingContext and embedded.
104 SandboxingFlagSet sandbox_flags;
105
106 // 5. Let origin be the result of determining the origin given browsingContext, about:blank, sandboxFlags, and browsingContext's creator origin.
107 auto origin = determine_the_origin(*browsing_context, AK::URL("about:blank"), sandbox_flags, browsing_context->m_creator_origin);
108
109 // FIXME: 6. Let permissionsPolicy be the result of creating a permissions policy given browsingContext and origin. [PERMISSIONSPOLICY]
110
111 // FIXME: 7. Let agent be the result of obtaining a similar-origin window agent given origin, group, and false.
112
113 JS::GCPtr<Window> window;
114
115 // 8. Let realm execution context be the result of creating a new JavaScript realm given agent and the following customizations:
116 auto realm_execution_context = Bindings::create_a_new_javascript_realm(
117 Bindings::main_thread_vm(),
118 [&](JS::Realm& realm) -> JS::Object* {
119 browsing_context->m_window_proxy = realm.heap().allocate<WindowProxy>(realm, realm).release_allocated_value_but_fixme_should_propagate_errors();
120
121 // - For the global object, create a new Window object.
122 window = HTML::Window::create(realm).release_value_but_fixme_should_propagate_errors();
123 return window.ptr();
124 },
125 [&](JS::Realm&) -> JS::Object* {
126 // - For the global this binding, use browsingContext's WindowProxy object.
127 return browsing_context->m_window_proxy;
128 });
129
130 // 9. Let topLevelCreationURL be about:blank if embedder is null; otherwise embedder's relevant settings object's top-level creation URL.
131 auto top_level_creation_url = !embedder ? AK::URL("about:blank") : relevant_settings_object(*embedder).top_level_creation_url;
132
133 // 10. Let topLevelOrigin be origin if embedder is null; otherwise embedder's relevant settings object's top-level origin.
134 auto top_level_origin = !embedder ? origin : relevant_settings_object(*embedder).origin();
135
136 // 11. Set up a window environment settings object with about:blank, realm execution context, null, topLevelCreationURL, and topLevelOrigin.
137 HTML::WindowEnvironmentSettingsObject::setup(
138 AK::URL("about:blank"),
139 move(realm_execution_context),
140 {},
141 top_level_creation_url,
142 top_level_origin)
143 .release_value_but_fixme_should_propagate_errors();
144
145 // 12. Let loadTimingInfo be a new document load timing info with its navigation start time set to the result of calling
146 // coarsen time with unsafeContextCreationTime and the new environment settings object's cross-origin isolated capability.
147 auto load_timing_info = DOM::DocumentLoadTimingInfo();
148 load_timing_info.navigation_start_time = HighResolutionTime::coarsen_time(
149 unsafe_context_creation_time,
150 verify_cast<WindowEnvironmentSettingsObject>(Bindings::host_defined_environment_settings_object(window->realm())).cross_origin_isolated_capability() == CanUseCrossOriginIsolatedAPIs::Yes);
151
152 // 13. Let coop be a new cross-origin opener policy.
153 auto coop = CrossOriginOpenerPolicy {};
154
155 // 14. If creator is non-null and creator's origin is same origin with creator's relevant settings object's top-level origin,
156 // then set coop to creator's browsing context's top-level browsing context's active document's cross-origin opener policy.
157 if (creator && creator->origin().is_same_origin(relevant_settings_object(*creator).top_level_origin)) {
158 VERIFY(creator->browsing_context());
159 auto* top_level_document = creator->browsing_context()->top_level_browsing_context().active_document();
160 VERIFY(top_level_document);
161 coop = top_level_document->cross_origin_opener_policy();
162 }
163
164 // 15. Let document be a new Document, marked as an HTML document in quirks mode,
165 // whose content type is "text/html",
166 // origin is origin,
167 // FIXME: active sandboxing flag set is sandboxFlags,
168 // FIXME: permissions policy is permissionsPolicy,
169 // cross-origin opener policy is coop,
170 // load timing info is loadTimingInfo,
171 // FIXME: navigation id is null,
172 // and which is ready for post-load tasks.
173 auto document = DOM::Document::create(window->realm()).release_value_but_fixme_should_propagate_errors();
174
175 // Non-standard
176 document->set_window({}, *window);
177 window->set_associated_document(*document);
178
179 document->set_quirks_mode(DOM::QuirksMode::Yes);
180 document->set_content_type("text/html");
181 document->set_origin(origin);
182 document->set_url(AK::URL("about:blank"));
183 document->set_cross_origin_opener_policy(coop);
184 document->set_load_timing_info(load_timing_info);
185 document->set_ready_for_post_load_tasks(true);
186
187 // FIXME: 16. Assert: document's URL and document's relevant settings object's creation URL are about:blank.
188
189 // 17. Set document's is initial about:blank to true.
190 document->set_is_initial_about_blank(true);
191
192 // 18. Ensure that document has a single child html node, which itself has two empty child nodes: a head element, and a body element.
193 auto html_node = document->create_element(HTML::TagNames::html).release_value();
194 MUST(html_node->append_child(document->create_element(HTML::TagNames::head).release_value()));
195 MUST(html_node->append_child(document->create_element(HTML::TagNames::body).release_value()));
196 MUST(document->append_child(html_node));
197
198 // 19. Set the active document of browsingContext to document.
199 browsing_context->set_active_document(*document);
200
201 // 20. If browsingContext's creator URL is non-null, then set document's referrer to the serialization of it.
202 if (browsing_context->m_creator_url.has_value()) {
203 document->set_referrer(browsing_context->m_creator_url->serialize());
204 }
205
206 // FIXME: 21. If creator is non-null, then set document's policy container to a clone of creator's policy container.
207
208 // 22. Append a new session history entry to browsingContext's session history whose URL is about:blank and document is document.
209 browsing_context->m_session_history.append(HTML::SessionHistoryEntry {
210 .url = AK::URL("about:blank"),
211 .document = document.ptr(),
212 .serialized_state = {},
213 .policy_container = {},
214 .scroll_restoration_mode = {},
215 .browsing_context_name = {},
216 .original_source_browsing_context = {},
217 });
218
219 // 23. Completely finish loading document.
220 document->completely_finish_loading();
221
222 // 24. Return browsingContext.
223 return *browsing_context;
224}
225
226BrowsingContext::BrowsingContext(Page& page, HTML::BrowsingContextContainer* container)
227 : m_page(page)
228 , m_loader(*this)
229 , m_event_handler({}, *this)
230 , m_container(container)
231{
232 m_cursor_blink_timer = Platform::Timer::create_repeating(500, [this] {
233 if (!is_focused_context())
234 return;
235 if (m_cursor_position.node() && m_cursor_position.node()->layout_node()) {
236 m_cursor_blink_state = !m_cursor_blink_state;
237 m_cursor_position.node()->layout_node()->set_needs_display();
238 }
239 });
240}
241
242BrowsingContext::~BrowsingContext() = default;
243
244void BrowsingContext::visit_edges(Cell::Visitor& visitor)
245{
246 Base::visit_edges(visitor);
247
248 for (auto& entry : m_session_history)
249 visitor.visit(entry.document);
250 visitor.visit(m_container);
251 visitor.visit(m_window_proxy);
252 visitor.visit(m_opener_browsing_context);
253 visitor.visit(m_group);
254 visitor.visit(m_parent);
255 visitor.visit(m_first_child);
256 visitor.visit(m_last_child);
257 visitor.visit(m_next_sibling);
258 visitor.visit(m_previous_sibling);
259}
260
261void BrowsingContext::did_edit(Badge<EditEventHandler>)
262{
263 reset_cursor_blink_cycle();
264
265 if (m_cursor_position.node() && is<DOM::Text>(*m_cursor_position.node())) {
266 auto& text_node = static_cast<DOM::Text&>(*m_cursor_position.node());
267 if (auto* input_element = text_node.owner_input_element())
268 input_element->did_edit_text_node({});
269 }
270}
271
272void BrowsingContext::reset_cursor_blink_cycle()
273{
274 m_cursor_blink_state = true;
275 m_cursor_blink_timer->restart();
276 if (m_cursor_position.is_valid() && m_cursor_position.node()->layout_node())
277 m_cursor_position.node()->layout_node()->set_needs_display();
278}
279
280// https://html.spec.whatwg.org/multipage/browsers.html#top-level-browsing-context
281bool BrowsingContext::is_top_level() const
282{
283 // A browsing context that has no parent browsing context is the top-level browsing context for itself and all of the browsing contexts for which it is an ancestor browsing context.
284 return !parent();
285}
286
287bool BrowsingContext::is_focused_context() const
288{
289 return m_page && &m_page->focused_context() == this;
290}
291
292// https://html.spec.whatwg.org/multipage/browsers.html#set-the-active-document
293void BrowsingContext::set_active_document(JS::NonnullGCPtr<DOM::Document> document)
294{
295 auto previously_active_document = active_document();
296
297 // 1. Let window be document's relevant global object.
298 auto& window = verify_cast<HTML::Window>(relevant_global_object(document));
299
300 // 2. Set document's visibility state to browsingContext's top-level browsing context's system visibility state.
301 document->set_visibility_state({}, top_level_browsing_context().system_visibility_state());
302
303 // 3. Set browsingContext's active window to window.
304 m_window_proxy->set_window({}, window);
305
306 // 4. Set window's associated Document to document.
307 window.set_associated_document(document);
308
309 // 5. Set window's relevant settings object's execution ready flag.
310 relevant_settings_object(window).execution_ready = true;
311
312 // AD-HOC:
313 document->set_browsing_context(this);
314
315 if (m_page && is_top_level())
316 m_page->client().page_did_change_title(document->title());
317
318 if (previously_active_document && previously_active_document != document.ptr())
319 previously_active_document->did_stop_being_active_document_in_browsing_context({});
320}
321
322void BrowsingContext::set_viewport_rect(CSSPixelRect const& rect)
323{
324 bool did_change = false;
325
326 if (m_size != rect.size()) {
327 m_size = rect.size();
328 if (auto* document = active_document()) {
329 // NOTE: Resizing the viewport changes the reference value for viewport-relative CSS lengths.
330 document->invalidate_style();
331 document->invalidate_layout();
332 }
333 did_change = true;
334 }
335
336 if (m_viewport_scroll_offset != rect.location()) {
337 m_viewport_scroll_offset = rect.location();
338 scroll_offset_did_change();
339 did_change = true;
340 }
341
342 if (did_change) {
343 for (auto* client : m_viewport_clients)
344 client->browsing_context_did_set_viewport_rect(rect);
345 }
346
347 // Schedule the HTML event loop to ensure that a `resize` event gets fired.
348 HTML::main_thread_event_loop().schedule();
349}
350
351void BrowsingContext::set_size(CSSPixelSize size)
352{
353 if (m_size == size)
354 return;
355 m_size = size;
356
357 if (auto* document = active_document()) {
358 document->invalidate_style();
359 document->invalidate_layout();
360 }
361
362 for (auto* client : m_viewport_clients)
363 client->browsing_context_did_set_viewport_rect(viewport_rect());
364
365 // Schedule the HTML event loop to ensure that a `resize` event gets fired.
366 HTML::main_thread_event_loop().schedule();
367}
368
369void BrowsingContext::set_needs_display()
370{
371 set_needs_display(viewport_rect());
372}
373
374void BrowsingContext::set_needs_display(CSSPixelRect const& rect)
375{
376 if (!viewport_rect().intersects(rect))
377 return;
378
379 if (is_top_level()) {
380 if (m_page)
381 m_page->client().page_did_invalidate(to_top_level_rect(rect));
382 return;
383 }
384
385 if (container() && container()->layout_node())
386 container()->layout_node()->set_needs_display();
387}
388
389void BrowsingContext::scroll_to(CSSPixelPoint position)
390{
391 if (active_document())
392 active_document()->force_layout();
393
394 if (m_page)
395 m_page->client().page_did_request_scroll_to(position);
396}
397
398void BrowsingContext::scroll_to_anchor(DeprecatedString const& fragment)
399{
400 JS::GCPtr<DOM::Document> document = active_document();
401 if (!document)
402 return;
403
404 auto element = document->get_element_by_id(fragment);
405 if (!element) {
406 auto candidates = document->get_elements_by_name(fragment);
407 for (auto& candidate : candidates->collect_matching_elements()) {
408 if (is<HTML::HTMLAnchorElement>(*candidate)) {
409 element = &verify_cast<HTML::HTMLAnchorElement>(*candidate);
410 break;
411 }
412 }
413 }
414
415 if (!element)
416 return;
417
418 document->force_layout();
419
420 if (!element->layout_node())
421 return;
422
423 auto& layout_node = *element->layout_node();
424
425 CSSPixelRect target_rect { layout_node.box_type_agnostic_position(), { viewport_rect().width(), viewport_rect().height() } };
426 if (is<Layout::Box>(layout_node)) {
427 auto& layout_box = verify_cast<Layout::Box>(layout_node);
428 auto padding_box = layout_box.box_model().padding_box();
429 target_rect.translate_by(-padding_box.left, -padding_box.top);
430 }
431
432 if (m_page)
433 m_page->client().page_did_request_scroll_into_view(target_rect);
434}
435
436CSSPixelRect BrowsingContext::to_top_level_rect(CSSPixelRect const& a_rect)
437{
438 auto rect = a_rect;
439 rect.set_location(to_top_level_position(a_rect.location()));
440 return rect;
441}
442
443CSSPixelPoint BrowsingContext::to_top_level_position(CSSPixelPoint a_position)
444{
445 auto position = a_position;
446 for (auto ancestor = parent(); ancestor; ancestor = ancestor->parent()) {
447 if (ancestor->is_top_level())
448 break;
449 if (!ancestor->container())
450 return {};
451 if (!ancestor->container()->layout_node())
452 return {};
453 position.translate_by(ancestor->container()->layout_node()->box_type_agnostic_position());
454 }
455 return position;
456}
457
458void BrowsingContext::set_cursor_position(DOM::Position position)
459{
460 if (m_cursor_position == position)
461 return;
462
463 if (m_cursor_position.node() && m_cursor_position.node()->layout_node())
464 m_cursor_position.node()->layout_node()->set_needs_display();
465
466 m_cursor_position = move(position);
467
468 if (m_cursor_position.node() && m_cursor_position.node()->layout_node())
469 m_cursor_position.node()->layout_node()->set_needs_display();
470
471 reset_cursor_blink_cycle();
472}
473
474static DeprecatedString visible_text_in_range(DOM::Range const& range)
475{
476 // NOTE: This is an adaption of Range stringification, but we skip over DOM nodes that don't have a corresponding layout node.
477 StringBuilder builder;
478
479 if (range.start_container() == range.end_container() && is<DOM::Text>(*range.start_container())) {
480 if (!range.start_container()->layout_node())
481 return ""sv;
482 return static_cast<DOM::Text const&>(*range.start_container()).data().substring(range.start_offset(), range.end_offset() - range.start_offset());
483 }
484
485 if (is<DOM::Text>(*range.start_container()) && range.start_container()->layout_node())
486 builder.append(static_cast<DOM::Text const&>(*range.start_container()).data().substring_view(range.start_offset()));
487
488 for (DOM::Node const* node = range.start_container(); node != range.end_container()->next_sibling(); node = node->next_in_pre_order()) {
489 if (is<DOM::Text>(*node) && range.contains_node(*node) && node->layout_node())
490 builder.append(static_cast<DOM::Text const&>(*node).data());
491 }
492
493 if (is<DOM::Text>(*range.end_container()) && range.end_container()->layout_node())
494 builder.append(static_cast<DOM::Text const&>(*range.end_container()).data().substring_view(0, range.end_offset()));
495
496 return builder.to_deprecated_string();
497}
498
499DeprecatedString BrowsingContext::selected_text() const
500{
501 auto* document = active_document();
502 if (!document)
503 return ""sv;
504 auto selection = const_cast<DOM::Document&>(*document).get_selection();
505 auto range = selection->range();
506 if (!range)
507 return ""sv;
508 return visible_text_in_range(*range);
509}
510
511void BrowsingContext::select_all()
512{
513 auto* document = active_document();
514 if (!document)
515 return;
516 auto* body = document->body();
517 if (!body)
518 return;
519 auto selection = document->get_selection();
520 if (!selection)
521 return;
522 (void)selection->select_all_children(*document->body());
523}
524
525void BrowsingContext::register_viewport_client(ViewportClient& client)
526{
527 auto result = m_viewport_clients.set(&client);
528 VERIFY(result == AK::HashSetResult::InsertedNewEntry);
529}
530
531void BrowsingContext::unregister_viewport_client(ViewportClient& client)
532{
533 bool was_removed = m_viewport_clients.remove(&client);
534 VERIFY(was_removed);
535}
536
537void BrowsingContext::register_frame_nesting(AK::URL const& url)
538{
539 m_frame_nesting_levels.ensure(url)++;
540}
541
542bool BrowsingContext::is_frame_nesting_allowed(AK::URL const& url) const
543{
544 return m_frame_nesting_levels.get(url).value_or(0) < 3;
545}
546
547bool BrowsingContext::increment_cursor_position_offset()
548{
549 if (!m_cursor_position.increment_offset())
550 return false;
551 reset_cursor_blink_cycle();
552 return true;
553}
554
555bool BrowsingContext::decrement_cursor_position_offset()
556{
557 if (!m_cursor_position.decrement_offset())
558 return false;
559 reset_cursor_blink_cycle();
560 return true;
561}
562
563DOM::Document* BrowsingContext::container_document()
564{
565 if (auto* container = this->container())
566 return &container->document();
567 return nullptr;
568}
569
570DOM::Document const* BrowsingContext::container_document() const
571{
572 if (auto* container = this->container())
573 return &container->document();
574 return nullptr;
575}
576
577// https://html.spec.whatwg.org/#rendering-opportunity
578bool BrowsingContext::has_a_rendering_opportunity() const
579{
580 // A browsing context has a rendering opportunity if the user agent is currently able to present the contents of the browsing context to the user,
581 // accounting for hardware refresh rate constraints and user agent throttling for performance reasons, but considering content presentable even if it's outside the viewport.
582
583 // FIXME: We should at the very least say `false` here if we're an inactive browser tab.
584 return true;
585}
586
587// https://html.spec.whatwg.org/multipage/interaction.html#currently-focused-area-of-a-top-level-browsing-context
588JS::GCPtr<DOM::Node> BrowsingContext::currently_focused_area()
589{
590 // 1. If topLevelBC does not have system focus, then return null.
591 if (!is_focused_context())
592 return nullptr;
593
594 // 2. Let candidate be topLevelBC's active document.
595 auto* candidate = active_document();
596
597 // 3. While candidate's focused area is a browsing context container with a non-null nested browsing context:
598 // set candidate to the active document of that browsing context container's nested browsing context.
599 while (candidate->focused_element()
600 && is<HTML::BrowsingContextContainer>(candidate->focused_element())
601 && static_cast<HTML::BrowsingContextContainer&>(*candidate->focused_element()).nested_browsing_context()) {
602 candidate = static_cast<HTML::BrowsingContextContainer&>(*candidate->focused_element()).nested_browsing_context()->active_document();
603 }
604
605 // 4. If candidate's focused area is non-null, set candidate to candidate's focused area.
606 if (candidate->focused_element()) {
607 // NOTE: We return right away here instead of assigning to candidate,
608 // since that would require compromising type safety.
609 return candidate->focused_element();
610 }
611
612 // 5. Return candidate.
613 return candidate;
614}
615
616// https://html.spec.whatwg.org/#the-rules-for-choosing-a-browsing-context-given-a-browsing-context-name
617BrowsingContext::ChosenBrowsingContext BrowsingContext::choose_a_browsing_context(StringView name, bool no_opener)
618{
619 // The rules for choosing a browsing context, given a browsing context name name, a browsing context current, and
620 // a boolean noopener are as follows:
621
622 // 1. Let chosen be null.
623 JS::GCPtr<BrowsingContext> chosen = nullptr;
624
625 // 2. Let windowType be "existing or none".
626 auto window_type = WindowType::ExistingOrNone;
627
628 // 3. Let sandboxingFlagSet be current's active document's active sandboxing flag set.
629 auto sandboxing_flag_set = active_document()->active_sandboxing_flag_set();
630
631 // 4. If name is the empty string or an ASCII case-insensitive match for "_self", then set chosen to current.
632 if (name.is_empty() || Infra::is_ascii_case_insensitive_match(name, "_self"sv)) {
633 chosen = this;
634 }
635
636 // 5. Otherwise, if name is an ASCII case-insensitive match for "_parent", set chosen to current's parent browsing
637 // context, if any, and current otherwise.
638 else if (Infra::is_ascii_case_insensitive_match(name, "_parent"sv)) {
639 if (auto parent = this->parent())
640 chosen = parent;
641 else
642 chosen = this;
643 }
644
645 // 6. Otherwise, if name is an ASCII case-insensitive match for "_top", set chosen to current's top-level browsing
646 // context, if any, and current otherwise.
647 else if (Infra::is_ascii_case_insensitive_match(name, "_top"sv)) {
648 chosen = &top_level_browsing_context();
649 }
650
651 // FIXME: 7. Otherwise, if name is not an ASCII case-insensitive match for "_blank", there exists a browsing context
652 // whose name is the same as name, current is familiar with that browsing context, and the user agent
653 // determines that the two browsing contexts are related enough that it is ok if they reach each other,
654 // set chosen to that browsing context. If there are multiple matching browsing contexts, the user agent
655 // should set chosen to one in some arbitrary consistent manner, such as the most recently opened, most
656 // recently focused, or more closely related.
657 else if (!Infra::is_ascii_case_insensitive_match(name, "_blank"sv)) {
658 dbgln("FIXME: Find matching browser context for name {}", name);
659 chosen = this;
660 } else {
661 // 8. Otherwise, a new browsing context is being requested, and what happens depends on the user agent's
662 // configuration and abilities — it is determined by the rules given for the first applicable option from
663 // the following list:
664
665 // --> If current's active window does not have transient activation and the user agent has been configured to
666 // not show popups (i.e., the user agent has a "popup blocker" enabled)
667 VERIFY(m_page);
668 if (!active_window()->has_transient_activation() && m_page->should_block_pop_ups()) {
669 // FIXME: The user agent may inform the user that a popup has been blocked.
670 dbgln("Pop-up blocked!");
671 }
672
673 // --> If sandboxingFlagSet has the sandboxed auxiliary navigation browsing context flag set
674 else if (sandboxing_flag_set.flags & SandboxingFlagSet::SandboxedAuxiliaryNavigation) {
675 // FIXME: The user agent may report to a developer console that a popup has been blocked.
676 dbgln("Pop-up blocked!");
677 }
678
679 // --> If the user agent has been configured such that in this instance it will create a new browsing context
680 else if (true) { // FIXME: When is this the case?
681 // 1. Set windowType to "new and unrestricted".
682 window_type = WindowType::NewAndUnrestricted;
683
684 // 2. If current's top-level browsing context's active document's cross-origin opener policy's value is
685 // "same-origin" or "same-origin-plus-COEP", then:
686 if (top_level_browsing_context().active_document()->cross_origin_opener_policy().value == CrossOriginOpenerPolicyValue::SameOrigin || top_level_browsing_context().active_document()->cross_origin_opener_policy().value == CrossOriginOpenerPolicyValue::SameOriginPlusCOEP) {
687 // 1. Let currentDocument be current's active document.
688 auto* current_document = top_level_browsing_context().active_document();
689
690 // 2. If currentDocument's origin is not same origin with currentDocument's relevant settings object's
691 // top-level origin, then set noopener to true, name to "_blank", and windowType to "new with no opener".
692 if (!current_document->origin().is_same_origin(current_document->relevant_settings_object().top_level_origin)) {
693 no_opener = true;
694 name = "_blank"sv;
695 window_type = WindowType::NewWithNoOpener;
696 }
697 }
698
699 // 3. If noopener is true, then set chosen to the result of creating a new top-level browsing context.
700 if (no_opener) {
701 chosen = HTML::BrowsingContext::create_a_new_top_level_browsing_context(*m_page);
702 }
703
704 // 4. Otherwise:
705 else {
706 // 1. Set chosen to the result of creating a new auxiliary browsing context with current.
707 // FIXME: We have no concept of auxiliary browsing context
708 chosen = HTML::BrowsingContext::create_a_new_top_level_browsing_context(*m_page);
709
710 // 2. If sandboxingFlagSet's sandboxed navigation browsing context flag is set, then current must be
711 // set as chosen's one permitted sandboxed navigator.
712 // FIXME: We have no concept of one permitted sandboxed navigator
713 }
714
715 // 5. If sandboxingFlagSet's sandbox propagates to auxiliary browsing contexts flag is set, then all the
716 // flags that are set in sandboxingFlagSet must be set in chosen's popup sandboxing flag set.
717 // FIXME: Our BrowsingContexts do not have SandboxingFlagSets yet, only documents do
718
719 // 6. If name is not an ASCII case-insensitive match for "_blank", then set chosen's name to name.
720 if (!Infra::is_ascii_case_insensitive_match(name, "_blank"sv))
721 chosen->set_name(name);
722 }
723
724 // --> If the user agent has been configured such that in this instance t will reuse current
725 else if (false) { // FIXME: When is this the case?
726 // Set chosen to current.
727 chosen = *this;
728 }
729
730 // --> If the user agent has been configured such that in this instance it will not find a browsing context
731 else if (false) { // FIXME: When is this the case?
732 // Do nothing.
733 }
734 }
735
736 // 9. Return chosen and windowType.
737 return { chosen, window_type };
738}
739
740// https://html.spec.whatwg.org/multipage/browsers.html#document-tree-child-browsing-context
741size_t BrowsingContext::document_tree_child_browsing_context_count() const
742{
743 size_t count = 0;
744
745 // A browsing context child is a document-tree child browsing context of parent if child is a child browsing context and child's container is in a document tree.
746 for_each_child([this, &count](BrowsingContext const& child) {
747 if (child.is_child_of(*this) && child.container()->in_a_document_tree())
748 ++count;
749 });
750
751 return count;
752}
753
754// https://html.spec.whatwg.org/multipage/browsers.html#child-browsing-context
755bool BrowsingContext::is_child_of(BrowsingContext const& parent) const
756{
757 // A browsing context child is said to be a child browsing context of another browsing context parent,
758 // if child's container document is non-null and child's container document's browsing context is parent.
759 return container_document() && container_document()->browsing_context() == &parent;
760}
761
762// https://html.spec.whatwg.org/multipage/dom.html#still-on-its-initial-about:blank-document
763bool BrowsingContext::still_on_its_initial_about_blank_document() const
764{
765 // A browsing context browsingContext is still on its initial about:blank Document
766 // if browsingContext's session history's size is 1
767 // and browsingContext's session history[0]'s document's is initial about:blank is true.
768 return m_session_history.size() == 1
769 && m_session_history[0].document
770 && m_session_history[0].document->is_initial_about_blank();
771}
772
773DOM::Document const* BrowsingContext::active_document() const
774{
775 auto* window = active_window();
776 if (!window)
777 return nullptr;
778 return &window->associated_document();
779}
780
781DOM::Document* BrowsingContext::active_document()
782{
783 auto* window = active_window();
784 if (!window)
785 return nullptr;
786 return &window->associated_document();
787}
788
789// https://html.spec.whatwg.org/multipage/browsers.html#active-window
790HTML::Window* BrowsingContext::active_window()
791{
792 return m_window_proxy->window();
793}
794
795// https://html.spec.whatwg.org/multipage/browsers.html#active-window
796HTML::Window const* BrowsingContext::active_window() const
797{
798 return m_window_proxy->window();
799}
800
801HTML::WindowProxy* BrowsingContext::window_proxy()
802{
803 return m_window_proxy.ptr();
804}
805
806HTML::WindowProxy const* BrowsingContext::window_proxy() const
807{
808 return m_window_proxy.ptr();
809}
810
811void BrowsingContext::scroll_offset_did_change()
812{
813 // https://w3c.github.io/csswg-drafts/cssom-view-1/#scrolling-events
814 // Whenever a viewport gets scrolled (whether in response to user interaction or by an API), the user agent must run these steps:
815
816 // 1. Let doc be the viewport’s associated Document.
817 auto* doc = active_document();
818 VERIFY(doc);
819
820 // 2. If doc is already in doc’s pending scroll event targets, abort these steps.
821 for (auto& target : doc->pending_scroll_event_targets()) {
822 if (target.ptr() == doc)
823 return;
824 }
825
826 // 3. Append doc to doc’s pending scroll event targets.
827 doc->pending_scroll_event_targets().append(*doc);
828}
829
830BrowsingContextGroup* BrowsingContext::group()
831{
832 return m_group;
833}
834
835void BrowsingContext::set_group(BrowsingContextGroup* group)
836{
837 m_group = group;
838}
839
840// https://html.spec.whatwg.org/multipage/browsers.html#bcg-remove
841void BrowsingContext::remove()
842{
843 // 1. Assert: browsingContext's group is non-null, because a browsing context only gets discarded once.
844 VERIFY(group());
845
846 // 2. Let group be browsingContext's group.
847 JS::NonnullGCPtr<BrowsingContextGroup> group = *this->group();
848
849 // 3. Set browsingContext's group to null.
850 set_group(nullptr);
851
852 // 4. Remove browsingContext from group's browsing context set.
853 group->browsing_context_set().remove(this);
854
855 // 5. If group's browsing context set is empty, then remove group from the user agent's browsing context group set.
856 // NOTE: This is done by ~BrowsingContextGroup() when the refcount reaches 0.
857}
858
859// https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate
860WebIDL::ExceptionOr<void> BrowsingContext::navigate(
861 JS::NonnullGCPtr<Fetch::Infrastructure::Request> resource,
862 BrowsingContext& source_browsing_context,
863 bool exceptions_enabled,
864 HistoryHandlingBehavior history_handling,
865 Optional<PolicyContainer> history_policy_container,
866 DeprecatedString navigation_type,
867 Optional<DeprecatedString> navigation_id,
868 Function<void(JS::NonnullGCPtr<Fetch::Infrastructure::Response>)> process_response_end_of_body)
869{
870 // 1. If resource is a URL, then set resource to a new request whose URL is resource.
871 // NOTE: This function only accepts resources that are already a request, so this is irrelevant.
872
873 // 2. If resource is a request and historyHandling is "reload", then set resource's reload-navigation flag.
874 if (history_handling == HistoryHandlingBehavior::Reload)
875 resource->set_reload_navigation(true);
876
877 // 3. If the source browsing context is not allowed to navigate browsingContext, then:
878 if (!source_browsing_context.is_allowed_to_navigate(*this)) {
879 // 1. If exceptionsEnabled is given and is true, then throw a "SecurityError" DOMException.
880 if (exceptions_enabled) {
881 VERIFY(source_browsing_context.active_document());
882 return WebIDL::SecurityError::create(source_browsing_context.active_document()->realm(), "Source browsing context not allowed to navigate"sv);
883 }
884
885 // FIXME: 2. Otherwise, the user agent may instead offer to open resource in a new top-level browsing context
886 // or in the top-level browsing context of the source browsing context, at the user's option,
887 // in which case the user agent must navigate that designated top-level browsing context
888 // to resource as if the user had requested it independently.
889 }
890
891 // 4. If navigationId is null:
892 if (!navigation_id.has_value()) {
893 // 1. If historyHandling is "reload", and browsingContext's active document's navigation id is not null,
894 if (history_handling == HistoryHandlingBehavior::Reload && active_document()->navigation_id().has_value()) {
895 // let navigationId be browsingContext's active document's navigation id.
896 navigation_id = active_document()->navigation_id();
897 } else {
898 // Otherwise let navigation id be the result of generating a random UUID. [UUID]
899 // FIXME: Generate a UUID.
900 navigation_id = "FIXME";
901 }
902 }
903
904 // FIXME: 5. If browsingContext's active document's unload counter is greater than 0,
905 // then invoke WebDriver BiDi navigation failed
906 // with a WebDriver BiDi navigation status whose id is navigationId, status is "canceled", and url is resource's url
907 // and return.
908
909 // 6. If historyHandling is "default", and any of the following are true:
910 // - browsingContext is still on its initial about:blank Document
911 // - resource is a request whose URL equals browsingContext's active document's URL
912 // - resource is a request whose URL's scheme is "javascript"
913 if (history_handling == HistoryHandlingBehavior::Default
914 && (still_on_its_initial_about_blank_document()
915 || resource->url().equals(active_document()->url())
916 || resource->url().scheme() == "javascript"sv)) {
917 // then set historyHandling to "replace".
918 history_handling = HistoryHandlingBehavior::Replace;
919 }
920
921 // 7. If historyHandling is not "reload", resource is a request,
922 // resource's URL equals browsingContext's active document's URL with exclude fragments set to true,
923 // and resource's URL's fragment is non-null, then:
924 if (history_handling != HistoryHandlingBehavior::Reload
925 && resource->url().equals(active_document()->url(), AK::URL::ExcludeFragment::Yes)
926 && !resource->url().fragment().is_null()) {
927 // 1. Navigate to a fragment given browsingContext, resource's URL, historyHandling, and navigationId.
928 TRY(navigate_to_a_fragment(resource->url(), history_handling, *navigation_id));
929
930 // 2. Return.
931 return {};
932 }
933
934 // FIXME: 8. Let incumbentNavigationOrigin be the origin of the incumbent settings object,
935 // or if no script was involved, the origin of the node document of the element that initiated the navigation.
936
937 // FIXME: 9. Let initiatorPolicyContainer be a clone of the source browsing context's active document's policy container.
938
939 // FIXME: 10. If resource is a request, then set resource's policy container to initiatorPolicyContainer.
940
941 // FIXME: 11. Cancel any preexisting but not yet mature attempt to navigate browsingContext,
942 // including canceling any instances of the fetch algorithm started by those attempts.
943 // If one of those attempts has already created and initialized a new Document object,
944 // abort that Document also.
945 // (Navigation attempts that have matured already have session history entries,
946 // and are therefore handled during the update the session history with the new page algorithm, later.)
947
948 // FIXME: 12. Let unloadPromptResult be the result of calling prompt to unload with the active document of browsingContext.
949 // If this instance of the navigation algorithm gets canceled while this step is running,
950 // the prompt to unload algorithm must nonetheless be run to completion.
951
952 // FIXME: 13. If unloadPromptResult is "refuse", then return a new WebDriver BiDi navigation status whose id is navigationId and status is "canceled".
953
954 // 14. Abort the active document of browsingContext.
955 active_document()->abort();
956
957 // FIXME: 15. If browsingContext is a child browsing context, then put it in the delaying load events mode.
958 // The user agent must take this child browsing context out of the delaying load events mode when this navigation algorithm later matures,
959 // or when it terminates (whether due to having run all the steps, or being canceled, or being aborted),
960 // whichever happens first.
961
962 // FIXME: 16. Let sandboxFlags be the result of determining the creation sandboxing flags given browsingContext and browsingContext's container.
963
964 // FIXME: 17. Let allowedToDownload be the result of running the allowed to download algorithm given the source browsing context and browsingContext.
965
966 // 18. Let hasTransientActivation be true if the source browsing context's active window has transient activation; otherwise false.
967 [[maybe_unused]] bool has_transient_activation = source_browsing_context.active_window()->has_transient_activation();
968
969 // FIXME: 19. Invoke WebDriver BiDi navigation started with browsingContext, and a new WebDriver BiDi navigation status whose id is navigationId, url is resource's url, and status is "pending".
970
971 // 20. Return, and continue running these steps in parallel.
972
973 // FIXME: Implement the rest of this algorithm
974 (void)history_policy_container;
975 (void)navigation_type;
976 (void)process_response_end_of_body;
977
978 // AD-HOC:
979 loader().load(resource->url(), FrameLoader::Type::IFrame);
980 return {};
981}
982
983// https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate-fragid
984WebIDL::ExceptionOr<void> BrowsingContext::navigate_to_a_fragment(AK::URL const& url, HistoryHandlingBehavior history_handling, DeprecatedString navigation_id)
985{
986 // 1. If historyHandling is not "replace",
987 if (history_handling != HistoryHandlingBehavior::Replace) {
988 // FIXME: then remove all the entries in browsingContext's session history after the current entry.
989 // (If the current entry is the last entry in the session history, then no entries are removed.)
990 }
991
992 // 2. Remove any tasks queued by the history traversal task source that are associated with any Document objects
993 // in browsingContext's top-level browsing context's document family.
994 HTML::main_thread_event_loop().task_queue().remove_tasks_matching([&](HTML::Task const& task) {
995 return task.source() == Task::Source::HistoryTraversal
996 && task.document()
997 && top_level_browsing_context().document_family_contains(*task.document());
998 });
999
1000 // 3. Append a new session history entry to the session history whose URL is url,
1001 // document is the current entry's document,
1002 // policy container is the current entry's policy-container
1003 // and scroll restoration mode is the current entry's scroll restoration mode.
1004 m_session_history.append(SessionHistoryEntry {
1005 .url = url,
1006 .document = current_entry().document,
1007 .serialized_state = {},
1008 .policy_container = current_entry().policy_container,
1009 .scroll_restoration_mode = current_entry().scroll_restoration_mode,
1010 .browsing_context_name = {},
1011 .original_source_browsing_context = {},
1012 });
1013
1014 // 4. Traverse the history to the new entry, with historyHandling set to historyHandling.
1015 // This will scroll to the fragment given in what is now the document's URL.
1016 TRY(traverse_the_history(m_session_history.size() - 1, history_handling));
1017
1018 // FIXME: 5. Invoke WebDriver BiDi fragment navigated with browsingContext,
1019 // and a new WebDriver BiDi navigation status whose id is navigationId, url is resource's url, and status is "complete".
1020 (void)navigation_id;
1021
1022 return {};
1023}
1024
1025// https://html.spec.whatwg.org/multipage/browsing-the-web.html#traverse-the-history
1026WebIDL::ExceptionOr<void> BrowsingContext::traverse_the_history(size_t entry_index, HistoryHandlingBehavior history_handling, bool explicit_history_navigation)
1027{
1028 auto* entry = &m_session_history[entry_index];
1029
1030 // 1. If entry's document is null, then:
1031 if (!entry->document) {
1032 // 1. Assert: historyHandling is "default".
1033 VERIFY(history_handling == HistoryHandlingBehavior::Default);
1034
1035 // 2. Let request be a new request whose URL is entry's URL.
1036 auto& vm = Bindings::main_thread_vm();
1037 auto request = Fetch::Infrastructure::Request::create(vm);
1038 request->set_url(entry->url);
1039
1040 // 3. If explicitHistoryNavigation is true, then set request's history-navigation flag.
1041 if (explicit_history_navigation)
1042 request->set_history_navigation(true);
1043
1044 // 4. Navigate the browsing context to request with historyHandling set to "entry update"
1045 // and with historyPolicyContainer set to entry's policy container.
1046 // The navigation must be done using the same source browsing context as was used the first time entry was created.
1047 VERIFY(entry->original_source_browsing_context);
1048 TRY(navigate(request, *entry->original_source_browsing_context, false, HistoryHandlingBehavior::EntryUpdate, entry->policy_container));
1049
1050 // 5. Return.
1051 return {};
1052 }
1053
1054 // FIXME: 2. Save persisted state to the current entry.
1055
1056 // 3. Let newDocument be entry's document.
1057 JS::GCPtr<DOM::Document> new_document = entry->document.ptr();
1058
1059 // 4. Assert: newDocument's is initial about:blank is false,
1060 // i.e., we never traverse back to the initial about:blank Document because it always gets replaced when we navigate away from it.
1061 VERIFY(!new_document->is_initial_about_blank());
1062
1063 // 5. If newDocument is different than the current entry's document, or historyHandling is "entry update" or "reload", then:
1064 if (new_document.ptr() != current_entry().document.ptr()
1065 || history_handling == HistoryHandlingBehavior::EntryUpdate) {
1066 // FIXME: 1. If newDocument's suspended timer handles is not empty:
1067 // FIXME: 1. Assert: newDocument's suspension time is not zero.
1068 // FIXME: 2. Let suspendDuration be the current high resolution time minus newDocument's suspension time.
1069 // FIXME: 3. Let activeTimers be newDocument's relevant global object's map of active timers.
1070 // FIXME: 4. For each handle in newDocument's suspended timer handles, if activeTimers[handle] exists, then increase activeTimers[handle] by suspendDuration.
1071 }
1072
1073 // 2. Remove any tasks queued by the history traversal task source
1074 // that are associated with any Document objects in the top-level browsing context's document family.
1075 HTML::main_thread_event_loop().task_queue().remove_tasks_matching([&](HTML::Task const& task) {
1076 return task.source() == Task::Source::HistoryTraversal
1077 && task.document()
1078 && top_level_browsing_context().document_family_contains(*task.document());
1079 });
1080
1081 // 3. If newDocument's origin is not same origin with the current entry's document's origin, then:
1082 if (!new_document->origin().is_same_origin(current_entry().document->origin())) {
1083 // FIXME: 1. Let entriesToUpdate be all entries in the session history whose document's origin is same origin as the active document
1084 // and that are contiguous with the current entry.
1085 // FIXME: 2. For each entryToUpdate of entriesToUpdate, set entryToUpdate's browsing context name to the current browsing context name.
1086 // FIXME: 3. If the browsing context is a top-level browsing context, but not an auxiliary browsing context whose disowned is false, then set the browsing context's name to the empty string.
1087 }
1088
1089 // 4. Set the active document of the browsing context to newDocument.
1090 set_active_document(*new_document);
1091
1092 // 5. If entry's browsing context name is not null, then:
1093 if (entry->browsing_context_name.has_value()) {
1094 // 1. Set the browsing context's name to entry's browsing context name.
1095 m_name = *entry->browsing_context_name;
1096
1097 // FIXME: 2. Let entriesToUpdate be all entries in the session history whose document's origin is same origin as the new active document's origin and that are contiguous with entry.
1098 // FIXME: 3. For each entryToUpdate of entriesToUpdate, set entryToUpdate's browsing context name to null.
1099 }
1100
1101 // FIXME: 6. If newDocument has any form controls whose autofill field name is "off", invoke the reset algorithm of each of those elements.
1102
1103 // 7. If newDocument's current document readiness "complete",
1104 if (new_document->ready_state() == "complete"sv) {
1105 // then queue a global task on the DOM manipulation task source given newDocument's relevant global object to run the following steps:
1106
1107 queue_global_task(Task::Source::DOMManipulation, relevant_global_object(*new_document), [new_document] {
1108 // 1. If newDocument's page showing flag is true, then abort these steps.
1109 if (new_document->page_showing())
1110 return;
1111
1112 // 2. Set newDocument's page showing flag to true.
1113 new_document->set_page_showing(true);
1114
1115 // 3. Update the visibility state of newDocument to "hidden".
1116 new_document->update_the_visibility_state(VisibilityState::Hidden);
1117
1118 // 4. Fire a page transition event named pageshow at newDocument's relevant global object with true.
1119 auto& window = verify_cast<HTML::Window>(relevant_global_object(*new_document));
1120 window.fire_a_page_transition_event(HTML::EventNames::pageshow, true);
1121 });
1122 }
1123
1124 // 6. Set newDocument's URL to entry's URL.
1125 new_document->set_url(entry->url);
1126
1127 // 7. Let hashChanged be false, and let oldURL and newURL be null.
1128 bool hash_changed = false;
1129 Optional<AK::URL> old_url;
1130 Optional<AK::URL> new_url;
1131
1132 // 8. If entry's URL's fragment is not identical to the current entry's URL's fragment,
1133 // and entry's document equals the current entry's document,
1134 if (entry->url.fragment() != current_entry().url.fragment()
1135 && entry->document.ptr() == current_entry().document.ptr()) {
1136 // then set hashChanged to true, set oldURL to the current entry's URL, and set newURL to entry's URL.
1137 hash_changed = true;
1138 old_url = current_entry().url;
1139 new_url = entry->url;
1140 }
1141
1142 // 9. If historyHandling is "replace", then remove the entry immediately before entry in the session history.
1143 if (history_handling == HistoryHandlingBehavior::Replace) {
1144 // FIXME: This is gnarly.
1145 m_session_history.remove(entry_index - 1);
1146 entry_index--;
1147 entry = &m_session_history[entry_index];
1148 }
1149
1150 // 10. If entry's persisted user state is null, and its URL's fragment is non-null, then scroll to the fragment.
1151 if (!entry->url.fragment().is_null()) {
1152 // FIXME: Implement the full "scroll to the fragment" algorithm:
1153 // https://html.spec.whatwg.org/multipage/browsing-the-web.html#scroll-to-the-fragment-identifier
1154 scroll_to_anchor(entry->url.fragment());
1155 }
1156
1157 // 11. Set the current entry to entry.
1158 m_session_history_index = entry_index;
1159
1160 // 12. Let targetRealm be the current Realm Record.
1161 auto* target_realm = Bindings::main_thread_vm().current_realm();
1162 VERIFY(target_realm);
1163
1164 // FIXME: 13. Let state be null.
1165 // FIXME: 14. If entry's serialized state is not null, then set state to StructuredDeserialize(entry's serialized state, targetRealm).
1166 // If this throws an exception, catch it and ignore the exception.
1167 // FIXME: 15. Set newDocument's History object's state to state.
1168 // FIXME: 16. Let stateChanged be true if newDocument has a latest entry, and that entry is not entry; otherwise let it be false.
1169 // FIXME: 17. Set newDocument's latest entry to entry.
1170 // FIXME: 18. If stateChanged is true, then fire an event named popstate at newDocument's relevant global object, using PopStateEvent, with the state attribute initialized to state.
1171 // FIXME: 19. Restore persisted state from entry.
1172
1173 // 20. If hashChanged is true,
1174 if (hash_changed) {
1175 // then queue a global task on the DOM manipulation task source given newDocument's relevant global object
1176 queue_global_task(Task::Source::DOMManipulation, relevant_global_object(*new_document), [new_document] {
1177 // to fire an event named hashchange at newDocument's relevant global object,
1178 // using HashChangeEvent, with the oldURL attribute initialized to oldURL
1179 // and the newURL attribute initialized to newURL.
1180
1181 // FIXME: Implement a proper HashChangeEvent class.
1182 auto event = DOM::Event::create(verify_cast<HTML::Window>(relevant_global_object(*new_document)).realm(), HTML::EventNames::hashchange).release_value_but_fixme_should_propagate_errors();
1183 new_document->dispatch_event(event);
1184 });
1185 }
1186
1187 return {};
1188}
1189
1190// https://html.spec.whatwg.org/multipage/browsers.html#allowed-to-navigate
1191bool BrowsingContext::is_allowed_to_navigate(BrowsingContext const& other) const
1192{
1193 VERIFY(active_window());
1194 VERIFY(active_document());
1195
1196 // 1. If A is not the same browsing context as B,
1197 // and A is not one of the ancestor browsing contexts of B,
1198 // and B is not a top-level browsing context,
1199 // FIXME: and A's active document's active sandboxing flag set has its sandboxed navigation browsing context flag set,
1200 // then return false.
1201 if (this != &other
1202 && !this->is_ancestor_of(other)
1203 && !other.is_top_level()) {
1204 return false;
1205 }
1206
1207 // 2. Otherwise, if B is a top-level browsing context, and is one of the ancestor browsing contexts of A, then:
1208 if (other.is_top_level() && other.is_ancestor_of(*this)) {
1209 // 1. If A's active window has transient activation
1210 // and A's active document's active sandboxing flag set has its sandboxed top-level navigation with user activation browsing context flag set,
1211 // then return false.
1212 if (active_window()->has_transient_activation()
1213 && active_document()->active_sandboxing_flag_set().flags & SandboxingFlagSet::SandboxedTopLevelNavigationWithUserActivation) {
1214 return false;
1215 }
1216
1217 // 2. Otherwise, if A's active window does not have transient activation
1218 // and A's active document's active sandboxing flag set has its sandboxed top-level navigation without user activation browsing context flag set,
1219 // then return false.
1220 if (!active_window()->has_transient_activation()
1221 && active_document()->active_sandboxing_flag_set().flags & SandboxingFlagSet::SandboxedTopLevelNavigationWithoutUserActivation) {
1222 return false;
1223 }
1224 }
1225
1226 // 3. Otherwise, if B is a top-level browsing context,
1227 // and is neither A nor one of the ancestor browsing contexts of A,
1228 // and A's Document's active sandboxing flag set has its sandboxed navigation browsing context flag set,
1229 // and A is not the one permitted sandboxed navigator of B,
1230 // then return false.
1231 if (other.is_top_level()
1232 && &other != this
1233 && !other.is_ancestor_of(*this)
1234 && active_document()->active_sandboxing_flag_set().flags & SandboxingFlagSet::SandboxedNavigation
1235 && this != other.the_one_permitted_sandboxed_navigator()) {
1236 return false;
1237 }
1238
1239 // 4. Return true.
1240 return true;
1241}
1242
1243// https://html.spec.whatwg.org/multipage/origin.html#one-permitted-sandboxed-navigator
1244BrowsingContext const* BrowsingContext::the_one_permitted_sandboxed_navigator() const
1245{
1246 // FIXME: Implement this.
1247 return nullptr;
1248}
1249
1250// https://html.spec.whatwg.org/multipage/browsers.html#document-family
1251Vector<JS::Handle<DOM::Document>> BrowsingContext::document_family() const
1252{
1253 HashTable<DOM::Document*> documents;
1254 for (auto& entry : m_session_history) {
1255 if (!entry.document)
1256 continue;
1257 if (documents.set(const_cast<DOM::Document*>(entry.document.ptr())) == AK::HashSetResult::ReplacedExistingEntry)
1258 continue;
1259 for (auto& context : entry.document->list_of_descendant_browsing_contexts()) {
1260 for (auto& document : context->document_family()) {
1261 documents.set(document.ptr());
1262 }
1263 }
1264 }
1265
1266 Vector<JS::Handle<DOM::Document>> family;
1267 for (auto* document : documents) {
1268 family.append(*document);
1269 }
1270 return family;
1271}
1272
1273// https://html.spec.whatwg.org/multipage/browsers.html#document-family
1274bool BrowsingContext::document_family_contains(DOM::Document const& document) const
1275{
1276 return document_family().first_matching([&](auto& entry) { return entry.ptr() == &document; }).has_value();
1277}
1278
1279VisibilityState BrowsingContext::system_visibility_state() const
1280{
1281 return m_system_visibility_state;
1282}
1283
1284// https://html.spec.whatwg.org/multipage/interaction.html#system-visibility-state
1285void BrowsingContext::set_system_visibility_state(VisibilityState visibility_state)
1286{
1287 if (m_system_visibility_state == visibility_state)
1288 return;
1289 m_system_visibility_state = visibility_state;
1290
1291 // When a user-agent determines that the system visibility state for top-level browsing context context
1292 // has changed to newState, it must queue a task on the user interaction task source to update
1293 // the visibility state of all the Document objects in the top-level browsing context's document family with newState.
1294 auto document_family = top_level_browsing_context().document_family();
1295
1296 // From the new navigable version, where it tells us what global object to use here:
1297 // 1. Let document be navigable's active document.
1298 // 2. Queue a global task on the user interaction task source given document's relevant global object to update the visibility state of document with newState.
1299 // FIXME: Update this function to fully match the navigable version.
1300 VERIFY(active_document());
1301 queue_global_task(Task::Source::UserInteraction, relevant_global_object(*active_document()), [visibility_state, document_family = move(document_family)] {
1302 for (auto& document : document_family) {
1303 document->update_the_visibility_state(visibility_state);
1304 }
1305 });
1306}
1307
1308// https://html.spec.whatwg.org/multipage/window-object.html#a-browsing-context-is-discarded
1309void BrowsingContext::discard()
1310{
1311 m_has_been_discarded = true;
1312
1313 // 1. Discard all Document objects for all the entries in browsingContext's session history.
1314 for (auto& entry : m_session_history) {
1315 if (entry.document)
1316 entry.document->discard();
1317 }
1318
1319 // AD-HOC:
1320 // FIXME: This should be in the session history!
1321 if (auto* document = active_document())
1322 document->discard();
1323
1324 // 2. If browsingContext is a top-level browsing context, then remove browsingContext.
1325 if (is_top_level())
1326 remove();
1327
1328 // AD-HOC:
1329 if (parent())
1330 parent()->remove_child(*this);
1331}
1332
1333// https://html.spec.whatwg.org/multipage/window-object.html#close-a-browsing-context
1334void BrowsingContext::close()
1335{
1336 VERIFY(active_document());
1337
1338 // FIXME: 1. If the result of calling prompt to unload with browsingContext's active document is "refuse", then return.
1339
1340 // 2. Unload browsingContext's active document.
1341 active_document()->unload();
1342
1343 // 3. Remove browsingContext from the user interface (e.g., close or hide its tab in a tabbed browser).
1344 if (m_page)
1345 m_page->client().page_did_close_browsing_context(*this);
1346
1347 // 4. Discard browsingContext.
1348 discard();
1349}
1350
1351void BrowsingContext::append_child(JS::NonnullGCPtr<BrowsingContext> child)
1352{
1353 VERIFY(!child->m_parent);
1354
1355 if (m_last_child)
1356 m_last_child->m_next_sibling = child;
1357 child->m_previous_sibling = m_last_child;
1358 child->m_parent = this;
1359 m_last_child = child;
1360 if (!m_first_child)
1361 m_first_child = m_last_child;
1362}
1363
1364void BrowsingContext::remove_child(JS::NonnullGCPtr<BrowsingContext> child)
1365{
1366 VERIFY(child->m_parent.ptr() == this);
1367
1368 if (m_first_child == child)
1369 m_first_child = child->m_next_sibling;
1370
1371 if (m_last_child == child)
1372 m_last_child = child->m_previous_sibling;
1373
1374 if (child->m_next_sibling)
1375 child->m_next_sibling->m_previous_sibling = child->m_previous_sibling;
1376
1377 if (child->m_previous_sibling)
1378 child->m_previous_sibling->m_next_sibling = child->m_next_sibling;
1379
1380 child->m_next_sibling = nullptr;
1381 child->m_previous_sibling = nullptr;
1382 child->m_parent = nullptr;
1383}
1384
1385JS::GCPtr<BrowsingContext> BrowsingContext::first_child() const
1386{
1387 return m_first_child;
1388}
1389JS::GCPtr<BrowsingContext> BrowsingContext::next_sibling() const
1390{
1391 return m_next_sibling;
1392}
1393
1394bool BrowsingContext::is_ancestor_of(BrowsingContext const& other) const
1395{
1396 for (auto ancestor = other.parent(); ancestor; ancestor = ancestor->parent()) {
1397 if (ancestor == this)
1398 return true;
1399 }
1400 return false;
1401}
1402
1403}