From a63c7549e14498d5d6b85298d5f637c9241a7f5f Mon Sep 17 00:00:00 2001 From: Idan Horowitz Date: Tue, 15 Nov 2022 02:03:48 +0200 Subject: [PATCH] LibWeb: Implement window.open This implementation is some-what complete, with the most common missing /broken feature being opening pages in new tabs using the "_blank" target. This is currently broken due to 2 reasons: - We currently always claim the Window does not have transient activation, as we do not track the transient activation timestamp yet. This means that all such window.open calls are detected as pop-ups, and as such they are blocked. This can be easily bypassed by unchecking the 'Block Pop-ups' checkbox in the debug menu. - There is currently no mechanism for the WebContent process to request a new tab to be created by the Browser process, and as such the call to BrowsingContext::choose_a_browsing_context does not actually open another tab. --- Userland/Libraries/LibWeb/HTML/Window.cpp | 149 ++++++++++++++++++++++ Userland/Libraries/LibWeb/HTML/Window.h | 2 + 2 files changed, 151 insertions(+) diff --git a/Userland/Libraries/LibWeb/HTML/Window.cpp b/Userland/Libraries/LibWeb/HTML/Window.cpp index 8a911c0684..4890025fa4 100644 --- a/Userland/Libraries/LibWeb/HTML/Window.cpp +++ b/Userland/Libraries/LibWeb/HTML/Window.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -297,6 +298,131 @@ static bool check_if_a_popup_window_is_requested(OrderedHashMap return false; } +// FIXME: This is based on the old 'browsing context' concept, which was replaced with 'navigable' +// https://html.spec.whatwg.org/multipage/window-object.html#window-open-steps +WebIDL::ExceptionOr> Window::open_impl(StringView url, StringView target, StringView features) +{ + auto& vm = this->vm(); + + // 1. If the event loop's termination nesting level is nonzero, return null. + if (HTML::main_thread_event_loop().termination_nesting_level() != 0) + return nullptr; + + // 2. Let source browsing context be the entry global object's browsing context. + auto* source_browsing_context = verify_cast(entry_global_object()).browsing_context(); + + // 3. If target is the empty string, then set target to "_blank". + if (target.is_empty()) + target = "_blank"sv; + + // 4. Let tokenizedFeatures be the result of tokenizing features. + auto tokenized_features = tokenize_open_features(features); + + // 5. Let noopener and noreferrer be false. + auto no_opener = false; + auto no_referrer = false; + + // 6. If tokenizedFeatures["noopener"] exists, then: + if (auto no_opener_feature = tokenized_features.get("noopener"sv); no_opener_feature.has_value()) { + // 1. Set noopener to the result of parsing tokenizedFeatures["noopener"] as a boolean feature. + no_opener = parse_boolean_feature(*no_opener_feature); + + // 2. Remove tokenizedFeatures["noopener"]. + tokenized_features.remove("noopener"sv); + } + + // 7. If tokenizedFeatures["noreferrer"] exists, then: + if (auto no_referrer_feature = tokenized_features.get("noreferrer"sv); no_referrer_feature.has_value()) { + // 1. Set noreferrer to the result of parsing tokenizedFeatures["noreferrer"] as a boolean feature. + no_referrer = parse_boolean_feature(*no_referrer_feature); + + // 2. Remove tokenizedFeatures["noreferrer"]. + tokenized_features.remove("noreferrer"sv); + } + + // 8. If noreferrer is true, then set noopener to true. + if (no_referrer) + no_opener = true; + + // 9. Let target browsing context and windowType be the result of applying the rules for choosing a browsing context given target, source browsing context, and noopener. + auto [target_browsing_context, window_type] = source_browsing_context->choose_a_browsing_context(target, no_opener); + + // 10. If target browsing context is null, then return null. + if (target_browsing_context == nullptr) + return nullptr; + + // 11. If windowType is either "new and unrestricted" or "new with no opener", then: + if (window_type == BrowsingContext::WindowType::NewAndUnrestricted || window_type == BrowsingContext::WindowType::NewWithNoOpener) { + // 1. Set the target browsing context's is popup to the result of checking if a popup window is requested, given tokenizedFeatures. + target_browsing_context->set_is_popup(check_if_a_popup_window_is_requested(tokenized_features)); + + // FIXME: 2. Set up browsing context features for target browsing context given tokenizedFeatures. [CSSOMVIEW] + // NOTE: While this is not implemented yet, all of observable actions taken by this operation are optional (implementation-defined). + + // 3. Let urlRecord be the URL record about:blank. + auto url_record = AK::URL("about:blank"sv); + + // 4. If url is not the empty string, then parse url relative to the entry settings object, and set urlRecord to the resulting URL record, if any. If the parse a URL algorithm failed, then throw a "SyntaxError" DOMException. + if (!url.is_empty()) { + url_record = entry_settings_object().parse_url(url); + if (!url_record.is_valid()) + return WebIDL::SyntaxError::create(realm(), "URL is not valid"); + } + + // FIXME: 5. If urlRecord matches about:blank, then perform the URL and history update steps given target browsing context's active document and urlRecord. + + // 6. Otherwise: + else { + // 1. Let request be a new request whose URL is urlRecord. + auto request = Fetch::Infrastructure::Request::create(vm); + request->set_url(url_record); + + // 2. If noreferrer is true, then set request's referrer to "no-referrer". + if (no_referrer) + request->set_referrer(Fetch::Infrastructure::Request::Referrer::NoReferrer); + + // 3. Navigate target browsing context to request, with exceptionsEnabled set to true and the source browsing context set to source browsing context. + TRY(target_browsing_context->navigate(request, *source_browsing_context, true)); + } + } + + // 12. Otherwise: + else { + // 1. If url is not the empty string, then: + if (!url.is_empty()) { + // 1. Let urlRecord be the URL record about:blank. + auto url_record = AK::URL("about:blank"sv); + + // 2. Parse url relative to the entry settings object, and set urlRecord to the resulting URL record, if any. If the parse a URL algorithm failed, then throw a "SyntaxError" DOMException. + url_record = entry_settings_object().parse_url(url); + if (!url_record.is_valid()) + return WebIDL::SyntaxError::create(realm(), "URL is not valid"); + + // 3. Let request be a new request whose URL is urlRecord. + auto request = Fetch::Infrastructure::Request::create(vm); + request->set_url(url_record); + + // 4. If noreferrer is true, then set request's referrer to "noreferrer". + if (no_referrer) + request->set_referrer(Fetch::Infrastructure::Request::Referrer::NoReferrer); + + // 5. Navigate target browsing context to request, with exceptionsEnabled set to true and the source browsing context set to source browsing context. + TRY(target_browsing_context->navigate(request, *source_browsing_context, true)); + } + + // 2. If noopener is false, then set target browsing context's opener browsing context to source browsing context. + if (!no_opener) + target_browsing_context->set_opener_browsing_context(source_browsing_context); + } + + // 13. If noopener is true or windowType is "new with no opener", then return null. + if (no_opener || window_type == BrowsingContext::WindowType::NewWithNoOpener) + return nullptr; + + // 14. Return target browsing context's WindowProxy object. + return target_browsing_context->window_proxy(); +} + void Window::alert_impl(String const& message) { if (auto* page = this->page()) @@ -950,6 +1076,7 @@ void Window::initialize_web_interfaces(Badge) define_native_accessor(realm, "innerHeight", inner_height_getter, {}, JS::Attribute::Enumerable); define_native_accessor(realm, "devicePixelRatio", device_pixel_ratio_getter, {}, JS::Attribute::Enumerable | JS::Attribute::Configurable); u8 attr = JS::Attribute::Writable | JS::Attribute::Enumerable | JS::Attribute::Configurable; + define_native_function(realm, "open", open, 0, attr); define_native_function(realm, "alert", alert, 0, attr); define_native_function(realm, "confirm", confirm, 0, attr); define_native_function(realm, "prompt", prompt, 0, attr); @@ -1059,6 +1186,28 @@ static JS::ThrowCompletionOr impl_from(JS::VM& vm) return vm.throw_completion(JS::ErrorType::NotAnObjectOfType, "Window"); } +JS_DEFINE_NATIVE_FUNCTION(Window::open) +{ + auto* impl = TRY(impl_from(vm)); + + // optional USVString url = "" + String url = ""; + if (!vm.argument(0).is_undefined()) + url = TRY(vm.argument(0).to_string(vm)); + + // optional DOMString target = "_blank" + String target = "_blank"; + if (!vm.argument(1).is_undefined()) + target = TRY(vm.argument(1).to_string(vm)); + + // optional [LegacyNullToEmptyString] DOMString features = "") + String features = ""; + if (!vm.argument(2).is_nullish()) + features = TRY(vm.argument(2).to_string(vm)); + + return TRY(Bindings::throw_dom_exception_if_needed(vm, [&] { return impl->open_impl(url, target, features); })); +} + JS_DEFINE_NATIVE_FUNCTION(Window::alert) { // https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#simple-dialogs diff --git a/Userland/Libraries/LibWeb/HTML/Window.h b/Userland/Libraries/LibWeb/HTML/Window.h index 9b5b405ff0..d9e7e6715e 100644 --- a/Userland/Libraries/LibWeb/HTML/Window.h +++ b/Userland/Libraries/LibWeb/HTML/Window.h @@ -61,6 +61,7 @@ public: bool import_maps_allowed() const { return m_import_maps_allowed; } void set_import_maps_allowed(bool import_maps_allowed) { m_import_maps_allowed = import_maps_allowed; } + WebIDL::ExceptionOr> open_impl(StringView url, StringView target, StringView features); void alert_impl(String const&); bool confirm_impl(String const&); String prompt_impl(String const&, String const&); @@ -245,6 +246,7 @@ private: JS_DECLARE_NATIVE_FUNCTION(session_storage_getter); JS_DECLARE_NATIVE_FUNCTION(origin_getter); + JS_DECLARE_NATIVE_FUNCTION(open); JS_DECLARE_NATIVE_FUNCTION(alert); JS_DECLARE_NATIVE_FUNCTION(confirm); JS_DECLARE_NATIVE_FUNCTION(prompt);