From aaa954f900816deb82d4b545bb6b8e28e7971432 Mon Sep 17 00:00:00 2001 From: sin-ack Date: Tue, 15 Mar 2022 14:37:58 +0000 Subject: [PATCH] LibWeb: Bring handling of anchor elements closer to spec This commit moves the regular handling of links to the anchor elements' activation behavior, and implements a few auxiliary algorithms as defined by the HTML specification. Note that certain things such as javascript links, fragments and opening a new tab are still handled directly in EventHandler, but they have been moved to handle_mouseup so that it behaves closer to how it would if it was entirely up-to-spec. --- .../Libraries/LibWeb/HTML/BrowsingContext.cpp | 114 ++++++++++++++++ .../Libraries/LibWeb/HTML/BrowsingContext.h | 2 + .../LibWeb/HTML/HTMLAnchorElement.cpp | 41 ++++++ .../Libraries/LibWeb/HTML/HTMLAnchorElement.h | 12 +- .../Libraries/LibWeb/HTML/HTMLAreaElement.h | 9 +- .../LibWeb/HTML/HTMLHyperlinkElementUtils.cpp | 122 ++++++++++++++++++ .../LibWeb/HTML/HTMLHyperlinkElementUtils.h | 11 +- .../Libraries/LibWeb/Page/EventHandler.cpp | 86 ++++++------ 8 files changed, 357 insertions(+), 40 deletions(-) diff --git a/Userland/Libraries/LibWeb/HTML/BrowsingContext.cpp b/Userland/Libraries/LibWeb/HTML/BrowsingContext.cpp index 53aed5d5f2..80a7c0454a 100644 --- a/Userland/Libraries/LibWeb/HTML/BrowsingContext.cpp +++ b/Userland/Libraries/LibWeb/HTML/BrowsingContext.cpp @@ -428,4 +428,118 @@ RefPtr BrowsingContext::currently_focused_area() return candidate; } +BrowsingContext* BrowsingContext::choose_a_browsing_context(StringView name, bool) +{ + // The rules for choosing a browsing context, given a browsing context name + // name, a browsing context current, and a boolean noopener are as follows: + + // 1. Let chosen be null. + BrowsingContext* chosen = nullptr; + + // FIXME: 2. Let windowType be "existing or none". + + // FIXME: 3. Let sandboxingFlagSet be current's active document's active + // sandboxing flag set. + + // 4. If name is the empty string or an ASCII case-insensitive match for "_self", then set chosen to current. + if (name.is_empty() || name.equals_ignoring_case("_self"sv)) + chosen = this; + + // 5. Otherwise, if name is an ASCII case-insensitive match for "_parent", + // set chosen to current's parent browsing context, if any, and current + // otherwise. + if (name.equals_ignoring_case("_parent"sv)) { + if (auto* parent = this->parent()) + chosen = parent; + else + chosen = this; + } + + // 6. Otherwise, if name is an ASCII case-insensitive match for "_top", set + // chosen to current's top-level browsing context, if any, and current + // otherwise. + if (name.equals_ignoring_case("_top"sv)) { + chosen = &top_level_browsing_context(); + } + + // FIXME: 7. Otherwise, if name is not an ASCII case-insensitive match for + // "_blank", there exists a browsing context whose name is the same as name, + // current is familiar with that browsing context, and the user agent + // determines that the two browsing contexts are related enough that it is + // ok if they reach each other, set chosen to that browsing context. If + // there are multiple matching browsing contexts, the user agent should set + // chosen to one in some arbitrary consistent manner, such as the most + // recently opened, most recently focused, or more closely related. + if (!name.equals_ignoring_case("_blank"sv)) { + chosen = this; + } else { + // 8. Otherwise, a new browsing context is being requested, and what + // happens depends on the user agent's configuration and abilities — it + // is determined by the rules given for the first applicable option from + // the following list: + dbgln("FIXME: Create a new browsing context!"); + + // --> If current's active window does not have transient activation and + // the user agent has been configured to not show popups (i.e., the + // user agent has a "popup blocker" enabled) + // + // The user agent may inform the user that a popup has been blocked. + + // --> If sandboxingFlagSet has the sandboxed auxiliary navigation + // browsing context flag set + // + // The user agent may report to a developer console that a popup has + // been blocked. + + // --> If the user agent has been configured such that in this instance + // it will create a new browsing context + // + // 1. Set windowType to "new and unrestricted". + + // 2. If current's top-level browsing context's active document's + // cross-origin opener policy's value is "same-origin" or + // "same-origin-plus-COEP", then: + + // 2.1. Let currentDocument be current's active document. + + // 2.2. If currentDocument's origin is not same origin with + // currentDocument's relevant settings object's top-level + // origin, then set noopener to true, name to "_blank", and + // windowType to "new with no opener". + + // 3. If noopener is true, then set chosen to the result of creating + // a new top-level browsing context. + + // 4. Otherwise: + + // 4.1. Set chosen to the result of creating a new auxiliary + // browsing context with current. + + // 4.2. If sandboxingFlagSet's sandboxed navigation browsing + // context flag is set, then current must be set as chosen's one + // permitted sandboxed navigator. + + // 5. If sandboxingFlagSet's sandbox propagates to auxiliary + // browsing contexts flag is set, then all the flags that are set in + // sandboxingFlagSet must be set in chosen's popup sandboxing flag + // set. + + // 6. If name is not an ASCII case-insensitive match for "_blank", + // then set chosen's name to name. + + // --> If the user agent has been configured such that in this instance + // it will reuse current + // + // Set chosen to current. + + // --> If the user agent has been configured such that in this instance + // it will not find a browsing context + // + // Do nothing. + } + + // 9. Return chosen and windowType. + return chosen; +} + } diff --git a/Userland/Libraries/LibWeb/HTML/BrowsingContext.h b/Userland/Libraries/LibWeb/HTML/BrowsingContext.h index 96980f8608..636893cf12 100644 --- a/Userland/Libraries/LibWeb/HTML/BrowsingContext.h +++ b/Userland/Libraries/LibWeb/HTML/BrowsingContext.h @@ -76,6 +76,8 @@ public: BrowsingContext const& top_level_browsing_context() const { return const_cast(this)->top_level_browsing_context(); } + BrowsingContext* choose_a_browsing_context(StringView name, bool noopener); + HTML::BrowsingContextContainer* container() { return m_container; } HTML::BrowsingContextContainer const* container() const { return m_container; } diff --git a/Userland/Libraries/LibWeb/HTML/HTMLAnchorElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLAnchorElement.cpp index 665b119027..3b2533fd13 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLAnchorElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLAnchorElement.cpp @@ -11,6 +11,9 @@ namespace Web::HTML { HTMLAnchorElement::HTMLAnchorElement(DOM::Document& document, DOM::QualifiedName qualified_name) : HTMLElement(document, move(qualified_name)) { + activation_behavior = [this](auto const& event) { + run_activation_behavior(event); + }; } HTMLAnchorElement::~HTMLAnchorElement() = default; @@ -33,4 +36,42 @@ void HTMLAnchorElement::set_hyperlink_element_utils_href(String href) set_attribute(HTML::AttributeNames::href, move(href)); } +void HTMLAnchorElement::run_activation_behavior(Web::DOM::Event const&) +{ + // The activation behavior of an a element element given an event event is: + + // 1. If element has no href attribute, then return. + if (href().is_empty()) + return; + + // 2. Let hyperlinkSuffix be null. + Optional hyperlink_suffix {}; + + // FIXME: 3. If event's target is an img with an ismap attribute + // specified, then: + // 3.1. Let x and y be 0. + // + // 3.2. If event's isTrusted attribute is initialized to true, then + // set x to the distance in CSS pixels from the left edge of the image + // to the location of the click, and set y to the distance in CSS + // pixels from the top edge of the image to the location of the click. + // + // 3.3. If x is negative, set x to 0. + // + // 3.4. If y is negative, set y to 0. + // + // 3.5. Set hyperlinkSuffix to the concatenation of U+003F (?), the + // value of x expressed as a base-ten integer using ASCII digits, + // U+002C (,), and the value of y expressed as a base-ten integer + // using ASCII digits. + + // FIXME: 4. If element has a download attribute, or if the user has + // expressed a preference to download the hyperlink, then download the + // hyperlink created by element given hyperlinkSuffix. + + // 5. Otherwise, follow the hyperlink created by element given + // hyperlinkSuffix. + follow_the_hyperlink(hyperlink_suffix); +} + } diff --git a/Userland/Libraries/LibWeb/HTML/HTMLAnchorElement.h b/Userland/Libraries/LibWeb/HTML/HTMLAnchorElement.h index 8fc77c2ada..b55236dd83 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLAnchorElement.h +++ b/Userland/Libraries/LibWeb/HTML/HTMLAnchorElement.h @@ -21,19 +21,29 @@ public: virtual ~HTMLAnchorElement() override; String target() const { return attribute(HTML::AttributeNames::target); } + String download() const { return attribute(HTML::AttributeNames::download); } virtual bool is_focusable() const override { return has_attribute(HTML::AttributeNames::href); } virtual bool is_html_anchor_element() const override { return true; } private: + void run_activation_behavior(Web::DOM::Event const&); + // ^DOM::Element virtual void parse_attribute(FlyString const& name, String const& value) override; // ^HTML::HTMLHyperlinkElementUtils - virtual DOM::Document const& hyperlink_element_utils_document() const override { return document(); } + virtual DOM::Document& hyperlink_element_utils_document() override { return document(); } virtual String hyperlink_element_utils_href() const override; virtual void set_hyperlink_element_utils_href(String) override; + virtual bool hyperlink_element_utils_is_html_anchor_element() const final { return true; } + virtual bool hyperlink_element_utils_is_connected() const final { return is_connected(); } + virtual String hyperlink_element_utils_target() const final { return target(); } + virtual void hyperlink_element_utils_queue_an_element_task(HTML::Task::Source source, Function steps) override + { + queue_an_element_task(source, move(steps)); + } }; } diff --git a/Userland/Libraries/LibWeb/HTML/HTMLAreaElement.h b/Userland/Libraries/LibWeb/HTML/HTMLAreaElement.h index df7a662d81..fecfb33af8 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLAreaElement.h +++ b/Userland/Libraries/LibWeb/HTML/HTMLAreaElement.h @@ -26,9 +26,16 @@ private: virtual void parse_attribute(FlyString const& name, String const& value) override; // ^HTML::HTMLHyperlinkElementUtils - virtual DOM::Document const& hyperlink_element_utils_document() const override { return document(); } + virtual DOM::Document& hyperlink_element_utils_document() override { return document(); } virtual String hyperlink_element_utils_href() const override; virtual void set_hyperlink_element_utils_href(String) override; + virtual bool hyperlink_element_utils_is_html_anchor_element() const override { return false; } + virtual bool hyperlink_element_utils_is_connected() const override { return is_connected(); } + virtual String hyperlink_element_utils_target() const override { return ""; } + virtual void hyperlink_element_utils_queue_an_element_task(HTML::Task::Source source, Function steps) override + { + queue_an_element_task(source, move(steps)); + } }; } diff --git a/Userland/Libraries/LibWeb/HTML/HTMLHyperlinkElementUtils.cpp b/Userland/Libraries/LibWeb/HTML/HTMLHyperlinkElementUtils.cpp index 25b80bb2f4..794cb51453 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLHyperlinkElementUtils.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLHyperlinkElementUtils.cpp @@ -7,6 +7,7 @@ #include #include #include +#include namespace Web::HTML { @@ -449,4 +450,125 @@ void HTMLHyperlinkElementUtils::update_href() // To update href, set the element's href content attribute's value to the element's url, serialized. } +bool HTMLHyperlinkElementUtils::cannot_navigate() const +{ + // An element element cannot navigate if one of the following is true: + + // 1. element's node document is not fully active + auto const& document = const_cast(this)->hyperlink_element_utils_document(); + if (!document.is_fully_active()) + return true; + + // 2. element is not an a element and is not connected. + if (!hyperlink_element_utils_is_html_anchor_element() && !hyperlink_element_utils_is_connected()) + return true; + + return false; +} + +// https://html.spec.whatwg.org/multipage/links.html#following-hyperlinks-2 +void HTMLHyperlinkElementUtils::follow_the_hyperlink(Optional hyperlink_suffix) +{ + // To follow the hyperlink created by an element subject, given an optional hyperlinkSuffix (default null): + + // 1. If subject cannot navigate, then return. + if (cannot_navigate()) + return; + + // FIXME: 2. Let replace be false. + + // 3. Let source be subject's node document's browsing context. + auto* source = hyperlink_element_utils_document().browsing_context(); + if (!source) + return; + + // 4. Let targetAttributeValue be the empty string. + // 5. If subject is an a or area element, then set targetAttributeValue to + // the result of getting an element's target given subject. + String target_attribute_value = get_an_elements_target(); + + // 6. Let noopener be the result of getting an element's noopener with subject and targetAttributeValue. + bool noopener = get_an_elements_noopener(target_attribute_value); + + // 7. Let target be the first return value of applying the rules for + // choosing a browsing context given targetAttributeValue, source, and + // noopener. + auto* target = source->choose_a_browsing_context(target_attribute_value, noopener); + + // 8. If target is null, then return. + if (!target) + return; + + // 9. Parse a URL given subject's href attribute, relative to subject's node + // document. + auto url = source->active_document()->parse_url(href()); + + // 10. If that is successful, let URL be the resulting URL string. + auto url_string = url.to_string(); + + // 11. Otherwise, if parsing the URL failed, the user agent may report the + // error to the user in a user-agent-specific manner, may queue an element + // task on the DOM manipulation task source given subject to navigate the + // target browsing context to an error page to report the error, or may + // ignore the error and do nothing. In any case, the user agent must then + // return. + + // 12. If hyperlinkSuffix is non-null, then append it to URL. + if (hyperlink_suffix.has_value()) { + StringBuilder url_builder; + url_builder.append(url_string); + url_builder.append(*hyperlink_suffix); + + url_string = url_builder.to_string(); + } + + // FIXME: 13. Let request be a new request whose URL is URL and whose + // referrer policy is the current state of subject's referrerpolicy content + // attribute. + + // FIXME: 14. If subject's link types includes the noreferrer keyword, then + // set request's referrer to "no-referrer". + + // 15. Queue an element task on the DOM manipulation task source given + // subject to navigate target to request with the source browsing context + // set to source. + // FIXME: "navigate" means implementing the navigation algorithm here: + // https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate + hyperlink_element_utils_queue_an_element_task(Task::Source::DOMManipulation, [url_string, target] { + target->loader().load(url_string, FrameLoader::Type::Navigation); + }); +} + +String HTMLHyperlinkElementUtils::get_an_elements_target() const +{ + // To get an element's target, given an a, area, or form element element, run these steps: + + // 1. If element has a target attribute, then return that attribute's value. + if (auto target = hyperlink_element_utils_target(); !target.is_empty()) + return target; + + // FIXME: 2. If element's node document contains a base element with a + // target attribute, then return the value of the target attribute of the + // first such base element. + + // 3. Return the empty string. + return ""; +} + +bool HTMLHyperlinkElementUtils::get_an_elements_noopener(StringView target) const +{ + // To get an element's noopener, given an a, area, or form element element and a string target: + + // FIXME: 1. If element's link types include the noopener or noreferrer + // keyword, then return true. + + // FIXME: 2. If element's link types do not include the opener keyword and + // target is an ASCII case-insensitive match for "_blank", then return true. + if (target.equals_ignoring_case("_blank"sv)) + return true; + + // 3. Return false. + return false; +} + } diff --git a/Userland/Libraries/LibWeb/HTML/HTMLHyperlinkElementUtils.h b/Userland/Libraries/LibWeb/HTML/HTMLHyperlinkElementUtils.h index 75f2e5f49f..f1612a2f83 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLHyperlinkElementUtils.h +++ b/Userland/Libraries/LibWeb/HTML/HTMLHyperlinkElementUtils.h @@ -8,6 +8,7 @@ #include #include +#include namespace Web::HTML { @@ -48,15 +49,23 @@ public: void set_hash(String); protected: - virtual DOM::Document const& hyperlink_element_utils_document() const = 0; + virtual DOM::Document& hyperlink_element_utils_document() = 0; virtual String hyperlink_element_utils_href() const = 0; virtual void set_hyperlink_element_utils_href(String) = 0; + virtual bool hyperlink_element_utils_is_html_anchor_element() const = 0; + virtual bool hyperlink_element_utils_is_connected() const = 0; + virtual String hyperlink_element_utils_target() const = 0; + virtual void hyperlink_element_utils_queue_an_element_task(HTML::Task::Source source, Function steps) = 0; void set_the_url(); + void follow_the_hyperlink(Optional hyperlink_suffix); private: void reinitialize_url() const; void update_href(); + bool cannot_navigate() const; + String get_an_elements_target() const; + bool get_an_elements_noopener(StringView target) const; Optional m_url; }; diff --git a/Userland/Libraries/LibWeb/Page/EventHandler.cpp b/Userland/Libraries/LibWeb/Page/EventHandler.cpp index b3da68ed98..a001e4a42a 100644 --- a/Userland/Libraries/LibWeb/Page/EventHandler.cpp +++ b/Userland/Libraries/LibWeb/Page/EventHandler.cpp @@ -194,7 +194,46 @@ bool EventHandler::handle_mouseup(const Gfx::IntPoint& position, unsigned button node->dispatch_event(UIEvents::MouseEvent::create(UIEvents::EventNames::mouseup, offset.x(), offset.y(), position.x(), position.y())); handled_event = true; - if (node.ptr() == m_mousedown_target) { + bool should_dispatch_event = true; + + // FIXME: This is ad-hoc and incorrect. The reason this exists is + // because we are missing browsing context navigation: + // + // https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate + // + // Additionally, we currently cannot spawn a new top-level + // browsing context for new tab operations, because the new + // top-level browsing context would be in another process. To + // fix this, there needs to be some way to be able to + // communicate with browsing contexts in remote WebContent + // processes, and then step 8 of this algorithm needs to be + // implemented in BrowsingContext::choose_a_browsing_context: + // + // https://html.spec.whatwg.org/multipage/browsers.html#the-rules-for-choosing-a-browsing-context-given-a-browsing-context-name + if (RefPtr link = node->enclosing_link_element()) { + NonnullRefPtr document = *m_browsing_context.active_document(); + auto href = link->href(); + auto url = document->parse_url(href); + dbgln("Web::EventHandler: Clicking on a link to {}", url); + if (button == GUI::MouseButton::Primary) { + if (href.starts_with("javascript:")) { + document->run_javascript(href.substring_view(11, href.length() - 11)); + } else if (!url.fragment().is_null() && url.equals(document->url(), AK::URL::ExcludeFragment::Yes)) { + m_browsing_context.scroll_to_anchor(url.fragment()); + } else if (modifiers != 0) { + if (m_browsing_context.is_top_level()) { + if (auto* page = m_browsing_context.page()) + page->client().page_did_click_link(url, link->target(), modifiers); + } + } + } else if (button == GUI::MouseButton::Middle) { + if (auto* page = m_browsing_context.page()) + page->client().page_did_middle_click_link(url, link->target(), modifiers); + should_dispatch_event = false; + } + } + + if (node.ptr() == m_mousedown_target && should_dispatch_event) { node->dispatch_event(UIEvents::MouseEvent::create(UIEvents::EventNames::click, offset.x(), offset.y(), position.x(), position.y())); } } @@ -272,37 +311,10 @@ bool EventHandler::handle_mousedown(const Gfx::IntPoint& position, unsigned butt return true; } - if (RefPtr link = node->enclosing_link_element()) { - auto href = link->href(); - auto url = document->parse_url(href); - dbgln("Web::EventHandler: Clicking on a link to {}", url); - if (button == GUI::MouseButton::Primary) { - if (href.starts_with("javascript:")) { - document->run_javascript(href.substring_view(11, href.length() - 11)); - } else if (!url.fragment().is_null() && url.equals(document->url(), AK::URL::ExcludeFragment::Yes)) { - m_browsing_context.scroll_to_anchor(url.fragment()); - } else { - document->set_active_element(link); - if (m_browsing_context.is_top_level()) { - if (auto* page = m_browsing_context.page()) - page->client().page_did_click_link(url, link->target(), modifiers); - } else { - // FIXME: Handle different targets! - m_browsing_context.loader().load(url, FrameLoader::Type::Navigation); - } - } - } else if (button == GUI::MouseButton::Secondary) { - if (auto* page = m_browsing_context.page()) - page->client().page_did_request_link_context_menu(m_browsing_context.to_top_level_position(position), url, link->target(), modifiers); - } else if (button == GUI::MouseButton::Middle) { - if (auto* page = m_browsing_context.page()) - page->client().page_did_middle_click_link(url, link->target(), modifiers); - } - } else { - if (button == GUI::MouseButton::Primary) { - auto result = paint_root()->hit_test(position.to_type(), Painting::HitTestType::TextCursor); - if (result.has_value() && result->dom_node()) { - + if (button == GUI::MouseButton::Primary) { + if (auto result = paint_root()->hit_test(position.to_type(), Painting::HitTestType::TextCursor); result.has_value()) { + auto paintable = result->paintable; + if (paintable->dom_node()) { // See if we want to focus something. bool did_focus_something = false; for (auto candidate = node; candidate; candidate = candidate->parent()) { @@ -316,15 +328,15 @@ bool EventHandler::handle_mousedown(const Gfx::IntPoint& position, unsigned butt // If we didn't focus anything, place the document text cursor at the mouse position. // FIXME: This is all rather strange. Find a better solution. if (!did_focus_something) { - m_browsing_context.set_cursor_position(DOM::Position(*result->dom_node(), result->index_in_node)); - layout_root()->set_selection({ { result->paintable->layout_node(), result->index_in_node }, {} }); + m_browsing_context.set_cursor_position(DOM::Position(*paintable->dom_node(), result->index_in_node)); + layout_root()->set_selection({ { paintable->layout_node(), result->index_in_node }, {} }); m_in_mouse_selection = true; } } - } else if (button == GUI::MouseButton::Secondary) { - if (auto* page = m_browsing_context.page()) - page->client().page_did_request_context_menu(m_browsing_context.to_top_level_position(position)); } + } else if (button == GUI::MouseButton::Secondary) { + if (auto* page = m_browsing_context.page()) + page->client().page_did_request_context_menu(m_browsing_context.to_top_level_position(position)); } return true; }