ソースを参照

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.
sin-ack 3 年 前
コミット
aaa954f900

+ 114 - 0
Userland/Libraries/LibWeb/HTML/BrowsingContext.cpp

@@ -428,4 +428,118 @@ RefPtr<DOM::Node> 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;
+}
+
 }

+ 2 - 0
Userland/Libraries/LibWeb/HTML/BrowsingContext.h

@@ -76,6 +76,8 @@ public:
 
     BrowsingContext const& top_level_browsing_context() const { return const_cast<BrowsingContext*>(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; }
 

+ 41 - 0
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<String> 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);
+}
+
 }

+ 11 - 1
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<void()> steps) override
+    {
+        queue_an_element_task(source, move(steps));
+    }
 };
 
 }

+ 8 - 1
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<void()> steps) override
+    {
+        queue_an_element_task(source, move(steps));
+    }
 };
 
 }

+ 122 - 0
Userland/Libraries/LibWeb/HTML/HTMLHyperlinkElementUtils.cpp

@@ -7,6 +7,7 @@
 #include <AK/URLParser.h>
 #include <LibWeb/DOM/Document.h>
 #include <LibWeb/HTML/HTMLHyperlinkElementUtils.h>
+#include <LibWeb/Loader/FrameLoader.h>
 
 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<HTMLHyperlinkElementUtils*>(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<String> 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;
+}
+
 }

+ 10 - 1
Userland/Libraries/LibWeb/HTML/HTMLHyperlinkElementUtils.h

@@ -8,6 +8,7 @@
 
 #include <AK/URL.h>
 #include <LibWeb/Forward.h>
+#include <LibWeb/HTML/EventLoop/Task.h>
 
 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<void()> steps) = 0;
 
     void set_the_url();
+    void follow_the_hyperlink(Optional<String> 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<AK::URL> m_url;
 };

+ 49 - 37
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<HTML::HTMLAnchorElement> 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<HTML::HTMLAnchorElement> 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<float>(), 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<float>(), 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;
 }