blob: 890c3de6d45503ebc295fc7a521bcd26649bb1ce [file] [log] [blame]
// Copyright 2014 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/script_injection.h"
#include <vector>
#include "base/lazy_instance.h"
#include "base/metrics/histogram.h"
#include "content/public/common/url_constants.h"
#include "content/public/renderer/render_view.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_messages.h"
#include "extensions/common/feature_switch.h"
#include "extensions/common/permissions/permissions_data.h"
#include "extensions/renderer/dom_activity_logger.h"
#include "extensions/renderer/extension_groups.h"
#include "extensions/renderer/extension_helper.h"
#include "extensions/renderer/script_context.h"
#include "extensions/renderer/user_script_slave.h"
#include "grit/extensions_renderer_resources.h"
#include "third_party/WebKit/public/web/WebDocument.h"
#include "third_party/WebKit/public/web/WebFrame.h"
#include "third_party/WebKit/public/web/WebScriptSource.h"
#include "third_party/WebKit/public/web/WebView.h"
#include "ui/base/resource/resource_bundle.h"
#include "url/gurl.h"
namespace extensions {
namespace {
// The id of the next pending injection.
int64 g_next_pending_id = 0;
// The number of an invalid request, which is used if the feature to delay
// script injection is not enabled.
const int64 kInvalidRequestId = -1;
// These two strings are injected before and after the Greasemonkey API and
// user script to wrap it in an anonymous scope.
const char kUserScriptHead[] = "(function (unsafeWindow) {\n";
const char kUserScriptTail[] = "\n})(window);";
// Greasemonkey API source that is injected with the scripts.
struct GreasemonkeyApiJsString {
GreasemonkeyApiJsString();
blink::WebScriptSource source;
};
// The below constructor, monstrous as it is, just makes a WebScriptSource from
// the GreasemonkeyApiJs resource.
GreasemonkeyApiJsString::GreasemonkeyApiJsString()
: source(blink::WebScriptSource(blink::WebString::fromUTF8(
ResourceBundle::GetSharedInstance().GetRawDataResource(
IDR_GREASEMONKEY_API_JS).as_string()))) {
}
base::LazyInstance<GreasemonkeyApiJsString> g_greasemonkey_api =
LAZY_INSTANCE_INITIALIZER;
} // namespace
ScriptInjection::ScriptsRunInfo::ScriptsRunInfo() : num_css(0u), num_js(0u) {
}
ScriptInjection::ScriptsRunInfo::~ScriptsRunInfo() {
}
struct ScriptInjection::PendingInjection {
PendingInjection(blink::WebFrame* web_frame,
UserScript::RunLocation run_location,
int page_id);
~PendingInjection();
// The globally-unique id of this request.
int64 id;
// The pointer to the web frame into which the script should be injected.
// This is weak, but safe because we remove pending requests when a frame is
// terminated.
blink::WebFrame* web_frame;
// The run location to inject at.
// Note: This could be a lie - we might inject well after this run location
// has come and gone. But we need to know it to know which scripts to inject.
UserScript::RunLocation run_location;
// The corresponding page id, to protect against races.
int page_id;
};
ScriptInjection::PendingInjection::PendingInjection(
blink::WebFrame* web_frame,
UserScript::RunLocation run_location,
int page_id)
: id(g_next_pending_id++),
web_frame(web_frame),
run_location(run_location),
page_id(page_id) {
}
ScriptInjection::PendingInjection::~PendingInjection() {
}
// static
GURL ScriptInjection::GetDocumentUrlForFrame(blink::WebFrame* frame) {
GURL data_source_url = ScriptContext::GetDataSourceURLForFrame(frame);
if (!data_source_url.is_empty() && frame->isViewSourceModeEnabled()) {
data_source_url = GURL(content::kViewSourceScheme + std::string(":") +
data_source_url.spec());
}
return data_source_url;
}
ScriptInjection::ScriptInjection(
scoped_ptr<UserScript> script,
UserScriptSlave* user_script_slave)
: script_(script.Pass()),
extension_id_(script_->extension_id()),
user_script_slave_(user_script_slave),
is_standalone_or_emulate_greasemonkey_(
script_->is_standalone() || script_->emulate_greasemonkey()) {
}
ScriptInjection::~ScriptInjection() {
}
void ScriptInjection::InjectIfAllowed(blink::WebFrame* frame,
UserScript::RunLocation run_location,
const GURL& document_url,
ScriptsRunInfo* scripts_run_info) {
if (!WantsToRun(frame, run_location, document_url))
return;
const Extension* extension = user_script_slave_->GetExtension(extension_id_);
DCHECK(extension); // WantsToRun() should be false if there's no extension.
// We use the top render view here (instead of the render view for the
// frame), because script injection on any frame requires permission for
// the top frame. Additionally, if we have to show any UI for permissions,
// it should only be done on the top frame.
content::RenderView* top_render_view =
content::RenderView::FromWebView(frame->top()->view());
int tab_id = ExtensionHelper::Get(top_render_view)->tab_id();
// By default, we allow injection.
bool should_inject = true;
// Check if the extension requires user consent for injection *and* we have a
// valid tab id (if we don't have a tab id, we have no UI surface to ask for
// user consent).
if (tab_id != -1 &&
extension->permissions_data()->RequiresActionForScriptExecution(
extension, tab_id, frame->top()->document().url())) {
int64 request_id = kInvalidRequestId;
int page_id = top_render_view->GetPageId();
// We only delay the injection if the feature is enabled.
// Otherwise, we simply treat this as a notification by passing an invalid
// id.
if (FeatureSwitch::scripts_require_action()->IsEnabled()) {
should_inject = false;
ScopedVector<PendingInjection>::iterator pending_injection =
pending_injections_.insert(
pending_injections_.end(),
new PendingInjection(frame, run_location, page_id));
request_id = (*pending_injection)->id;
}
top_render_view->Send(
new ExtensionHostMsg_RequestContentScriptPermission(
top_render_view->GetRoutingID(),
extension->id(),
page_id,
request_id));
}
if (should_inject)
Inject(frame, run_location, scripts_run_info);
}
bool ScriptInjection::NotifyScriptPermitted(
int64 request_id,
content::RenderView* render_view,
ScriptsRunInfo* scripts_run_info,
blink::WebFrame** frame_out) {
ScopedVector<PendingInjection>::iterator iter = pending_injections_.begin();
while (iter != pending_injections_.end() && (*iter)->id != request_id)
++iter;
// No matching request.
if (iter == pending_injections_.end())
return false;
// We found the request, so pull it out of the pending list.
scoped_ptr<PendingInjection> pending_injection(*iter);
pending_injections_.weak_erase(iter);
// Ensure the Page ID and Extension are still valid. Otherwise, don't inject.
if (render_view->GetPageId() != pending_injection->page_id)
return false;
const Extension* extension = user_script_slave_->GetExtension(extension_id_);
if (!extension)
return false;
// Everything matches! Inject the script.
if (frame_out)
*frame_out = pending_injection->web_frame;
Inject(pending_injection->web_frame,
pending_injection->run_location,
scripts_run_info);
return true;
}
void ScriptInjection::FrameDetached(blink::WebFrame* frame) {
// Any pending injections associated with the given frame will never run.
// Remove them.
for (ScopedVector<PendingInjection>::iterator iter =
pending_injections_.begin();
iter != pending_injections_.end();) {
if ((*iter)->web_frame == frame)
iter = pending_injections_.erase(iter);
else
++iter;
}
}
void ScriptInjection::SetScript(scoped_ptr<UserScript> script) {
script_.reset(script.release());
}
bool ScriptInjection::WantsToRun(blink::WebFrame* frame,
UserScript::RunLocation run_location,
const GURL& document_url) const {
if (frame->parent() && !script_->match_all_frames())
return false; // Only match subframes if the script declared it wanted to.
const Extension* extension = user_script_slave_->GetExtension(extension_id_);
// Since extension info is sent separately from user script info, they can
// be out of sync. We just ignore this situation.
if (!extension)
return false;
// Content scripts are not tab-specific.
static const int kNoTabId = -1;
// We don't have a process id in this context.
static const int kNoProcessId = -1;
GURL effective_document_url = ScriptContext::GetEffectiveDocumentURL(
frame, document_url, script_->match_about_blank());
if (!script_->MatchesURL(effective_document_url))
return false;
if (!extension->permissions_data()->CanRunContentScriptOnPage(
extension,
effective_document_url,
frame->top()->document().url(),
kNoTabId,
kNoProcessId,
NULL /* ignore error */)) {
return false;
}
return ShouldInjectCSS(run_location) || ShouldInjectJS(run_location);
}
void ScriptInjection::Inject(blink::WebFrame* frame,
UserScript::RunLocation run_location,
ScriptsRunInfo* scripts_run_info) const {
DCHECK(frame);
DCHECK(scripts_run_info);
DCHECK(WantsToRun(frame, run_location, GetDocumentUrlForFrame(frame)));
DCHECK(user_script_slave_->GetExtension(extension_id_));
if (ShouldInjectCSS(run_location))
InjectCSS(frame, scripts_run_info);
if (ShouldInjectJS(run_location))
InjectJS(frame, scripts_run_info);
}
bool ScriptInjection::ShouldInjectJS(UserScript::RunLocation run_location)
const {
return !script_->js_scripts().empty() &&
script_->run_location() == run_location;
}
bool ScriptInjection::ShouldInjectCSS(UserScript::RunLocation run_location)
const {
return !script_->css_scripts().empty() &&
run_location == UserScript::DOCUMENT_START;
}
void ScriptInjection::InjectJS(blink::WebFrame* frame,
ScriptsRunInfo* scripts_run_info) const {
const UserScript::FileList& js_scripts = script_->js_scripts();
std::vector<blink::WebScriptSource> sources;
scripts_run_info->num_js += js_scripts.size();
for (UserScript::FileList::const_iterator iter = js_scripts.begin();
iter != js_scripts.end();
++iter) {
std::string content = iter->GetContent().as_string();
// We add this dumb function wrapper for standalone user script to
// emulate what Greasemonkey does.
// TODO(aa): I think that maybe "is_standalone" scripts don't exist
// anymore. Investigate.
if (is_standalone_or_emulate_greasemonkey_) {
content.insert(0, kUserScriptHead);
content += kUserScriptTail;
}
sources.push_back(blink::WebScriptSource(
blink::WebString::fromUTF8(content), iter->url()));
}
// Emulate Greasemonkey API for scripts that were converted to extensions
// and "standalone" user scripts.
if (is_standalone_or_emulate_greasemonkey_)
sources.insert(sources.begin(), g_greasemonkey_api.Get().source);
int isolated_world_id =
user_script_slave_->GetIsolatedWorldIdForExtension(
user_script_slave_->GetExtension(extension_id_), frame);
base::ElapsedTimer exec_timer;
DOMActivityLogger::AttachToWorld(isolated_world_id, extension_id_);
frame->executeScriptInIsolatedWorld(isolated_world_id,
&sources.front(),
sources.size(),
EXTENSION_GROUP_CONTENT_SCRIPTS);
UMA_HISTOGRAM_TIMES("Extensions.InjectScriptTime", exec_timer.Elapsed());
for (std::vector<blink::WebScriptSource>::const_iterator iter =
sources.begin();
iter != sources.end();
++iter) {
scripts_run_info->executing_scripts[extension_id_].insert(
GURL(iter->url).path());
}
}
void ScriptInjection::InjectCSS(blink::WebFrame* frame,
ScriptsRunInfo* scripts_run_info) const {
const UserScript::FileList& css_scripts = script_->css_scripts();
scripts_run_info->num_css += css_scripts.size();
for (UserScript::FileList::const_iterator iter = css_scripts.begin();
iter != css_scripts.end();
++iter) {
frame->document().insertStyleSheet(
blink::WebString::fromUTF8(iter->GetContent().as_string()));
}
}
} // namespace extensions