summaryrefslogtreecommitdiff
path: root/chromium/extensions/renderer/script_injection_manager.cc
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/extensions/renderer/script_injection_manager.cc')
-rw-r--r--chromium/extensions/renderer/script_injection_manager.cc514
1 files changed, 514 insertions, 0 deletions
diff --git a/chromium/extensions/renderer/script_injection_manager.cc b/chromium/extensions/renderer/script_injection_manager.cc
new file mode 100644
index 00000000000..0e234d0906b
--- /dev/null
+++ b/chromium/extensions/renderer/script_injection_manager.cc
@@ -0,0 +1,514 @@
+// 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_manager.h"
+
+#include <utility>
+
+#include "base/auto_reset.h"
+#include "base/bind.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/values.h"
+#include "content/public/renderer/render_frame.h"
+#include "content/public/renderer/render_frame_observer.h"
+#include "content/public/renderer/render_thread.h"
+#include "extensions/common/extension.h"
+#include "extensions/common/extension_messages.h"
+#include "extensions/common/extension_set.h"
+#include "extensions/renderer/extension_frame_helper.h"
+#include "extensions/renderer/extension_injection_host.h"
+#include "extensions/renderer/programmatic_script_injector.h"
+#include "extensions/renderer/renderer_extension_registry.h"
+#include "extensions/renderer/script_injection.h"
+#include "extensions/renderer/scripts_run_info.h"
+#include "extensions/renderer/web_ui_injection_host.h"
+#include "ipc/ipc_message_macros.h"
+#include "third_party/WebKit/public/web/WebDocument.h"
+#include "third_party/WebKit/public/web/WebFrame.h"
+#include "third_party/WebKit/public/web/WebLocalFrame.h"
+#include "third_party/WebKit/public/web/WebView.h"
+#include "url/gurl.h"
+
+namespace extensions {
+
+namespace {
+
+// The length of time to wait after the DOM is complete to try and run user
+// scripts.
+const int kScriptIdleTimeoutInMs = 200;
+
+// Returns the RunLocation that follows |run_location|.
+UserScript::RunLocation NextRunLocation(UserScript::RunLocation run_location) {
+ switch (run_location) {
+ case UserScript::DOCUMENT_START:
+ return UserScript::DOCUMENT_END;
+ case UserScript::DOCUMENT_END:
+ return UserScript::DOCUMENT_IDLE;
+ case UserScript::DOCUMENT_IDLE:
+ return UserScript::RUN_LOCATION_LAST;
+ case UserScript::UNDEFINED:
+ case UserScript::RUN_DEFERRED:
+ case UserScript::BROWSER_DRIVEN:
+ case UserScript::RUN_LOCATION_LAST:
+ break;
+ }
+ NOTREACHED();
+ return UserScript::RUN_LOCATION_LAST;
+}
+
+} // namespace
+
+class ScriptInjectionManager::RFOHelper : public content::RenderFrameObserver {
+ public:
+ RFOHelper(content::RenderFrame* render_frame,
+ ScriptInjectionManager* manager);
+ ~RFOHelper() override;
+
+ private:
+ // RenderFrameObserver implementation.
+ bool OnMessageReceived(const IPC::Message& message) override;
+ void DidCreateNewDocument() override;
+ void DidCreateDocumentElement() override;
+ void DidFailProvisionalLoad(const blink::WebURLError& error) override;
+ void DidFinishDocumentLoad() override;
+ void DidFinishLoad() override;
+ void FrameDetached() override;
+ void OnDestruct() override;
+
+ virtual void OnExecuteCode(const ExtensionMsg_ExecuteCode_Params& params);
+ virtual void OnExecuteDeclarativeScript(int tab_id,
+ const ExtensionId& extension_id,
+ int script_id,
+ const GURL& url);
+ virtual void OnPermitScriptInjection(int64_t request_id);
+
+ // Tells the ScriptInjectionManager to run tasks associated with
+ // document_idle.
+ void RunIdle();
+
+ void StartInjectScripts(UserScript::RunLocation run_location);
+
+ // Indicate that the frame is no longer valid because it is starting
+ // a new load or closing.
+ void InvalidateAndResetFrame();
+
+ // The owning ScriptInjectionManager.
+ ScriptInjectionManager* manager_;
+
+ bool should_run_idle_;
+
+ base::WeakPtrFactory<RFOHelper> weak_factory_;
+};
+
+ScriptInjectionManager::RFOHelper::RFOHelper(content::RenderFrame* render_frame,
+ ScriptInjectionManager* manager)
+ : content::RenderFrameObserver(render_frame),
+ manager_(manager),
+ should_run_idle_(true),
+ weak_factory_(this) {
+}
+
+ScriptInjectionManager::RFOHelper::~RFOHelper() {
+}
+
+bool ScriptInjectionManager::RFOHelper::OnMessageReceived(
+ const IPC::Message& message) {
+ bool handled = true;
+ IPC_BEGIN_MESSAGE_MAP(ScriptInjectionManager::RFOHelper, message)
+ IPC_MESSAGE_HANDLER(ExtensionMsg_ExecuteCode, OnExecuteCode)
+ IPC_MESSAGE_HANDLER(ExtensionMsg_PermitScriptInjection,
+ OnPermitScriptInjection)
+ IPC_MESSAGE_HANDLER(ExtensionMsg_ExecuteDeclarativeScript,
+ OnExecuteDeclarativeScript)
+ IPC_MESSAGE_UNHANDLED(handled = false)
+ IPC_END_MESSAGE_MAP()
+ return handled;
+}
+
+void ScriptInjectionManager::RFOHelper::DidCreateNewDocument() {
+ // A new document is going to be shown, so invalidate the old document state.
+ // Check that the frame's state is known before invalidating the frame,
+ // because it is possible that a script injection was scheduled before the
+ // page was loaded, e.g. by navigating to a javascript: URL before the page
+ // has loaded.
+ if (manager_->frame_statuses_.count(render_frame()) != 0)
+ InvalidateAndResetFrame();
+}
+
+void ScriptInjectionManager::RFOHelper::DidCreateDocumentElement() {
+ ExtensionFrameHelper::Get(render_frame())
+ ->ScheduleAtDocumentStart(
+ base::Bind(&ScriptInjectionManager::RFOHelper::StartInjectScripts,
+ weak_factory_.GetWeakPtr(), UserScript::DOCUMENT_START));
+}
+
+void ScriptInjectionManager::RFOHelper::DidFailProvisionalLoad(
+ const blink::WebURLError& error) {
+ FrameStatusMap::iterator it = manager_->frame_statuses_.find(render_frame());
+ if (it != manager_->frame_statuses_.end() &&
+ it->second == UserScript::DOCUMENT_START) {
+ // Since the provisional load failed, the frame stays at its previous loaded
+ // state and origin (or the parent's origin for new/about:blank frames).
+ // Reset the frame to DOCUMENT_IDLE in order to reflect that the frame is
+ // done loading, and avoid any deadlock in the system.
+ //
+ // We skip injection of DOCUMENT_END and DOCUMENT_IDLE scripts, because the
+ // injections closely follow the DOMContentLoaded (and onload) events, which
+ // are not triggered after a failed provisional load.
+ // This assumption is verified in the checkDOMContentLoadedEvent subtest of
+ // ExecuteScriptApiTest.FrameWithHttp204 (browser_tests).
+ InvalidateAndResetFrame();
+ should_run_idle_ = false;
+ manager_->frame_statuses_[render_frame()] = UserScript::DOCUMENT_IDLE;
+ }
+}
+
+void ScriptInjectionManager::RFOHelper::DidFinishDocumentLoad() {
+ DCHECK(content::RenderThread::Get());
+ ExtensionFrameHelper::Get(render_frame())
+ ->ScheduleAtDocumentEnd(
+ base::Bind(&ScriptInjectionManager::RFOHelper::StartInjectScripts,
+ weak_factory_.GetWeakPtr(), UserScript::DOCUMENT_END));
+
+ // We try to run idle in two places: here and DidFinishLoad.
+ // DidFinishDocumentLoad() corresponds to completing the document's load,
+ // whereas DidFinishLoad corresponds to completing the document and all
+ // subresources' load. We don't want to hold up script injection for a
+ // particularly slow subresource, so we set a delayed task from here - but if
+ // we finish everything before that point (i.e., DidFinishLoad() is
+ // triggered), then there's no reason to keep waiting.
+ base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
+ FROM_HERE,
+ base::Bind(&ScriptInjectionManager::RFOHelper::RunIdle,
+ weak_factory_.GetWeakPtr()),
+ base::TimeDelta::FromMilliseconds(kScriptIdleTimeoutInMs));
+}
+
+void ScriptInjectionManager::RFOHelper::DidFinishLoad() {
+ DCHECK(content::RenderThread::Get());
+ // Ensure that we don't block any UI progress by running scripts.
+ base::ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE,
+ base::Bind(&ScriptInjectionManager::RFOHelper::RunIdle,
+ weak_factory_.GetWeakPtr()));
+}
+
+void ScriptInjectionManager::RFOHelper::FrameDetached() {
+ // The frame is closing - invalidate.
+ InvalidateAndResetFrame();
+}
+
+void ScriptInjectionManager::RFOHelper::OnDestruct() {
+ manager_->RemoveObserver(this);
+}
+
+void ScriptInjectionManager::RFOHelper::OnExecuteCode(
+ const ExtensionMsg_ExecuteCode_Params& params) {
+ manager_->HandleExecuteCode(params, render_frame());
+}
+
+void ScriptInjectionManager::RFOHelper::OnExecuteDeclarativeScript(
+ int tab_id,
+ const ExtensionId& extension_id,
+ int script_id,
+ const GURL& url) {
+ // TODO(markdittmer): URL-checking isn't the best security measure.
+ // Begin script injection workflow only if the current URL is identical to
+ // the one that matched declarative conditions in the browser.
+ if (render_frame()->GetWebFrame()->document().url() == url) {
+ manager_->HandleExecuteDeclarativeScript(render_frame(),
+ tab_id,
+ extension_id,
+ script_id,
+ url);
+ }
+}
+
+void ScriptInjectionManager::RFOHelper::OnPermitScriptInjection(
+ int64_t request_id) {
+ manager_->HandlePermitScriptInjection(request_id);
+}
+
+void ScriptInjectionManager::RFOHelper::RunIdle() {
+ // Only notify the manager if the frame hasn't either been removed or already
+ // had idle run since the task to RunIdle() was posted.
+ if (should_run_idle_) {
+ should_run_idle_ = false;
+ manager_->StartInjectScripts(render_frame(), UserScript::DOCUMENT_IDLE);
+ }
+}
+
+void ScriptInjectionManager::RFOHelper::StartInjectScripts(
+ UserScript::RunLocation run_location) {
+ manager_->StartInjectScripts(render_frame(), run_location);
+}
+
+void ScriptInjectionManager::RFOHelper::InvalidateAndResetFrame() {
+ // Invalidate any pending idle injections, and reset the frame inject on idle.
+ weak_factory_.InvalidateWeakPtrs();
+ // We reset to inject on idle, because the frame can be reused (in the case of
+ // navigation).
+ should_run_idle_ = true;
+ manager_->InvalidateForFrame(render_frame());
+}
+
+ScriptInjectionManager::ScriptInjectionManager(
+ UserScriptSetManager* user_script_set_manager)
+ : user_script_set_manager_(user_script_set_manager),
+ user_script_set_manager_observer_(this) {
+ user_script_set_manager_observer_.Add(user_script_set_manager_);
+}
+
+ScriptInjectionManager::~ScriptInjectionManager() {
+ for (const auto& injection : pending_injections_)
+ injection->invalidate_render_frame();
+ for (const auto& injection : running_injections_)
+ injection->invalidate_render_frame();
+}
+
+void ScriptInjectionManager::OnRenderFrameCreated(
+ content::RenderFrame* render_frame) {
+ rfo_helpers_.push_back(make_scoped_ptr(new RFOHelper(render_frame, this)));
+}
+
+void ScriptInjectionManager::OnExtensionUnloaded(
+ const std::string& extension_id) {
+ for (auto iter = pending_injections_.begin();
+ iter != pending_injections_.end();) {
+ if ((*iter)->host_id().id() == extension_id) {
+ (*iter)->OnHostRemoved();
+ iter = pending_injections_.erase(iter);
+ } else {
+ ++iter;
+ }
+ }
+}
+
+void ScriptInjectionManager::OnInjectionFinished(
+ ScriptInjection* injection) {
+ auto iter =
+ std::find_if(running_injections_.begin(), running_injections_.end(),
+ [injection](const scoped_ptr<ScriptInjection>& mode) {
+ return injection == mode.get();
+ });
+ if (iter != running_injections_.end())
+ running_injections_.erase(iter);
+}
+
+void ScriptInjectionManager::OnUserScriptsUpdated(
+ const std::set<HostID>& changed_hosts,
+ const std::vector<UserScript*>& scripts) {
+ for (auto iter = pending_injections_.begin();
+ iter != pending_injections_.end();) {
+ if (changed_hosts.count((*iter)->host_id()) > 0)
+ iter = pending_injections_.erase(iter);
+ else
+ ++iter;
+ }
+}
+
+void ScriptInjectionManager::RemoveObserver(RFOHelper* helper) {
+ for (auto iter = rfo_helpers_.begin(); iter != rfo_helpers_.end(); ++iter) {
+ if (iter->get() == helper) {
+ rfo_helpers_.erase(iter);
+ break;
+ }
+ }
+}
+
+void ScriptInjectionManager::InvalidateForFrame(content::RenderFrame* frame) {
+ // If the frame invalidated is the frame being injected into, we need to
+ // note it.
+ active_injection_frames_.erase(frame);
+
+ for (auto iter = pending_injections_.begin();
+ iter != pending_injections_.end();) {
+ if ((*iter)->render_frame() == frame)
+ iter = pending_injections_.erase(iter);
+ else
+ ++iter;
+ }
+
+ frame_statuses_.erase(frame);
+}
+
+void ScriptInjectionManager::StartInjectScripts(
+ content::RenderFrame* frame,
+ UserScript::RunLocation run_location) {
+ FrameStatusMap::iterator iter = frame_statuses_.find(frame);
+ // We also don't execute if we detect that the run location is somehow out of
+ // order. This can happen if:
+ // - The first run location reported for the frame isn't DOCUMENT_START, or
+ // - The run location reported doesn't immediately follow the previous
+ // reported run location.
+ // We don't want to run because extensions may have requirements that scripts
+ // running in an earlier run location have run by the time a later script
+ // runs. Better to just not run.
+ // Note that we check run_location > NextRunLocation() in the second clause
+ // (as opposed to !=) because earlier signals (like DidCreateDocumentElement)
+ // can happen multiple times, so we can receive earlier/equal run locations.
+ if ((iter == frame_statuses_.end() &&
+ run_location != UserScript::DOCUMENT_START) ||
+ (iter != frame_statuses_.end() &&
+ run_location > NextRunLocation(iter->second))) {
+ // We also invalidate the frame, because the run order of pending injections
+ // may also be bad.
+ InvalidateForFrame(frame);
+ return;
+ } else if (iter != frame_statuses_.end() && iter->second >= run_location) {
+ // Certain run location signals (like DidCreateDocumentElement) can happen
+ // multiple times. Ignore the subsequent signals.
+ return;
+ }
+
+ // Otherwise, all is right in the world, and we can get on with the
+ // injections!
+ frame_statuses_[frame] = run_location;
+ InjectScripts(frame, run_location);
+}
+
+void ScriptInjectionManager::InjectScripts(
+ content::RenderFrame* frame,
+ UserScript::RunLocation run_location) {
+ // Find any injections that want to run on the given frame.
+ ScriptInjectionVector frame_injections;
+ for (auto iter = pending_injections_.begin();
+ iter != pending_injections_.end();) {
+ if ((*iter)->render_frame() == frame) {
+ frame_injections.push_back(std::move(*iter));
+ iter = pending_injections_.erase(iter);
+ } else {
+ ++iter;
+ }
+ }
+
+ // Add any injections for user scripts.
+ int tab_id = ExtensionFrameHelper::Get(frame)->tab_id();
+ user_script_set_manager_->GetAllInjections(&frame_injections, frame, tab_id,
+ run_location);
+
+ // Note that we are running in |frame|.
+ active_injection_frames_.insert(frame);
+
+ ScriptsRunInfo scripts_run_info(frame, run_location);
+ for (auto iter = frame_injections.begin(); iter != frame_injections.end();) {
+ // It's possible for the frame to be invalidated in the course of injection
+ // (if a script removes its own frame, for example). If this happens, abort.
+ if (!active_injection_frames_.count(frame))
+ break;
+ scoped_ptr<ScriptInjection> injection(std::move(*iter));
+ iter = frame_injections.erase(iter);
+ TryToInject(std::move(injection), run_location, &scripts_run_info);
+ }
+
+ // We are done running in the frame.
+ active_injection_frames_.erase(frame);
+
+ scripts_run_info.LogRun();
+}
+
+void ScriptInjectionManager::TryToInject(
+ scoped_ptr<ScriptInjection> injection,
+ UserScript::RunLocation run_location,
+ ScriptsRunInfo* scripts_run_info) {
+ // Try to inject the script. If the injection is waiting (i.e., for
+ // permission), add it to the list of pending injections. If the injection
+ // has blocked, add it to the list of running injections.
+ // The Unretained below is safe because this object owns all the
+ // ScriptInjections, so is guaranteed to outlive them.
+ switch (injection->TryToInject(
+ run_location,
+ scripts_run_info,
+ base::Bind(&ScriptInjectionManager::OnInjectionFinished,
+ base::Unretained(this)))) {
+ case ScriptInjection::INJECTION_WAITING:
+ pending_injections_.push_back(std::move(injection));
+ break;
+ case ScriptInjection::INJECTION_BLOCKED:
+ running_injections_.push_back(std::move(injection));
+ break;
+ case ScriptInjection::INJECTION_FINISHED:
+ break;
+ }
+}
+
+void ScriptInjectionManager::HandleExecuteCode(
+ const ExtensionMsg_ExecuteCode_Params& params,
+ content::RenderFrame* render_frame) {
+ scoped_ptr<const InjectionHost> injection_host;
+ if (params.host_id.type() == HostID::EXTENSIONS) {
+ injection_host = ExtensionInjectionHost::Create(params.host_id.id());
+ if (!injection_host)
+ return;
+ } else if (params.host_id.type() == HostID::WEBUI) {
+ injection_host.reset(
+ new WebUIInjectionHost(params.host_id));
+ }
+
+ scoped_ptr<ScriptInjection> injection(new ScriptInjection(
+ scoped_ptr<ScriptInjector>(
+ new ProgrammaticScriptInjector(params, render_frame)),
+ render_frame, std::move(injection_host),
+ static_cast<UserScript::RunLocation>(params.run_at)));
+
+ FrameStatusMap::const_iterator iter = frame_statuses_.find(render_frame);
+ UserScript::RunLocation run_location =
+ iter == frame_statuses_.end() ? UserScript::UNDEFINED : iter->second;
+
+ ScriptsRunInfo scripts_run_info(render_frame, run_location);
+ TryToInject(std::move(injection), run_location, &scripts_run_info);
+}
+
+void ScriptInjectionManager::HandleExecuteDeclarativeScript(
+ content::RenderFrame* render_frame,
+ int tab_id,
+ const ExtensionId& extension_id,
+ int script_id,
+ const GURL& url) {
+ scoped_ptr<ScriptInjection> injection =
+ user_script_set_manager_->GetInjectionForDeclarativeScript(
+ script_id,
+ render_frame,
+ tab_id,
+ url,
+ extension_id);
+ if (injection.get()) {
+ ScriptsRunInfo scripts_run_info(render_frame, UserScript::BROWSER_DRIVEN);
+ // TODO(markdittmer): Use return value of TryToInject for error handling.
+ TryToInject(std::move(injection), UserScript::BROWSER_DRIVEN,
+ &scripts_run_info);
+
+ scripts_run_info.LogRun();
+ }
+}
+
+void ScriptInjectionManager::HandlePermitScriptInjection(int64_t request_id) {
+ auto iter = pending_injections_.begin();
+ for (; iter != pending_injections_.end(); ++iter) {
+ if ((*iter)->request_id() == request_id) {
+ DCHECK((*iter)->host_id().type() == HostID::EXTENSIONS);
+ break;
+ }
+ }
+ if (iter == pending_injections_.end())
+ return;
+
+ // At this point, because the request is present in pending_injections_, we
+ // know that this is the same page that issued the request (otherwise,
+ // RFOHelper::InvalidateAndResetFrame would have caused it to be cleared out).
+
+ scoped_ptr<ScriptInjection> injection(std::move(*iter));
+ pending_injections_.erase(iter);
+
+ ScriptsRunInfo scripts_run_info(injection->render_frame(),
+ UserScript::RUN_DEFERRED);
+ ScriptInjection::InjectionResult res = injection->OnPermissionGranted(
+ &scripts_run_info);
+ if (res == ScriptInjection::INJECTION_BLOCKED)
+ running_injections_.push_back(std::move(injection));
+ scripts_run_info.LogRun();
+}
+
+} // namespace extensions