mirror of
https://github.com/electron/electron.git
synced 2026-10-28 11:44:46 +08:00
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>
This commit is contained in:
1 parent
ab1a105caa
commit
bc658b2924
40 files changed
+822
-51
No files matched your search
@@ -52,7 +52,7 @@ provisional and may be removed.
|
||||
- `short_name`
|
||||
- `host_permissions` (Manifest V3)
|
||||
- `manifest_version`
|
||||
- `background` (Manifest V2)
|
||||
- `background`
|
||||
- `minimum_chrome_version`
|
||||
|
||||
See [Manifest file format](https://developer.chrome.com/docs/extensions/mv3/manifest/) for more information about the purpose of each possible key.
|
||||
@@ -103,6 +103,10 @@ The following events of `chrome.management` are supported:
|
||||
- `chrome.management.onEnabled`
|
||||
- `chrome.management.onDisabled`
|
||||
|
||||
Apart from `chrome.management.getSelf` and
|
||||
`chrome.management.getPermissionWarningsByManifest`, these require the
|
||||
`management` permission.
|
||||
|
||||
See [official documentation](https://developer.chrome.com/docs/extensions/reference/management) for more information.
|
||||
|
||||
### `chrome.runtime`
|
||||
|
||||
@@ -340,6 +340,7 @@ void ElectronContentClient::AddAdditionalSchemes(Schemes* schemes) {
|
||||
schemes->cors_enabled_schemes.push_back(extensions::kExtensionScheme);
|
||||
schemes->csp_bypassing_schemes.push_back(extensions::kExtensionScheme);
|
||||
schemes->code_cache_schemes.push_back(extensions::kExtensionScheme);
|
||||
schemes->extension_schemes.push_back(extensions::kExtensionScheme);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -189,16 +189,22 @@
|
||||
#include "extensions/browser/process_map.h"
|
||||
#include "extensions/browser/renderer_startup_helper.h"
|
||||
#include "extensions/browser/service_worker/service_worker_host.h"
|
||||
#include "extensions/browser/service_worker/service_worker_task_queue.h"
|
||||
#include "extensions/browser/url_loader_factory_manager.h"
|
||||
#include "extensions/common/api/mime_handler.mojom.h"
|
||||
#include "extensions/common/constants.h"
|
||||
#include "extensions/common/extension.h"
|
||||
#include "extensions/common/extension_set.h"
|
||||
#include "extensions/common/manifest_handlers/background_info.h"
|
||||
#include "extensions/common/manifest_handlers/sandboxed_page_info.h"
|
||||
#include "extensions/common/mojom/event_router.mojom.h"
|
||||
#include "extensions/common/mojom/guest_view.mojom.h"
|
||||
#include "extensions/common/mojom/renderer_host.mojom.h"
|
||||
#include "extensions/common/mojom/web_request_host.mojom.h"
|
||||
#include "extensions/common/switches.h"
|
||||
#include "shell/browser/extensions/electron_extension_system.h"
|
||||
#include "shell/browser/extensions/electron_extension_web_contents_observer.h"
|
||||
#include "third_party/blink/public/common/features.h"
|
||||
#endif
|
||||
|
||||
#if BUILDFLAG(ENABLE_PLUGINS)
|
||||
@@ -370,6 +376,20 @@ const extensions::Extension* GetEnabledExtensionFromSecurityPrincipal(
|
||||
return registry->enabled_extensions().GetByID(
|
||||
extensions::ExtensionId(principal.GetHost()));
|
||||
}
|
||||
|
||||
// Returns the enabled extension whose background service worker is registered
|
||||
// at `scope`, if any. Only the root scope hosts the background worker.
|
||||
const extensions::Extension* GetServiceWorkerBasedExtensionForScope(
|
||||
const GURL& scope,
|
||||
content::BrowserContext* context) {
|
||||
const extensions::Extension* extension =
|
||||
GetEnabledExtensionFromEffectiveURL(context, scope);
|
||||
if (!extension || scope != extension->url() ||
|
||||
!extensions::BackgroundInfo::IsServiceWorkerBased(extension)) {
|
||||
return nullptr;
|
||||
}
|
||||
return extension;
|
||||
}
|
||||
#endif // BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
||||
|
||||
#if BUILDFLAG(IS_LINUX)
|
||||
@@ -681,6 +701,16 @@ void ElectronBrowserClient::AppendExtraCommandLineSwitches(
|
||||
command_line->AppendSwitch(switches::kEnableSandbox);
|
||||
}
|
||||
|
||||
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
||||
// Tells the renderer to turn on web platform features that are only
|
||||
// exposed to extensions.
|
||||
if (render_process_host &&
|
||||
extensions::ProcessMap::Get(render_process_host->GetBrowserContext())
|
||||
->GetEnabledExtensionByProcessID(render_process_host->GetID())) {
|
||||
command_line->AppendSwitch(extensions::switches::kExtensionProcess);
|
||||
}
|
||||
#endif
|
||||
|
||||
renderer_process_sandboxed_[unsafe_process_id] =
|
||||
!command_line->HasSwitch(sandbox::policy::switches::kNoSandbox);
|
||||
}
|
||||
@@ -875,14 +905,19 @@ void ElectronBrowserClient::SiteInstanceGotProcessAndSite(
|
||||
content::SiteInstance* site_instance) {
|
||||
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
||||
auto* browser_context = site_instance->GetBrowserContext();
|
||||
extensions::ExtensionRegistry* registry =
|
||||
extensions::ExtensionRegistry::Get(browser_context);
|
||||
const content::SecurityPrincipal& principal =
|
||||
site_instance->GetSecurityPrincipal();
|
||||
const extensions::Extension* extension =
|
||||
registry->enabled_extensions().GetExtensionOrAppByURL(
|
||||
site_instance->GetSecurityPrincipal().GetDeprecatedSiteURL());
|
||||
GetEnabledExtensionFromSecurityPrincipal(browser_context, principal);
|
||||
if (!extension)
|
||||
return;
|
||||
|
||||
// Frames isolated in a sandbox (see
|
||||
// ShouldAllowCrossProcessSandboxedFrameForPrecursor) have no extension API
|
||||
// access, so their process must not count as an extension process.
|
||||
if (principal.IsSandboxed())
|
||||
return;
|
||||
|
||||
extensions::ProcessMap::Get(browser_context)
|
||||
->Insert(extension->id(), site_instance->GetProcess()->GetID());
|
||||
#endif // BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
||||
@@ -921,6 +956,99 @@ bool ElectronBrowserClient::ShouldUseProcessPerSite(
|
||||
#endif
|
||||
}
|
||||
|
||||
content::AllowServiceWorkerResult ElectronBrowserClient::AllowServiceWorker(
|
||||
const GURL& scope,
|
||||
const net::SiteForCookies& site_for_cookies,
|
||||
const std::optional<url::Origin>& top_frame_origin,
|
||||
const blink::StorageKey& storage_key,
|
||||
const GURL& script_url,
|
||||
content::BrowserContext* context) {
|
||||
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
||||
if (top_frame_origin &&
|
||||
top_frame_origin->scheme() == extensions::kExtensionScheme) {
|
||||
const extensions::Extension* extension =
|
||||
GetEnabledExtensionFromEffectiveURL(context,
|
||||
top_frame_origin->GetURL());
|
||||
// An extension URL without an enabled extension must not run a worker.
|
||||
if (!extension)
|
||||
return content::AllowServiceWorkerResult::No();
|
||||
|
||||
// The root scope of a service worker-based extension is reserved for the
|
||||
// script named in its manifest. `script_url` is empty when the caller
|
||||
// does not know it, which never happens at registration.
|
||||
if (extensions::BackgroundInfo::IsServiceWorkerBased(extension) &&
|
||||
!script_url.is_empty() && scope == extension->url() &&
|
||||
script_url !=
|
||||
extensions::BackgroundInfo::GetBackgroundServiceWorkerScriptURL(
|
||||
extension)) {
|
||||
return content::AllowServiceWorkerResult::No();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return content::ContentBrowserClient::AllowServiceWorker(
|
||||
scope, site_for_cookies, top_frame_origin, storage_key, script_url,
|
||||
context);
|
||||
}
|
||||
|
||||
bool ElectronBrowserClient::MayDeleteServiceWorkerRegistration(
|
||||
const GURL& scope,
|
||||
content::BrowserContext* browser_context) {
|
||||
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
||||
const extensions::Extension* extension =
|
||||
GetServiceWorkerBasedExtensionForScope(scope, browser_context);
|
||||
if (extension) {
|
||||
// The extension system owns the registration of a loaded extension's
|
||||
// background worker: nothing re-registers it if, say, clearing storage
|
||||
// data deletes it. A missing or outdated registration is not in use.
|
||||
const base::Version registered_version =
|
||||
extensions::ServiceWorkerTaskQueue::Get(browser_context)
|
||||
->RetrieveRegisteredServiceWorkerVersion(extension->id());
|
||||
return !registered_version.IsValid() ||
|
||||
registered_version != extension->version();
|
||||
}
|
||||
#endif
|
||||
return content::ContentBrowserClient::MayDeleteServiceWorkerRegistration(
|
||||
scope, browser_context);
|
||||
}
|
||||
|
||||
bool ElectronBrowserClient::ShouldTryToUpdateServiceWorkerRegistration(
|
||||
const GURL& scope,
|
||||
content::BrowserContext* browser_context) {
|
||||
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
||||
// Extension workers are updated with their extension, not by the web
|
||||
// service worker update flow.
|
||||
if (GetServiceWorkerBasedExtensionForScope(scope, browser_context))
|
||||
return false;
|
||||
#endif
|
||||
return content::ContentBrowserClient::
|
||||
ShouldTryToUpdateServiceWorkerRegistration(scope, browser_context);
|
||||
}
|
||||
|
||||
bool ElectronBrowserClient::IsClipboardAllowedForServiceWorker(
|
||||
const url::Origin& origin) {
|
||||
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
||||
// The renderer only exposes navigator.clipboard to extension service
|
||||
// workers, and only while this feature is on.
|
||||
return base::FeatureList::IsEnabled(
|
||||
blink::features::kClipboardOnExtensionServiceWorker) &&
|
||||
origin.scheme() == extensions::kExtensionScheme;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ElectronBrowserClient::AllowWorkerWriteToClipboard(
|
||||
const url::Origin& origin,
|
||||
content::BrowserContext* browser_context) {
|
||||
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
||||
return origin.scheme() == extensions::kExtensionScheme &&
|
||||
extensions::util::HasClipboardWritePermission(origin.host(),
|
||||
browser_context);
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void ElectronBrowserClient::GetMediaDeviceIDSalt(
|
||||
content::RenderFrameHost* rfh,
|
||||
const net::SiteForCookies& site_for_cookies,
|
||||
@@ -1502,6 +1630,26 @@ bool ElectronBrowserClient::ShouldTreatAsFirstPartyWhenTopLevel(
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ElectronBrowserClient::HasWebRequestAPIProxy(
|
||||
content::BrowserContext* browser_context) {
|
||||
// Requests are proxied for Electron's webRequest module as well as for
|
||||
// extensions using the webRequest or declarativeNetRequest APIs.
|
||||
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
|
||||
v8::HandleScope scope(isolate);
|
||||
auto* web_request = api::WebRequest::FromOrCreate(isolate, browser_context);
|
||||
if (web_request && web_request->HasListener())
|
||||
return true;
|
||||
|
||||
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
||||
const auto* web_request_api =
|
||||
extensions::BrowserContextKeyedAPIFactory<extensions::WebRequestAPI>::Get(
|
||||
browser_context);
|
||||
return web_request_api && web_request_api->MayHaveProxies();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ElectronBrowserClient::WillInterceptWebSocket(
|
||||
content::RenderFrameHost* frame) {
|
||||
if (!frame)
|
||||
@@ -1956,6 +2104,34 @@ bool ElectronBrowserClient::DoesSiteRequireDedicatedProcess(
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ElectronBrowserClient::ShouldAllowCrossProcessSandboxedFrameForPrecursor(
|
||||
content::BrowserContext* browser_context,
|
||||
const GURL& precursor,
|
||||
const GURL& url) {
|
||||
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
||||
// Only pages an extension's manifest lists as sandboxed, and sandboxed
|
||||
// about:srcdoc and data: documents, are isolated without API access. Other
|
||||
// sandboxed extension documents may not control their sandbox status (an
|
||||
// embedder's sandbox is inherited), so they stay in the extension's process.
|
||||
const extensions::ExtensionId extension_id =
|
||||
extensions::ExtensionSet::GetExtensionIdByURL(precursor);
|
||||
if (!precursor.is_empty() && !extension_id.empty() && !url.IsAboutSrcdoc() &&
|
||||
!url.SchemeIs(url::kDataScheme)) {
|
||||
const extensions::Extension* extension =
|
||||
extensions::ExtensionRegistry::Get(browser_context)
|
||||
->enabled_extensions()
|
||||
.GetByID(extension_id);
|
||||
if (extension) {
|
||||
return extensions::SandboxedPageInfo::IsSandboxedPage(extension,
|
||||
url.GetPath());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return content::ContentBrowserClient::
|
||||
ShouldAllowCrossProcessSandboxedFrameForPrecursor(browser_context,
|
||||
precursor, url);
|
||||
}
|
||||
|
||||
void ElectronBrowserClient::BindHostReceiverForRenderer(
|
||||
content::RenderProcessHost* render_process_host,
|
||||
mojo::GenericPendingReceiver receiver) {
|
||||
|
||||
@@ -107,6 +107,10 @@ class ElectronBrowserClient : public content::ContentBrowserClient,
|
||||
#endif // BUILDFLAG(ENABLE_PDF_VIEWER)
|
||||
bool DoesSiteRequireDedicatedProcess(content::BrowserContext* browser_context,
|
||||
const GURL& effective_site_url) override;
|
||||
bool ShouldAllowCrossProcessSandboxedFrameForPrecursor(
|
||||
content::BrowserContext* browser_context,
|
||||
const GURL& precursor,
|
||||
const GURL& url) override;
|
||||
void BindHostReceiverForRenderer(
|
||||
content::RenderProcessHost* render_process_host,
|
||||
mojo::GenericPendingReceiver receiver) override;
|
||||
@@ -280,6 +284,7 @@ class ElectronBrowserClient : public content::ContentBrowserClient,
|
||||
handshake_client,
|
||||
WebSocketOptions options) override;
|
||||
bool WillInterceptWebSocket(content::RenderFrameHost*) override;
|
||||
bool HasWebRequestAPIProxy(content::BrowserContext* browser_context) override;
|
||||
void WillCreateURLLoaderFactory(
|
||||
content::BrowserContext* browser_context,
|
||||
content::RenderFrameHost* frame,
|
||||
@@ -376,6 +381,23 @@ class ElectronBrowserClient : public content::ContentBrowserClient,
|
||||
bool ShouldUseProcessPerSite(
|
||||
content::BrowserContext* browser_context,
|
||||
const content::SecurityPrincipal& security_principal) override;
|
||||
content::AllowServiceWorkerResult AllowServiceWorker(
|
||||
const GURL& scope,
|
||||
const net::SiteForCookies& site_for_cookies,
|
||||
const std::optional<url::Origin>& top_frame_origin,
|
||||
const blink::StorageKey& storage_key,
|
||||
const GURL& script_url,
|
||||
content::BrowserContext* context) override;
|
||||
bool MayDeleteServiceWorkerRegistration(
|
||||
const GURL& scope,
|
||||
content::BrowserContext* browser_context) override;
|
||||
bool ShouldTryToUpdateServiceWorkerRegistration(
|
||||
const GURL& scope,
|
||||
content::BrowserContext* browser_context) override;
|
||||
bool IsClipboardAllowedForServiceWorker(const url::Origin& origin) override;
|
||||
bool AllowWorkerWriteToClipboard(
|
||||
const url::Origin& origin,
|
||||
content::BrowserContext* browser_context) override;
|
||||
void GetMediaDeviceIDSalt(
|
||||
content::RenderFrameHost* rfh,
|
||||
const net::SiteForCookies& site_for_cookies,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "shell/browser/extensions/electron_browser_context_keyed_service_factories.h"
|
||||
|
||||
#include "extensions/browser/api/cookies/cookies_api.h"
|
||||
#include "extensions/browser/updater/update_service_factory.h"
|
||||
#include "shell/browser/extensions/electron_extension_system_factory.h"
|
||||
|
||||
@@ -14,6 +15,10 @@ void EnsureBrowserContextKeyedServiceFactoriesBuilt() {
|
||||
// extensions embedders (and namely chrome.)
|
||||
UpdateServiceFactory::GetInstance();
|
||||
|
||||
// Dispatches chrome.cookies.onChanged. Embedders build this factory; it is
|
||||
// not part of the //extensions list.
|
||||
CookiesAPI::GetFactoryInstance();
|
||||
|
||||
ElectronExtensionSystemFactory::GetInstance();
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#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"
|
||||
@@ -20,16 +21,22 @@
|
||||
#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 {
|
||||
|
||||
@@ -145,6 +152,36 @@ 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_);
|
||||
|
||||
@@ -179,7 +216,7 @@ void ElectronExtensionLoader::FinishExtensionLoad(
|
||||
}
|
||||
}
|
||||
|
||||
std::move(cb).Run(extension.get(), result.second);
|
||||
std::move(cb).Run(extension.get(), error_or_warnings);
|
||||
}
|
||||
|
||||
void ElectronExtensionLoader::FinishExtensionReload(
|
||||
|
||||
@@ -72,6 +72,10 @@ class ElectronExtensionLoader : public ExtensionRegistrar::Delegate {
|
||||
void FinishExtensionLoad(
|
||||
base::OnceCallback<void(const Extension*, const std::string&)> cb,
|
||||
std::pair<scoped_refptr<const Extension>, std::string> result);
|
||||
void AddLoadedExtension(
|
||||
base::OnceCallback<void(const Extension*, const std::string&)> cb,
|
||||
scoped_refptr<const Extension> extension,
|
||||
const std::string& error_or_warnings);
|
||||
void DoLoadExtensionForReload(const ExtensionId& extension_id,
|
||||
const base::FilePath& path);
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "extensions/browser/null_app_sorting.h"
|
||||
#include "extensions/browser/quota_service.h"
|
||||
#include "extensions/browser/service_worker_manager.h"
|
||||
#include "extensions/browser/state_store.h"
|
||||
#include "extensions/browser/user_script_manager.h"
|
||||
#include "extensions/common/constants.h"
|
||||
#include "shell/browser/extensions/electron_extension_loader.h"
|
||||
@@ -42,7 +43,23 @@ ElectronExtensionSystem::ElectronExtensionSystem(
|
||||
BrowserContext* browser_context)
|
||||
: browser_context_(browser_context),
|
||||
store_factory_(base::MakeRefCounted<value_store::ValueStoreFactoryImpl>(
|
||||
browser_context->GetPath())) {}
|
||||
browser_context->GetPath())) {
|
||||
// Extensions cannot be loaded in in-memory sessions, which must not write
|
||||
// to disk either.
|
||||
if (browser_context->IsOffTheRecord())
|
||||
return;
|
||||
|
||||
// Created here rather than in InitForRegularProfile() because keyed
|
||||
// services built with the browser context, such as AlarmManager, look the
|
||||
// stores up when they are constructed. The rules and scripts stores load
|
||||
// immediately so their contents are ready before the first network request.
|
||||
state_store_ = std::make_unique<StateStore>(
|
||||
browser_context, store_factory_, StateStore::BackendType::STATE, true);
|
||||
rules_store_ = std::make_unique<StateStore>(
|
||||
browser_context, store_factory_, StateStore::BackendType::RULES, false);
|
||||
dynamic_user_scripts_store_ = std::make_unique<StateStore>(
|
||||
browser_context, store_factory_, StateStore::BackendType::SCRIPTS, false);
|
||||
}
|
||||
|
||||
ElectronExtensionSystem::~ElectronExtensionSystem() = default;
|
||||
|
||||
@@ -141,15 +158,15 @@ UserScriptManager* ElectronExtensionSystem::user_script_manager() {
|
||||
}
|
||||
|
||||
StateStore* ElectronExtensionSystem::state_store() {
|
||||
return nullptr;
|
||||
return state_store_.get();
|
||||
}
|
||||
|
||||
StateStore* ElectronExtensionSystem::rules_store() {
|
||||
return nullptr;
|
||||
return rules_store_.get();
|
||||
}
|
||||
|
||||
StateStore* ElectronExtensionSystem::dynamic_user_scripts_store() {
|
||||
return nullptr;
|
||||
return dynamic_user_scripts_store_.get();
|
||||
}
|
||||
|
||||
scoped_refptr<value_store::ValueStoreFactory>
|
||||
|
||||
@@ -86,6 +86,13 @@ class ElectronExtensionSystem : public ExtensionSystem {
|
||||
|
||||
raw_ptr<content::BrowserContext> browser_context_; // Not owned.
|
||||
|
||||
// Persist per-extension state such as alarms, declarative rules and dynamic
|
||||
// content scripts. Null for in-memory sessions. Declared before the members
|
||||
// that keep pointers to them.
|
||||
std::unique_ptr<StateStore> state_store_;
|
||||
std::unique_ptr<StateStore> rules_store_;
|
||||
std::unique_ptr<StateStore> dynamic_user_scripts_store_;
|
||||
|
||||
std::unique_ptr<ServiceWorkerManager> service_worker_manager_;
|
||||
std::unique_ptr<QuotaService> quota_service_;
|
||||
std::unique_ptr<UserScriptManager> user_script_manager_;
|
||||
|
||||
@@ -7,8 +7,11 @@
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "components/value_store/value_store_factory.h"
|
||||
#include "extensions/browser/api/cookies/cookies_api_delegate.h"
|
||||
#include "extensions/browser/api/storage/settings_namespace.h"
|
||||
#include "extensions/browser/guest_view/extensions_guest_view_manager_delegate.h"
|
||||
#include "extensions/browser/guest_view/mime_handler_view/mime_handler_view_guest_delegate.h"
|
||||
@@ -81,6 +84,40 @@ class ElectronMimeHandlerViewGuestDelegate
|
||||
content::BrowserContext* browser_context) override {}
|
||||
};
|
||||
|
||||
class ElectronCookiesApiDelegate : public CookiesApiDelegate {
|
||||
public:
|
||||
ElectronCookiesApiDelegate() = default;
|
||||
~ElectronCookiesApiDelegate() override = default;
|
||||
|
||||
// disable copy
|
||||
ElectronCookiesApiDelegate(const ElectronCookiesApiDelegate&) = delete;
|
||||
ElectronCookiesApiDelegate& operator=(const ElectronCookiesApiDelegate&) =
|
||||
delete;
|
||||
|
||||
// CookiesApiDelegate:
|
||||
std::vector<CookieStoreContext> GetCookieStoreContexts(
|
||||
content::BrowserContext& context,
|
||||
bool include_incognito) override {
|
||||
// A session is its own cookie store and has no off-the-record sibling.
|
||||
std::vector<int> tab_ids;
|
||||
for (const auto& contents :
|
||||
electron::api::WebContents::GetWebContentsList()) {
|
||||
if (contents && contents->web_contents() &&
|
||||
contents->web_contents()->GetBrowserContext() == &context) {
|
||||
tab_ids.push_back(contents->ID());
|
||||
}
|
||||
}
|
||||
|
||||
// Only stores with an open tab are listed.
|
||||
std::vector<CookieStoreContext> cookie_stores;
|
||||
if (!tab_ids.empty()) {
|
||||
cookie_stores.push_back(
|
||||
{raw_ref<content::BrowserContext>(context), std::move(tab_ids)});
|
||||
}
|
||||
return cookie_stores;
|
||||
}
|
||||
};
|
||||
|
||||
ElectronExtensionsAPIClient::ElectronExtensionsAPIClient() = default;
|
||||
ElectronExtensionsAPIClient::~ElectronExtensionsAPIClient() = default;
|
||||
|
||||
@@ -115,6 +152,12 @@ void ElectronExtensionsAPIClient::AttachWebContentsHelpers(
|
||||
web_contents);
|
||||
}
|
||||
|
||||
CookiesApiDelegate* ElectronExtensionsAPIClient::GetCookiesApiDelegate() {
|
||||
if (!cookies_api_delegate_)
|
||||
cookies_api_delegate_ = std::make_unique<ElectronCookiesApiDelegate>();
|
||||
return cookies_api_delegate_.get();
|
||||
}
|
||||
|
||||
ManagementAPIDelegate*
|
||||
ElectronExtensionsAPIClient::CreateManagementAPIDelegate() const {
|
||||
return new ElectronManagementAPIDelegate;
|
||||
|
||||
@@ -35,9 +35,11 @@ class ElectronExtensionsAPIClient : public ExtensionsAPIClient {
|
||||
ManagementAPIDelegate* CreateManagementAPIDelegate() const override;
|
||||
std::unique_ptr<guest_view::GuestViewManagerDelegate>
|
||||
CreateGuestViewManagerDelegate() const override;
|
||||
CookiesApiDelegate* GetCookiesApiDelegate() override;
|
||||
|
||||
private:
|
||||
std::unique_ptr<ElectronMessagingDelegate> messaging_delegate_;
|
||||
std::unique_ptr<CookiesApiDelegate> cookies_api_delegate_;
|
||||
};
|
||||
|
||||
} // namespace extensions
|
||||
|
||||
@@ -397,6 +397,18 @@ ElectronExtensionsBrowserClient::GetScriptExecutorForTab(
|
||||
return contents ? contents->script_executor() : nullptr;
|
||||
}
|
||||
|
||||
void ElectronExtensionsBrowserClient::GetTabAndWindowIdForWebContents(
|
||||
content::WebContents* web_contents,
|
||||
int* tab_id,
|
||||
int* window_id) {
|
||||
// Tab ids are WebContents ids, the same ids chrome.tabs hands out. Electron
|
||||
// has no window concept for extensions.
|
||||
auto* contents =
|
||||
web_contents ? electron::api::WebContents::From(web_contents) : nullptr;
|
||||
*tab_id = contents ? contents->ID() : -1;
|
||||
*window_id = -1;
|
||||
}
|
||||
|
||||
extensions::KioskDelegate* ElectronExtensionsBrowserClient::GetKioskDelegate() {
|
||||
if (!kiosk_delegate_)
|
||||
kiosk_delegate_ = std::make_unique<ElectronKioskDelegate>();
|
||||
|
||||
@@ -156,6 +156,9 @@ class ElectronExtensionsBrowserClient
|
||||
content::WebContents** web_contents) const override;
|
||||
extensions::ScriptExecutor* GetScriptExecutorForTab(
|
||||
content::WebContents& web_contents) override;
|
||||
void GetTabAndWindowIdForWebContents(content::WebContents* web_contents,
|
||||
int* tab_id,
|
||||
int* window_id) override;
|
||||
extensions::KioskDelegate* GetKioskDelegate() override;
|
||||
std::string GetApplicationLocale() override;
|
||||
void RegisterBrowserInterfaceBindersForFrame(
|
||||
|
||||
@@ -15,15 +15,6 @@
|
||||
"dependencies": ["manifest:action"],
|
||||
"contexts": ["privileged_extension"]
|
||||
},
|
||||
"action.isEnabled": {
|
||||
"channel": "stable"
|
||||
},
|
||||
"action.getBadgeTextColor": {
|
||||
"channel": "stable"
|
||||
},
|
||||
"action.setBadgeTextColor": {
|
||||
"channel": "stable"
|
||||
},
|
||||
"extension": {
|
||||
"channel": "stable",
|
||||
"extension_types": ["extension"],
|
||||
@@ -45,12 +36,6 @@
|
||||
],
|
||||
"max_manifest_version": 2
|
||||
},
|
||||
"mimeHandlerViewGuestInternal": {
|
||||
"internal": true,
|
||||
"contexts": "all",
|
||||
"channel": "stable",
|
||||
"matches": ["<all_urls>"]
|
||||
},
|
||||
"pdfViewerPrivate": {
|
||||
"dependencies": ["permission:pdfViewerPrivate"],
|
||||
"contexts": ["privileged_extension"]
|
||||
@@ -72,11 +57,5 @@
|
||||
},
|
||||
"tabs.executeScript": {
|
||||
"max_manifest_version": 2
|
||||
},
|
||||
"tabs.insertCSS": {
|
||||
"max_manifest_version": 2
|
||||
},
|
||||
"tabs.removeCSS": {
|
||||
"max_manifest_version": 2
|
||||
}
|
||||
}
|
||||
@@ -12,14 +12,6 @@
|
||||
"extension_types": ["extension"],
|
||||
"platforms": ["win", "mac"]
|
||||
},
|
||||
"file_browser_handlers": {
|
||||
"channel": "stable",
|
||||
"extension_types": ["extension", "legacy_packaged_app"]
|
||||
},
|
||||
"file_system_provider_capabilities": {
|
||||
"channel": "stable",
|
||||
"extension_types": ["extension", "platform_app"]
|
||||
},
|
||||
"minimum_chrome_version": {
|
||||
"channel": "stable",
|
||||
"extension_types": [
|
||||
|
||||
@@ -19,12 +19,6 @@
|
||||
"channel": "stable",
|
||||
"extension_types": ["extension", "platform_app"]
|
||||
},
|
||||
"aiLanguageModelOriginTrial": {
|
||||
"channel": "stable",
|
||||
"extension_types": [
|
||||
"extension"
|
||||
]
|
||||
},
|
||||
"background": {
|
||||
"channel": "stable",
|
||||
"extension_types": [
|
||||
@@ -135,9 +129,9 @@
|
||||
"channel": "stable",
|
||||
"extension_types": ["extension", "platform_app"]
|
||||
},
|
||||
"idltest": {
|
||||
"channel": "trunk",
|
||||
"extension_types": ["extension"]
|
||||
"management": {
|
||||
"channel": "stable",
|
||||
"extension_types": ["extension", "legacy_packaged_app"]
|
||||
},
|
||||
"notifications": {
|
||||
"channel": "stable",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "extensions/common/features/simple_feature.h"
|
||||
#include "extensions/common/permissions/permission_message_provider.h"
|
||||
#include "extensions/common/url_pattern_set.h"
|
||||
#include "shell/common/application_info.h"
|
||||
#include "shell/common/extensions/electron_extensions_api_provider.h"
|
||||
|
||||
using extensions::ExtensionsClient;
|
||||
@@ -86,8 +87,7 @@ ElectronExtensionsClient::GetPermissionMessageProvider() const {
|
||||
}
|
||||
|
||||
const std::string ElectronExtensionsClient::GetProductName() {
|
||||
// TODO(samuelmaddock):
|
||||
return "app_shell";
|
||||
return GetApplicationName();
|
||||
}
|
||||
|
||||
void ElectronExtensionsClient::FilterHostPermissions(
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
#include "third_party/abseil-cpp/absl/strings/str_format.h"
|
||||
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
|
||||
#include "third_party/blink/public/common/associated_interfaces/associated_interface_registry.h"
|
||||
#include "third_party/blink/public/common/features.h"
|
||||
#include "third_party/blink/public/common/web_preferences/web_preferences.h"
|
||||
#include "third_party/blink/public/platform/web_runtime_features.h"
|
||||
#include "third_party/blink/public/platform/web_security_origin.h"
|
||||
@@ -95,14 +96,18 @@
|
||||
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
||||
#include "extensions/common/constants.h"
|
||||
#include "extensions/common/extensions_client.h"
|
||||
#include "extensions/common/switches.h"
|
||||
#include "extensions/renderer/api/core_extensions_renderer_api_provider.h"
|
||||
#include "extensions/renderer/dispatcher.h"
|
||||
#include "extensions/renderer/extension_frame_helper.h"
|
||||
#include "extensions/renderer/extension_localization_throttle.h"
|
||||
#include "extensions/renderer/extension_web_view_helper.h"
|
||||
#include "extensions/renderer/guest_view/mime_handler_view/mime_handler_view_container_manager.h"
|
||||
#include "shell/common/extensions/electron_extensions_client.h"
|
||||
#include "shell/renderer/extensions/electron_extensions_renderer_api_provider.h"
|
||||
#include "shell/renderer/extensions/electron_extensions_renderer_client.h"
|
||||
#include "third_party/blink/public/common/loader/url_loader_throttle.h"
|
||||
#include "third_party/blink/public/platform/url_loader_throttle_provider.h"
|
||||
#endif // BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
||||
|
||||
namespace electron {
|
||||
@@ -112,6 +117,29 @@ content::RenderFrame* GetRenderFrame(v8::Isolate* const isolate,
|
||||
|
||||
namespace {
|
||||
|
||||
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
||||
// Localizes __MSG_*__ placeholders in stylesheets loaded from extensions.
|
||||
class ExtensionURLLoaderThrottleProvider
|
||||
: public blink::URLLoaderThrottleProvider {
|
||||
public:
|
||||
// blink::URLLoaderThrottleProvider:
|
||||
std::unique_ptr<blink::URLLoaderThrottleProvider> Clone() override {
|
||||
return std::make_unique<ExtensionURLLoaderThrottleProvider>();
|
||||
}
|
||||
std::vector<std::unique_ptr<blink::URLLoaderThrottle>> CreateThrottles(
|
||||
base::optional_ref<const blink::LocalFrameToken> local_frame_token,
|
||||
const network::ResourceRequest& request) override {
|
||||
std::vector<std::unique_ptr<blink::URLLoaderThrottle>> throttles;
|
||||
if (auto throttle = extensions::ExtensionLocalizationThrottle::MaybeCreate(
|
||||
local_frame_token, request.url)) {
|
||||
throttles.push_back(std::move(throttle));
|
||||
}
|
||||
return throttles;
|
||||
}
|
||||
void SetOnline(bool is_online) override {}
|
||||
};
|
||||
#endif // BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
||||
|
||||
void SetIsWebView(v8::Isolate* isolate, v8::Local<v8::Object> object) {
|
||||
gin_helper::Dictionary dict(isolate, object);
|
||||
dict.SetHidden("isWebView", true);
|
||||
@@ -313,6 +341,18 @@ void RendererClientBase::RenderThreadStarted() {
|
||||
#endif
|
||||
}
|
||||
|
||||
void RendererClientBase::SetRuntimeFeaturesDefaultsBeforeBlinkInitialization() {
|
||||
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
||||
// Web platform features that are only exposed to extensions.
|
||||
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
|
||||
extensions::switches::kExtensionProcess) &&
|
||||
base::FeatureList::IsEnabled(
|
||||
blink::features::kClipboardOnExtensionServiceWorker)) {
|
||||
blink::WebRuntimeFeatures::EnableClipboardOnExtensionServiceWorker(true);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void RendererClientBase::ExposeInterfacesToBrowser(mojo::BinderMap* binders) {
|
||||
// NOTE: Do not add binders directly within this method. Instead, modify the
|
||||
// definition of |ExposeElectronRendererInterfacesToBrowser()| to ensure
|
||||
@@ -521,6 +561,26 @@ void RendererClientBase::RunScriptsAtDocumentEnd(
|
||||
#endif
|
||||
}
|
||||
|
||||
std::unique_ptr<blink::URLLoaderThrottleProvider>
|
||||
RendererClientBase::CreateURLLoaderThrottleProvider(
|
||||
blink::URLLoaderThrottleProviderType provider_type) {
|
||||
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
||||
return std::make_unique<ExtensionURLLoaderThrottleProvider>();
|
||||
#else
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool RendererClientBase::ShouldNotifyServiceWorkerOnWebSocketActivity(
|
||||
v8::Local<v8::Context> context) {
|
||||
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
||||
return extensions::Dispatcher::ShouldNotifyServiceWorkerOnWebSocketActivity(
|
||||
context);
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void RendererClientBase::DidInitializeServiceWorkerContextOnWorkerThread(
|
||||
blink::WebServiceWorkerContextProxy* context_proxy,
|
||||
const GURL& service_worker_scope,
|
||||
|
||||
@@ -120,6 +120,7 @@ class RendererClientBase : public content::ContentRendererClient
|
||||
|
||||
// content::ContentRendererClient:
|
||||
void RenderThreadStarted() override;
|
||||
void SetRuntimeFeaturesDefaultsBeforeBlinkInitialization() override;
|
||||
void ExposeInterfacesToBrowser(mojo::BinderMap* binders) override;
|
||||
void RenderFrameCreated(content::RenderFrame*) override;
|
||||
void SetPendingCreateNewWindowStartupData(mojo_base::BigBuffer data) override;
|
||||
@@ -169,6 +170,11 @@ class RendererClientBase : public content::ContentRendererClient
|
||||
void WebViewCreated(blink::WebView* web_view,
|
||||
bool was_created_by_renderer,
|
||||
const url::Origin* outermost_origin) override;
|
||||
std::unique_ptr<blink::URLLoaderThrottleProvider>
|
||||
CreateURLLoaderThrottleProvider(
|
||||
blink::URLLoaderThrottleProviderType provider_type) override;
|
||||
bool ShouldNotifyServiceWorkerOnWebSocketActivity(
|
||||
v8::Local<v8::Context> context) override;
|
||||
|
||||
private:
|
||||
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
|
||||
|
||||
+279
-1
@@ -19,11 +19,12 @@ import { randomUUID } from 'node:crypto';
|
||||
import { once } from 'node:events';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as http from 'node:http';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
import { emittedNTimes, emittedUntil } from './lib/events-helpers.ts';
|
||||
import { ifit, listen, startRemoteControlApp, waitUntil } from './lib/spec-helpers.ts';
|
||||
import { defer, ifit, listen, startRemoteControlApp, waitUntil } from './lib/spec-helpers.ts';
|
||||
import { expectWarningMessages } from './lib/warning-helpers.ts';
|
||||
import { closeAllWindows, closeWindow, cleanupWebContents } from './lib/window-helpers.ts';
|
||||
|
||||
@@ -90,6 +91,20 @@ describe('chrome extensions', () => {
|
||||
).resolves.toHaveProperty('id');
|
||||
});
|
||||
|
||||
it('supports chrome.management with the management permission', async () => {
|
||||
const customSession = session.fromPartition(`persist:${crypto.randomUUID()}`);
|
||||
const extension = await customSession.extensions.loadExtension(
|
||||
path.join(fixtures, 'extensions', 'chrome-management')
|
||||
);
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
|
||||
await w.loadURL(`${extension.url}page.html`);
|
||||
|
||||
const all = await w.webContents.executeJavaScript('chrome.management.getAll()');
|
||||
expect(all.map((info: any) => info.id)).toContain(extension.id);
|
||||
const info = await w.webContents.executeJavaScript(`chrome.management.get('${extension.id}')`);
|
||||
expect(info).toMatchObject({ id: extension.id, name: 'chrome-management' });
|
||||
});
|
||||
|
||||
describe('Chrome Web Store origin', () => {
|
||||
// The core //extensions feature files expose some APIs to web pages on the
|
||||
// Chrome Web Store origin. Electron does not support chrome.webstorePrivate
|
||||
@@ -421,6 +436,39 @@ describe('chrome extensions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('chrome.cookies', () => {
|
||||
let customSession: Session;
|
||||
let w: BrowserWindow;
|
||||
|
||||
beforeEach(async () => {
|
||||
customSession = session.fromPartition(`persist:${crypto.randomUUID()}`);
|
||||
const extension = await customSession.extensions.loadExtension(
|
||||
path.join(fixtures, 'extensions', 'chrome-cookies')
|
||||
);
|
||||
w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
|
||||
await w.loadURL(`${extension.url}page.html`);
|
||||
});
|
||||
|
||||
it('lists the session as a cookie store', async () => {
|
||||
const stores = await w.webContents.executeJavaScript('chrome.cookies.getAllCookieStores()');
|
||||
expect(stores).toEqual([{ id: '0', tabIds: [w.webContents.id] }]);
|
||||
});
|
||||
|
||||
it('sets and gets cookies and emits onChanged', async () => {
|
||||
const cookieUrl = `${url}/`;
|
||||
const { change, cookie } = await w.webContents.executeJavaScript(`(async () => {
|
||||
const changed = new Promise((resolve) => chrome.cookies.onChanged.addListener(resolve));
|
||||
await chrome.cookies.set({ url: ${JSON.stringify(cookieUrl)}, name: 'key', value: 'value' });
|
||||
return { change: await changed, cookie: await chrome.cookies.get({ url: ${JSON.stringify(cookieUrl)}, name: 'key' }) };
|
||||
})()`);
|
||||
expect(cookie).toMatchObject({ name: 'key', value: 'value', storeId: '0' });
|
||||
expect(change).toMatchObject({ removed: false, cause: 'explicit', cookie: { name: 'key', value: 'value' } });
|
||||
|
||||
const cookies = await customSession.cookies.get({ url: cookieUrl, name: 'key' });
|
||||
expect(cookies.map((c) => c.value)).toEqual(['value']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('chrome.storage', () => {
|
||||
it('stores and retrieves a key', async () => {
|
||||
const customSession = session.fromPartition(`persist:${randomUUID()}`);
|
||||
@@ -624,6 +672,44 @@ describe('chrome extensions', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('reports the tab id of the requesting WebContents', async () => {
|
||||
const extension = await customSession.extensions.loadExtension(
|
||||
path.join(fixtures, 'extensions', 'request-tab-id')
|
||||
);
|
||||
const page = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
|
||||
await page.loadURL(`${extension.url}page.html`);
|
||||
// Wait for the service worker to register its listener.
|
||||
await page.webContents.executeJavaScript("chrome.runtime.sendMessage({ type: 'ping' })");
|
||||
|
||||
await w.loadURL(`${url}/?tab-id`);
|
||||
const request = await page.webContents.executeJavaScript(
|
||||
"chrome.runtime.sendMessage({ type: 'wait-for-request', marker: 'tab-id' })"
|
||||
);
|
||||
expect(request.tabId).toBe(w.webContents.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('chrome.declarativeNetRequest', () => {
|
||||
it('matches rules restricted to a tab', async () => {
|
||||
const customSession = session.fromPartition(`persist:${crypto.randomUUID()}`);
|
||||
const extension = await customSession.extensions.loadExtension(
|
||||
path.join(fixtures, 'extensions', 'request-tab-id')
|
||||
);
|
||||
const page = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
|
||||
const tab = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
|
||||
const otherTab = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
|
||||
await Promise.all([page.loadURL(`${extension.url}page.html`), tab.loadURL(url), otherTab.loadURL(url)]);
|
||||
|
||||
await page.webContents.executeJavaScript(
|
||||
`chrome.runtime.sendMessage({ type: 'block-in-tab', marker: 'blocked-in-tab', tabId: ${tab.webContents.id} })`
|
||||
);
|
||||
|
||||
const canFetch = (contents: WebContents) =>
|
||||
contents.executeJavaScript(`fetch('${url}/blocked-in-tab').then(() => true, () => false)`);
|
||||
expect(await canFetch(otherTab.webContents)).toBe(true);
|
||||
expect(await canFetch(tab.webContents)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('chrome.tabs', () => {
|
||||
@@ -1077,6 +1163,39 @@ describe('chrome extensions', () => {
|
||||
const textContent = await w.webContents.executeJavaScript('document.body.textContent');
|
||||
expect(textContent).toBe('script loaded ok\n');
|
||||
});
|
||||
|
||||
it('isolates only manifest-sandboxed pages from the extension process', async () => {
|
||||
const { id } = await session.defaultSession.extensions.loadExtension(
|
||||
path.join(fixtures, 'extensions', 'sandboxed-pages')
|
||||
);
|
||||
const w = new BrowserWindow({ show: false });
|
||||
await w.loadURL(`chrome-extension://${id}/page.html`);
|
||||
|
||||
const { mainFrame } = w.webContents;
|
||||
const findFrame = (file: string) => mainFrame.frames.find((frame) => frame.url.endsWith(file))!;
|
||||
const hasExtensionApi = (frame: Electron.WebFrameMain) =>
|
||||
frame.executeJavaScript("typeof globalThis.chrome?.runtime?.getManifest === 'function'");
|
||||
|
||||
// Listed under "sandbox" in the manifest: its own process, no API access.
|
||||
const sandboxed = findFrame('sandboxed.html');
|
||||
expect(sandboxed.processId).not.toBe(mainFrame.processId);
|
||||
expect(await hasExtensionApi(sandboxed)).toBe(false);
|
||||
|
||||
// Sandboxed by its embedder only: stays in the extension's process.
|
||||
const inherited = findFrame('frame.html');
|
||||
expect(inherited.processId).toBe(mainFrame.processId);
|
||||
expect(await hasExtensionApi(inherited)).toBe(true);
|
||||
});
|
||||
|
||||
it('localizes messages in stylesheets', async () => {
|
||||
const { id } = await session.defaultSession.extensions.loadExtension(
|
||||
path.join(fixtures, 'extensions', 'css-localization')
|
||||
);
|
||||
const w = new BrowserWindow({ show: false });
|
||||
await w.loadURL(`chrome-extension://${id}/page.html`);
|
||||
const content = await w.webContents.executeJavaScript("getComputedStyle(document.body, '::after').content");
|
||||
expect(content).toBe(`"${id}"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('manifest v3', () => {
|
||||
@@ -1195,6 +1314,165 @@ describe('chrome extensions', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Loads a generated extension in a fresh app, reports what its worker saw
|
||||
// and quits. `write` replaces the extension's files between launches.
|
||||
const setUpRestartingExtension = async () => {
|
||||
const partition = `persist:${crypto.randomUUID()}`;
|
||||
const extensionPath = await fs.mkdtemp(path.join(os.tmpdir(), 'electron-extension-'));
|
||||
defer(() => fs.rm(extensionPath, { recursive: true, force: true }));
|
||||
|
||||
const writeExtension = async (version: string, marker: string) => {
|
||||
await fs.writeFile(
|
||||
path.join(extensionPath, 'manifest.json'),
|
||||
JSON.stringify({
|
||||
name: 'restart',
|
||||
version,
|
||||
manifest_version: 3,
|
||||
background: { service_worker: 'background.js' }
|
||||
})
|
||||
);
|
||||
await fs.writeFile(path.join(extensionPath, 'page.html'), '<!doctype html>');
|
||||
await fs.writeFile(
|
||||
path.join(extensionPath, 'background.js'),
|
||||
`chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
sendResponse({ marker: ${JSON.stringify(marker)} });
|
||||
});`
|
||||
);
|
||||
};
|
||||
|
||||
const runRemoteControlApp = async () => {
|
||||
const rc = await startRemoteControlApp();
|
||||
const exitPromise = once(rc.process, 'exit');
|
||||
|
||||
const state = await rc.remotely(
|
||||
async (partition: string, extensionPath: string) => {
|
||||
const { BrowserWindow, session } = require('electron/main');
|
||||
const { setTimeout } = require('node:timers/promises');
|
||||
|
||||
const customSession = session.fromPartition(partition);
|
||||
const extension = await customSession.extensions.loadExtension(extensionPath);
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
|
||||
await w.loadURL(`${extension.url}page.html`);
|
||||
|
||||
const state = await w.webContents.executeJavaScript('chrome.runtime.sendMessage({})');
|
||||
|
||||
// Give Chromium some time to write the extension's prefs to disk.
|
||||
await setTimeout(500);
|
||||
|
||||
global.setTimeout(() => require('electron').app.quit());
|
||||
|
||||
return state;
|
||||
},
|
||||
partition,
|
||||
extensionPath
|
||||
);
|
||||
|
||||
await exitPromise;
|
||||
return state;
|
||||
};
|
||||
|
||||
return { write: writeExtension, launch: runRemoteControlApp };
|
||||
};
|
||||
|
||||
it('runs the worker script on disk after restarting the app', async () => {
|
||||
const extension = await setUpRestartingExtension();
|
||||
|
||||
await extension.write('1.0', 'first');
|
||||
expect((await extension.launch()).marker).toBe('first');
|
||||
|
||||
// Same version, new script.
|
||||
await extension.write('1.0', 'second');
|
||||
expect((await extension.launch()).marker).toBe('second');
|
||||
});
|
||||
|
||||
it('persists alarms and dynamic content scripts across restarts', async () => {
|
||||
const partition = `persist:${crypto.randomUUID()}`;
|
||||
const extensionPath = path.join(fixtures, 'extensions', 'persistent-state');
|
||||
|
||||
const runRemoteControlApp = async (script: string) => {
|
||||
const rc = await startRemoteControlApp();
|
||||
const exitPromise = once(rc.process, 'exit');
|
||||
|
||||
const result = await rc.remotely(
|
||||
async (partition: string, extensionPath: string, script: string) => {
|
||||
const { BrowserWindow, session } = require('electron/main');
|
||||
const { setTimeout } = require('node:timers/promises');
|
||||
|
||||
const customSession = session.fromPartition(partition);
|
||||
const extension = await customSession.extensions.loadExtension(extensionPath);
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
|
||||
await w.loadURL(`${extension.url}page.html`);
|
||||
const result = await w.webContents.executeJavaScript(script);
|
||||
|
||||
// Give Chromium some time to write the extension's state to disk.
|
||||
await setTimeout(500);
|
||||
|
||||
global.setTimeout(() => require('electron').app.quit());
|
||||
|
||||
return result;
|
||||
},
|
||||
partition,
|
||||
extensionPath,
|
||||
script
|
||||
);
|
||||
|
||||
await exitPromise;
|
||||
return result;
|
||||
};
|
||||
|
||||
const getState = `(async () => ({
|
||||
alarms: (await chrome.alarms.getAll()).map((alarm) => alarm.name),
|
||||
scripts: (await chrome.scripting.getRegisteredContentScripts()).map((script) => script.id)
|
||||
}))()`;
|
||||
|
||||
const created = await runRemoteControlApp(`(async () => {
|
||||
await chrome.alarms.create('persisted', { delayInMinutes: 60 });
|
||||
await chrome.scripting.registerContentScripts([
|
||||
{ id: 'persisted', js: ['content.js'], matches: ['<all_urls>'], persistAcrossSessions: true }
|
||||
]);
|
||||
return ${getState};
|
||||
})()`);
|
||||
expect(created).toEqual({ alarms: ['persisted'], scripts: ['persisted'] });
|
||||
|
||||
expect(await runRemoteControlApp(getState)).toEqual({ alarms: ['persisted'], scripts: ['persisted'] });
|
||||
});
|
||||
|
||||
it('keeps the background service worker when storage data is cleared', async () => {
|
||||
const customSession = session.fromPartition(`persist:${crypto.randomUUID()}`);
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } });
|
||||
const extension = await customSession.extensions.loadExtension(
|
||||
path.join(fixtures, 'extensions', 'mv3-service-worker')
|
||||
);
|
||||
|
||||
const messageWorker = async () => {
|
||||
await w.loadURL(url);
|
||||
w.webContents.executeJavaScript("window.postMessage('fetch-confirmation', '*')");
|
||||
const [{ message: responseString }] = await once(w.webContents, 'console-message');
|
||||
return JSON.parse(responseString).message;
|
||||
};
|
||||
|
||||
// A reply means the worker has finished registering. Until then the
|
||||
// registration is not protected.
|
||||
expect(await messageWorker()).toBe('Hello from background.js');
|
||||
|
||||
await customSession.clearStorageData();
|
||||
|
||||
const worker = await customSession.serviceWorkers.startWorkerForScope(extension.url);
|
||||
expect(worker.scope).toBe(extension.url);
|
||||
expect(await messageWorker()).toBe('Hello from background.js');
|
||||
});
|
||||
|
||||
it('can write to the clipboard from the background service worker', { tags: ['serial'] }, async () => {
|
||||
const customSession = session.fromPartition(`persist:${crypto.randomUUID()}`);
|
||||
const extension = await customSession.extensions.loadExtension(
|
||||
path.join(fixtures, 'extensions', 'clipboard-service-worker')
|
||||
);
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { session: customSession } });
|
||||
await w.loadURL(`${extension.url}page.html`);
|
||||
const result = await w.webContents.executeJavaScript("chrome.runtime.sendMessage('from a service worker')");
|
||||
expect(result).toEqual({ written: true });
|
||||
});
|
||||
|
||||
it('can run chrome extension APIs', async () => {
|
||||
const customSession = session.fromPartition(`persist:${randomUUID()}`);
|
||||
const w = new BrowserWindow({ show: false, webPreferences: { session: customSession, nodeIntegration: true } });
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "chrome-cookies",
|
||||
"version": "1.0",
|
||||
"permissions": ["cookies"],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"manifest_version": 3
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<!doctype html>
|
||||
<title>chrome.cookies</title>
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "chrome-management",
|
||||
"version": "1.0",
|
||||
"permissions": ["management"],
|
||||
"manifest_version": 3
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<!doctype html>
|
||||
<title>chrome.management</title>
|
||||
@@ -0,0 +1,8 @@
|
||||
/* global chrome */
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
navigator.clipboard.writeText(message).then(
|
||||
() => sendResponse({ written: true }),
|
||||
(error) => sendResponse({ written: false, error: String(error) })
|
||||
);
|
||||
return true;
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "clipboard-service-worker",
|
||||
"version": "1.0",
|
||||
"permissions": ["clipboardWrite"],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"manifest_version": 3
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<!doctype html>
|
||||
<title>clipboard</title>
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "css-localization",
|
||||
"version": "1.0",
|
||||
"manifest_version": 3
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<!doctype html>
|
||||
<title>css-localization</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<body></body>
|
||||
@@ -0,0 +1,3 @@
|
||||
body::after {
|
||||
content: '__MSG_@@extension_id__';
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
document.documentElement.dataset.persistentState = 'true';
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "persistent-state",
|
||||
"version": "1.0",
|
||||
"permissions": ["alarms", "scripting"],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"manifest_version": 3
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<!doctype html>
|
||||
<title>persistent-state</title>
|
||||
@@ -0,0 +1,43 @@
|
||||
/* global chrome */
|
||||
const seen = [];
|
||||
const waiting = [];
|
||||
|
||||
chrome.webRequest.onBeforeRequest.addListener(
|
||||
(details) => {
|
||||
seen.push({ url: details.url, tabId: details.tabId });
|
||||
for (const check of waiting.splice(0)) check();
|
||||
},
|
||||
{ urls: ['<all_urls>'] }
|
||||
);
|
||||
|
||||
function waitForRequest(marker) {
|
||||
return new Promise((resolve) => {
|
||||
const check = () => {
|
||||
const request = seen.find((r) => r.url.includes(marker));
|
||||
if (request) resolve(request);
|
||||
else waiting.push(check);
|
||||
};
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.type === 'wait-for-request') {
|
||||
waitForRequest(message.marker).then(sendResponse);
|
||||
} else if (message.type === 'block-in-tab') {
|
||||
chrome.declarativeNetRequest
|
||||
.updateSessionRules({
|
||||
addRules: [
|
||||
{
|
||||
id: 1,
|
||||
condition: { urlFilter: message.marker, tabIds: [message.tabId] },
|
||||
action: { type: 'block' }
|
||||
}
|
||||
]
|
||||
})
|
||||
.then(() => sendResponse(true));
|
||||
} else {
|
||||
sendResponse(null);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "request-tab-id",
|
||||
"version": "1.0",
|
||||
"permissions": ["webRequest", "declarativeNetRequest"],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"manifest_version": 3
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<!doctype html>
|
||||
<title>request-tab-id</title>
|
||||
@@ -0,0 +1,2 @@
|
||||
<!doctype html>
|
||||
<title>frame</title>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "sandboxed-pages",
|
||||
"version": "1.0",
|
||||
"sandbox": {
|
||||
"pages": ["sandboxed.html"]
|
||||
},
|
||||
"manifest_version": 3
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<!doctype html>
|
||||
<title>page</title>
|
||||
<iframe id="sandboxed" src="sandboxed.html"></iframe>
|
||||
<iframe id="inherited" sandbox="allow-scripts" src="frame.html"></iframe>
|
||||
@@ -0,0 +1,2 @@
|
||||
<!doctype html>
|
||||
<title>sandboxed</title>
|
||||
Reference in new issue
Block a user