// 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 #include #include #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 pass_key, ScriptsExecutedOnceCallback observer, content::WebContents* web_contents, mojom::ExecuteCodeParamsPtr params, ScriptExecutor::FrameScope scope, const std::set& 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 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 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 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 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 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 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> 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 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 invalid_injection_results_; // The callback to run after all injections complete. ScriptExecutor::ScriptFinishedCallback callback_; base::WeakPtrFactory 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& 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(), observer_, web_contents_, std::move(params), frame_scope, frame_ids, std::move(callback)); } } // namespace extensions