| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "extensions/browser/extension_function_dispatcher.h" |
| |
| #include <algorithm> |
| #include <optional> |
| #include <utility> |
| |
| #include "base/debug/crash_logging.h" |
| #include "base/functional/bind.h" |
| #include "base/json/json_string_value_serializer.h" |
| #include "base/lazy_instance.h" |
| #include "base/logging.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/process/process.h" |
| #include "base/scoped_observation.h" |
| #include "base/trace_event/typed_macros.h" |
| #include "base/tracing/protos/chrome_track_event.pbzero.h" |
| #include "base/values.h" |
| #include "build/build_config.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/render_process_host_observer.h" |
| #include "content/public/browser/service_worker_context.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_contents_observer.h" |
| #include "content/public/common/result_codes.h" |
| #include "content/public/common/url_constants.h" |
| #include "extensions/browser/api_activity_monitor.h" |
| #include "extensions/browser/bad_message.h" |
| #include "extensions/browser/extension_function_registry.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/browser/extension_system.h" |
| #include "extensions/browser/extension_util.h" |
| #include "extensions/browser/extensions_browser_client.h" |
| #include "extensions/browser/process_manager.h" |
| #include "extensions/browser/process_map.h" |
| #include "extensions/browser/quota_service.h" |
| #include "extensions/browser/script_injection_tracker.h" |
| #include "extensions/browser/service_worker/service_worker_keepalive.h" |
| #include "extensions/common/constants.h" |
| #include "extensions/common/extension_api.h" |
| #include "extensions/common/extension_set.h" |
| #include "extensions/common/extension_urls.h" |
| #include "extensions/common/mojom/context_type.mojom.h" |
| #include "extensions/common/trace_util.h" |
| #include "ipc/ipc_message.h" |
| #include "ipc/ipc_message_macros.h" |
| #include "mojo/public/cpp/bindings/message.h" |
| |
| using content::BrowserThread; |
| using perfetto::protos::pbzero::ChromeTrackEvent; |
| |
| namespace extensions { |
| namespace { |
| |
| // Notifies the ApiActivityMonitor that an extension API function has been |
| // called. May be called from any thread. |
| void NotifyApiFunctionCalled(const ExtensionId& extension_id, |
| const std::string& api_name, |
| const base::Value::List& args, |
| content::BrowserContext* browser_context) { |
| activity_monitor::OnApiFunctionCalled(browser_context, extension_id, api_name, |
| args); |
| } |
| |
| bool IsRequestFromServiceWorker(const mojom::RequestParams& request_params) { |
| return request_params.service_worker_version_id != |
| blink::mojom::kInvalidServiceWorkerVersionId; |
| } |
| |
| // Calls ResponseCallback with an empty result. |
| void ResponseCallbackOnError(ExtensionFunction::ResponseCallback callback, |
| ExtensionFunction::ResponseType type, |
| const std::string& error) { |
| std::move(callback).Run(type, base::Value::List(), error, nullptr); |
| } |
| |
| std::optional<bad_message::BadMessageReason> ValidateRequest( |
| const mojom::RequestParams& params, |
| content::RenderFrameHost* render_frame_host, |
| content::RenderProcessHost& render_process_host) { |
| if ((render_frame_host && IsRequestFromServiceWorker(params)) || |
| (!render_frame_host && !IsRequestFromServiceWorker(params))) { |
| return bad_message::EFD_BAD_MESSAGE; |
| } |
| |
| if (!util::CanRendererActOnBehalfOfExtension( |
| params.extension_id, render_frame_host, render_process_host, |
| /*include_user_scripts=*/true)) { |
| return bad_message::EFD_INVALID_EXTENSION_ID_FOR_PROCESS; |
| } |
| |
| // TODO(crbug.com/40055124): Validate `params.user_gesture`. |
| |
| return std::nullopt; |
| } |
| |
| const char* ToString(bad_message::BadMessageReason bad_message_code) { |
| switch (bad_message_code) { |
| case bad_message::BadMessageReason::EFD_BAD_MESSAGE: |
| return "LocalFrameHost::Request got a bad message."; |
| case bad_message::BadMessageReason::EFD_INVALID_EXTENSION_ID_FOR_PROCESS: |
| return "LocalFrameHost::Request: renderer never hosted such extension"; |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| // Helper for logging crash keys related to a the IPC payload from |
| // mojom::RequestParams. |
| class ScopedRequestParamsCrashKeys { |
| public: |
| explicit ScopedRequestParamsCrashKeys(const mojom::RequestParams& params) |
| : name_(GetNameCrashKey(), params.name), |
| extension_id_(GetExtensionIdCrashKey(), params.extension_id) {} |
| |
| ~ScopedRequestParamsCrashKeys() = default; |
| |
| // No copy constructor and no copy assignment operator. |
| ScopedRequestParamsCrashKeys(const ScopedRequestParamsCrashKeys&) = delete; |
| ScopedRequestParamsCrashKeys& operator=(const ScopedRequestParamsCrashKeys&) = |
| delete; |
| |
| private: |
| static base::debug::CrashKeyString* GetNameCrashKey() { |
| static auto* crash_key = base::debug::AllocateCrashKeyString( |
| "RequestParams-name", base::debug::CrashKeySize::Size256); |
| return crash_key; |
| } |
| |
| static base::debug::CrashKeyString* GetExtensionIdCrashKey() { |
| static auto* crash_key = base::debug::AllocateCrashKeyString( |
| "RequestParams-extension_id", base::debug::CrashKeySize::Size64); |
| return crash_key; |
| } |
| |
| base::debug::ScopedCrashKeyString name_; |
| base::debug::ScopedCrashKeyString extension_id_; |
| }; |
| |
| } // namespace |
| |
| WindowController* |
| ExtensionFunctionDispatcher::Delegate::GetExtensionWindowController() const { |
| return nullptr; |
| } |
| |
| content::WebContents* |
| ExtensionFunctionDispatcher::Delegate::GetAssociatedWebContents() const { |
| return nullptr; |
| } |
| |
| content::WebContents* |
| ExtensionFunctionDispatcher::Delegate::GetVisibleWebContents() const { |
| return GetAssociatedWebContents(); |
| } |
| |
| ExtensionFunctionDispatcher::ExtensionFunctionDispatcher( |
| content::BrowserContext* browser_context) |
| : browser_context_(browser_context), delegate_(nullptr) {} |
| |
| ExtensionFunctionDispatcher::~ExtensionFunctionDispatcher() { |
| } |
| |
| void ExtensionFunctionDispatcher::Dispatch( |
| mojom::RequestParamsPtr params, |
| content::RenderFrameHost& frame, |
| mojom::LocalFrameHost::RequestCallback callback) { |
| content::RenderProcessHost& process = *frame.GetProcess(); |
| TRACE_EVENT("extensions", "ExtensionFunctionDispatcher::Dispatch", |
| ChromeTrackEvent::kRenderProcessHost, process, |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(params->extension_id)); |
| |
| ScopedRequestParamsCrashKeys request_params_crash_keys(*params); |
| SCOPED_CRASH_KEY_STRING256( |
| "extensions", "frame.GetSiteInstance()", |
| frame.GetSiteInstance()->GetSiteURL().possibly_invalid_spec()); |
| |
| if (auto bad_message_code = ValidateRequest(*params, &frame, process)) { |
| // Kill the renderer if it's an invalid request. |
| debug::ScopedScriptInjectionTrackerFailureCrashKeys tracker_keys( |
| frame, params->extension_id); |
| bad_message::ReceivedBadMessage(&process, *bad_message_code); |
| std::move(callback).Run(/*kFailed=*/true, base::Value::List(), |
| ToString(*bad_message_code), nullptr); |
| return; |
| } |
| |
| // TODO(crbug.com/40056469): Validate (or remove) `params.source_url`. |
| DispatchWithCallbackInternal( |
| *params, &frame, *frame.GetProcess(), |
| base::BindOnce( |
| [](mojom::LocalFrameHost::RequestCallback callback, |
| ExtensionFunction::ResponseType type, base::Value::List results, |
| const std::string& error, |
| mojom::ExtraResponseDataPtr response_data) { |
| std::move(callback).Run( |
| type == ExtensionFunction::ResponseType::kSucceeded, |
| std::move(results), error, std::move(response_data)); |
| }, |
| std::move(callback))); |
| } |
| |
| void ExtensionFunctionDispatcher::DispatchForServiceWorker( |
| mojom::RequestParamsPtr params, |
| int render_process_id, |
| mojom::ServiceWorkerHost::RequestWorkerCallback callback) { |
| ScopedRequestParamsCrashKeys request_params_crash_keys(*params); |
| |
| // The IPC might race with RenderProcessHost destruction. This may only |
| // happen in scenarios that are already inherently racey, so dropping the IPC |
| // is okay and won't lead to any additional risk of data loss. Continuing is |
| // impossible, because WorkerResponseCallbackWrapper requires render process |
| // host to be around. |
| content::RenderProcessHost* rph = |
| content::RenderProcessHost::FromID(render_process_id); |
| if (!rph) { |
| std::move(callback).Run(/*kFailed=*/true, base::Value::List(), "No RPH", |
| nullptr); |
| return; |
| } |
| |
| TRACE_EVENT("extensions", |
| "ExtensionFunctionDispatcher::DispatchForServiceWorker", |
| ChromeTrackEvent::kRenderProcessHost, *rph, |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(params->extension_id)); |
| if (auto bad_message_code = ValidateRequest(*params, nullptr, *rph)) { |
| // Kill the renderer if it's an invalid request. |
| bad_message::ReceivedBadMessage(render_process_id, *bad_message_code); |
| std::move(callback).Run(/*kFailed=*/true, base::Value::List(), |
| ToString(*bad_message_code), nullptr); |
| return; |
| } |
| |
| WorkerId worker_id{params->extension_id, render_process_id, |
| params->service_worker_version_id, |
| params->worker_thread_id}; |
| // Ignore if the worker has already stopped. |
| if (!ProcessManager::Get(browser_context_)->HasServiceWorker(worker_id)) { |
| std::move(callback).Run(/*kFailed=*/true, base::Value::List(), "No SW", |
| nullptr); |
| return; |
| } |
| |
| DispatchWithCallbackInternal( |
| *params, nullptr, *rph, |
| base::BindOnce( |
| [](mojom::ServiceWorkerHost::RequestWorkerCallback callback, |
| ExtensionFunction::ResponseType type, base::Value::List results, |
| const std::string& error, |
| mojom::ExtraResponseDataPtr response_data) { |
| std::move(callback).Run( |
| type == ExtensionFunction::ResponseType::kSucceeded, |
| std::move(results), error, std::move(response_data)); |
| }, |
| std::move(callback))); |
| } |
| |
| void ExtensionFunctionDispatcher::DispatchWithCallbackInternal( |
| const mojom::RequestParams& params, |
| content::RenderFrameHost* render_frame_host, |
| content::RenderProcessHost& render_process_host, |
| ExtensionFunction::ResponseCallback callback) { |
| ProcessMap* process_map = ProcessMap::Get(browser_context_); |
| if (!process_map) { |
| constexpr char kProcessNotFound[] = |
| "The process for the extension is not found."; |
| ResponseCallbackOnError(std::move(callback), |
| ExtensionFunction::ResponseType::kFailed, |
| kProcessNotFound); |
| return; |
| } |
| |
| const int render_process_id = render_process_host.GetDeprecatedID(); |
| |
| const GURL* render_frame_host_url = nullptr; |
| if (render_frame_host) { |
| render_frame_host_url = &render_frame_host->GetLastCommittedURL(); |
| DCHECK_EQ(render_process_id, |
| render_frame_host->GetProcess()->GetDeprecatedID()); |
| } |
| |
| ExtensionRegistry* registry = ExtensionRegistry::Get(browser_context_); |
| const Extension* extension = |
| registry->enabled_extensions().GetByID(params.extension_id); |
| // Check if the call is from a hosted app. Hosted apps can only make call from |
| // render frames, so we can use `render_frame_host_url`. |
| // TODO(devlin): Isn't `params.extension_id` still populated for hosted app |
| // calls? |
| if (!extension && render_frame_host_url) { |
| extension = registry->enabled_extensions().GetHostedAppByURL( |
| *render_frame_host_url); |
| } |
| |
| if (!process_map->CanProcessHostContextType(extension, render_process_host, |
| params.context_type)) { |
| // TODO(crbug.com/40055126): Ideally, we'd be able to mark some |
| // of these as bad messages. We can't do that in all cases because there |
| // are times some of these might legitimately fail (for instance, during |
| // extension unload), but there are others that should never, ever happen |
| // (privileged extension contexts in web processes). |
| static constexpr char kInvalidContextType[] = |
| "Invalid context type provided."; |
| ResponseCallbackOnError(std::move(callback), |
| ExtensionFunction::ResponseType::kFailed, |
| kInvalidContextType); |
| return; |
| } |
| |
| if (params.context_type == mojom::ContextType::kUntrustedWebUi) { |
| // TODO(crbug.com/40265193): We should, at minimum, be using an |
| // origin here. It'd be even better if we could have a more robust way of |
| // checking that a process can host untrusted webui. |
| if (extension || !render_frame_host_url || |
| !render_frame_host_url->SchemeIs(content::kChromeUIUntrustedScheme)) { |
| constexpr char kInvalidWebUiUntrustedContext[] = |
| "Context indicated it was untrusted webui, but is invalid."; |
| ResponseCallbackOnError(std::move(callback), |
| ExtensionFunction::ResponseType::kFailed, |
| kInvalidWebUiUntrustedContext); |
| return; |
| } |
| } |
| |
| const bool is_worker_request = IsRequestFromServiceWorker(params); |
| |
| scoped_refptr<ExtensionFunction> function = CreateExtensionFunction( |
| params, extension, render_process_id, is_worker_request, |
| render_frame_host_url, params.context_type, |
| ExtensionAPI::GetSharedInstance(), std::move(callback), |
| render_frame_host); |
| if (!function.get()) { |
| return; |
| } |
| |
| if (extension && |
| ExtensionsBrowserClient::Get()->CanExtensionCrossIncognito( |
| extension, browser_context_)) { |
| function->set_include_incognito_information(true); |
| } |
| |
| if (!extension) { |
| if (function->source_context_type() == mojom::ContextType::kWebUi) { |
| base::UmaHistogramSparse("Extensions.Functions.WebUICalls", |
| function->histogram_value()); |
| } else if (function->source_context_type() == |
| mojom::ContextType::kUntrustedWebUi) { |
| base::UmaHistogramSparse("Extensions.Functions.WebUIUntrustedCalls", |
| function->histogram_value()); |
| } else if (function->source_context_type() == |
| mojom::ContextType::kWebPage) { |
| base::UmaHistogramSparse("Extensions.Functions.NonExtensionWebPageCalls", |
| function->histogram_value()); |
| } |
| |
| // Skip the quota, event page, activity logging stuff if there |
| // isn't an extension, e.g. if the function call was from WebUI. |
| function->RunWithValidation().Execute(); |
| return; |
| } |
| |
| // Fetch the ProcessManager before |this| is possibly invalidated. |
| ProcessManager* process_manager = ProcessManager::Get(browser_context_); |
| |
| ExtensionSystem* extension_system = ExtensionSystem::Get(browser_context_); |
| QuotaService* quota = extension_system->quota_service(); |
| std::string violation_error = |
| quota->Assess(extension->id(), function.get(), params.arguments, |
| base::TimeTicks::Now()); |
| |
| function->set_request_uuid(base::Uuid::GenerateRandomV4()); |
| |
| // Increment the keepalive to ensure the extension doesn't shut down while |
| // it's executing an API function. This is balanced in |
| // `OnExtensionFunctionCompleted()`. |
| if (IsRequestFromServiceWorker(params)) { |
| CHECK(function->worker_id()); |
| content::ServiceWorkerExternalRequestTimeoutType timeout_type = |
| function->ShouldKeepWorkerAliveIndefinitely() |
| ? content::ServiceWorkerExternalRequestTimeoutType::kDoesNotTimeout |
| : content::ServiceWorkerExternalRequestTimeoutType::kDefault; |
| function->set_service_worker_keepalive( |
| std::make_unique<ServiceWorkerKeepalive>( |
| browser_context_, *function->worker_id(), timeout_type, |
| Activity::API_FUNCTION, function->name())); |
| } else { |
| process_manager->IncrementLazyKeepaliveCount( |
| function->extension(), Activity::API_FUNCTION, function->name()); |
| } |
| |
| if (violation_error.empty()) { |
| // See crbug.com/39178. |
| ExtensionsBrowserClient::Get()->PermitExternalProtocolHandler(); |
| NotifyApiFunctionCalled(extension->id(), params.name, params.arguments, |
| browser_context_); |
| |
| // Since sandboxed frames listed in the manifest don't get access to the |
| // extension APIs, this will only be true in an extension frame in an iframe |
| // with the sandbox attribute specified, or served with a CSP header. |
| bool is_sandboxed = |
| render_frame_host && render_frame_host->IsSandboxed( |
| network::mojom::WebSandboxFlags::kOrigin); |
| // Note: Deliberately don't include external component extensions here - |
| // this lets us differentiate between "built-in" extension calls and |
| // external extension calls |
| if (extension->location() == mojom::ManifestLocation::kComponent) { |
| base::UmaHistogramSparse("Extensions.Functions.ComponentExtensionCalls", |
| function->histogram_value()); |
| if (is_sandboxed) { |
| base::UmaHistogramBoolean( |
| "Extensions.Functions.DidSandboxedComponentExtensionAPICall", true); |
| } |
| } else { |
| base::UmaHistogramSparse("Extensions.Functions.ExtensionCalls", |
| function->histogram_value()); |
| if (is_sandboxed) { |
| base::UmaHistogramBoolean( |
| "Extensions.Functions.DidSandboxedExtensionAPICall", true); |
| } |
| } |
| |
| if (IsRequestFromServiceWorker(params)) { |
| base::UmaHistogramSparse( |
| "Extensions.Functions.ExtensionServiceWorkerCalls", |
| function->histogram_value()); |
| } |
| |
| if (extension->manifest_version() == 3) { |
| base::UmaHistogramSparse("Extensions.Functions.ExtensionMV3Calls", |
| function->histogram_value()); |
| } |
| |
| base::ElapsedTimer timer; |
| function->RunWithValidation().Execute(); |
| // TODO(devlin): Once we have a baseline metric for how long functions take, |
| // we can create a handful of buckets and record the function name so that |
| // we can find what the fastest/slowest are. |
| // Note: Many functions execute finish asynchronously, so this time is not |
| // always a representation of total time taken. See also |
| // Extensions.Functions.TotalExecutionTime. |
| UMA_HISTOGRAM_TIMES("Extensions.Functions.SynchronousExecutionTime", |
| timer.Elapsed()); |
| } else { |
| function->OnQuotaExceeded(violation_error); |
| } |
| |
| // Note: do not access |this| after this point. We may have been deleted |
| // if `function->RunWithValidation()` resulted in closing the execution |
| // context for this function. |
| } |
| |
| void ExtensionFunctionDispatcher::OnExtensionFunctionCompleted( |
| ExtensionFunction& extension_function) { |
| if (!extension_function.extension()) { |
| // The function had no associated extension; nothing to clean up. |
| return; |
| } |
| |
| if (!extension_function.browser_context()) { |
| // The ExtensionFunction's browser context is null'ed out when the browser |
| // context is being shut down. If this happens, there's nothing to clean up. |
| return; |
| } |
| |
| if (!ExtensionRegistry::Get(browser_context_) |
| ->enabled_extensions() |
| .GetByID(extension_function.extension()->id())) { |
| // The extension may have been unloaded (the ExtensionFunction holds a |
| // reference to it, so it's still safe to access). If so, there's nothing to |
| // // clean up. |
| return; |
| } |
| |
| ProcessManager* process_manager = ProcessManager::Get(browser_context_); |
| if (extension_function.is_from_service_worker()) { |
| CHECK(extension_function.request_uuid().is_valid()); |
| CHECK(extension_function.worker_id()); |
| |
| extension_function.ResetServiceWorkerKeepalive(); |
| } else { |
| process_manager->DecrementLazyKeepaliveCount(extension_function.extension(), |
| Activity::API_FUNCTION, |
| extension_function.name()); |
| } |
| } |
| |
| WindowController* |
| ExtensionFunctionDispatcher::GetExtensionWindowController() const { |
| return delegate_ ? delegate_->GetExtensionWindowController() : nullptr; |
| } |
| |
| content::WebContents* |
| ExtensionFunctionDispatcher::GetAssociatedWebContents() const { |
| return delegate_ ? delegate_->GetAssociatedWebContents() : nullptr; |
| } |
| |
| content::WebContents* |
| ExtensionFunctionDispatcher::GetVisibleWebContents() const { |
| return delegate_ ? delegate_->GetVisibleWebContents() : nullptr; |
| } |
| |
| void ExtensionFunctionDispatcher::AddResponseTarget(ExtensionFunction* func) { |
| response_targets_.insert(func); |
| } |
| |
| void ExtensionFunctionDispatcher::ProcessResponseAck( |
| const base::Uuid& request_uuid) { |
| auto iter = std::ranges::find_if( |
| response_targets_, [request_uuid](ExtensionFunction* function) { |
| return function->request_uuid() == request_uuid; |
| }); |
| if (iter == response_targets_.end()) { |
| return; |
| } |
| // Calling this may cause the instance to delete itself, so no |
| // referencing it after this! |
| (*iter)->OnResponseAck(); |
| response_targets_.erase(iter); |
| } |
| |
| scoped_refptr<ExtensionFunction> |
| ExtensionFunctionDispatcher::CreateExtensionFunction( |
| const mojom::RequestParams& params, |
| const Extension* extension, |
| int requesting_process_id, |
| bool is_worker_request, |
| const GURL* render_frame_host_url, |
| mojom::ContextType context_type, |
| ExtensionAPI* api, |
| ExtensionFunction::ResponseCallback callback, |
| content::RenderFrameHost* render_frame_host) { |
| constexpr char kCreationFailed[] = "Access to extension API denied."; |
| |
| scoped_refptr<ExtensionFunction> function = |
| ExtensionFunctionRegistry::GetInstance().NewFunction(params.name); |
| if (!function) { |
| LOG(ERROR) << "Unknown Extension API - " << params.name; |
| ResponseCallbackOnError(std::move(callback), |
| ExtensionFunction::ResponseType::kFailed, |
| kCreationFailed); |
| return nullptr; |
| } |
| |
| function->SetArgs(params.arguments.Clone()); |
| |
| // Determine the source URL. When possible, prefer fetching this value from |
| // the RenderFrameHost, but fallback to the value in the `params` object if |
| // necessary. |
| // We can't use the frame URL in the case of a worker-based request (where |
| // there is no frame). |
| if (is_worker_request) { |
| // TODO(crbug.com/40056469): Validate this URL further. Or, better, |
| // remove it from `mojom::RequestParams`. |
| function->set_source_url(params.source_url); |
| } else { |
| DCHECK(render_frame_host_url); |
| function->set_source_url(*render_frame_host_url); |
| } |
| |
| function->set_has_callback(params.has_callback); |
| function->set_user_gesture(params.user_gesture); |
| function->set_extension(extension); |
| if (params.js_callstack.has_value()) { |
| function->set_js_callstack(*params.js_callstack); |
| } |
| function->set_response_callback(std::move(callback)); |
| function->set_source_context_type(context_type); |
| function->set_source_process_id(requesting_process_id); |
| if (is_worker_request) { |
| CHECK(extension); |
| WorkerId worker_id; |
| worker_id.thread_id = params.worker_thread_id; |
| worker_id.version_id = params.service_worker_version_id; |
| worker_id.render_process_id = requesting_process_id; |
| worker_id.extension_id = extension->id(); |
| function->set_worker_id(std::move(worker_id)); |
| } else { |
| function->SetRenderFrameHost(render_frame_host); |
| } |
| |
| // Note: `SetDispatcher()` also initializes the `browser_context_` member |
| // for `ExtensionFunction`, which is necessary for properly performing |
| // permission checks. |
| function->SetDispatcher(weak_ptr_factory_.GetWeakPtr()); |
| |
| if (!function->HasPermission()) { |
| LOG(ERROR) << "Permission denied for " << params.name; |
| function->RespondWithError(kCreationFailed); |
| return nullptr; |
| } |
| |
| return function; |
| } |
| } // namespace extensions |