/* * Copyright (c) 2018-2025, Andreas Kling * Copyright (c) 2021-2023, Linus Groh * Copyright (c) 2021-2025, Luke Wilde * Copyright (c) 2021-2024, Sam Atkins * Copyright (c) 2024, Matthew Olsson * Copyright (c) 2025, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Web::DOM { GC_DEFINE_ALLOCATOR(Document); // https://html.spec.whatwg.org/multipage/origin.html#obtain-browsing-context-navigation static GC::Ref obtain_a_browsing_context_to_use_for_a_navigation_response(HTML::NavigationParams const& navigation_params) { // 1. Let browsingContext be navigationParams's navigable's active browsing context. auto& browsing_context = *navigation_params.navigable->active_browsing_context(); // 2. If browsingContext is not a top-level browsing context, return browsingContext. if (!browsing_context.is_top_level()) return browsing_context; // 3. Let coopEnforcementResult be navigationParams's COOP enforcement result. auto& coop_enforcement_result = navigation_params.coop_enforcement_result; // 4. Let swapGroup be coopEnforcementResult's needs a browsing context group switch. auto swap_group = coop_enforcement_result.needs_a_browsing_context_group_switch; // 5. Let sourceOrigin be browsingContext's active document's origin. auto& source_origin = browsing_context.active_document()->origin(); // 6. Let destinationOrigin be navigationParams's origin. auto& destination_origin = navigation_params.origin; // 7. If sourceOrigin is not same site with destinationOrigin: if (!source_origin.is_same_site(destination_origin)) { // FIXME: 1. If either of sourceOrigin or destinationOrigin have a scheme that is not an HTTP(S) scheme // and the user agent considers it necessary for sourceOrigin and destinationOrigin to be // isolated from each other (for implementation-defined reasons), optionally set swapGroup to true. // FIXME: 2. If navigationParams's user involvement is "browser UI", optionally set swapGroup to true. } // FIXME: 8. If browsingContext's group's browsing context set's size is 1, optionally set swapGroup to true. // 9. If swapGroup is false, then: if (!swap_group) { // 1. If coopEnforcementResult's would need a browsing context group switch due to report-only is true, // set browsingContext's virtual browsing context group ID to a new unique identifier. if (coop_enforcement_result.would_need_a_browsing_context_group_switch_due_to_report_only) { // FIXME: set browsingContext's virtual browsing context group ID to a new unique identifier. } // 2. Return browsingContext. return browsing_context; } // 10. Let newBrowsingContext be the first return value of creating a new top-level browsing context and document. auto browsing_context_and_document = MUST(HTML::create_a_new_top_level_browsing_context_and_document(browsing_context.page())); auto new_browsing_context = browsing_context_and_document.browsing_context; // 11. Let navigationCOOP be navigationParams's cross-origin opener policy. auto navigation_coop = navigation_params.opener_policy; // FIXME: 12. If navigationCOOP's value is "same-origin-plus-COEP", then set newBrowsingContext's group's cross-origin // isolation mode to either "logical" or "concrete". The choice of which is implementation-defined. // 13. Let sandboxFlags be a clone of navigationParams's final sandboxing flag set. auto sandbox_flags = navigation_params.final_sandboxing_flag_set; // 14. If sandboxFlags is not empty, then: if (!is_empty(sandbox_flags)) { // 1. Assert: navigationCOOP's value is "unsafe-none". VERIFY(navigation_coop.value == HTML::OpenerPolicyValue::UnsafeNone); // 2. Assert: newBrowsingContext's popup sandboxing flag set is empty. VERIFY(is_empty(new_browsing_context->popup_sandboxing_flag_set())); // 3. Set newBrowsingContext's popup sandboxing flag set to sandboxFlags. new_browsing_context->set_popup_sandboxing_flag_set(sandbox_flags); } // 15. Return newBrowsingContext. return new_browsing_context; } // https://html.spec.whatwg.org/multipage/browsing-the-web.html#initialise-the-document-object WebIDL::ExceptionOr> Document::create_and_initialize(Type type, String content_type, HTML::NavigationParams const& navigation_params) { // 1. Let browsingContext be the result of obtaining a browsing context to use for a navigation response given navigationParams. auto browsing_context = obtain_a_browsing_context_to_use_for_a_navigation_response(navigation_params); // FIXME: 2. Let permissionsPolicy be the result of creating a permissions policy from a response given navigationParams's navigable's container, navigationParams's origin, and navigationParams's response. // 3. Let creationURL be navigationParams's response's URL. auto creation_url = navigation_params.response->url(); // 4. If navigationParams's request is non-null, then set creationURL to navigationParams's request's current URL. if (navigation_params.request) { creation_url = navigation_params.request->current_url(); } // 5. Let window be null. GC::Ptr window; // 6. If browsingContext's active document's is initial about:blank is true, // and browsingContext's active document's origin is same origin-domain with navigationParams's origin, // then set window to browsingContext's active window. // FIXME: still_on_its_initial_about_blank_document() is not in the spec anymore. // However, replacing this with the spec-mandated is_initial_about_blank() results in the browsing context // holding an incorrect active document for the replace from initial about:blank to the real document. // See #22293 for more details. if (false && (browsing_context->active_document() && browsing_context->active_document()->origin().is_same_origin(navigation_params.origin))) { window = browsing_context->active_window(); } // 7. Otherwise: else { // FIXME: 1. Let oacHeader be the result of getting a structured field value given `Origin-Agent-Cluster` and "item" from response's header list. // FIXME: 2. Let requestsOAC be true if oacHeader is not null and oacHeader[0] is the boolean true; otherwise false. [[maybe_unused]] auto requests_oac = false; // FIXME: 3. If navigationParams's reserved environment is a non-secure context, then set requestsOAC to false. // FIXME: 4. Let agent be the result of obtaining a similar-origin window agent given navigationParams's origin, browsingContext's group, and requestsOAC. // 5. Let realm execution context be the result of creating a new JavaScript realm given agent and the following customizations: auto realm_execution_context = Bindings::create_a_new_javascript_realm( Bindings::main_thread_vm(), [&](JS::Realm& realm) -> JS::Object* { // - For the global object, create a new Window object. window = HTML::Window::create(realm); return window; }, [&](JS::Realm&) -> JS::Object* { // - For the global this binding, use browsingContext's WindowProxy object. return browsing_context->window_proxy(); }); // 6. Set window to the global object of realmExecutionContext's Realm component. window = as(realm_execution_context->realm->global_object()); // 7. Let topLevelCreationURL be creationURL. auto top_level_creation_url = creation_url; // 8. Let topLevelOrigin be navigationParams's origin. auto top_level_origin = navigation_params.origin; // 9. If navigable's container is not null, then: if (navigation_params.navigable->container()) { // 1. Let parentEnvironment be navigable's container's relevant settings object. auto& parent_environment = HTML::relevant_settings_object(*navigation_params.navigable->container()); // 2. Set topLevelCreationURL to parentEnvironment's top-level creation URL. top_level_creation_url = parent_environment.top_level_creation_url; // 3. Set topLevelOrigin to parentEnvironment's top-level origin. top_level_origin = parent_environment.top_level_origin; } // 10. Set up a window environment settings object with creationURL, realm execution context, // navigationParams's reserved environment, topLevelCreationURL, and topLevelOrigin. // FIXME: Why do we assume `creation_url` is non-empty here? Is this a spec bug? // FIXME: Why do we assume `top_level_creation_url` is non-empty here? Is this a spec bug? HTML::WindowEnvironmentSettingsObject::setup( browsing_context->page(), creation_url.value(), move(realm_execution_context), navigation_params.reserved_environment, top_level_creation_url.value(), top_level_origin); } // 8. Let loadTimingInfo be a new document load timing info with its navigation start time set to navigationParams's response's timing info's start time. DOM::DocumentLoadTimingInfo load_timing_info; // AD-HOC: The response object no longer has an associated timing info object. For now, we use response's non-standard response time property, // which represents the time that the time that the response object was created. auto response_creation_time = navigation_params.response->response_time().nanoseconds() / 1e6; load_timing_info.navigation_start_time = HighResolutionTime::coarsen_time(response_creation_time, HTML::relevant_settings_object(*window).cross_origin_isolated_capability() == HTML::CanUseCrossOriginIsolatedAPIs::Yes); // 9. Let document be a new Document, with // type: type // content type: contentType // origin: navigationParams's origin // browsing context: browsingContext // policy container: navigationParams's policy container // FIXME: permissions policy: permissionsPolicy // active sandboxing flag set: navigationParams's final sandboxing flag set // FIXME: opener policy: navigationParams's opener policy // load timing info: loadTimingInfo // FIXME: was created via cross-origin redirects: navigationParams's response's has cross-origin redirects // during-loading navigation ID for WebDriver BiDi: navigationParams's id // URL: creationURL // current document readiness: "loading" // about base URL: navigationParams's about base URL // allow declarative shadow roots: true auto document = HTML::HTMLDocument::create(window->realm()); document->m_type = type; document->m_content_type = move(content_type); document->set_origin(navigation_params.origin); document->set_browsing_context(browsing_context); document->m_policy_container = navigation_params.policy_container; document->m_active_sandboxing_flag_set = navigation_params.final_sandboxing_flag_set; document->m_navigation_id = navigation_params.id; document->set_load_timing_info(load_timing_info); document->set_url(*creation_url); document->m_readiness = HTML::DocumentReadyState::Loading; document->m_about_base_url = navigation_params.about_base_url; document->set_allow_declarative_shadow_roots(true); document->m_window = window; // NOTE: Non-standard: Pull out the Last-Modified header for use in the lastModified property. if (auto maybe_last_modified = navigation_params.response->header_list()->get("Last-Modified"sv.bytes()); maybe_last_modified.has_value()) document->m_last_modified = Core::DateTime::parse("%a, %d %b %Y %H:%M:%S %Z"sv, maybe_last_modified.value()); // NOTE: Non-standard: Pull out the Content-Language header to determine the document's language. if (auto maybe_http_content_language = navigation_params.response->header_list()->get("Content-Language"sv.bytes()); maybe_http_content_language.has_value()) { if (auto maybe_content_language = String::from_utf8(maybe_http_content_language.value()); !maybe_content_language.is_error()) document->m_http_content_language = maybe_content_language.release_value(); } // 10. Set window's associated Document to document. window->set_associated_document(*document); // 11. Run CSP initialization for a Document given document. document->run_csp_initialization(); // 12. If navigationParams's request is non-null, then: if (navigation_params.request) { // 1. Set document's referrer to the empty string. document->m_referrer = String {}; // 2. Let referrer be navigationParams's request's referrer. auto const& referrer = navigation_params.request->referrer(); // 3. If referrer is a URL record, then set document's referrer to the serialization of referrer. if (referrer.has()) { document->m_referrer = referrer.get().serialize(); } } // FIXME: 13: If navigationParams's fetch controller is not null, then: // FIXME: 14. Create the navigation timing entry for document, with navigationParams's response's timing info, redirectCount, navigationParams's navigation timing type, and // navigationParams's response's service worker timing info. // 15. If navigationParams's response has a `Refresh` header, then: if (auto maybe_refresh = navigation_params.response->header_list()->get("Refresh"sv.bytes()); maybe_refresh.has_value()) { // 1. Let value be the isomorphic decoding of the value of the header. auto value = Infra::isomorphic_decode(maybe_refresh.value()); // 2. Run the shared declarative refresh steps with document and value. document->shared_declarative_refresh_steps(value, nullptr); } // FIXME: 16. If navigationParams's commit early hints is not null, then call navigationParams's commit early hints with document. // FIXME: 17. Process link headers given document, navigationParams's response, and "pre-media". // 18. Return document. return document; } WebIDL::ExceptionOr> Document::construct_impl(JS::Realm& realm) { return Document::create(realm); } GC::Ref Document::create(JS::Realm& realm, URL::URL const& url) { return realm.create(realm, url); } GC::Ref Document::create_for_fragment_parsing(JS::Realm& realm) { return realm.create(realm, URL::about_blank(), TemporaryDocumentForFragmentParsing::Yes); } Document::Document(JS::Realm& realm, const URL::URL& url, TemporaryDocumentForFragmentParsing temporary_document_for_fragment_parsing) : ParentNode(realm, *this, NodeType::DOCUMENT_NODE) , m_page(Bindings::principal_host_defined_page(realm)) , m_style_computer(make(*this)) , m_url(url) , m_temporary_document_for_fragment_parsing(temporary_document_for_fragment_parsing) , m_editing_host_manager(EditingHostManager::create(realm, *this)) { m_legacy_platform_object_flags = PlatformObject::LegacyPlatformObjectFlags { .supports_named_properties = true, .has_legacy_override_built_ins_interface_extended_attribute = true, }; m_cursor_blink_timer = Core::Timer::create_repeating(500, [this] { auto cursor_position = this->cursor_position(); if (!cursor_position) return; auto node = cursor_position->node(); if (!node) return; auto navigable = this->navigable(); if (!navigable || !navigable->is_focused()) return; node->document().update_layout(UpdateLayoutReason::CursorBlinkTimer); if (node->paintable()) { m_cursor_blink_state = !m_cursor_blink_state; node->paintable()->set_needs_display(); } }); HTML::main_thread_event_loop().register_document({}, *this); } Document::~Document() { HTML::main_thread_event_loop().unregister_document({}, *this); } void Document::initialize(JS::Realm& realm) { Base::initialize(realm); WEB_SET_PROTOTYPE_FOR_INTERFACE(Document); m_selection = realm.create(realm, *this); m_list_of_available_images = realm.create(); page().client().page_did_create_new_document(*this); } // https://html.spec.whatwg.org/multipage/document-lifecycle.html#populate-with-html/head/body WebIDL::ExceptionOr Document::populate_with_html_head_and_body() { // 1. Let html be the result of creating an element given document, "html", and the HTML namespace. auto html = TRY(DOM::create_element(*this, HTML::TagNames::html, Namespace::HTML)); // 2. Let head be the result of creating an element given document, "head", and the HTML namespace. auto head = TRY(DOM::create_element(*this, HTML::TagNames::head, Namespace::HTML)); // 3. Let body be the result of creating an element given document, "body", and the HTML namespace. auto body = TRY(DOM::create_element(*this, HTML::TagNames::body, Namespace::HTML)); // 4. Append html to document. TRY(append_child(html)); // 5. Append head to html. TRY(html->append_child(head)); // 6. Append body to html. TRY(html->append_child(body)); return {}; } void Document::visit_edges(Cell::Visitor& visitor) { Base::visit_edges(visitor); visitor.visit(m_page); visitor.visit(m_window); visitor.visit(m_layout_root); visitor.visit(m_style_sheets); visitor.visit(m_hovered_node); visitor.visit(m_inspected_node); visitor.visit(m_highlighted_node); visitor.visit(m_active_favicon); visitor.visit(m_focused_element); visitor.visit(m_active_element); visitor.visit(m_target_element); visitor.visit(m_implementation); visitor.visit(m_current_script); visitor.visit(m_associated_inert_template_document); visitor.visit(m_appropriate_template_contents_owner_document); visitor.visit(m_pending_parsing_blocking_script); visitor.visit(m_history); visitor.visit(m_browsing_context); visitor.visit(m_applets); visitor.visit(m_anchors); visitor.visit(m_images); visitor.visit(m_embeds); visitor.visit(m_links); visitor.visit(m_forms); visitor.visit(m_scripts); visitor.visit(m_all); visitor.visit(m_fonts); visitor.visit(m_selection); visitor.visit(m_first_base_element_with_href_in_tree_order); visitor.visit(m_first_base_element_with_target_in_tree_order); visitor.visit(m_parser); visitor.visit(m_lazy_load_intersection_observer); visitor.visit(m_visual_viewport); visitor.visit(m_latest_entry); visitor.visit(m_default_timeline); visitor.visit(m_scripts_to_execute_when_parsing_has_finished); visitor.visit(m_scripts_to_execute_in_order_as_soon_as_possible); visitor.visit(m_scripts_to_execute_as_soon_as_possible); visitor.visit(m_node_iterators); visitor.visit(m_document_observers); visitor.visit(m_document_observers_being_notified); visitor.visit(m_pending_scroll_event_targets); visitor.visit(m_pending_scrollend_event_targets); visitor.visit(m_resize_observers); visitor.visit(m_shared_resource_requests); visitor.visit(m_associated_animation_timelines); visitor.visit(m_list_of_available_images); for (auto* form_associated_element : m_form_associated_elements_with_form_attribute) visitor.visit(form_associated_element->form_associated_element_to_html_element()); visitor.visit(m_potentially_named_elements); for (auto& event : m_pending_animation_event_queue) { visitor.visit(event.event); visitor.visit(event.animation); visitor.visit(event.target); } visitor.visit(m_adopted_style_sheets); visitor.visit(m_shadow_roots); visitor.visit(m_top_layer_elements); visitor.visit(m_top_layer_pending_removals); visitor.visit(m_showing_auto_popover_list); visitor.visit(m_showing_hint_popover_list); visitor.visit(m_console_client); visitor.visit(m_editing_host_manager); visitor.visit(m_local_storage_holder); visitor.visit(m_session_storage_holder); visitor.visit(m_render_blocking_elements); visitor.visit(m_policy_container); } // https://w3c.github.io/selection-api/#dom-document-getselection GC::Ptr Document::get_selection() const { // The method must return the selection associated with this if this has an associated browsing context, // and it must return null otherwise. if (!browsing_context()) return {}; return m_selection; } // https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-document-write WebIDL::ExceptionOr Document::write(Vector const& text) { // The document.write(...text) method steps are to run the document write steps with this, text, false, and "Document write". return run_the_document_write_steps(text, AddLineFeed::No, TrustedTypes::InjectionSink::DocumentWrite); } // https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-document-writeln WebIDL::ExceptionOr Document::writeln(Vector const& text) { // The document.writeln(...text) method steps are to run the document write steps with this, text, true, and "Document writeln". return run_the_document_write_steps(text, AddLineFeed::Yes, TrustedTypes::InjectionSink::DocumentWriteln); } // https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#document-write-steps WebIDL::ExceptionOr Document::run_the_document_write_steps(Vector const& text, AddLineFeed line_feed, TrustedTypes::InjectionSink sink) { // 1. Let string be the empty string. StringBuilder string; // 2. Let isTrusted be false if text contains a string; otherwise true. // FIXME: We currently only accept strings. Revisit this once we support the TrustedHTML type. auto is_trusted = true; // 3. For each value of text: for (auto const& value : text) { // FIXME: 1. If value is a TrustedHTML object, then append value's associated data to string. // 2. Otherwise, append value to string. string.append(value); } // FIXME: 4. If isTrusted is false, set string to the result of invoking the Get Trusted Type compliant string algorithm // with TrustedHTML, this's relevant global object, string, sink, and "script". (void)is_trusted; (void)sink; // 5. If lineFeed is true, append U+000A LINE FEED to string. if (line_feed == AddLineFeed::Yes) string.append('\n'); // 6. If document is an XML document, then throw an "InvalidStateError" DOMException. if (m_type == Type::XML) return WebIDL::InvalidStateError::create(realm(), "write() called on XML document."_string); // 7. If document's throw-on-dynamic-markup-insertion counter is greater than 0, then throw an "InvalidStateError" DOMException. if (m_throw_on_dynamic_markup_insertion_counter > 0) return WebIDL::InvalidStateError::create(realm(), "throw-on-dynamic-markup-insertion-counter greater than zero."_string); // 8. If document's active parser was aborted is true, then return. if (m_active_parser_was_aborted) return {}; // 9. If the insertion point is undefined, then: if (!(m_parser && m_parser->tokenizer().is_insertion_point_defined())) { // 1. If document's unload counter is greater than 0 or document's ignore-destructive-writes counter is greater than 0, then return. if (m_unload_counter > 0 || m_ignore_destructive_writes_counter > 0) return {}; // 2. Run the document open steps with document. TRY(open()); } // 10. Insert string into the input stream just before the insertion point. m_parser->tokenizer().insert_input_at_insertion_point(string.string_view()); // 11. If document's pending parsing-blocking script is null, then have the HTML parser process string, one code // point at a time, processing resulting tokens as they are emitted, and stopping when the tokenizer reaches // the insertion point or when the processing of the tokenizer is aborted by the tree construction stage (this // can happen if a script end tag token is emitted by the tokenizer). if (!pending_parsing_blocking_script()) m_parser->run(HTML::HTMLTokenizer::StopAtInsertionPoint::Yes); return {}; } // https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-document-open WebIDL::ExceptionOr Document::open(Optional const&, Optional const&) { // If document belongs to a child navigable, we need to make sure its initial navigation is done, // because subsequent steps will modify "initial about:blank" to false, which would cause // initial navigation to fail in case it was "about:blank". if (auto navigable = this->navigable(); navigable && navigable->container() && !navigable->container()->content_navigable_has_session_history_entry_and_ready_for_navigation()) { HTML::main_thread_event_loop().spin_processing_tasks_with_source_until(HTML::Task::Source::NavigationAndTraversal, GC::create_function(heap(), [navigable_container = navigable->container()] { return navigable_container->content_navigable_has_session_history_entry_and_ready_for_navigation(); })); } // 1. If document is an XML document, then throw an "InvalidStateError" DOMException exception. if (m_type == Type::XML) return WebIDL::InvalidStateError::create(realm(), "open() called on XML document."_string); // 2. If document's throw-on-dynamic-markup-insertion counter is greater than 0, then throw an "InvalidStateError" DOMException. if (m_throw_on_dynamic_markup_insertion_counter > 0) return WebIDL::InvalidStateError::create(realm(), "throw-on-dynamic-markup-insertion-counter greater than zero."_string); // FIXME: 3. Let entryDocument be the entry global object's associated Document. auto& entry_document = *this; // 4. If document's origin is not same origin to entryDocument's origin, then throw a "SecurityError" DOMException. if (origin() != entry_document.origin()) return WebIDL::SecurityError::create(realm(), "Document.origin() not the same as entryDocument's."_string); // 5. If document has an active parser whose script nesting level is greater than 0, then return document. if (m_parser && m_parser->script_nesting_level() > 0) return this; // 6. Similarly, if document's unload counter is greater than 0, then return document. if (m_unload_counter > 0) return this; // 7. If document's active parser was aborted is true, then return document. if (m_active_parser_was_aborted) return this; // FIXME: 8. If document's browsing context is non-null and there is an existing attempt to navigate document's browsing context, then stop document loading given document. // FIXME: 9. For each shadow-including inclusive descendant node of document, erase all event listeners and handlers given node. // FIXME 10. If document is the associated Document of document's relevant global object, then erase all event listeners and handlers given document's relevant global object. // 11. Replace all with null within document, without firing any mutation events. replace_all(nullptr); // https://w3c.github.io/editing/docs/execCommand/#state-override // When document.open() is called and a document's singleton objects are all replaced by new instances of those // objects, editing state associated with that document (including the CSS styling flag, default single-line // container name, and any state overrides or value overrides) must be reset. set_css_styling_flag(false); set_default_single_line_container_name(HTML::TagNames::div); reset_command_state_overrides(); reset_command_value_overrides(); // 12. If document is fully active, then: if (is_fully_active()) { // 1. Let newURL be a copy of entryDocument's URL. auto new_url = entry_document.url(); // 2. If entryDocument is not document, then set newURL's fragment to null. if (&entry_document != this) new_url.set_fragment({}); // FIXME: 3. Run the URL and history update steps with document and newURL. } // 13. Set document's is initial about:blank to false. set_is_initial_about_blank(false); // FIXME: 14. If document's iframe load in progress flag is set, then set document's mute iframe load flag. // 15. Set document to no-quirks mode. set_quirks_mode(QuirksMode::No); // 16. Create a new HTML parser and associate it with document. This is a script-created parser (meaning that it can be closed by the document.open() and document.close() methods, and that the tokenizer will wait for an explicit call to document.close() before emitting an end-of-file token). The encoding confidence is irrelevant. m_parser = HTML::HTMLParser::create_for_scripting(*this); // 17. Set the insertion point to point at just before the end of the input stream (which at this point will be empty). m_parser->tokenizer().update_insertion_point(); // 18. Update the current document readiness of document to "loading". update_readiness(HTML::DocumentReadyState::Loading); // 19. Return document. return this; } // https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-document-open-window WebIDL::ExceptionOr> Document::open(StringView url, StringView name, StringView features) { // 1. If this is not fully active, then throw an "InvalidAccessError" DOMException exception. if (!is_fully_active()) return WebIDL::InvalidAccessError::create(realm(), "Cannot perform open on a document that isn't fully active."_string); // 2. Return the result of running the window open steps with url, name, and features. return window()->window_open_steps(url, name, features); } // https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#closing-the-input-stream WebIDL::ExceptionOr Document::close() { // 1. If document is an XML document, then throw an "InvalidStateError" DOMException exception. if (m_type == Type::XML) return WebIDL::InvalidStateError::create(realm(), "close() called on XML document."_string); // 2. If document's throw-on-dynamic-markup-insertion counter is greater than 0, then throw an "InvalidStateError" DOMException. if (m_throw_on_dynamic_markup_insertion_counter > 0) return WebIDL::InvalidStateError::create(realm(), "throw-on-dynamic-markup-insertion-counter greater than zero."_string); // 3. If there is no script-created parser associated with the document, then return. if (!m_parser) return {}; // 4. Insert an explicit "EOF" character at the end of the parser's input stream. m_parser->tokenizer().insert_eof(); // 5. If there is a pending parsing-blocking script, then return. if (pending_parsing_blocking_script()) return {}; // 6. Run the tokenizer, processing resulting tokens as they are emitted, and stopping when the tokenizer reaches the explicit "EOF" character or spins the event loop. m_parser->run(); // AD-HOC: This ensures that a load event is fired if the node navigable's container is an iframe. completely_finish_loading(); return {}; } // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-document-defaultview GC::Ptr Document::default_view() { // If this's browsing context is null, then return null. if (!browsing_context()) return {}; // 2. Return this's browsing context's WindowProxy object. return browsing_context()->window_proxy(); } GC::Ptr Document::default_view() const { return const_cast(this)->default_view(); } URL::Origin const& Document::origin() const { return m_origin; } void Document::set_origin(URL::Origin const& origin) { m_origin = origin; } void Document::schedule_style_update() { if (!browsing_context()) return; // NOTE: Update of the style is a step in HTML event loop processing. HTML::main_thread_event_loop().schedule(); } void Document::schedule_layout_update() { if (!browsing_context()) return; // NOTE: Update of the layout is a step in HTML event loop processing. HTML::main_thread_event_loop().schedule(); } bool Document::is_child_allowed(Node const& node) const { switch (node.type()) { case NodeType::DOCUMENT_NODE: case NodeType::TEXT_NODE: return false; case NodeType::COMMENT_NODE: return true; case NodeType::DOCUMENT_TYPE_NODE: return !first_child_of_type(); case NodeType::ELEMENT_NODE: return !first_child_of_type(); default: return false; } } Element* Document::document_element() { return first_child_of_type(); } Element const* Document::document_element() const { return first_child_of_type(); } // https://html.spec.whatwg.org/multipage/dom.html#the-html-element-2 HTML::HTMLHtmlElement* Document::html_element() { // The html element of a document is its document element, if it's an html element, and null otherwise. auto* html = document_element(); if (is(html)) return as(html); return nullptr; } // https://html.spec.whatwg.org/multipage/dom.html#the-head-element-2 HTML::HTMLHeadElement* Document::head() { // The head element of a document is the first head element that is a child of the html element, if there is one, // or null otherwise. auto* html = html_element(); if (!html) return nullptr; return html->first_child_of_type(); } // https://html.spec.whatwg.org/multipage/dom.html#the-title-element-2 GC::Ptr Document::title_element() { // The title element of a document is the first title element in the document (in tree order), if there is one, or // null otherwise. GC::Ptr title_element = nullptr; for_each_in_subtree_of_type([&](auto& title_element_in_tree) { title_element = title_element_in_tree; return TraversalDecision::Break; }); return title_element; } // https://html.spec.whatwg.org/multipage/dom.html#dom-document-dir StringView Document::dir() const { // The dir IDL attribute on Document objects must reflect the dir content attribute of the html // element, if any, limited to only known values. If there is no such element, then the // attribute must return the empty string and do nothing on setting. if (auto html = html_element()) return html->dir(); return ""sv; } // https://html.spec.whatwg.org/multipage/dom.html#dom-document-dir void Document::set_dir(String const& dir) { // The dir IDL attribute on Document objects must reflect the dir content attribute of the html // element, if any, limited to only known values. If there is no such element, then the // attribute must return the empty string and do nothing on setting. if (auto html = html_element()) html->set_dir(dir); } // https://html.spec.whatwg.org/multipage/dom.html#the-body-element-2 HTML::HTMLElement* Document::body() { // The body element of a document is the first of the html element's children that is either // a body element or a frameset element, or null if there is no such element. auto* html = html_element(); if (!html) return nullptr; for (auto* child = html->first_child(); child; child = child->next_sibling()) { if (is(*child) || is(*child)) return static_cast(child); } return nullptr; } // https://html.spec.whatwg.org/multipage/dom.html#dom-document-body WebIDL::ExceptionOr Document::set_body(HTML::HTMLElement* new_body) { if (!is(new_body) && !is(new_body)) return WebIDL::HierarchyRequestError::create(realm(), "Invalid document body element, must be 'body' or 'frameset'"_string); auto* existing_body = body(); if (existing_body) { (void)TRY(existing_body->parent()->replace_child(*new_body, *existing_body)); return {}; } auto* document_element = this->document_element(); if (!document_element) return WebIDL::HierarchyRequestError::create(realm(), "Missing document element"_string); (void)TRY(document_element->append_child(*new_body)); return {}; } // https://html.spec.whatwg.org/multipage/dom.html#document.title String Document::title() const { String value; // 1. If the document element is an SVG svg element, then let value be the child text content of the first SVG title // element that is a child of the document element. if (auto const* document_element = this->document_element(); is(document_element)) { if (auto const* title_element = document_element->first_child_of_type()) value = title_element->child_text_content(); } // 2. Otherwise, let value be the child text content of the title element, or the empty string if the title element // is null. else if (auto title_element = this->title_element()) { value = title_element->text_content().value_or(String {}); } // 3. Strip and collapse ASCII whitespace in value. auto title = Infra::strip_and_collapse_whitespace(value).release_value_but_fixme_should_propagate_errors(); // 4. Return value. return title; } // https://html.spec.whatwg.org/multipage/dom.html#document.title WebIDL::ExceptionOr Document::set_title(String const& title) { auto* document_element = this->document_element(); // -> If the document element is an SVG svg element if (is(document_element)) { GC::Ptr element; // 1. If there is an SVG title element that is a child of the document element, let element be the first such // element. if (auto* title_element = document_element->first_child_of_type()) { element = title_element; } // 2. Otherwise: else { // 1. Let element be the result of creating an element given the document element's node document, "title", // and the SVG namespace. element = TRY(DOM::create_element(*this, HTML::TagNames::title, Namespace::SVG)); // 2. Insert element as the first child of the document element. document_element->insert_before(*element, document_element->first_child()); } // 3. String replace all with the given value within element. element->string_replace_all(title); } // -> If the document element is in the HTML namespace else if (document_element && document_element->namespace_uri() == Namespace::HTML) { auto title_element = this->title_element(); auto* head_element = this->head(); // 1. If the title element is null and the head element is null, then return. if (title_element == nullptr && head_element == nullptr) return {}; GC::Ptr element; // 2. If the title element is non-null, let element be the title element. if (title_element) { element = title_element; } // 3. Otherwise: else { // 1. Let element be the result of creating an element given the document element's node document, "title", // and the HTML namespace. element = TRY(DOM::create_element(*this, HTML::TagNames::title, Namespace::HTML)); // 2. Append element to the head element. TRY(head_element->append_child(*element)); } // 4. String replace all with the given value within element. element->string_replace_all(title); } // -> Otherwise else { // Do nothing. return {}; } return {}; } void Document::tear_down_layout_tree() { m_layout_root = nullptr; m_paintable = nullptr; m_needs_full_layout_tree_update = true; } Color Document::background_color() const { // CSS2 says we should use the HTML element's background color unless it's transparent... if (auto* html_element = this->html_element(); html_element && html_element->layout_node()) { auto color = html_element->layout_node()->computed_values().background_color(); if (color.alpha()) return color; } // ...in which case we use the BODY element's background color. if (auto* body_element = body(); body_element && body_element->layout_node()) { auto color = body_element->layout_node()->computed_values().background_color(); return color; } // By default, the document is transparent. // The outermost canvas is colored by the PageHost. return Color::Transparent; } Vector const* Document::background_layers() const { auto* body_element = body(); if (!body_element) return {}; auto body_layout_node = body_element->layout_node(); if (!body_layout_node) return {}; return &body_layout_node->background_layers(); } void Document::update_base_element(Badge) { GC::Ptr base_element_with_href = nullptr; GC::Ptr base_element_with_target = nullptr; for_each_in_subtree_of_type([&base_element_with_href, &base_element_with_target](HTML::HTMLBaseElement const& base_element_in_tree) { if (!base_element_with_href && base_element_in_tree.has_attribute(HTML::AttributeNames::href)) { base_element_with_href = &base_element_in_tree; if (base_element_with_target) return TraversalDecision::Break; } if (!base_element_with_target && base_element_in_tree.has_attribute(HTML::AttributeNames::target)) { base_element_with_target = &base_element_in_tree; if (base_element_with_href) return TraversalDecision::Break; } return TraversalDecision::Continue; }); m_first_base_element_with_href_in_tree_order = base_element_with_href; m_first_base_element_with_target_in_tree_order = base_element_with_target; } GC::Ptr Document::first_base_element_with_href_in_tree_order() const { return m_first_base_element_with_href_in_tree_order; } GC::Ptr Document::first_base_element_with_target_in_tree_order() const { return m_first_base_element_with_target_in_tree_order; } // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#fallback-base-url URL::URL Document::fallback_base_url() const { // 1. If document is an iframe srcdoc document, then: if (HTML::url_matches_about_srcdoc(m_url)) { // 1. Assert: document's about base URL is non-null. VERIFY(m_about_base_url.has_value()); // 2. Return document's about base URL. return m_about_base_url.value(); } // 2. If document's URL matches about:blank and document's about base URL is non-null, then return document's about base URL. if (HTML::url_matches_about_blank(m_url) && m_about_base_url.has_value()) return m_about_base_url.value(); // 3. Return document's URL. return m_url; } // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#document-base-url URL::URL Document::base_url() const { // 1. If there is no base element that has an href attribute in the Document, then return the Document's fallback base URL. auto base_element = first_base_element_with_href_in_tree_order(); if (!base_element) return fallback_base_url(); // 2. Otherwise, return the frozen base URL of the first base element in the Document that has an href attribute, in tree order. return base_element->frozen_base_url(); } // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#parse-a-url Optional Document::parse_url(StringView url) const { // 1. Let baseURL be environment's base URL, if environment is a Document object; otherwise environment's API base URL. auto base_url = this->base_url(); // 2. Return the result of applying the URL parser to url, with baseURL. return DOMURL::parse(url, base_url); } // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#encoding-parsing-a-url Optional Document::encoding_parse_url(StringView url) const { // 1. Let encoding be UTF-8. // 2. If environment is a Document object, then set encoding to environment's character encoding. auto encoding = encoding_or_default(); // 3. Otherwise, if environment's relevant global object is a Window object, set encoding to environment's relevant // global object's associated Document's character encoding. // 4. Let baseURL be environment's base URL, if environment is a Document object; otherwise environment's API base URL. auto base_url = this->base_url(); // 5. Return the result of applying the URL parser to url, with baseURL and encoding. return DOMURL::parse(url, base_url, encoding); } // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#encoding-parsing-and-serializing-a-url Optional Document::encoding_parse_and_serialize_url(StringView url) const { // 1. Let url be the result of encoding-parsing a URL given url, relative to environment. auto parsed_url = encoding_parse_url(url); // 2. If url is failure, then return failure. if (!parsed_url.has_value()) return {}; // 3. Return the result of applying the URL serializer to url. return parsed_url->serialize(); } void Document::invalidate_layout_tree(InvalidateLayoutTreeReason reason) { if (m_layout_root) dbgln_if(UPDATE_LAYOUT_DEBUG, "DROP TREE {}", to_string(reason)); tear_down_layout_tree(); schedule_layout_update(); } static void propagate_scrollbar_width_to_viewport(Element& root_element, Layout::Viewport& viewport) { // https://drafts.csswg.org/css-scrollbars/#scrollbar-width // UAs must apply the scrollbar-color value set on the root element to the viewport. auto& viewport_computed_values = viewport.mutable_computed_values(); auto& root_element_computed_values = root_element.layout_node()->computed_values(); viewport_computed_values.set_scrollbar_width(root_element_computed_values.scrollbar_width()); } // https://drafts.csswg.org/css-overflow-3/#overflow-propagation static void propagate_overflow_to_viewport(Element& root_element, Layout::Viewport& viewport) { // https://drafts.csswg.org/css-contain-2/#contain-property // Additionally, when any containments are active on either the HTML or elements, propagation of // properties from the element to the initial containing block, the viewport, or the canvas background, is // disabled. Notably, this affects: // - 'overflow' and its longhands (see CSS Overflow 3 § 3.3 Overflow Viewport Propagation) if (root_element.is_html_html_element() && !root_element.computed_properties()->contain().is_empty()) return; auto* body_element = root_element.first_child_of_type(); if (body_element && !body_element->computed_properties()->contain().is_empty()) return; // UAs must apply the overflow-* values set on the root element to the viewport // when the root element’s display value is not none. auto overflow_origin_node = root_element.layout_node(); auto& viewport_computed_values = viewport.mutable_computed_values(); // However, when the root element is an [HTML] html element (including XML syntax for HTML) // whose overflow value is visible (in both axes), and that element has as a child // a body element whose display value is also not none, // user agents must instead apply the overflow-* values of the first such child element to the viewport. if (root_element.is_html_html_element()) { auto root_element_layout_node = root_element.layout_node(); auto& root_element_computed_values = root_element_layout_node->mutable_computed_values(); if (root_element_computed_values.overflow_x() == CSS::Overflow::Visible && root_element_computed_values.overflow_y() == CSS::Overflow::Visible) { auto* body_element = root_element.first_child_of_type(); if (body_element && body_element->layout_node()) overflow_origin_node = body_element->layout_node(); } } // NOTE: This is where we assign the chosen overflow values to the viewport. auto& overflow_origin_computed_values = overflow_origin_node->mutable_computed_values(); viewport_computed_values.set_overflow_x(overflow_origin_computed_values.overflow_x()); viewport_computed_values.set_overflow_y(overflow_origin_computed_values.overflow_y()); // The element from which the value is propagated must then have a used overflow value of visible. overflow_origin_computed_values.set_overflow_x(CSS::Overflow::Visible); overflow_origin_computed_values.set_overflow_y(CSS::Overflow::Visible); } void Document::update_layout(UpdateLayoutReason reason) { auto navigable = this->navigable(); if (!navigable || navigable->active_document() != this) return; // NOTE: If our parent document needs a relayout, we must do that *first*. // This is necessary as the parent layout may cause our viewport to change. if (navigable->container() && &navigable->container()->document() != this) navigable->container()->document().update_layout(reason); update_style(); if (!m_needs_layout_update && m_layout_root) return; // NOTE: If this is a document hosting