123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412 |
- // 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/browser/script_executor.h"
- #include <map>
- #include <set>
- #include <string>
- #include "base/bind.h"
- #include "base/check_op.h"
- #include "base/containers/contains.h"
- #include "base/containers/cxx20_erase.h"
- #include "base/dcheck_is_on.h"
- #include "base/hash/hash.h"
- #include "base/memory/weak_ptr.h"
- #include "base/pickle.h"
- #include "base/ranges/algorithm.h"
- #include "base/strings/stringprintf.h"
- #include "base/types/pass_key.h"
- #include "content/public/browser/render_frame_host.h"
- #include "content/public/browser/render_process_host.h"
- #include "content/public/browser/render_view_host.h"
- #include "content/public/browser/web_contents.h"
- #include "content/public/browser/web_contents_observer.h"
- #include "extensions/browser/content_script_tracker.h"
- #include "extensions/browser/extension_api_frame_id_map.h"
- #include "extensions/browser/extension_registry.h"
- #include "extensions/browser/extension_web_contents_observer.h"
- #include "extensions/common/extension_messages.h"
- #include "extensions/common/mojom/host_id.mojom.h"
- #include "ipc/ipc_message.h"
- #include "ipc/ipc_message_macros.h"
- namespace base {
- class ListValue;
- } // namespace base
- namespace extensions {
- namespace {
- // A handler for a single injection request. On creation this will send the
- // injection request to the renderer, and it will be destroyed after either the
- // corresponding response comes from the renderer, or the renderer is destroyed.
- class Handler : public content::WebContentsObserver {
- public:
- // OnceCallback version of ScriptExecutor::ScriptsExecutedNotification:
- using ScriptsExecutedOnceCallback = base::OnceCallback<
- void(content::WebContents*, const ExecutingScriptsMap&, const GURL&)>;
- Handler(base::PassKey<ScriptExecutor> pass_key,
- ScriptsExecutedOnceCallback observer,
- content::WebContents* web_contents,
- mojom::ExecuteCodeParamsPtr params,
- ScriptExecutor::FrameScope scope,
- const std::set<int>& frame_ids,
- ScriptExecutor::ScriptFinishedCallback callback)
- : content::WebContentsObserver(web_contents),
- observer_(std::move(observer)),
- host_id_(params->host_id->type, params->host_id->id),
- callback_(std::move(callback)) {
- for (int frame_id : frame_ids) {
- content::RenderFrameHost* frame =
- ExtensionApiFrameIdMap::GetRenderFrameHostById(web_contents,
- frame_id);
- if (!frame) {
- AddWillNotInjectResult(
- frame_id, ExtensionApiFrameIdMap::DocumentId(),
- base::StringPrintf("No frame with ID: %d", frame_id));
- continue;
- }
- DCHECK(!base::Contains(pending_render_frames_, frame));
- if (!frame->IsRenderFrameLive()) {
- ExtensionApiFrameIdMap::DocumentId document_id =
- ExtensionApiFrameIdMap::GetDocumentId(frame);
- AddWillNotInjectResult(
- frame_id, document_id,
- base::StringPrintf("Frame with ID %d is not ready", frame_id));
- continue;
- }
- if (frame->IsErrorDocument()) {
- ExtensionApiFrameIdMap::DocumentId document_id =
- ExtensionApiFrameIdMap::GetDocumentId(frame);
- AddWillNotInjectResult(
- frame_id, document_id,
- base::StringPrintf("Frame with ID %d is showing error page",
- frame_id));
- continue;
- }
- // `frame_id` can be a FrameTreeNodeId of the primary main frame. In such
- // cases, ExtensionApiFrameIdMap::GetFrameId(frame) resolves the given
- // `frame` as 0. To keep the original ID as is, pass `frame_id` and use it
- // directly to prepare a relevant FrameResult.
- PushPendingRenderFrame(frame, frame_id);
- }
- // If there is a single frame specified (and it was valid), we consider it
- // the "root" frame, which is used in result ordering and error collection.
- if (frame_ids.size() == 1 && pending_render_frames_.size() == 1)
- root_frame_token_ = pending_render_frames_[0]->GetFrameToken();
- // If we are to include subframes, iterate over all descendants of frames in
- // `pending_render_frames_` and add them if they are alive (and not already
- // contained in `pending_frames`).
- if (scope == ScriptExecutor::INCLUDE_SUB_FRAMES) {
- // We iterate over the requested frames. Note we can't use an iterator
- // as the for loop will mutate `pending_render_frames_`.
- const size_t requested_frame_count = pending_render_frames_.size();
- for (size_t i = 0; i < requested_frame_count; ++i) {
- pending_render_frames_.at(i)->ForEachRenderFrameHost(
- base::BindRepeating(&Handler::MaybeAddSubFrame,
- base::Unretained(this)));
- }
- }
- for (content::RenderFrameHost* frame : pending_render_frames_)
- SendExecuteCode(pass_key, params.Clone(), frame);
- if (pending_render_frames_.empty())
- Finish();
- }
- Handler(const Handler&) = delete;
- Handler& operator=(const Handler&) = delete;
- private:
- // This class manages its own lifetime.
- ~Handler() override {}
- // content::WebContentsObserver:
- // TODO(devlin): Could we just rely on the RenderFrameDeleted() notification?
- // If so, we could remove this.
- void WebContentsDestroyed() override {
- for (content::RenderFrameHost* frame : pending_render_frames_) {
- UpdateResultWithErrorFormat(
- frame, "Tab containing frame with ID %d was removed.");
- }
- pending_render_frames_.clear();
- Finish();
- }
- void RenderFrameDeleted(
- content::RenderFrameHost* render_frame_host) override {
- int erased_count = base::Erase(pending_render_frames_, render_frame_host);
- DCHECK_LE(erased_count, 1);
- if (erased_count == 0)
- return;
- UpdateResultWithErrorFormat(render_frame_host,
- "Frame with ID %d was removed.");
- if (pending_render_frames_.empty())
- Finish();
- }
- content::RenderFrameHost::FrameIterationAction MaybeAddSubFrame(
- content::RenderFrameHost* frame) {
- // Avoid inner web contents. If we need to execute scripts on inner
- // WebContents this class needs to be updated.
- // See https://crbug.com/1301320.
- if (content::WebContents::FromRenderFrameHost(frame) != web_contents()) {
- return content::RenderFrameHost::FrameIterationAction::kSkipChildren;
- }
- if (!frame->IsRenderFrameLive() ||
- base::Contains(pending_render_frames_, frame)) {
- return content::RenderFrameHost::FrameIterationAction::kContinue;
- }
- PushPendingRenderFrame(frame, ExtensionApiFrameIdMap::GetFrameId(frame));
- return content::RenderFrameHost::FrameIterationAction::kContinue;
- }
- void PushPendingRenderFrame(raw_ptr<content::RenderFrameHost> frame,
- int frame_id) {
- pending_render_frames_.push_back(frame);
- // Preallocate the results to hold the initial `frame_id` and `document_id`.
- // As the primary main frame uses a magic number 0 for the `frame_id`, it
- // can be changed if the primary page is changed. It happens on pre-rendered
- // page activation or portal page activation on MPArch. The `document_id`
- // can be stale if navigation happens and the same renderer is reused in the
- // case, e.g. navigation from about:blank, or same-origin navigation.
- ScriptExecutor::FrameResult result;
- result.frame_id = frame_id;
- result.document_id = ExtensionApiFrameIdMap::GetDocumentId(frame);
- DCHECK(!base::Contains(results_, frame->GetFrameToken()));
- results_[frame->GetFrameToken()] = std::move(result);
- }
- void AddWillNotInjectResult(
- int frame_id,
- const ExtensionApiFrameIdMap::DocumentId& document_id,
- std::string error) {
- ScriptExecutor::FrameResult result;
- result.frame_id = frame_id;
- result.document_id = document_id;
- result.error = std::move(error);
- invalid_injection_results_.push_back(std::move(result));
- }
- void UpdateResult(content::RenderFrameHost* render_frame_host,
- const std::string& error,
- const GURL& url,
- absl::optional<base::Value> result) {
- ScriptExecutor::FrameResult& frame_result =
- GetFrameResult(render_frame_host->GetFrameToken());
- frame_result.frame_responded = true;
- frame_result.error = error;
- frame_result.url = url;
- if (result.has_value())
- frame_result.value = std::move(*result);
- }
- void UpdateResultWithErrorFormat(content::RenderFrameHost* render_frame_host,
- const char* format) {
- ScriptExecutor::FrameResult& frame_result =
- GetFrameResult(render_frame_host->GetFrameToken());
- frame_result.error = base::StringPrintf(format, frame_result.frame_id);
- }
- ScriptExecutor::FrameResult& GetFrameResult(
- const blink::LocalFrameToken& frame_token) {
- DCHECK(base::Contains(results_, frame_token));
- return results_[frame_token];
- }
- // Sends an ExecuteCode message to the given frame host, and increments
- // the number of pending messages.
- void SendExecuteCode(base::PassKey<ScriptExecutor> pass_key,
- mojom::ExecuteCodeParamsPtr params,
- content::RenderFrameHost* frame) {
- DCHECK(frame->IsRenderFrameLive());
- DCHECK(base::Contains(pending_render_frames_, frame));
- ContentScriptTracker::WillExecuteCode(pass_key, frame, host_id_);
- ExtensionWebContentsObserver::GetForWebContents(web_contents())
- ->GetLocalFrame(frame)
- ->ExecuteCode(std::move(params),
- base::BindOnce(&Handler::OnExecuteCodeFinished,
- weak_ptr_factory_.GetWeakPtr(),
- frame->GetProcess()->GetID(),
- frame->GetRoutingID()));
- }
- // Handles the ExecuteCodeFinished message.
- void OnExecuteCodeFinished(int render_process_id,
- int render_frame_id,
- const std::string& error,
- const GURL& on_url,
- absl::optional<base::Value> result) {
- auto* render_frame_host =
- content::RenderFrameHost::FromID(render_process_id, render_frame_id);
- if (!render_frame_host)
- return;
- DCHECK(!pending_render_frames_.empty());
- size_t erased = base::Erase(pending_render_frames_, render_frame_host);
- DCHECK_EQ(1u, erased);
- // TODO(devlin): Do we need to trust the renderer for the URL here? Is there
- // a risk of the frame having navigated since the injection happened?
- UpdateResult(render_frame_host, error, on_url, std::move(result));
- // Wait until the final request finishes before reporting back.
- if (pending_render_frames_.empty())
- Finish();
- }
- void Finish() {
- DCHECK(pending_render_frames_.empty());
- DCHECK(!results_.empty() || !invalid_injection_results_.empty());
- // TODO(devlin): This would be simpler (and more thorough) if we could just
- // invoke the observer for each frame. Investigate.
- if (observer_ && root_frame_token_.has_value()) {
- ScriptExecutor::FrameResult& root_frame_result =
- GetFrameResult(*root_frame_token_);
- if (root_frame_result.error.empty() &&
- host_id_.type == mojom::HostID::HostType::kExtensions) {
- std::move(observer_).Run(web_contents(), {{host_id_.id, {}}},
- root_frame_result.url);
- }
- }
- if (callback_) {
- std::vector<ScriptExecutor::FrameResult> all_results =
- std::move(invalid_injection_results_);
- all_results.reserve(invalid_injection_results_.size() + results_.size());
- for (auto& kv : results_)
- all_results.push_back(std::move(kv.second));
- std::move(callback_).Run(std::move(all_results));
- }
- delete this;
- }
- ScriptsExecutedOnceCallback observer_;
- // The id of the host (the extension or the webui) doing the injection.
- mojom::HostID host_id_;
- // The the root frame key to search FrameResult, if only a single frame is
- // explicitly specified.
- absl::optional<blink::LocalFrameToken> root_frame_token_;
- // The hosts of the still-running injections. Note: this is a vector because
- // order matters (some tests - and therefore perhaps some extensions - rely on
- // the execution mirroring the frame tree hierarchy). The contents, however,
- // should be unique (i.e., no duplicated frames).
- // TODO(devlin): Extensions *shouldn't* rely on order here, because there's
- // never a guarantee. We should probably just adjust the test and disregard
- // order (except the root frame).
- std::vector<raw_ptr<content::RenderFrameHost>> pending_render_frames_;
- // The results of script injections into frames, keyed by LocalFrameToken.
- // Note that the keying host here may be invalid if the host was since
- // destroyed, and should never be accessed.
- // We key these by LocalFrameToken rather than frame ID because the frame ID
- // for a given frame may change if the frame changes lifecycle state (such as
- // pre-rendered page being activated).
- std::map<blink::LocalFrameToken, ScriptExecutor::FrameResult> results_;
- // A collection of results for frames that will never be injected into;
- // these are separate from `results_` because they may not be a valid
- // RenderFrameHost* to key them by (if there's no corresponding frame).
- std::vector<ScriptExecutor::FrameResult> invalid_injection_results_;
- // The callback to run after all injections complete.
- ScriptExecutor::ScriptFinishedCallback callback_;
- base::WeakPtrFactory<Handler> weak_ptr_factory_{this};
- };
- } // namespace
- ScriptExecutor::FrameResult::FrameResult() = default;
- ScriptExecutor::FrameResult::FrameResult(FrameResult&&) = default;
- ScriptExecutor::FrameResult& ScriptExecutor::FrameResult::operator=(
- FrameResult&&) = default;
- ScriptExecutor::ScriptExecutor(content::WebContents* web_contents)
- : web_contents_(web_contents) {
- CHECK(web_contents_);
- }
- ScriptExecutor::~ScriptExecutor() {}
- // static
- std::string ScriptExecutor::GenerateInjectionKey(const mojom::HostID& host_id,
- const GURL& script_url,
- const std::string& code) {
- const std::string& source = script_url.is_valid() ? script_url.spec() : code;
- return base::StringPrintf("%c%s%zu", script_url.is_valid() ? 'F' : 'C',
- host_id.id.c_str(), base::FastHash(source));
- }
- void ScriptExecutor::ExecuteScript(const mojom::HostID& host_id,
- mojom::CodeInjectionPtr injection,
- ScriptExecutor::FrameScope frame_scope,
- const std::set<int>& frame_ids,
- ScriptExecutor::MatchAboutBlank about_blank,
- mojom::RunLocation run_at,
- ScriptExecutor::ProcessType process_type,
- const GURL& webview_src,
- ScriptFinishedCallback callback) {
- if (host_id.type == mojom::HostID::HostType::kExtensions) {
- // Don't execute if the extension has been unloaded.
- const Extension* extension =
- ExtensionRegistry::Get(web_contents_->GetBrowserContext())
- ->enabled_extensions()
- .GetByID(host_id.id);
- if (!extension)
- return;
- } else {
- CHECK(process_type == WEB_VIEW_PROCESS);
- }
- #if DCHECK_IS_ON()
- if (injection->is_css()) {
- bool expect_injection_key =
- host_id.type == mojom::HostID::HostType::kExtensions;
- if (injection->get_css()->operation ==
- mojom::CSSInjection::Operation::kRemove) {
- DCHECK(expect_injection_key)
- << "Only extensions (with injection keys supplied) can remove CSS.";
- }
- DCHECK(base::ranges::all_of(
- injection->get_css()->sources,
- [expect_injection_key](const mojom::CSSSourcePtr& source) {
- return expect_injection_key == source->key.has_value();
- }));
- }
- #endif
- auto params = mojom::ExecuteCodeParams::New();
- params->host_id = host_id.Clone();
- params->injection = std::move(injection);
- params->match_about_blank = (about_blank == MATCH_ABOUT_BLANK);
- params->run_at = run_at;
- params->is_web_view = (process_type == WEB_VIEW_PROCESS);
- params->webview_src = webview_src;
- // Handler handles IPCs and deletes itself on completion.
- new Handler(base::PassKey<ScriptExecutor>(), observer_, web_contents_,
- std::move(params), frame_scope, frame_ids, std::move(callback));
- }
- } // namespace extensions
|