// 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/user_script_scheduler.h"
#include "base/bind.h"
#include "base/logging.h"
#include "base/message_loop/message_loop.h"
#include "content/public/renderer/render_view.h"
#include "content/public/renderer/v8_value_converter.h"
#include "extensions/common/error_utils.h"
#include "extensions/common/extension_messages.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/permissions/permissions_data.h"
#include "extensions/renderer/dispatcher.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 "third_party/WebKit/public/platform/WebString.h"
#include "third_party/WebKit/public/platform/WebVector.h"
#include "third_party/WebKit/public/web/WebDocument.h"
#include "third_party/WebKit/public/web/WebFrame.h"
#include "third_party/WebKit/public/web/WebScopedUserGesture.h"
#include "third_party/WebKit/public/web/WebScriptSource.h"
#include "third_party/WebKit/public/web/WebView.h"
#include "v8/include/v8.h"
namespace {
// The length of time to wait after the DOM is complete to try and run user
// scripts.
const int kUserScriptIdleTimeoutMs = 200;
}
using blink::WebDocument;
using blink::WebFrame;
using blink::WebString;
using blink::WebVector;
using blink::WebView;
namespace extensions {
UserScriptScheduler::UserScriptScheduler(WebFrame* frame,
Dispatcher* dispatcher)
: weak_factory_(this),
frame_(frame),
current_location_(UserScript::UNDEFINED),
has_run_idle_(false),
dispatcher_(dispatcher) {
for (int i = UserScript::UNDEFINED; i < UserScript::RUN_LOCATION_LAST; ++i) {
pending_execution_map_[static_cast<UserScript::RunLocation>(i)] =
std::queue<linked_ptr<ExtensionMsg_ExecuteCode_Params> >();
}
}
UserScriptScheduler::~UserScriptScheduler() {
}
void UserScriptScheduler::ExecuteCode(
const ExtensionMsg_ExecuteCode_Params& params) {
UserScript::RunLocation run_at =
static_cast<UserScript::RunLocation>(params.run_at);
if (current_location_ < run_at) {
pending_execution_map_[run_at].push(
linked_ptr<ExtensionMsg_ExecuteCode_Params>(
new ExtensionMsg_ExecuteCode_Params(params)));
return;
}
ExecuteCodeImpl(params);
}
void UserScriptScheduler::DidCreateDocumentElement() {
current_location_ = UserScript::DOCUMENT_START;
MaybeRun();
}
void UserScriptScheduler::DidFinishDocumentLoad() {
current_location_ = UserScript::DOCUMENT_END;
MaybeRun();
// Schedule a run for DOCUMENT_IDLE
base::MessageLoop::current()->PostDelayedTask(
FROM_HERE,
base::Bind(&UserScriptScheduler::IdleTimeout, weak_factory_.GetWeakPtr()),
base::TimeDelta::FromMilliseconds(kUserScriptIdleTimeoutMs));
}
void UserScriptScheduler::DidFinishLoad() {
current_location_ = UserScript::DOCUMENT_IDLE;
// Ensure that running scripts does not keep any progress UI running.
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(&UserScriptScheduler::MaybeRun, weak_factory_.GetWeakPtr()));
}
void UserScriptScheduler::DidStartProvisionalLoad() {
// The frame is navigating, so reset the state since we'll want to inject
// scripts once the load finishes.
current_location_ = UserScript::UNDEFINED;
has_run_idle_ = false;
weak_factory_.InvalidateWeakPtrs();
std::map<UserScript::RunLocation, ExecutionQueue>::iterator itr =
pending_execution_map_.begin();
for (itr = pending_execution_map_.begin();
itr != pending_execution_map_.end(); ++itr) {
while (!itr->second.empty())
itr->second.pop();
}
}
void UserScriptScheduler::IdleTimeout() {
current_location_ = UserScript::DOCUMENT_IDLE;
MaybeRun();
}
void UserScriptScheduler::MaybeRun() {
if (current_location_ == UserScript::UNDEFINED)
return;
if (!has_run_idle_ && current_location_ == UserScript::DOCUMENT_IDLE) {
has_run_idle_ = true;
dispatcher_->user_script_slave()->InjectScripts(
frame_, UserScript::DOCUMENT_IDLE);
}
// Run all tasks from the current time and earlier.
for (int i = UserScript::DOCUMENT_START;
i <= current_location_; ++i) {
UserScript::RunLocation run_time = static_cast<UserScript::RunLocation>(i);
while (!pending_execution_map_[run_time].empty()) {
linked_ptr<ExtensionMsg_ExecuteCode_Params>& params =
pending_execution_map_[run_time].front();
ExecuteCodeImpl(*params);
pending_execution_map_[run_time].pop();
}
}
}
void UserScriptScheduler::ExecuteCodeImpl(
const ExtensionMsg_ExecuteCode_Params& params) {
const Extension* extension = dispatcher_->extensions()->GetByID(
params.extension_id);
content::RenderView* render_view =
content::RenderView::FromWebView(frame_->view());
ExtensionHelper* extension_helper = ExtensionHelper::Get(render_view);
base::ListValue execution_results;
// Since extension info is sent separately from user script info, they can
// be out of sync. We just ignore this situation.
if (!extension) {
render_view->Send(
new ExtensionHostMsg_ExecuteCodeFinished(render_view->GetRoutingID(),
params.request_id,
std::string(), // no error
-1,
GURL(std::string()),
execution_results));
return;
}
std::vector<WebFrame*> frame_vector;
frame_vector.push_back(frame_);
if (params.all_frames)
GetAllChildFrames(frame_, &frame_vector);
std::string error;
scoped_ptr<blink::WebScopedUserGesture> gesture;
if (params.user_gesture)
gesture.reset(new blink::WebScopedUserGesture);
GURL top_url = frame_->document().url();
for (std::vector<WebFrame*>::iterator frame_it = frame_vector.begin();
frame_it != frame_vector.end(); ++frame_it) {
WebFrame* child_frame = *frame_it;
CHECK(child_frame) << top_url;
// We recheck access here in the renderer for extra safety against races
// with navigation.
//
// But different frames can have different URLs, and the extension might
// only have access to a subset of them. For the top frame, we can
// immediately send an error and stop because the browser process
// considers that an error too.
//
// For child frames, we just skip ones the extension doesn't have access
// to and carry on.
GURL document_url = ScriptContext::GetEffectiveDocumentURL(
child_frame, child_frame->document().url(), params.match_about_blank);
bool can_execute_script =
extension->permissions_data()->CanAccessPage(extension,
document_url,
top_url,
extension_helper->tab_id(),
-1, // no process ID.
NULL); // ignore error.
if ((!params.is_web_view && !can_execute_script) ||
(params.is_web_view && document_url != params.webview_src)) {
if (child_frame->parent()) {
continue;
} else {
error = ErrorUtils::FormatErrorMessage(
manifest_errors::kCannotAccessPage, document_url.spec());
break;
}
}
if (params.is_javascript) {
blink::WebScriptSource source(
WebString::fromUTF8(params.code), params.file_url);
v8::HandleScope scope(v8::Isolate::GetCurrent());
scoped_ptr<content::V8ValueConverter> v8_converter(
content::V8ValueConverter::create());
v8::Local<v8::Value> script_value;
if (params.in_main_world) {
DOMActivityLogger::AttachToWorld(DOMActivityLogger::kMainWorldId,
extension->id());
script_value = child_frame->executeScriptAndReturnValue(source);
} else {
blink::WebVector<v8::Local<v8::Value> > results;
std::vector<blink::WebScriptSource> sources;
sources.push_back(source);
int isolated_world_id =
dispatcher_->user_script_slave()->GetIsolatedWorldIdForExtension(
extension, child_frame);
DOMActivityLogger::AttachToWorld(isolated_world_id, extension->id());
child_frame->executeScriptInIsolatedWorld(
isolated_world_id, &sources.front(),
sources.size(), EXTENSION_GROUP_CONTENT_SCRIPTS, &results);
// We only expect one value back since we only pushed one source
if (results.size() == 1 && !results[0].IsEmpty())
script_value = results[0];
}
if (params.wants_result && !script_value.IsEmpty()) {
// It's safe to always use the main world context when converting here.
// V8ValueConverterImpl shouldn't actually care about the context scope,
// and it switches to v8::Object's creation context when encountered.
v8::Local<v8::Context> context = child_frame->mainWorldScriptContext();
base::Value* result = v8_converter->FromV8Value(script_value, context);
// Always append an execution result (i.e. no result == null result) so
// that |execution_results| lines up with the frames.
execution_results.Append(
result ? result : base::Value::CreateNullValue());
}
} else {
child_frame->document().insertStyleSheet(
WebString::fromUTF8(params.code));
}
}
render_view->Send(new ExtensionHostMsg_ExecuteCodeFinished(
render_view->GetRoutingID(),
params.request_id,
error,
render_view->GetPageId(),
ScriptContext::GetDataSourceURLForFrame(frame_),
execution_results));
}
bool UserScriptScheduler::GetAllChildFrames(
WebFrame* parent_frame,
std::vector<WebFrame*>* frames_vector) const {
if (!parent_frame)
return false;
for (WebFrame* child_frame = parent_frame->firstChild(); child_frame;
child_frame = child_frame->nextSibling()) {
frames_vector->push_back(child_frame);
GetAllChildFrames(child_frame, frames_vector);
}
return true;
}
} // namespace extensions