blob: cd591a867a07f3455765bc1458fdb5d8c004620f [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "extensions/renderer/extension_frame_helper.h"
#include <set>
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_util.h"
#include "base/timer/elapsed_timer.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_view.h"
#include "extensions/common/api/messaging/message.h"
#include "extensions/common/api/messaging/port_id.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension_messages.h"
#include "extensions/common/manifest_handlers/background_info.h"
#include "extensions/renderer/api/automation/automation_api_helper.h"
#include "extensions/renderer/console.h"
#include "extensions/renderer/dispatcher.h"
#include "extensions/renderer/extension_bindings_system.h"
#include "extensions/renderer/renderer_messaging_service.h"
#include "extensions/renderer/script_context.h"
#include "extensions/renderer/script_context_set.h"
#include "third_party/blink/public/platform/web_security_origin.h"
#include "third_party/blink/public/web/web_console_message.h"
#include "third_party/blink/public/web/web_document.h"
#include "third_party/blink/public/web/web_document_loader.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_settings.h"
#include "third_party/blink/public/web/web_view.h"
namespace extensions {
namespace {
constexpr int kMainWorldId = 0;
base::LazyInstance<std::set<const ExtensionFrameHelper*>>::DestructorAtExit
g_frame_helpers = LAZY_INSTANCE_INITIALIZER;
// Returns true if the render frame corresponding with |frame_helper| matches
// the given criteria.
bool RenderFrameMatches(const ExtensionFrameHelper* frame_helper,
ViewType match_view_type,
int match_window_id,
int match_tab_id,
const std::string& match_extension_id) {
if (match_view_type != VIEW_TYPE_INVALID &&
frame_helper->view_type() != match_view_type)
return false;
// Not all frames have a valid ViewType, e.g. devtools, most GuestViews, and
// unclassified detached WebContents.
if (frame_helper->view_type() == VIEW_TYPE_INVALID)
return false;
// This logic matches ExtensionWebContentsObserver::GetExtensionFromFrame.
blink::WebSecurityOrigin origin =
frame_helper->render_frame()->GetWebFrame()->GetSecurityOrigin();
if (origin.IsUnique() ||
!base::EqualsASCII(origin.Protocol().Utf16(), kExtensionScheme) ||
!base::EqualsASCII(origin.Host().Utf16(), match_extension_id.c_str()))
return false;
if (match_window_id != extension_misc::kUnknownWindowId &&
frame_helper->browser_window_id() != match_window_id)
return false;
if (match_tab_id != extension_misc::kUnknownTabId &&
frame_helper->tab_id() != match_tab_id)
return false;
return true;
}
// Runs every callback in |callbacks_to_be_run_and_cleared| while |frame_helper|
// is valid, and clears |callbacks_to_be_run_and_cleared|.
void RunCallbacksWhileFrameIsValid(
base::WeakPtr<ExtensionFrameHelper> frame_helper,
std::vector<base::Closure>* callbacks_to_be_run_and_cleared) {
// The JavaScript code can cause re-entrancy. To avoid a deadlock, don't run
// callbacks that are added during the iteration.
std::vector<base::Closure> callbacks;
callbacks_to_be_run_and_cleared->swap(callbacks);
for (auto& callback : callbacks) {
callback.Run();
if (!frame_helper.get())
return; // Frame and ExtensionFrameHelper invalidated by callback.
}
}
enum class PortType {
EXTENSION,
TAB,
NATIVE_APP,
};
// Returns an extension hosted in the |render_frame| (or nullptr if the frame
// doesn't host an extension).
const Extension* GetExtensionFromFrame(content::RenderFrame* render_frame) {
DCHECK(render_frame);
ScriptContext* context =
ScriptContextSet::GetMainWorldContextForFrame(render_frame);
return context ? context->effective_extension() : nullptr;
}
} // namespace
ExtensionFrameHelper::ExtensionFrameHelper(content::RenderFrame* render_frame,
Dispatcher* extension_dispatcher)
: content::RenderFrameObserver(render_frame),
content::RenderFrameObserverTracker<ExtensionFrameHelper>(render_frame),
view_type_(VIEW_TYPE_INVALID),
tab_id_(-1),
browser_window_id_(-1),
extension_dispatcher_(extension_dispatcher),
did_create_current_document_element_(false),
weak_ptr_factory_(this) {
g_frame_helpers.Get().insert(this);
if (render_frame->IsMainFrame()) {
// Manages its own lifetime.
new AutomationApiHelper(render_frame);
}
}
ExtensionFrameHelper::~ExtensionFrameHelper() {
g_frame_helpers.Get().erase(this);
}
// static
std::vector<content::RenderFrame*> ExtensionFrameHelper::GetExtensionFrames(
const std::string& extension_id,
int browser_window_id,
int tab_id,
ViewType view_type) {
std::vector<content::RenderFrame*> render_frames;
for (const ExtensionFrameHelper* helper : g_frame_helpers.Get()) {
if (RenderFrameMatches(helper, view_type, browser_window_id, tab_id,
extension_id))
render_frames.push_back(helper->render_frame());
}
return render_frames;
}
// static
v8::Local<v8::Array> ExtensionFrameHelper::GetV8MainFrames(
v8::Local<v8::Context> context,
const std::string& extension_id,
int browser_window_id,
int tab_id,
ViewType view_type) {
// WebFrame::ScriptCanAccess uses the isolate's current context. We need to
// make sure that the current context is the one we're expecting.
DCHECK(context == context->GetIsolate()->GetCurrentContext());
std::vector<content::RenderFrame*> render_frames =
GetExtensionFrames(extension_id, browser_window_id, tab_id, view_type);
v8::Local<v8::Array> v8_frames = v8::Array::New(context->GetIsolate());
int v8_index = 0;
for (content::RenderFrame* frame : render_frames) {
if (!frame->IsMainFrame())
continue;
blink::WebLocalFrame* web_frame = frame->GetWebFrame();
if (!blink::WebFrame::ScriptCanAccess(web_frame))
continue;
v8::Local<v8::Context> frame_context = web_frame->MainWorldScriptContext();
if (!frame_context.IsEmpty()) {
v8::Local<v8::Value> window = frame_context->Global();
CHECK(!window.IsEmpty());
v8::Maybe<bool> maybe =
v8_frames->CreateDataProperty(context, v8_index++, window);
CHECK(maybe.IsJust() && maybe.FromJust());
}
}
return v8_frames;
}
// static
content::RenderFrame* ExtensionFrameHelper::GetBackgroundPageFrame(
const std::string& extension_id) {
for (const ExtensionFrameHelper* helper : g_frame_helpers.Get()) {
if (RenderFrameMatches(helper, VIEW_TYPE_EXTENSION_BACKGROUND_PAGE,
extension_misc::kUnknownWindowId,
extension_misc::kUnknownTabId, extension_id)) {
blink::WebLocalFrame* web_frame = helper->render_frame()->GetWebFrame();
// Check if this is the top frame.
if (web_frame->Top() == web_frame)
return helper->render_frame();
}
}
return nullptr;
}
v8::Local<v8::Value> ExtensionFrameHelper::GetV8BackgroundPageMainFrame(
v8::Isolate* isolate,
const std::string& extension_id) {
content::RenderFrame* main_frame = GetBackgroundPageFrame(extension_id);
v8::Local<v8::Value> background_page;
blink::WebLocalFrame* web_frame =
main_frame ? main_frame->GetWebFrame() : nullptr;
if (web_frame && blink::WebFrame::ScriptCanAccess(web_frame))
background_page = web_frame->MainWorldScriptContext()->Global();
else
background_page = v8::Undefined(isolate);
return background_page;
}
// static
content::RenderFrame* ExtensionFrameHelper::FindFrame(
content::RenderFrame* relative_to_frame,
const std::string& name) {
// Only pierce browsing instance boundaries if |relative_to_frame| is an
// extension.
const Extension* extension = GetExtensionFromFrame(relative_to_frame);
if (!extension)
return nullptr;
for (const ExtensionFrameHelper* target : g_frame_helpers.Get()) {
// Skip frames with a mismatched name.
if (target->render_frame()->GetWebFrame()->AssignedName().Utf8() != name)
continue;
// Only pierce browsing instance boundaries if the target frame is from the
// same extension (but not when another extension shares the same renderer
// process because of reuse trigerred by process limit).
if (extension != GetExtensionFromFrame(target->render_frame()))
continue;
// TODO(lukasza): https://crbug.com/764487: Investigate if we can further
// restrict scenarios that allow piercing of browsing instance boundaries.
// We hope that the piercing is only needed if the source or target frames
// are for background contents or background page.
ViewType target_view_type = target->view_type();
ViewType source_view_type =
ExtensionFrameHelper::Get(relative_to_frame)->view_type();
UMA_HISTOGRAM_ENUMERATION(
"Extensions.BrowsingInstanceViolation.ExtensionType",
extension->GetType(), Manifest::NUM_LOAD_TYPES);
UMA_HISTOGRAM_ENUMERATION(
"Extensions.BrowsingInstanceViolation.SourceExtensionViewType",
source_view_type, VIEW_TYPE_LAST + 1);
UMA_HISTOGRAM_ENUMERATION(
"Extensions.BrowsingInstanceViolation.TargetExtensionViewType",
target_view_type, VIEW_TYPE_LAST + 1);
bool is_background_source_or_target =
source_view_type == VIEW_TYPE_EXTENSION_BACKGROUND_PAGE ||
source_view_type == VIEW_TYPE_BACKGROUND_CONTENTS ||
target_view_type == VIEW_TYPE_EXTENSION_BACKGROUND_PAGE ||
target_view_type == VIEW_TYPE_BACKGROUND_CONTENTS;
UMA_HISTOGRAM_BOOLEAN(
"Extensions.BrowsingInstanceViolation.IsBackgroundSourceOrTarget",
is_background_source_or_target);
return target->render_frame();
}
return nullptr;
}
// static
bool ExtensionFrameHelper::IsContextForEventPage(const ScriptContext* context) {
content::RenderFrame* render_frame = context->GetRenderFrame();
return context->extension() && render_frame &&
BackgroundInfo::HasLazyBackgroundPage(context->extension()) &&
ExtensionFrameHelper::Get(render_frame)->view_type() ==
VIEW_TYPE_EXTENSION_BACKGROUND_PAGE;
}
void ExtensionFrameHelper::DidCreateDocumentElement() {
did_create_current_document_element_ = true;
extension_dispatcher_->DidCreateDocumentElement(
render_frame()->GetWebFrame());
}
void ExtensionFrameHelper::DidCreateNewDocument() {
did_create_current_document_element_ = false;
}
void ExtensionFrameHelper::RunScriptsAtDocumentStart() {
DCHECK(did_create_current_document_element_);
RunCallbacksWhileFrameIsValid(weak_ptr_factory_.GetWeakPtr(),
&document_element_created_callbacks_);
// |this| might be dead by now.
}
void ExtensionFrameHelper::RunScriptsAtDocumentEnd() {
RunCallbacksWhileFrameIsValid(weak_ptr_factory_.GetWeakPtr(),
&document_load_finished_callbacks_);
// |this| might be dead by now.
}
void ExtensionFrameHelper::RunScriptsAtDocumentIdle() {
RunCallbacksWhileFrameIsValid(weak_ptr_factory_.GetWeakPtr(),
&document_idle_callbacks_);
// |this| might be dead by now.
}
void ExtensionFrameHelper::ScheduleAtDocumentStart(
const base::Closure& callback) {
document_element_created_callbacks_.push_back(callback);
}
void ExtensionFrameHelper::ScheduleAtDocumentEnd(
const base::Closure& callback) {
document_load_finished_callbacks_.push_back(callback);
}
void ExtensionFrameHelper::ScheduleAtDocumentIdle(
const base::Closure& callback) {
document_idle_callbacks_.push_back(callback);
}
void ExtensionFrameHelper::DidStartProvisionalLoad(
blink::WebDocumentLoader* document_loader,
bool is_content_initiated) {
// New window created by chrome.app.window.create() must not start parsing the
// document immediately. The chrome.app.window.create() callback (if any)
// needs to be called prior to the new window's 'load' event. The parser will
// be resumed when it happens. It doesn't apply to sandboxed pages.
if (view_type_ == VIEW_TYPE_APP_WINDOW && render_frame()->IsMainFrame() &&
!has_started_first_navigation_ &&
GURL(document_loader->GetRequest().Url()).SchemeIs(kExtensionScheme) &&
!ScriptContext::IsSandboxedPage(document_loader->GetRequest().Url())) {
document_loader->BlockParser();
}
has_started_first_navigation_ = true;
if (!delayed_main_world_script_initialization_)
return;
delayed_main_world_script_initialization_ = false;
v8::HandleScope handle_scope(v8::Isolate::GetCurrent());
v8::Local<v8::Context> context =
render_frame()->GetWebFrame()->MainWorldScriptContext();
v8::Context::Scope context_scope(context);
extension_dispatcher_->DidCreateScriptContext(render_frame()->GetWebFrame(),
context, kMainWorldId);
// TODO(devlin): Add constants for main world id, no extension group.
}
void ExtensionFrameHelper::DidCreateScriptContext(
v8::Local<v8::Context> context,
int world_id) {
if (world_id == kMainWorldId &&
render_frame()->IsBrowserSideNavigationPending()) {
DCHECK(!delayed_main_world_script_initialization_);
// Defer initializing the extensions script context now because it depends
// on having the URL of the provisional load which isn't available at this
// point with PlzNavigate.
delayed_main_world_script_initialization_ = true;
} else {
extension_dispatcher_->DidCreateScriptContext(render_frame()->GetWebFrame(),
context, world_id);
}
}
void ExtensionFrameHelper::WillReleaseScriptContext(
v8::Local<v8::Context> context,
int world_id) {
extension_dispatcher_->WillReleaseScriptContext(
render_frame()->GetWebFrame(), context, world_id);
}
bool ExtensionFrameHelper::OnMessageReceived(const IPC::Message& message) {
bool handled = true;
IPC_BEGIN_MESSAGE_MAP(ExtensionFrameHelper, message)
IPC_MESSAGE_HANDLER(ExtensionMsg_ValidateMessagePort,
OnExtensionValidateMessagePort)
IPC_MESSAGE_HANDLER(ExtensionMsg_DispatchOnConnect,
OnExtensionDispatchOnConnect)
IPC_MESSAGE_HANDLER(ExtensionMsg_DeliverMessage, OnExtensionDeliverMessage)
IPC_MESSAGE_HANDLER(ExtensionMsg_DispatchOnDisconnect,
OnExtensionDispatchOnDisconnect)
IPC_MESSAGE_HANDLER(ExtensionMsg_SetTabId, OnExtensionSetTabId)
IPC_MESSAGE_HANDLER(ExtensionMsg_UpdateBrowserWindowId,
OnUpdateBrowserWindowId)
IPC_MESSAGE_HANDLER(ExtensionMsg_NotifyRenderViewType,
OnNotifyRendererViewType)
IPC_MESSAGE_HANDLER(ExtensionMsg_Response, OnExtensionResponse)
IPC_MESSAGE_HANDLER(ExtensionMsg_MessageInvoke, OnExtensionMessageInvoke)
IPC_MESSAGE_HANDLER(ExtensionMsg_SetFrameName, OnSetFrameName)
IPC_MESSAGE_HANDLER(ExtensionMsg_AppWindowClosed, OnAppWindowClosed)
IPC_MESSAGE_HANDLER(ExtensionMsg_SetSpatialNavigationEnabled,
OnSetSpatialNavigationEnabled)
IPC_MESSAGE_UNHANDLED(handled = false)
IPC_END_MESSAGE_MAP()
return handled;
}
void ExtensionFrameHelper::OnExtensionValidateMessagePort(const PortId& id) {
extension_dispatcher_->bindings_system()
->GetMessagingService()
->ValidateMessagePort(extension_dispatcher_->script_context_set(), id,
render_frame());
}
void ExtensionFrameHelper::OnExtensionDispatchOnConnect(
const PortId& target_port_id,
const std::string& channel_name,
const ExtensionMsg_TabConnectionInfo& source,
const ExtensionMsg_ExternalConnectionInfo& info,
const std::string& tls_channel_id) {
extension_dispatcher_->bindings_system()
->GetMessagingService()
->DispatchOnConnect(extension_dispatcher_->script_context_set(),
target_port_id, channel_name, source, info,
tls_channel_id, render_frame());
}
void ExtensionFrameHelper::OnExtensionDeliverMessage(const PortId& target_id,
const Message& message) {
extension_dispatcher_->bindings_system()
->GetMessagingService()
->DeliverMessage(extension_dispatcher_->script_context_set(), target_id,
message, render_frame());
}
void ExtensionFrameHelper::OnExtensionDispatchOnDisconnect(
const PortId& id,
const std::string& error_message) {
extension_dispatcher_->bindings_system()
->GetMessagingService()
->DispatchOnDisconnect(extension_dispatcher_->script_context_set(), id,
error_message, render_frame());
}
void ExtensionFrameHelper::OnExtensionSetTabId(int tab_id) {
CHECK_EQ(tab_id_, -1);
CHECK_GE(tab_id, 0);
tab_id_ = tab_id;
}
void ExtensionFrameHelper::OnUpdateBrowserWindowId(int browser_window_id) {
browser_window_id_ = browser_window_id;
}
void ExtensionFrameHelper::OnNotifyRendererViewType(ViewType type) {
// TODO(devlin): It'd be really nice to be able to
// DCHECK_EQ(VIEW_TYPE_INVALID, view_type_) here.
view_type_ = type;
}
void ExtensionFrameHelper::OnExtensionResponse(int request_id,
bool success,
const base::ListValue& response,
const std::string& error) {
extension_dispatcher_->OnExtensionResponse(request_id,
success,
response,
error);
}
void ExtensionFrameHelper::OnExtensionMessageInvoke(
const std::string& extension_id,
const std::string& module_name,
const std::string& function_name,
const base::ListValue& args) {
extension_dispatcher_->InvokeModuleSystemMethod(
render_frame(), extension_id, module_name, function_name, args);
}
void ExtensionFrameHelper::OnSetFrameName(const std::string& name) {
render_frame()->GetWebFrame()->SetName(blink::WebString::FromUTF8(name));
}
void ExtensionFrameHelper::OnAppWindowClosed(bool send_onclosed) {
DCHECK(render_frame()->IsMainFrame());
if (!send_onclosed)
return;
v8::HandleScope scope(v8::Isolate::GetCurrent());
v8::Local<v8::Context> v8_context =
render_frame()->GetWebFrame()->MainWorldScriptContext();
ScriptContext* script_context =
ScriptContextSet::GetContextByV8Context(v8_context);
if (!script_context)
return;
script_context->module_system()->CallModuleMethodSafe("app.window",
"onAppWindowClosed");
}
void ExtensionFrameHelper::OnSetSpatialNavigationEnabled(bool enabled) {
render_frame()
->GetRenderView()
->GetWebView()
->GetSettings()
->SetSpatialNavigationEnabled(enabled);
}
void ExtensionFrameHelper::OnDestruct() {
delete this;
}
void ExtensionFrameHelper::DraggableRegionsChanged() {
if (!render_frame()->IsMainFrame())
return;
blink::WebVector<blink::WebDraggableRegion> webregions =
render_frame()->GetWebFrame()->GetDocument().DraggableRegions();
std::vector<DraggableRegion> regions;
for (blink::WebDraggableRegion& webregion : webregions) {
render_frame()->GetRenderView()->ConvertViewportToWindowViaWidget(
&webregion.bounds);
regions.push_back(DraggableRegion());
DraggableRegion& region = regions.back();
region.bounds = webregion.bounds;
region.draggable = webregion.draggable;
}
Send(new ExtensionHostMsg_UpdateDraggableRegions(routing_id(), regions));
}
} // namespace extensions