#include "extensions/renderer/script_injection_manager.h"
#include <memory>
#include <utility>
#include "base/auto_reset.h"
#include "base/containers/cxx20_erase.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/memory/weak_ptr.h"
#include "base/ranges/algorithm.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 {
const int kScriptIdleTimeoutInMs = 200;
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();
}
}
class ScriptInjectionManager::RFOHelper : public content::RenderFrameObserver {
public:
RFOHelper(content::RenderFrame* render_frame,
ScriptInjectionManager* manager);
~RFOHelper() override;
void Initialize();
private:
void DidCreateNewDocument() override;
void DidCreateDocumentElement() override;
void DidFailProvisionalLoad() override;
void DidDispatchDOMContentLoadedEvent() override;
void WillDetach() override;
void OnDestruct() override;
void OnStop() override;
void RunIdle();
void StartInjectScripts(mojom::RunLocation run_location);
void InvalidateAndResetFrame(bool force_reset);
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() {
DidCreateNewDocument();
if (!render_frame()->IsMainFrame()) {
DidCreateDocumentElement();
}
}
void ScriptInjectionManager::RFOHelper::DidCreateNewDocument() {
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) {
constexpr bool kForceReset = true;
InvalidateAndResetFrame(kForceReset);
should_run_idle_ = false;
manager_->frame_statuses_[render_frame()] =
mojom::RunLocation::kDocumentIdle;
}
}
void ScriptInjectionManager::RFOHelper::DidDispatchDOMContentLoadedEvent() {
DCHECK(content::RenderThread::Get());
ExtensionFrameHelper::Get(render_frame())
->ScheduleAtDocumentEnd(base::BindOnce(
&ScriptInjectionManager::RFOHelper::StartInjectScripts,
weak_factory_.GetWeakPtr(), mojom::RunLocation::kDocumentEnd));
render_frame()
->GetTaskRunner(blink::TaskType::kInternalDefault)
->PostDelayedTask(
FROM_HERE,
base::BindOnce(&ScriptInjectionManager::RFOHelper::RunIdle,
weak_factory_.GetWeakPtr()),
base::Milliseconds(kScriptIdleTimeoutInMs));
ExtensionFrameHelper::Get(render_frame())
->ScheduleAtDocumentIdle(
base::BindOnce(&ScriptInjectionManager::RFOHelper::RunIdle,
weak_factory_.GetWeakPtr()));
}
void ScriptInjectionManager::RFOHelper::WillDetach() {
constexpr bool kForceReset = true;
InvalidateAndResetFrame(kForceReset);
}
void ScriptInjectionManager::RFOHelper::OnDestruct() {
manager_->RemoveObserver(this);
}
void ScriptInjectionManager::RFOHelper::OnStop() {
DidFailProvisionalLoad();
}
void ScriptInjectionManager::RFOHelper::RunIdle() {
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) {
weak_factory_.InvalidateWeakPtrs();
should_run_idle_ = true;
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) {
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);
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) {
InvalidateForFrame(frame);
return;
} else if (iter != frame_statuses_.end() && iter->second >= run_location) {
return;
}
frame_statuses_[frame] = run_location;
InjectScripts(frame, run_location);
}
void ScriptInjectionManager::InjectScripts(content::RenderFrame* frame,
mojom::RunLocation run_location) {
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;
}
}
int tab_id = ExtensionFrameHelper::Get(frame)->tab_id();
user_script_set_manager_->GetAllInjections(&frame_injections, frame, tab_id,
run_location);
active_injection_frames_.insert(frame);
ScriptsRunInfo scripts_run_info(frame, run_location);
for (auto iter = frame_injections.begin(); iter != frame_injections.end();) {
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);
}
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) {
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);
TryToInject(std::move(injection), mojom::RunLocation::kBrowserDriven,
&scripts_run_info);
scripts_run_info.LogRun(activity_logging_enabled_);
}
}
void ScriptInjectionManager::OnPermitScriptInjectionHandled(
ScriptInjection* injection) {
auto iter = base::ranges::find(pending_injections_, injection,
&std::unique_ptr<ScriptInjection>::get);
if (iter == pending_injections_.end())
return;
DCHECK((*iter)->host_id().type == mojom::HostID::HostType::kExtensions);
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_);
}
}