blob: f311f400c04d7b2d518ea833ce7a2b5bfcf2894b [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_manager.h"
#include <memory>
#include <utility>
#include "base/auto_reset.h"
#include "base/bind.h"
#include "base/feature_list.h"
#include "base/memory/weak_ptr.h"
#include "base/threading/thread_task_runner_handle.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_features.h"
#include "extensions/common/extension_messages.h"
#include "extensions/common/extension_set.h"
#include "extensions/common/mojom/host_id.mojom.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/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/public/platform/web_url_error.h"
#include "third_party/blink/public/web/web_document.h"
#include "third_party/blink/public/web/web_frame.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_view.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|.
absl::optional<mojom::RunLocation> NextRunLocation(
mojom::RunLocation run_location) {
switch (run_location) {
case mojom::RunLocation::kDocumentStart:
return mojom::RunLocation::kDocumentEnd;
case mojom::RunLocation::kDocumentEnd:
return mojom::RunLocation::kDocumentIdle;
case mojom::RunLocation::kDocumentIdle:
return absl::nullopt;
case mojom::RunLocation::kUndefined:
case mojom::RunLocation::kRunDeferred:
case mojom::RunLocation::kBrowserDriven:
return absl::nullopt;
}
NOTREACHED();
}
} // namespace
class ScriptInjectionManager::RFOHelper : public content::RenderFrameObserver {
public:
RFOHelper(content::RenderFrame* render_frame,
ScriptInjectionManager* manager);
~RFOHelper() override;
void Initialize();
private:
// RenderFrameObserver implementation.
void DidCreateNewDocument() override;
void DidCreateDocumentElement() override;
void DidFailProvisionalLoad() override;
void DidFinishDocumentLoad() override;
void WillDetach() override;
void OnDestruct() override;
void OnStop() override;
// Tells the ScriptInjectionManager to run tasks associated with
// document_idle.
void RunIdle();
void StartInjectScripts(mojom::RunLocation run_location);
// Indicate that the frame is no longer valid because it is starting
// a new load or closing.
void InvalidateAndResetFrame(bool force_reset);
// The owning ScriptInjectionManager.
ScriptInjectionManager* manager_;
bool should_run_idle_ = true;
base::WeakPtrFactory<RFOHelper> weak_factory_{this};
};
ScriptInjectionManager::RFOHelper::RFOHelper(content::RenderFrame* render_frame,
ScriptInjectionManager* manager)
: content::RenderFrameObserver(render_frame), manager_(manager) {}
ScriptInjectionManager::RFOHelper::~RFOHelper() {
}
void ScriptInjectionManager::RFOHelper::Initialize() {
// Set up for the initial empty document, for which the Document created
// events do not happen as it's already present.
DidCreateNewDocument();
// The initial empty document for a main frame may have scripts attached to it
// but we do not want to invalidate the frame and lose them when the next
// document loads. For example the IncognitoApiTest.IncognitoSplitMode test
// does `chrome.tabs.create()` with a script to be run, which is added to the
// frame before it navigates, so it needs to be preserved. However scripts in
// child frames are expected to be run inside the initial empty document. For
// example the ExecuteScriptApiTest.FrameWithHttp204 test creates a child
// frame at about:blank and expects to run injected scripts inside it.
// This is all quite inconsistent however tests both depend on us queuing and
// not queueing the kDocumentStart events in the initial empty document.
if (!render_frame()->IsMainFrame()) {
DidCreateDocumentElement();
}
}
void ScriptInjectionManager::RFOHelper::DidCreateNewDocument() {
// A new document is going to be shown, so invalidate the old document state.
// Don't force-reset 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.
constexpr bool kForceReset = false;
InvalidateAndResetFrame(kForceReset);
}
void ScriptInjectionManager::RFOHelper::DidCreateDocumentElement() {
ExtensionFrameHelper::Get(render_frame())
->ScheduleAtDocumentStart(base::BindOnce(
&ScriptInjectionManager::RFOHelper::StartInjectScripts,
weak_factory_.GetWeakPtr(), mojom::RunLocation::kDocumentStart));
}
void ScriptInjectionManager::RFOHelper::DidFailProvisionalLoad() {
auto it = manager_->frame_statuses_.find(render_frame());
if (it != manager_->frame_statuses_.end() &&
it->second == mojom::RunLocation::kDocumentStart) {
// 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 kDocumentIdle in order to reflect that the frame is
// done loading, and avoid any deadlock in the system.
//
// We skip injection of kDocumentEnd and kDocumentIdle 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).
constexpr bool kForceReset = true;
InvalidateAndResetFrame(kForceReset);
should_run_idle_ = false;
manager_->frame_statuses_[render_frame()] =
mojom::RunLocation::kDocumentIdle;
}
}
void ScriptInjectionManager::RFOHelper::DidFinishDocumentLoad() {
DCHECK(content::RenderThread::Get());
ExtensionFrameHelper::Get(render_frame())
->ScheduleAtDocumentEnd(base::BindOnce(
&ScriptInjectionManager::RFOHelper::StartInjectScripts,
weak_factory_.GetWeakPtr(), mojom::RunLocation::kDocumentEnd));
// We try to run idle in two places: a delayed task here and in response to
// ContentRendererClient::RunScriptsAtDocumentIdle(). DidFinishDocumentLoad()
// corresponds to completing the document's load, whereas
// RunScriptsAtDocumentIdle() corresponds to completing the document and all
// subresources' load (but before the window.onload event). 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., RunScriptsAtDocumentIdle() is triggered), then there's no reason to
// keep waiting.
render_frame()
->GetTaskRunner(blink::TaskType::kInternalDefault)
->PostDelayedTask(
FROM_HERE,
base::BindOnce(&ScriptInjectionManager::RFOHelper::RunIdle,
weak_factory_.GetWeakPtr()),
base::TimeDelta::FromMilliseconds(kScriptIdleTimeoutInMs));
ExtensionFrameHelper::Get(render_frame())
->ScheduleAtDocumentIdle(
base::BindOnce(&ScriptInjectionManager::RFOHelper::RunIdle,
weak_factory_.GetWeakPtr()));
}
void ScriptInjectionManager::RFOHelper::WillDetach() {
// The frame is closing - invalidate.
constexpr bool kForceReset = true;
InvalidateAndResetFrame(kForceReset);
}
void ScriptInjectionManager::RFOHelper::OnDestruct() {
manager_->RemoveObserver(this);
}
void ScriptInjectionManager::RFOHelper::OnStop() {
// If the navigation request fails (e.g. 204/205/downloads), notify the
// extension to avoid keeping the frame in a START state indefinitely which
// leads to deadlocks.
DidFailProvisionalLoad();
}
void ScriptInjectionManager::RFOHelper::RunIdle() {
// Only notify the manager if the frame hasn't already had idle run since the
// task to RunIdle() was posted.
if (should_run_idle_) {
should_run_idle_ = false;
manager_->StartInjectScripts(render_frame(),
mojom::RunLocation::kDocumentIdle);
}
}
void ScriptInjectionManager::RFOHelper::StartInjectScripts(
mojom::RunLocation run_location) {
manager_->StartInjectScripts(render_frame(), run_location);
}
void ScriptInjectionManager::RFOHelper::InvalidateAndResetFrame(
bool force_reset) {
// 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;
// Reset the frame if either |force_reset| is true, or if the manager is
// keeping track of the state of the frame (in which case we need to clean it
// up).
if (force_reset || manager_->frame_statuses_.count(render_frame()) != 0)
manager_->InvalidateForFrame(render_frame());
}
ScriptInjectionManager::ScriptInjectionManager(
UserScriptSetManager* user_script_set_manager)
: user_script_set_manager_(user_script_set_manager) {
user_script_set_manager_observation_.Observe(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(std::make_unique<RFOHelper>(render_frame, this));
rfo_helpers_.back()->Initialize();
}
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) {
base::EraseIf(running_injections_,
[&injection](const std::unique_ptr<ScriptInjection>& mode) {
return injection == mode.get();
});
}
void ScriptInjectionManager::OnUserScriptsUpdated(
const mojom::HostID& changed_host) {
base::EraseIf(
pending_injections_,
[&changed_host](const std::unique_ptr<ScriptInjection>& injection) {
return changed_host == injection->host_id();
});
}
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);
base::EraseIf(pending_injections_,
[&frame](const std::unique_ptr<ScriptInjection>& injection) {
return injection->render_frame() == frame;
});
frame_statuses_.erase(frame);
}
void ScriptInjectionManager::StartInjectScripts(
content::RenderFrame* frame,
mojom::RunLocation run_location) {
auto 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 kDocumentStart, 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.
bool invalid_run_order = false;
if (iter == frame_statuses_.end()) {
invalid_run_order = (run_location != mojom::RunLocation::kDocumentStart);
} else {
absl::optional<mojom::RunLocation> next = NextRunLocation(iter->second);
if (next)
invalid_run_order = run_location > next.value();
}
if (invalid_run_order) {
// 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,
mojom::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;
std::unique_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(activity_logging_enabled_);
}
void ScriptInjectionManager::OnInjectionStatusUpdated(
ScriptInjection::InjectionStatus status,
ScriptInjection* injection) {
switch (status) {
case ScriptInjection::InjectionStatus::kPermitted:
ScriptInjectionManager::OnPermitScriptInjectionHandled(injection);
break;
case ScriptInjection::InjectionStatus::kFinished:
ScriptInjectionManager::OnInjectionFinished(injection);
break;
}
}
void ScriptInjectionManager::TryToInject(
std::unique_ptr<ScriptInjection> injection,
mojom::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::BindOnce(&ScriptInjectionManager::OnInjectionStatusUpdated,
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(
mojom::ExecuteCodeParamsPtr params,
mojom::LocalFrame::ExecuteCodeCallback callback,
content::RenderFrame* render_frame) {
std::unique_ptr<const InjectionHost> injection_host;
if (params->host_id->type == mojom::HostID::HostType::kExtensions) {
injection_host = ExtensionInjectionHost::Create(params->host_id->id);
if (!injection_host) {
std::move(callback).Run(base::EmptyString(), GURL::EmptyGURL(),
absl::nullopt);
return;
}
} else if (params->host_id->type == mojom::HostID::HostType::kWebUi) {
injection_host = std::make_unique<WebUIInjectionHost>(*params->host_id);
}
mojom::RunLocation run_at = params->run_at;
auto injection = std::make_unique<ScriptInjection>(
std::make_unique<ProgrammaticScriptInjector>(std::move(params),
std::move(callback)),
render_frame, std::move(injection_host), run_at,
activity_logging_enabled_);
FrameStatusMap::const_iterator iter = frame_statuses_.find(render_frame);
mojom::RunLocation run_location = iter == frame_statuses_.end()
? mojom::RunLocation::kUndefined
: iter->second;
ScriptsRunInfo scripts_run_info(render_frame, run_location);
TryToInject(std::move(injection), run_location, &scripts_run_info);
}
void ScriptInjectionManager::ExecuteDeclarativeScript(
content::RenderFrame* render_frame,
int tab_id,
const ExtensionId& extension_id,
const std::string& script_id,
const GURL& url) {
std::unique_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,
mojom::RunLocation::kBrowserDriven);
// TODO(https://crbug.com/1186525): Use return value of TryToInject for
// error handling.
TryToInject(std::move(injection), mojom::RunLocation::kBrowserDriven,
&scripts_run_info);
scripts_run_info.LogRun(activity_logging_enabled_);
}
}
void ScriptInjectionManager::OnPermitScriptInjectionHandled(
ScriptInjection* injection) {
auto iter =
std::find_if(pending_injections_.begin(), pending_injections_.end(),
[injection](const std::unique_ptr<ScriptInjection>& mode) {
return injection == mode.get();
});
if (iter == pending_injections_.end())
return;
DCHECK((*iter)->host_id().type == mojom::HostID::HostType::kExtensions);
// At this point, because the injection 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).
std::unique_ptr<ScriptInjection> script_injection(std::move(*iter));
pending_injections_.erase(iter);
ScriptsRunInfo scripts_run_info(script_injection->render_frame(),
mojom::RunLocation::kRunDeferred);
ScriptInjection::InjectionResult res =
script_injection->OnPermissionGranted(&scripts_run_info);
if (res == ScriptInjection::INJECTION_BLOCKED)
running_injections_.push_back(std::move(script_injection));
scripts_run_info.LogRun(activity_logging_enabled_);
}
} // namespace extensions