blob: bb5f28c92cd16f6ddcdd8a78067452a6a6f9cd50 [file] [log] [blame]
/*
* Copyright (C) 2006, 2007, 2008, 2010 Apple Inc. All rights reserved.
* Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "third_party/blink/renderer/core/page/create_window.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_macros.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/network/public/mojom/request_context_frame_type.mojom-blink.h"
#include "third_party/blink/public/common/dom_storage/session_storage_namespace_id.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/frame/from_ad_state.h"
#include "third_party/blink/public/platform/web_input_event.h"
#include "third_party/blink/public/platform/web_url_request.h"
#include "third_party/blink/public/web/web_view_client.h"
#include "third_party/blink/public/web/web_window_features.h"
#include "third_party/blink/renderer/core/core_initializer.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/events/current_input_event.h"
#include "third_party/blink/renderer/core/exported/web_view_impl.h"
#include "third_party/blink/renderer/core/frame/ad_tracker.h"
#include "third_party/blink/renderer/core/frame/csp/content_security_policy.h"
#include "third_party/blink/renderer/core/frame/frame_client.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/core/loader/frame_load_request.h"
#include "third_party/blink/renderer/core/page/chrome_client.h"
#include "third_party/blink/renderer/core/page/focus_controller.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/core/probe/core_probes.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_request.h"
#include "third_party/blink/renderer/platform/weborigin/kurl.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/weborigin/security_policy.h"
namespace blink {
// Though isspace() considers \t and \v to be whitespace, Win IE doesn't when
// parsing window features.
static bool IsWindowFeaturesSeparator(UChar c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '=' ||
c == ',' || c == '\f';
}
WebWindowFeatures GetWindowFeaturesFromString(const String& feature_string) {
WebWindowFeatures window_features;
// This code follows the HTML spec, specifically
// https://html.spec.whatwg.org/#concept-window-open-features-tokenize
if (feature_string.IsEmpty())
return window_features;
window_features.menu_bar_visible = false;
window_features.status_bar_visible = false;
window_features.tool_bar_visible = false;
window_features.scrollbars_visible = false;
unsigned key_begin, key_end;
unsigned value_begin, value_end;
String buffer = feature_string.DeprecatedLower();
unsigned length = buffer.length();
for (unsigned i = 0; i < length;) {
// skip to first non-separator (start of key name), but don't skip
// past the end of the string
while (i < length && IsWindowFeaturesSeparator(buffer[i]))
i++;
key_begin = i;
// skip to first separator (end of key name), but don't skip past
// the end of the string
while (i < length && !IsWindowFeaturesSeparator(buffer[i]))
i++;
key_end = i;
SECURITY_DCHECK(i <= length);
// skip separators past the key name, except '=', and don't skip past
// the end of the string
while (i < length && buffer[i] != '=') {
if (buffer[i] == ',' || !IsWindowFeaturesSeparator(buffer[i]))
break;
i++;
}
if (i < length && IsWindowFeaturesSeparator(buffer[i])) {
// skip to first non-separator (start of value), but don't skip
// past a ',' or the end of the string.
while (i < length && IsWindowFeaturesSeparator(buffer[i])) {
if (buffer[i] == ',')
break;
i++;
}
value_begin = i;
SECURITY_DCHECK(i <= length);
// skip to first separator (end of value)
while (i < length && !IsWindowFeaturesSeparator(buffer[i]))
i++;
value_end = i;
SECURITY_DCHECK(i <= length);
} else {
// No value given.
value_begin = i;
value_end = i;
}
String key_string(
buffer.Substring(key_begin, key_end - key_begin).LowerASCII());
String value_string(
buffer.Substring(value_begin, value_end - value_begin).LowerASCII());
// Listing a key with no value is shorthand for key=yes
int value;
if (value_string.IsEmpty() || value_string == "yes")
value = 1;
else
value = value_string.ToInt();
if (key_string.IsEmpty())
continue;
if (key_string == "left" || key_string == "screenx") {
window_features.x_set = true;
window_features.x = value;
} else if (key_string == "top" || key_string == "screeny") {
window_features.y_set = true;
window_features.y = value;
} else if (key_string == "width" || key_string == "innerwidth") {
window_features.width_set = true;
window_features.width = value;
} else if (key_string == "height" || key_string == "innerheight") {
window_features.height_set = true;
window_features.height = value;
} else if (key_string == "menubar") {
window_features.menu_bar_visible = value;
} else if (key_string == "toolbar" || key_string == "location") {
window_features.tool_bar_visible |= static_cast<bool>(value);
} else if (key_string == "status") {
window_features.status_bar_visible = value;
} else if (key_string == "scrollbars") {
window_features.scrollbars_visible = value;
} else if (key_string == "resizable") {
window_features.resizable = value;
} else if (key_string == "noopener") {
window_features.noopener = true;
} else if (key_string == "background") {
window_features.background = true;
} else if (key_string == "persistent") {
window_features.persistent = true;
}
}
return window_features;
}
static Frame* ReuseExistingWindow(LocalFrame& active_frame,
LocalFrame& lookup_frame,
const AtomicString& frame_name,
const KURL& destination_url) {
if (!frame_name.IsEmpty() && !EqualIgnoringASCIICase(frame_name, "_blank")) {
if (Frame* frame = lookup_frame.FindFrameForNavigation(
frame_name, active_frame, destination_url)) {
if (!EqualIgnoringASCIICase(frame_name, "_self")) {
if (Page* page = frame->GetPage()) {
if (page == active_frame.GetPage())
page->GetFocusController().SetFocusedFrame(frame);
else
page->GetChromeClient().Focus(&active_frame);
}
}
return frame;
}
}
return nullptr;
}
static void MaybeLogWindowOpen(LocalFrame& opener_frame) {
AdTracker* ad_tracker = opener_frame.GetAdTracker();
if (!ad_tracker)
return;
bool is_ad_subframe = opener_frame.IsAdSubframe();
bool is_ad_script_in_stack = ad_tracker->IsAdScriptInStack();
FromAdState state =
blink::GetFromAdState(is_ad_subframe, is_ad_script_in_stack);
// Log to UMA.
UMA_HISTOGRAM_ENUMERATION("Blink.WindowOpen.FromAdState", state);
// Log to UKM.
ukm::UkmRecorder* ukm_recorder = opener_frame.GetDocument()->UkmRecorder();
ukm::SourceId source_id = opener_frame.GetDocument()->UkmSourceID();
if (source_id != ukm::kInvalidSourceId) {
ukm::builders::AbusiveExperienceHeuristic_WindowOpen(source_id)
.SetFromAdSubframe(is_ad_subframe)
.SetFromAdScript(is_ad_script_in_stack)
.Record(ukm_recorder);
}
}
static Frame* CreateNewWindow(LocalFrame& opener_frame,
const FrameLoadRequest& request,
const WebWindowFeatures& features,
bool force_new_foreground_tab,
bool& created) {
Page* old_page = opener_frame.GetPage();
if (!old_page)
return nullptr;
NavigationPolicy policy = force_new_foreground_tab
? kNavigationPolicyNewForegroundTab
: NavigationPolicyForCreateWindow(features);
const SandboxFlags sandbox_flags =
opener_frame.GetDocument()->IsSandboxed(
kSandboxPropagatesToAuxiliaryBrowsingContexts)
? opener_frame.GetSecurityContext()->GetSandboxFlags()
: kSandboxNone;
SessionStorageNamespaceId new_namespace_id =
AllocateSessionStorageNamespaceId();
if (base::FeatureList::IsEnabled(features::kOnionSoupDOMStorage)) {
// TODO(dmurph): Don't copy session storage when features.noopener is true:
// https://html.spec.whatwg.org/multipage/browsers.html#copy-session-storage
// https://crbug.com/771959
CoreInitializer::GetInstance().CloneSessionStorage(old_page,
new_namespace_id);
}
Page* page = old_page->GetChromeClient().CreateWindow(
&opener_frame, request, features, policy, sandbox_flags,
new_namespace_id);
if (!page)
return nullptr;
if (page == old_page) {
Frame* frame = &opener_frame.Tree().Top();
if (!opener_frame.CanNavigate(*frame))
return nullptr;
if (request.GetShouldSetOpener() == kMaybeSetOpener)
frame->Client()->SetOpener(&opener_frame);
return frame;
}
DCHECK(page->MainFrame());
LocalFrame& frame = *ToLocalFrame(page->MainFrame());
page->SetWindowFeatures(features);
frame.View()->SetCanHaveScrollbars(features.scrollbars_visible);
IntRect window_rect = page->GetChromeClient().RootWindowRect(frame);
if (features.x_set)
window_rect.SetX(features.x);
if (features.y_set)
window_rect.SetY(features.y);
if (features.width_set)
window_rect.SetWidth(features.width);
if (features.height_set)
window_rect.SetHeight(features.height);
page->GetChromeClient().SetWindowRectWithAdjustment(window_rect, frame);
page->GetChromeClient().Show(policy);
MaybeLogWindowOpen(opener_frame);
created = true;
return &frame;
}
static Frame* CreateWindowHelper(LocalFrame& opener_frame,
LocalFrame& active_frame,
LocalFrame& lookup_frame,
const FrameLoadRequest& request,
const WebWindowFeatures& features,
bool force_new_foreground_tab,
bool& created) {
DCHECK(request.GetResourceRequest().RequestorOrigin() ||
opener_frame.GetDocument()->Url().IsEmpty());
DCHECK_EQ(request.GetResourceRequest().GetFrameType(),
network::mojom::RequestContextFrameType::kAuxiliary);
probe::windowOpen(opener_frame.GetDocument(),
request.GetResourceRequest().Url(), request.FrameName(),
features,
LocalFrame::HasTransientUserActivation(&opener_frame));
created = false;
Frame* window =
features.noopener || force_new_foreground_tab
? nullptr
: ReuseExistingWindow(active_frame, lookup_frame, request.FrameName(),
request.GetResourceRequest().Url());
if (!window) {
// Sandboxed frames cannot open new auxiliary browsing contexts.
if (opener_frame.GetDocument()->IsSandboxed(kSandboxPopups)) {
// FIXME: This message should be moved off the console once a solution to
// https://bugs.webkit.org/show_bug.cgi?id=103274 exists.
opener_frame.GetDocument()->AddConsoleMessage(ConsoleMessage::Create(
kSecurityMessageSource, kErrorMessageLevel,
"Blocked opening '" +
request.GetResourceRequest().Url().ElidedString() +
"' in a new window because the request was made in a sandboxed "
"frame whose 'allow-popups' permission is not set."));
return nullptr;
}
}
if (window) {
// JS can run inside ReuseExistingWindow (via onblur), which can detach
// the target window.
if (!window->Client())
return nullptr;
if (request.GetShouldSetOpener() == kMaybeSetOpener)
window->Client()->SetOpener(&opener_frame);
return window;
}
return CreateNewWindow(opener_frame, request, features,
force_new_foreground_tab, created);
}
DOMWindow* CreateWindow(const String& url_string,
const AtomicString& frame_name,
const String& window_features_string,
LocalDOMWindow& incumbent_window,
LocalFrame& entered_window_frame,
LocalFrame& opener_frame,
ExceptionState& exception_state) {
LocalFrame* active_frame = incumbent_window.GetFrame();
DCHECK(active_frame);
KURL completed_url =
url_string.IsEmpty()
? KURL(g_empty_string)
: entered_window_frame.GetDocument()->CompleteURL(url_string);
if (!completed_url.IsEmpty() && !completed_url.IsValid()) {
UseCounter::Count(active_frame, WebFeature::kWindowOpenWithInvalidURL);
exception_state.ThrowDOMException(
DOMExceptionCode::kSyntaxError,
"Unable to open a window with invalid URL '" +
completed_url.GetString() + "'.\n");
return nullptr;
}
if (completed_url.ProtocolIsJavaScript() &&
opener_frame.GetDocument()->GetContentSecurityPolicy() &&
!ContentSecurityPolicy::ShouldBypassMainWorld(
opener_frame.GetDocument())) {
String script_source = DecodeURLEscapeSequences(
completed_url.GetString(), DecodeURLMode::kUTF8OrIsomorphic);
if (!opener_frame.GetDocument()
->GetContentSecurityPolicy()
->AllowJavaScriptURLs(nullptr, script_source,
opener_frame.GetDocument()->Url(),
OrdinalNumber())) {
return nullptr;
}
}
WebWindowFeatures window_features =
GetWindowFeaturesFromString(window_features_string);
FrameLoadRequest frame_request(incumbent_window.document(),
ResourceRequest(completed_url), frame_name);
frame_request.SetShouldSetOpener(window_features.noopener ? kNeverSetOpener
: kMaybeSetOpener);
frame_request.GetResourceRequest().SetFrameType(
network::mojom::RequestContextFrameType::kAuxiliary);
// Normally, FrameLoader would take care of setting the referrer for a
// navigation that is triggered from javascript. However, creating a window
// goes through sufficient processing that it eventually enters FrameLoader as
// an embedder-initiated navigation. FrameLoader assumes no responsibility
// for generating an embedder-initiated navigation's referrer, so we need to
// ensure the proper referrer is set now.
// TODO(domfarolino): Stop setting ResourceRequest's HTTP Referrer and store
// this is a separate member. See https://crbug.com/850813.
frame_request.GetResourceRequest().SetHTTPReferrer(
SecurityPolicy::GenerateReferrer(
active_frame->GetDocument()->GetReferrerPolicy(), completed_url,
active_frame->GetDocument()->OutgoingReferrer()));
// Records HasUserGesture before the value is invalidated inside
// createWindow(LocalFrame& openerFrame, ...).
// This value will be set in ResourceRequest loaded in a new LocalFrame.
bool has_user_gesture = LocalFrame::HasTransientUserActivation(&opener_frame);
// We pass the opener frame for the lookupFrame in case the active frame is
// different from the opener frame, and the name references a frame relative
// to the opener frame.
bool created;
Frame* new_frame = CreateWindowHelper(
opener_frame, *active_frame, opener_frame, frame_request, window_features,
false /* force_new_foreground_tab */, created);
if (!new_frame)
return nullptr;
if (new_frame->DomWindow()->IsInsecureScriptAccess(incumbent_window,
completed_url))
return window_features.noopener ? nullptr : new_frame->DomWindow();
// TODO(dcheng): Special case for window.open("about:blank") to ensure it
// loads synchronously into a new window. This is our historical behavior, and
// it's consistent with the creation of a new iframe with src="about:blank".
// Perhaps we could get rid of this if we started reporting the initial empty
// document's url as about:blank? See crbug.com/471239.
// TODO(japhet): This special case is also necessary for behavior asserted by
// some extensions tests. Using NavigationScheduler::scheduleNavigationChange
// causes the navigation to be flagged as a client redirect, which is
// observable via the webNavigation extension api.
if (created) {
FrameLoadRequest request(incumbent_window.document(),
ResourceRequest(completed_url));
request.GetResourceRequest().SetHasUserGesture(has_user_gesture);
if (const WebInputEvent* input_event = CurrentInputEvent::Get()) {
request.SetInputStartTime(input_event->TimeStamp());
}
new_frame->Navigate(request, WebFrameLoadType::kStandard);
} else if (!url_string.IsEmpty()) {
new_frame->ScheduleNavigation(*incumbent_window.document(), completed_url,
WebFrameLoadType::kStandard,
has_user_gesture ? UserGestureStatus::kActive
: UserGestureStatus::kNone);
}
return window_features.noopener ? nullptr : new_frame->DomWindow();
}
void CreateWindowForRequest(const FrameLoadRequest& request,
LocalFrame& opener_frame) {
DCHECK(request.GetResourceRequest().RequestorOrigin() ||
(opener_frame.GetDocument() &&
opener_frame.GetDocument()->Url().IsEmpty()));
if (opener_frame.GetDocument()->PageDismissalEventBeingDispatched() !=
Document::kNoDismissal)
return;
if (opener_frame.GetDocument() &&
opener_frame.GetDocument()->IsSandboxed(kSandboxPopups))
return;
WebWindowFeatures features;
features.noopener = request.GetShouldSetOpener() == kNeverSetOpener;
bool created;
Frame* new_frame = CreateWindowHelper(
opener_frame, opener_frame, opener_frame, request, features,
true /* force_new_foreground_tab */, created);
if (!new_frame)
return;
if (request.GetShouldSendReferrer() == kMaybeSendReferrer) {
// TODO(japhet): Does network::mojom::ReferrerPolicy need to be proagated
// for RemoteFrames?
if (new_frame->IsLocalFrame())
ToLocalFrame(new_frame)->GetDocument()->SetReferrerPolicy(
opener_frame.GetDocument()->GetReferrerPolicy());
}
// TODO(japhet): Form submissions on RemoteFrames don't work yet.
FrameLoadRequest new_request(nullptr, request.GetResourceRequest());
new_request.SetForm(request.Form());
if (const WebInputEvent* input_event = CurrentInputEvent::Get()) {
new_request.SetInputStartTime(input_event->TimeStamp());
}
auto blob_url_token = request.GetBlobURLToken();
if (blob_url_token)
new_request.SetBlobURLToken(std::move(blob_url_token));
if (new_frame->IsLocalFrame())
ToLocalFrame(new_frame)->Loader().StartNavigation(new_request);
}
} // namespace blink