/* * Copyright (c) 2018-2022, Andreas Kling * Copyright (c) 2021-2022, Linus Groh * Copyright (c) 2021, Luke Wilde * Copyright (c) 2021, Sam Atkins * * 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 namespace Web::DOM { // https://html.spec.whatwg.org/multipage/origin.html#obtain-browsing-context-navigation static JS::NonnullGCPtr obtain_a_browsing_context_to_use_for_a_navigation_response( HTML::BrowsingContext& browsing_context, HTML::SandboxingFlagSet sandbox_flags, HTML::CrossOriginOpenerPolicy navigation_coop, HTML::CrossOriginOpenerPolicyEnforcementResult coop_enforcement_result) { // 1. If browsingContext is not a top-level browsing context, return browsingContext. if (!browsing_context.is_top_level()) return browsing_context; // 2. If coopEnforcementResult's needs a browsing context group switch is false, then: if (!coop_enforcement_result.needs_a_browsing_context_group_switch) { // 1. If coopEnforcementResult's would need a browsing context group switch due to report-only is true, if (coop_enforcement_result.would_need_a_browsing_context_group_switch_due_to_report_only) { // FIXME: set browsing context's virtual browsing context group ID to a new unique identifier. } // 2. Return browsingContext. return browsing_context; } // 3. Let newBrowsingContext be the result of creating a new top-level browsing context. VERIFY(browsing_context.page()); auto new_browsing_context = HTML::BrowsingContext::create_a_new_top_level_browsing_context(*browsing_context.page()); // FIXME: 4. If navigationCOOP's value is "same-origin-plurs-COEP", then set newBrowsingContext's group's // cross-origin isolation mode to either "logical" or "concrete". The choice of which is implementation-defined. // 5. If sandboxFlags is not empty, then: if (!sandbox_flags.is_empty()) { // 1. Assert navigationCOOP's value is "unsafe-none". VERIFY(navigation_coop.value == HTML::CrossOriginOpenerPolicyValue::UnsafeNone); // 2. Assert: newBrowsingContext's popup sandboxing flag set is empty. // 3. Set newBrowsingContext's popup sandboxing flag set to a clone of sandboxFlags. } // 6. Discard browsingContext. browsing_context.discard(); // 7. Return newBrowsingContext. return new_browsing_context; } // https://html.spec.whatwg.org/multipage/browsing-the-web.html#initialise-the-document-object JS::NonnullGCPtr Document::create_and_initialize(Type type, DeprecatedString content_type, HTML::NavigationParams navigation_params) { // 1. Let browsingContext be the result of the obtaining a browsing context to use for a navigation response // given navigationParams's browsing context, navigationParams's final sandboxing flag set, // navigationParams's cross-origin opener policy, and navigationParams's COOP enforcement result. auto browsing_context = obtain_a_browsing_context_to_use_for_a_navigation_response( *navigation_params.browsing_context, navigation_params.final_sandboxing_flag_set, navigation_params.cross_origin_opener_policy, navigation_params.coop_enforcement_result); // FIXME: 2. Let permissionsPolicy be the result of creating a permissions policy from a response // given browsingContext, 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(); } JS::GCPtr window; // 5. If browsingContext is still on its initial about:blank Document, // and navigationParams's history handling is "replace", // and browsingContext's active document's origin is same origin-domain with navigationParams's origin, // then do nothing. if (browsing_context->still_on_its_initial_about_blank_document() && navigation_params.history_handling == HTML::HistoryHandlingBehavior::Replace && (browsing_context->active_document() && browsing_context->active_document()->origin().is_same_origin(navigation_params.origin))) { // Do nothing. // NOTE: This means that both the initial about:blank Document, and the new Document that is about to be created, will share the same Window object. window = browsing_context->active_window(); } // 6. 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. Let topLevelCreationURL be creationURL. auto top_level_creation_url = creation_url; // 7. Let topLevelOrigin be navigationParams's origin. auto top_level_origin = navigation_params.origin; // 8. If browsingContext is not a top-level browsing context, then: if (!browsing_context->is_top_level()) { // 1. Let parentEnvironment be browsingContext's container's relevant settings object. VERIFY(browsing_context->container()); auto& parent_environment = HTML::relevant_settings_object(*browsing_context->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; } // 9. 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( creation_url.value(), move(realm_execution_context), navigation_params.reserved_environment, top_level_creation_url.value(), top_level_origin); } // FIXME: 7. Let loadTimingInfo be a new document load timing info with its navigation start time set to response's timing info's start time. // 8. Let document be a new Document, // whose type is type, // content type is contentType, // origin is navigationParams's origin, // policy container is navigationParams's policy container, // FIXME: permissions policy is permissionsPolicy, // active sandboxing flag set is navigationParams's final sandboxing flag set, // FIXME: and cross-origin opener policy is navigationParams's cross-origin opener policy, // FIXME: load timing info is loadTimingInfo, // and navigation id is navigationParams's id. auto document = Document::create(window->realm()); document->m_type = type; document->m_content_type = move(content_type); document->set_origin(navigation_params.origin); 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->m_window = window; window->set_associated_document(*document); // 9. Set document's URL to creationURL. document->m_url = creation_url.value(); // 10. Set document's current document readiness to "loading". document->m_readiness = HTML::DocumentReadyState::Loading; // FIXME: 11. Run CSP initialization for a Document given document. // 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 = DeprecatedString::empty(); // 2. Let referrer be navigationParams's request's referrer. auto& 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. Let historyHandling be navigationParams's history handling. // FIXME: 14: Let navigationTimingType be the result of switching on navigationParams's history handling... // FIXME: 15. Let redirectCount be 0 if navigationParams's has cross-origin redirects is true; // otherwise navigationParams's request's redirect count. // FIXME: 16. Create the navigation timing entry for document, with navigationParams's response's timing info, // redirectCount, navigationTimingType, and navigationParams's response's service worker timing info. // FIXME: 17. If navigationParams's response has a `Refresh` header, then... // FIXME: 18. If navigationParams's commit early hints is not null, then call navigationParams's commit early hints with document. // FIXME: 19. Process link headers given document, navigationParams's response, and "pre-media". // 20. Return document. return document; } JS::NonnullGCPtr Document::construct_impl(JS::Realm& realm) { return Document::create(realm); } JS::NonnullGCPtr Document::create(JS::Realm& realm, AK::URL const& url) { return realm.heap().allocate(realm, realm, url); } Document::Document(JS::Realm& realm, const AK::URL& url) : ParentNode(realm, *this, NodeType::DOCUMENT_NODE) , m_style_computer(make(*this)) , m_url(url) { HTML::main_thread_event_loop().register_document({}, *this); m_style_update_timer = Platform::Timer::create_single_shot(0, [this] { update_style(); }); m_layout_update_timer = Platform::Timer::create_single_shot(0, [this] { force_layout(); }); } Document::~Document() { HTML::main_thread_event_loop().unregister_document({}, *this); } void Document::initialize(JS::Realm& realm) { Base::initialize(realm); set_prototype(&Bindings::ensure_web_prototype(realm, "Document")); } void Document::visit_edges(Cell::Visitor& visitor) { Base::visit_edges(visitor); visitor.visit(m_window.ptr()); visitor.visit(m_style_sheets.ptr()); visitor.visit(m_hovered_node.ptr()); visitor.visit(m_inspected_node.ptr()); visitor.visit(m_active_favicon.ptr()); visitor.visit(m_focused_element.ptr()); visitor.visit(m_active_element.ptr()); visitor.visit(m_implementation.ptr()); visitor.visit(m_current_script.ptr()); visitor.visit(m_associated_inert_template_document.ptr()); visitor.visit(m_appropriate_template_contents_owner_document); visitor.visit(m_pending_parsing_blocking_script.ptr()); visitor.visit(m_history.ptr()); 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_selection); visitor.visit(m_first_base_element_with_href_in_tree_order); visitor.visit(m_parser); for (auto& script : m_scripts_to_execute_when_parsing_has_finished) visitor.visit(script.ptr()); for (auto& script : m_scripts_to_execute_as_soon_as_possible) visitor.visit(script.ptr()); for (auto& node_iterator : m_node_iterators) visitor.visit(node_iterator); for (auto& target : m_pending_scroll_event_targets) visitor.visit(target.ptr()); for (auto& target : m_pending_scrollend_event_targets) visitor.visit(target.ptr()); } // https://w3c.github.io/selection-api/#dom-document-getselection JS::GCPtr Document::get_selection() { // 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 nullptr; } if (!m_selection) { m_selection = Selection::Selection::create(realm(), *this); } return m_selection; } // https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-document-write WebIDL::ExceptionOr Document::write(Vector const& strings) { StringBuilder builder; builder.join(""sv, strings); return run_the_document_write_steps(builder.build()); } // https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-document-writeln WebIDL::ExceptionOr Document::writeln(Vector const& strings) { StringBuilder builder; builder.join(""sv, strings); builder.append("\n"sv); return run_the_document_write_steps(builder.build()); } // https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#document-write-steps WebIDL::ExceptionOr Document::run_the_document_write_steps(DeprecatedString input) { // 1. 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."); // 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."); // 3. If document's active parser was aborted is true, then return. if (m_active_parser_was_aborted) return {}; // 4. 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()); } // 5. Insert input into the input stream just before the insertion point. m_parser->tokenizer().insert_input_at_insertion_point(input); // 6. If there is no pending parsing-blocking script, have the HTML parser process input, 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(); return {}; } // https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-document-open WebIDL::ExceptionOr Document::open(DeprecatedString const&, DeprecatedString const&) { // 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."); // 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."); // 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."); // 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); // 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(DeprecatedString const& url, DeprecatedString const& name, DeprecatedString const& 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."sv); // 2. Return the result of running the window open steps with url, name, and features. return window().open_impl(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."); // 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."); // 3. If there is no script-created parser associated with the document, then return. if (!m_parser) return {}; // FIXME: 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 {}; // FIXME: 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(); return {}; } HTML::Origin Document::origin() const { return m_origin; } void Document::set_origin(HTML::Origin const& origin) { m_origin = origin; } void Document::schedule_style_update() { if (m_style_update_timer->is_active()) return; m_style_update_timer->start(); } void Document::schedule_layout_update() { if (m_layout_update_timer->is_active()) return; m_layout_update_timer->start(); } 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(); } HTML::HTMLHtmlElement* Document::html_element() { auto* html = document_element(); if (is(html)) return verify_cast(html); return nullptr; } HTML::HTMLHeadElement* Document::head() { auto* html = html_element(); if (!html) return nullptr; return html->first_child_of_type(); } HTML::HTMLElement* Document::body() { auto* html = html_element(); if (!html) return nullptr; auto* first_body = html->first_child_of_type(); if (first_body) return first_body; auto* first_frameset = html->first_child_of_type(); if (first_frameset) return first_frameset; 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'"); 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"); (void)TRY(document_element->append_child(*new_body)); return {}; } DeprecatedString Document::title() const { auto* head_element = head(); if (!head_element) return {}; auto* title_element = head_element->first_child_of_type(); if (!title_element) return {}; auto raw_title = title_element->text_content(); StringBuilder builder; bool last_was_space = false; for (auto code_point : Utf8View(raw_title)) { if (is_ascii_space(code_point)) { last_was_space = true; } else { if (last_was_space && !builder.is_empty()) builder.append(' '); builder.append_code_point(code_point); last_was_space = false; } } return builder.to_deprecated_string(); } void Document::set_title(DeprecatedString const& title) { auto* head_element = const_cast(head()); if (!head_element) return; JS::GCPtr title_element = head_element->first_child_of_type(); if (!title_element) { title_element = &static_cast(*create_element(HTML::TagNames::title).release_value()); MUST(head_element->append_child(*title_element)); } title_element->remove_all_children(true); MUST(title_element->append_child(heap().allocate(realm(), *this, title))); if (auto* page = this->page()) { if (browsing_context() == &page->top_level_browsing_context()) page->client().page_did_change_title(title); } } void Document::tear_down_layout_tree() { if (!m_layout_root) return; // Gather up all the layout nodes in a vector and detach them from parents // while the vector keeps them alive. Vector> layout_nodes; m_layout_root->for_each_in_inclusive_subtree([&](auto& layout_node) { layout_nodes.append(layout_node); return IterationDecision::Continue; }); for (auto& layout_node : layout_nodes) { if (layout_node->parent()) layout_node->parent()->remove_child(*layout_node); } m_layout_root = nullptr; } Color Document::background_color(Gfx::Palette const& palette) 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(); if (color.alpha()) return color; } // If both HTML and BODY are transparent, we fall back to the system's "base" palette color. return palette.base(); } 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) { JS::GCPtr base_element; for_each_in_subtree_of_type([&base_element](HTML::HTMLBaseElement const& base_element_in_tree) { if (base_element_in_tree.has_attribute(HTML::AttributeNames::href)) { base_element = &base_element_in_tree; return IterationDecision::Break; } return IterationDecision::Continue; }); m_first_base_element_with_href_in_tree_order = base_element; } JS::GCPtr Document::first_base_element_with_href_in_tree_order() const { return m_first_base_element_with_href_in_tree_order; } // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#fallback-base-url AK::URL Document::fallback_base_url() const { // FIXME: 1. If document is an iframe srcdoc document, then return the document base URL of document's browsing context's container document. // FIXME: 2. If document's URL is about:blank, and document's browsing context's creator base URL is non-null, then return that creator base URL. // 3. Return document's URL. return m_url; } // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#document-base-url AK::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 AK::URL Document::parse_url(DeprecatedString const& url) const { // FIXME: Pass in document's character encoding. return base_url().complete_url(url); } void Document::set_needs_layout() { if (m_needs_layout) return; m_needs_layout = true; schedule_layout_update(); } void Document::force_layout() { tear_down_layout_tree(); update_layout(); } void Document::invalidate_layout() { tear_down_layout_tree(); schedule_layout_update(); } void Document::update_layout() { // 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 (browsing_context() && browsing_context()->container()) browsing_context()->container()->document().update_layout(); update_style(); if (!m_needs_layout && m_layout_root) return; // NOTE: If this is a document hosting