Files
electron/shell/browser/extensions/electron_extension_loader.cc
T
Sam MaddockandClaude bc658b2924 fix: extension code drift (#55155)
* fix: report tab ids to extension webRequest and declarativeNetRequest

ElectronExtensionsBrowserClient did not override
GetTabAndWindowIdForWebContents, so the default of -1 reached every
caller: chrome.webRequest reported details.tabId as -1 and
declarativeNetRequest rules with a tabIds condition never matched.
Return the WebContents id, the same id chrome.tabs uses.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: keep extension service workers registered when clearing storage

Chrome's embedder refuses to delete or web-update the registration of
an extension's background service worker and only lets the manifest's
worker script register at the extension's root scope. Electron used
the content defaults, so session.clearStorageData() deleted the
registration and the extension stopped receiving events until it was
reloaded.

Override MayDeleteServiceWorkerRegistration,
ShouldTryToUpdateServiceWorkerRegistration and AllowServiceWorker to
match.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: crash in chrome.cookies.getAllCookieStores

The cookies API now lives in //extensions, so extensions with the
"cookies" permission can call it in Electron. Core code dereferences
ExtensionsAPIClient::GetCookiesApiDelegate(), which Electron did not
provide, crashing the browser process, and the CookiesAPI factory that
dispatches cookies.onChanged is only built by embedders.

Provide a delegate that lists the session as its own cookie store and
build the factory.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: restore the management permission feature

The "management" entry was dropped from Electron's permission features
when the file was resynced with Chrome's in #44653. The management API
feature depends on it, so a manifest requesting the permission lost it
and chrome.management.getAll, get, onEnabled and onDisabled were
unavailable.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: localize messages in extension stylesheets

Chromium substitutes __MSG_*__ placeholders in extension CSS with a
renderer URL loader throttle that only Chrome's throttle provider
installs. Electron had no provider, so placeholders such as
__MSG_@@extension_id__ were left as literal text. Install the throttle
from RendererClientBase.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: let WebSocket activity keep extension service workers alive

Chrome forwards ShouldNotifyServiceWorkerOnWebSocketActivity to the
extensions dispatcher so traffic on a WebSocket resets an extension
service worker's idle timer. Electron did not, so a worker holding an
open socket was still stopped after 30 seconds.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: keep sandboxed extension frames out of the extension process map

Chrome isolates only manifest-sandboxed extension pages (and sandboxed
about:srcdoc and data: documents) in a separate process and leaves that
process out of the extension ProcessMap, so it cannot be granted
extension API access. Electron isolated every sandboxed extension frame
and recorded each of those processes as a privileged extension process.

Override ShouldAllowCrossProcessSandboxedFrameForPrecursor and skip
sandboxed principals in SiteInstanceGotProcessAndSite to match.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: support navigator.clipboard in extension service workers

Chromium exposes navigator.clipboard to extension service workers, but
every piece that enables it lives in Chrome's embedder. Register
chrome-extension as an extension scheme with Blink, pass
--extension-process to extension renderers and turn the runtime feature
on there, and answer IsClipboardAllowedForServiceWorker and
AllowWorkerWriteToClipboard in the browser. Writes still require the
clipboardWrite permission.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: run the extension service worker script on disk after a restart

A service worker registration outlives the app, and Chromium trusts a
stored registration as current while the extension's version is
unchanged. Electron loads unpacked extensions on every launch, so an
extension whose files changed without a version bump kept running the
worker script from the launch that first registered it.

Unregister a worker left by a previous launch before adding the
extension so it is registered again from the files being loaded.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: persist extension alarms and dynamic content scripts

ElectronExtensionSystem returned null for the state, rules and dynamic
user scripts stores, so chrome.alarms and content scripts registered
with persistAcrossSessions were forgotten when the app quit. Create the
three StateStores as Chrome's extension system does.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: report webRequest proxying to the service worker loader

content asks HasWebRequestAPIProxy before starting a service worker
AutoPreload request, because a preload that is raced and dropped is
reported to webRequest listeners as a failed request. Answer for both
Electron's webRequest module and extensions that proxy requests.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: use the app name in extension warnings

ElectronExtensionsClient::GetProductName still returned "app_shell",
the name of the Chromium embedder it was copied from, which extension
warning messages interpolate.

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: remove stale extension feature entries

These keys name permissions, manifest keys and API functions that
Chromium has removed or that Electron's schemas do not define, so they
matched nothing.

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: update extension support notes

The background key is supported for Manifest V3 service workers too,
and chrome.management needs the management permission.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-23 13:41:53 -04:00

303 lines
12 KiB
C++

// Copyright 2018 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 "shell/browser/extensions/electron_extension_loader.h"
#include <utility>
#include "base/auto_reset.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/notimplemented.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/threading/thread_restrictions.h"
#include "base/time/time.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/service_worker_context.h"
#include "content/public/browser/storage_partition.h"
#include "extensions/browser/extension_file_task_runner.h"
#include "extensions/browser/extension_pref_names.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/pref_names.h"
#include "extensions/browser/service_worker/service_worker_task_queue.h"
#include "extensions/common/constants.h"
#include "extensions/common/error_utils.h"
#include "extensions/common/file_util.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/manifest_handlers/background_info.h"
#include "third_party/blink/public/common/service_worker/service_worker_status_code.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
namespace extensions {
namespace {
std::pair<scoped_refptr<const Extension>, std::string> LoadUnpacked(
const base::FilePath& extension_dir,
int load_flags) {
// app_shell only supports unpacked extensions.
// NOTE: If you add packed extension support consider removing the flag
// FOLLOW_SYMLINKS_ANYWHERE below. Packed extensions should not have symlinks.
if (!base::DirectoryExists(extension_dir)) {
std::string err = "Extension directory not found: " +
base::UTF16ToUTF8(extension_dir.LossyDisplayName());
return std::make_pair(nullptr, err);
}
// remove _metadata folder. Otherwise, the following warning will be thrown
// Cannot load extension with file or directory name _metadata.
// Filenames starting with "_" are reserved for use by the system.
// see: https://bugs.chromium.org/p/chromium/issues/detail?id=377278
base::FilePath metadata_dir = extension_dir.Append(kMetadataFolder);
if (base::DirectoryExists(metadata_dir)) {
base::DeletePathRecursively(metadata_dir);
}
std::u16string load_error;
scoped_refptr<Extension> extension = file_util::LoadExtension(
extension_dir, extensions::mojom::ManifestLocation::kCommandLine,
load_flags, &load_error);
if (!extension.get()) {
std::string err = "Loading extension at " +
base::UTF16ToUTF8(extension_dir.LossyDisplayName()) +
" failed with: " + base::UTF16ToUTF8(load_error);
return std::make_pair(nullptr, err);
}
std::string warnings;
// Log warnings.
if (!extension->install_warnings().empty()) {
std::string warning_prefix =
"Warnings loading extension at " +
base::UTF16ToUTF8(extension_dir.LossyDisplayName());
for (const auto& warning : extension->install_warnings()) {
std::string unrecognized_manifest_error = ErrorUtils::FormatErrorMessage(
manifest_errors::kUnrecognizedManifestKey, warning.key);
if (warning.message == unrecognized_manifest_error) {
// filter kUnrecognizedManifestKey error. This error does not have any
// impact e.g: Unrecognized manifest key 'minimum_chrome_version' etc.
LOG(WARNING) << warning_prefix << ": " << warning.message;
} else {
warnings += " " + warning.message + "\n";
}
}
if (warnings != "") {
warnings = warning_prefix + ":\n" + warnings;
}
}
return std::make_pair(extension, warnings);
}
} // namespace
ElectronExtensionLoader::ElectronExtensionLoader(
content::BrowserContext* browser_context)
: browser_context_(browser_context),
extension_registrar_(ExtensionRegistrar::Get(browser_context)) {
extension_registrar_->Init(
this, /*extensions_enabled=*/true, base::CommandLine::ForCurrentProcess(),
browser_context_->GetPath().AppendASCII(kInstallDirectoryName),
browser_context_->GetPath().AppendASCII(kUnpackedInstallDirectoryName));
}
ElectronExtensionLoader::~ElectronExtensionLoader() = default;
void ElectronExtensionLoader::LoadExtension(
const base::FilePath& extension_dir,
int load_flags,
base::OnceCallback<void(const Extension*, const std::string&)> cb) {
GetExtensionFileTaskRunner()->PostTaskAndReplyWithResult(
FROM_HERE, base::BindOnce(&LoadUnpacked, extension_dir, load_flags),
base::BindOnce(&ElectronExtensionLoader::FinishExtensionLoad,
weak_factory_.GetWeakPtr(), std::move(cb)));
}
void ElectronExtensionLoader::ReloadExtension(const ExtensionId& extension_id) {
const Extension* extension = ExtensionRegistry::Get(browser_context_)
->GetInstalledExtension(extension_id);
// We shouldn't be trying to reload extensions that haven't been added.
DCHECK(extension);
// This should always start false since it's only set here, or in
// LoadExtensionForReload() as a result of the call below.
DCHECK_EQ(false, did_schedule_reload_);
base::AutoReset<bool> reset_did_schedule_reload(&did_schedule_reload_, false);
extension_registrar_->ReloadExtensionWithQuietFailure(extension_id);
if (did_schedule_reload_)
return;
}
void ElectronExtensionLoader::UnloadExtension(
const ExtensionId& extension_id,
extensions::UnloadedExtensionReason reason) {
extension_registrar_->RemoveExtension(extension_id, reason);
}
void ElectronExtensionLoader::FinishExtensionLoad(
base::OnceCallback<void(const Extension*, const std::string&)> cb,
std::pair<scoped_refptr<const Extension>, std::string> result) {
scoped_refptr<const Extension> extension = result.first;
base::OnceClosure add_extension = base::BindOnce(
&ElectronExtensionLoader::AddLoadedExtension, weak_factory_.GetWeakPtr(),
std::move(cb), extension, result.second);
// A worker registered by a previous launch keeps running the script it was
// registered with, and the extension's files can change between launches
// without its version changing. Unregister it so the worker is registered
// again from the files being loaded now.
auto* task_queue = ServiceWorkerTaskQueue::Get(browser_context_);
if (extension && BackgroundInfo::IsServiceWorkerBased(extension.get()) &&
task_queue->RetrieveRegisteredServiceWorkerVersion(extension->id())
.IsValid()) {
task_queue->RemoveRegisteredServiceWorkerInfo(extension->id());
util::GetStoragePartitionForExtensionId(extension->id(), browser_context_)
->GetServiceWorkerContext()
->UnregisterServiceWorkerImmediately(
extension->url(),
blink::StorageKey::CreateFirstParty(extension->origin()),
base::IgnoreArgs<blink::ServiceWorkerStatusCode>(
std::move(add_extension)));
return;
}
std::move(add_extension).Run();
}
void ElectronExtensionLoader::AddLoadedExtension(
base::OnceCallback<void(const Extension*, const std::string&)> cb,
scoped_refptr<const Extension> extension,
const std::string& error_or_warnings) {
if (extension) {
ExtensionPrefs* extension_prefs = ExtensionPrefs::Get(browser_context_);
if (BackgroundInfo::IsServiceWorkerBased(extension.get())) {
// Tell Chromium that it needs to start the extension's service worker.
// Chromium usually does this only when an extension is first installed
// because Chrome will restart the service worker when the browser
// relaunches. In Electron, we make a fresh install on every app start,
// so we need to run the fresh install logic again.
extension_prefs->UpdateExtensionPref(
extension.get()->id(), extensions::kPrefHasStartedServiceWorker,
base::Value(false));
}
extension_registrar_->AddExtension(extension);
// Write extension install time to ExtensionPrefs.
// This is required by extensions::WebRequestAPI
// which calls extensions::ExtensionPrefs::GetInstallTime.
//
// Implementation for writing the pref was based on
// PreferenceAPIBase::SetExtensionControlledPref.
{
ExtensionPrefs::ScopedDictionaryUpdate update(
extension_prefs, extension.get()->id(),
extensions::pref_names::kPrefPreferences);
auto preference = update.Create();
const int64_t now_usec =
base::Time::Now().since_origin().InMicroseconds();
preference->SetString("install_time", base::NumberToString(now_usec));
}
}
std::move(cb).Run(extension.get(), error_or_warnings);
}
void ElectronExtensionLoader::FinishExtensionReload(
const ExtensionId& old_extension_id,
std::pair<scoped_refptr<const Extension>, std::string> result) {
scoped_refptr<const Extension> extension = result.first;
if (extension) {
extension_registrar_->AddExtension(extension);
}
}
void ElectronExtensionLoader::PreAddExtension(const Extension* extension,
const Extension* old_extension) {
if (old_extension)
return;
// The extension might be disabled if a previous reload attempt failed. In
// that case, we want to remove that disable reason.
ExtensionPrefs* extension_prefs = ExtensionPrefs::Get(browser_context_);
extension_prefs->RemoveDisableReason(extension->id(),
disable_reason::DISABLE_RELOAD);
}
void ElectronExtensionLoader::PostActivateExtension(
scoped_refptr<const Extension> extension) {}
void ElectronExtensionLoader::PostDeactivateExtension(
scoped_refptr<const Extension> extension) {}
void ElectronExtensionLoader::PreUninstallExtension(
scoped_refptr<const Extension> extension) {}
void ElectronExtensionLoader::PostUninstallExtension(
scoped_refptr<const Extension> extension,
base::OnceClosure done_callback) {}
void ElectronExtensionLoader::DoLoadExtensionForReload(
const ExtensionId& extension_id,
const base::FilePath& path) {
CHECK(!path.empty());
// TODO(nornagon): we should save whether file access was granted
// when loading this extension and retain it here. As is, reloading an
// extension will cause the file access permission to be dropped.
int load_flags = Extension::FOLLOW_SYMLINKS_ANYWHERE;
GetExtensionFileTaskRunner()->PostTaskAndReplyWithResult(
FROM_HERE, base::BindOnce(&LoadUnpacked, path, load_flags),
base::BindOnce(&ElectronExtensionLoader::FinishExtensionReload,
weak_factory_.GetWeakPtr(), extension_id));
did_schedule_reload_ = true;
}
void ElectronExtensionLoader::LoadExtensionForReload(
const ExtensionId& extension_id,
const base::FilePath& path) {
DoLoadExtensionForReload(extension_id, path);
}
void ElectronExtensionLoader::LoadExtensionForReloadWithQuietFailure(
const ExtensionId& extension_id,
const base::FilePath& path) {
DoLoadExtensionForReload(extension_id, path);
}
void ElectronExtensionLoader::ShowExtensionDisabledError(
const Extension* extension,
bool is_remote_install) {}
bool ElectronExtensionLoader::CanEnableExtension(const Extension* extension) {
return true;
}
bool ElectronExtensionLoader::CanDisableExtension(const Extension* extension) {
// Extensions cannot be disabled by the user.
return false;
}
void ElectronExtensionLoader::GrantActivePermissions(
const Extension* extension) {
NOTIMPLEMENTED();
}
} // namespace extensions