diff --git a/docs/api/structures/custom-scheme.md b/docs/api/structures/custom-scheme.md index 3476ede7a4..f5b2f70921 100644 --- a/docs/api/structures/custom-scheme.md +++ b/docs/api/structures/custom-scheme.md @@ -11,3 +11,5 @@ * `stream` boolean (optional) - Default false. * `codeCache` boolean (optional) - Enable V8 code cache for the scheme, only works when `standard` is also set to true. Default false. + * `allowExtensions` boolean (optional) - Allow Chrome extensions to be used + on pages served over this protocol. Default false. diff --git a/patches/chromium/.patches b/patches/chromium/.patches index 36ce8ca65c..e76874ca83 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -149,3 +149,4 @@ fix_restore_sdk_inputs_cross-toolchain_deps_for_macos.patch fix_use_fresh_lazynow_for_onendworkitemimpl_after_didruntask.patch fix_pulseaudio_stream_and_icon_names.patch fix_fire_menu_popup_start_for_dynamically_created_aria_menus.patch +feat_allow_enabling_extensions_on_custom_protocols.patch diff --git a/patches/chromium/feat_allow_enabling_extensions_on_custom_protocols.patch b/patches/chromium/feat_allow_enabling_extensions_on_custom_protocols.patch new file mode 100644 index 0000000000..3e7699d481 --- /dev/null +++ b/patches/chromium/feat_allow_enabling_extensions_on_custom_protocols.patch @@ -0,0 +1,164 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Niklas Wenzel +Date: Wed, 25 Feb 2026 16:24:03 +0100 +Subject: feat: allow enabling extensions on custom protocols + +This allows us to use Chrome extensions on custom protocols. + +The patch can't really be upstreamed, unfortunately, because there are +other URLPattern functions that we don't patch that Chrome needs. + +Patching those properly would require replacing the bitmask logic in +URLPattern with a more flexible solution. This would be a larger effort +and Chromium might reject it for performance reasons. + +See: https://source.chromium.org/chromium/chromium/src/+/main:extensions/common/url_pattern.h;l=53-74;drc=50dbcddad2f8e36ddfcec21d4551f389df425c37 + +This patch makes it work in the context of Electron. + +diff --git a/extensions/browser/api/content_settings/content_settings_helpers.cc b/extensions/browser/api/content_settings/content_settings_helpers.cc +index ea484a282d820da78e8dc1db27ad0ba6e070ac2c..a0e361cf2d2960de4f429a9d37459e26614a17c4 100644 +--- a/extensions/browser/api/content_settings/content_settings_helpers.cc ++++ b/extensions/browser/api/content_settings/content_settings_helpers.cc +@@ -37,7 +37,7 @@ ContentSettingsPattern ParseExtensionPattern(std::string_view pattern_str, + std::string* error) { + const int kAllowedSchemes = + URLPattern::SCHEME_HTTP | URLPattern::SCHEME_HTTPS | +- URLPattern::SCHEME_FILE; ++ URLPattern::SCHEME_FILE | URLPattern::SCHEME_ELECTRON_CUSTOM_PROTOCOLS; + URLPattern url_pattern(kAllowedSchemes); + URLPattern::ParseResult result = url_pattern.Parse(pattern_str); + if (result != URLPattern::ParseResult::kSuccess) { +diff --git a/extensions/browser/api/web_request/extension_web_request_event_router.h b/extensions/browser/api/web_request/extension_web_request_event_router.h +index 57ed3cf54b2921df09ad84906b3da7527c6080bb..ffe3a0894c612adaa429a783827c85038d959a95 100644 +--- a/extensions/browser/api/web_request/extension_web_request_event_router.h ++++ b/extensions/browser/api/web_request/extension_web_request_event_router.h +@@ -53,7 +53,8 @@ inline constexpr int kWebRequestFilterValidSchemes = + URLPattern::SCHEME_HTTP | URLPattern::SCHEME_HTTPS | + URLPattern::SCHEME_FTP | URLPattern::SCHEME_FILE | + URLPattern::SCHEME_EXTENSION | URLPattern::SCHEME_WS | +- URLPattern::SCHEME_WSS | URLPattern::SCHEME_UUID_IN_PACKAGE; ++ URLPattern::SCHEME_WSS | URLPattern::SCHEME_UUID_IN_PACKAGE | ++ URLPattern::SCHEME_ELECTRON_CUSTOM_PROTOCOLS; + + class WebRequestEventRouter : public KeyedService { + public: +diff --git a/extensions/common/extension.cc b/extensions/common/extension.cc +index 0e0152871689c51d4e00f39f6ad607da90e6c9be..f365d0582d03a1432159a7ee6c8fde2de4c79634 100644 +--- a/extensions/common/extension.cc ++++ b/extensions/common/extension.cc +@@ -220,7 +220,8 @@ const int Extension::kValidHostPermissionSchemes = + URLPattern::SCHEME_CHROMEUI | URLPattern::SCHEME_HTTP | + URLPattern::SCHEME_HTTPS | URLPattern::SCHEME_FILE | + URLPattern::SCHEME_FTP | URLPattern::SCHEME_WS | URLPattern::SCHEME_WSS | +- URLPattern::SCHEME_UUID_IN_PACKAGE; ++ URLPattern::SCHEME_UUID_IN_PACKAGE | ++ URLPattern::SCHEME_ELECTRON_CUSTOM_PROTOCOLS; + + // + // Extension +diff --git a/extensions/common/url_pattern.cc b/extensions/common/url_pattern.cc +index d4328ca22fdeefd3dca88bfe959dfb849705b109..ba24e788d4a2e467d24f6369e2d93ea3b4a0c9d7 100644 +--- a/extensions/common/url_pattern.cc ++++ b/extensions/common/url_pattern.cc +@@ -140,6 +140,11 @@ bool URLPattern::IsValidSchemeForExtensions(std::string_view scheme) { + return true; + } + } ++ for (auto& extension_scheme : url::GetExtensionSchemes()) { ++ if (scheme == extension_scheme) { ++ return true; ++ } ++ } + return false; + } + +@@ -401,6 +406,14 @@ bool URLPattern::IsValidScheme(std::string_view scheme) const { + } + } + ++ if (valid_schemes_ & URLPattern::SCHEME_ELECTRON_CUSTOM_PROTOCOLS) { ++ for (auto& extension_scheme : url::GetExtensionSchemes()) { ++ if (scheme == extension_scheme) { ++ return true; ++ } ++ } ++ } ++ + return false; + } + +diff --git a/extensions/common/url_pattern.h b/extensions/common/url_pattern.h +index 4d09251b0160644d86682ad3db7c41b50f360e6f..8a626e14eff2d58d8218a7b0df820c6c0522b00f 100644 +--- a/extensions/common/url_pattern.h ++++ b/extensions/common/url_pattern.h +@@ -64,6 +64,9 @@ class URLPattern { + SCHEME_DATA = 1 << 9, + SCHEME_UUID_IN_PACKAGE = 1 << 10, + ++ // Represents the schemes returned by url::GetExtensionSchemes(). ++ SCHEME_ELECTRON_CUSTOM_PROTOCOLS = 1 << 11, ++ + // IMPORTANT! + // SCHEME_ALL will match every scheme, including chrome://, chrome- + // extension://, about:, etc. Because this has lots of security +diff --git a/extensions/common/user_script.cc b/extensions/common/user_script.cc +index f680ef4d31d580a285abe51387e3df043d4458f1..afde40d56d7874aa04ea2b1d881e5cab79fd7661 100644 +--- a/extensions/common/user_script.cc ++++ b/extensions/common/user_script.cc +@@ -69,7 +69,8 @@ enum { + kValidUserScriptSchemes = URLPattern::SCHEME_CHROMEUI | + URLPattern::SCHEME_HTTP | URLPattern::SCHEME_HTTPS | + URLPattern::SCHEME_FILE | URLPattern::SCHEME_FTP | +- URLPattern::SCHEME_UUID_IN_PACKAGE ++ URLPattern::SCHEME_UUID_IN_PACKAGE | ++ URLPattern::SCHEME_ELECTRON_CUSTOM_PROTOCOLS + }; + + // static +diff --git a/url/url_util.cc b/url/url_util.cc +index 1ee0feaad9be437914b101195e862965fcaccff4..7412af58409285fbe9b426c5b2bb8510d362091c 100644 +--- a/url/url_util.cc ++++ b/url/url_util.cc +@@ -135,6 +135,9 @@ struct SchemeRegistry { + // Embedder schemes that have V8 code cache enabled in js and wasm scripts. + std::vector code_cache_schemes = {}; + ++ // Embedder schemes on which Chrome extensions can be used. ++ std::vector extension_schemes = {}; ++ + // Schemes with a predefined default custom handler. + std::vector predefined_handler_schemes; + +@@ -679,6 +682,15 @@ const std::vector& GetCodeCacheSchemes() { + return GetSchemeRegistry().code_cache_schemes; + } + ++void AddExtensionScheme(std::string_view new_scheme) { ++ DoAddScheme(new_scheme, ++ &GetSchemeRegistryWithoutLocking()->extension_schemes); ++} ++ ++const std::vector& GetExtensionSchemes() { ++ return GetSchemeRegistry().extension_schemes; ++} ++ + void AddPredefinedHandlerScheme(std::string_view new_scheme, + std::string_view handler) { + DoAddSchemeWithHandler( +diff --git a/url/url_util.h b/url/url_util.h +index 6906dd1c903209f3bb6d9ca346e845f1dfeef050..f1928796d5c8a01a51ac9237394bdf6236920998 100644 +--- a/url/url_util.h ++++ b/url/url_util.h +@@ -124,6 +124,11 @@ COMPONENT_EXPORT(URL) const std::vector& GetEmptyDocumentSchemes(); + COMPONENT_EXPORT(URL) void AddCodeCacheScheme(std::string_view new_scheme); + COMPONENT_EXPORT(URL) const std::vector& GetCodeCacheSchemes(); + ++// Adds an application-defined scheme to the list of schemes on which Chrome ++// extensions can be used. ++COMPONENT_EXPORT(URL) void AddExtensionScheme(std::string_view new_scheme); ++COMPONENT_EXPORT(URL) const std::vector& GetExtensionSchemes(); ++ + // Adds a scheme with a predefined default handler. + // + // This pair of strings must be normalized protocol handler parameters as diff --git a/patches/devtools_frontend/.patches b/patches/devtools_frontend/.patches index 3cf8d9fc4a..32cd70838d 100644 --- a/patches/devtools_frontend/.patches +++ b/patches/devtools_frontend/.patches @@ -1 +1,2 @@ chore_expose_ui_to_allow_electron_to_set_dock_side.patch +feat_allow_enabling_extension_panels_on_custom_protocols.patch diff --git a/patches/devtools_frontend/feat_allow_enabling_extension_panels_on_custom_protocols.patch b/patches/devtools_frontend/feat_allow_enabling_extension_panels_on_custom_protocols.patch new file mode 100644 index 0000000000..6fca7fb4a1 --- /dev/null +++ b/patches/devtools_frontend/feat_allow_enabling_extension_panels_on_custom_protocols.patch @@ -0,0 +1,42 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Niklas Wenzel +Date: Wed, 25 Feb 2026 16:23:07 +0100 +Subject: feat: allow enabling extension panels on custom protocols + +This allows us to show Chrome extension panels on pages served over +custom protocols. + +diff --git a/front_end/core/root/Runtime.ts b/front_end/core/root/Runtime.ts +index 4f91e0d7b1d289f5eaaaf7c4e174679881fd9a54..cfdfdfe03edfdf89374763f5b3086f0eb15fa72c 100644 +--- a/front_end/core/root/Runtime.ts ++++ b/front_end/core/root/Runtime.ts +@@ -645,6 +645,7 @@ export type HostConfig = Platform.TypeScriptUtilities.RecursivePartial<{ + * or guest mode, rather than a "normal" profile. + */ + isOffTheRecord: boolean, ++ devToolsExtensionSchemes: string[], + devToolsEnableOriginBoundCookies: HostConfigEnableOriginBoundCookies, + devToolsAnimationStylesInStylesTab: HostConfigAnimationStylesInStylesTab, + devToolsJpegXlImageFormat: HostConfigJpegXlImageFormat, +diff --git a/front_end/panels/common/ExtensionServer.ts b/front_end/panels/common/ExtensionServer.ts +index b33686022049d7dda75f2f5c0877573c334635fa..70adb4ad2d51995277d7a054b6af17c30af39a3f 100644 +--- a/front_end/panels/common/ExtensionServer.ts ++++ b/front_end/panels/common/ExtensionServer.ts +@@ -12,6 +12,7 @@ import * as Host from '../../core/host/host.js'; + import * as i18n from '../../core/i18n/i18n.js'; + import * as Platform from '../../core/platform/platform.js'; + import * as _ProtocolClient from '../../core/protocol_client/protocol_client.js'; // eslint-disable-line @typescript-eslint/no-unused-vars ++import * as Root from '../../core/root/root.js'; + import * as SDK from '../../core/sdk/sdk.js'; + import type * as Protocol from '../../generated/protocol.js'; + import * as Bindings from '../../models/bindings/bindings.js'; +@@ -1622,7 +1623,8 @@ export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper { opt.Get("corsEnabled", &(out->options.corsEnabled)); opt.Get("stream", &(out->options.stream)); opt.Get("codeCache", &(out->options.codeCache)); + opt.Get("allowExtensions", &(out->options.allowExtensions)); } return true; } @@ -124,7 +126,7 @@ void RegisterSchemesAsPrivileged(gin_helper::ErrorThrower thrower, } std::vector secure_schemes, cspbypassing_schemes, fetch_schemes, - service_worker_schemes, cors_schemes; + service_worker_schemes, cors_schemes, extension_schemes; for (const auto& custom_scheme : custom_schemes) { // Register scheme to privileged list (https, wss, data, chrome-extension) if (custom_scheme.options.standard) { @@ -160,6 +162,10 @@ void RegisterSchemesAsPrivileged(gin_helper::ErrorThrower thrower, GetCodeCacheSchemes().push_back(custom_scheme.scheme); url::AddCodeCacheScheme(custom_scheme.scheme.c_str()); } + if (custom_scheme.options.allowExtensions) { + extension_schemes.push_back(custom_scheme.scheme); + url::AddExtensionScheme(custom_scheme.scheme.c_str()); + } } const auto AppendSchemesToCmdLine = [](const std::string_view switch_name, @@ -179,6 +185,8 @@ void RegisterSchemesAsPrivileged(gin_helper::ErrorThrower thrower, AppendSchemesToCmdLine(electron::switches::kFetchSchemes, fetch_schemes); AppendSchemesToCmdLine(electron::switches::kServiceWorkerSchemes, service_worker_schemes); + AppendSchemesToCmdLine(electron::switches::kExtensionSchemes, + extension_schemes); AppendSchemesToCmdLine(electron::switches::kStandardSchemes, GetStandardSchemes()); AppendSchemesToCmdLine(electron::switches::kStreamingSchemes, diff --git a/shell/browser/electron_browser_client.cc b/shell/browser/electron_browser_client.cc index 88aa9d7d95..b405c08c68 100644 --- a/shell/browser/electron_browser_client.cc +++ b/shell/browser/electron_browser_client.cc @@ -558,7 +558,7 @@ void ElectronBrowserClient::AppendExtraCommandLineSwitches( if (process_type == ::switches::kUtilityProcess || process_type == ::switches::kRendererProcess) { // Copy following switches to child process. - static constexpr std::array kCommonSwitchNames = { + static constexpr std::array kCommonSwitchNames = { switches::kStandardSchemes.c_str(), switches::kEnableSandbox.c_str(), switches::kSecureSchemes.c_str(), @@ -568,7 +568,8 @@ void ElectronBrowserClient::AppendExtraCommandLineSwitches( switches::kServiceWorkerSchemes.c_str(), switches::kStreamingSchemes.c_str(), switches::kNoStdioInit.c_str(), - switches::kCodeCacheSchemes.c_str()}; + switches::kCodeCacheSchemes.c_str(), + switches::kExtensionSchemes.c_str()}; command_line->CopySwitchesFrom(*base::CommandLine::ForCurrentProcess(), kCommonSwitchNames); if (process_type == ::switches::kUtilityProcess || diff --git a/shell/browser/ui/inspectable_web_contents.cc b/shell/browser/ui/inspectable_web_contents.cc index b0181566cc..a6f5f55339 100644 --- a/shell/browser/ui/inspectable_web_contents.cc +++ b/shell/browser/ui/inspectable_web_contents.cc @@ -58,6 +58,7 @@ #include "third_party/blink/public/common/page/page_zoom.h" #include "ui/display/display.h" #include "ui/display/screen.h" +#include "url/url_util.h" #include "v8/include/v8.h" #if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS) @@ -869,6 +870,13 @@ void InspectableWebContents::GetSyncInformation(DispatchCallback callback) { void InspectableWebContents::GetHostConfig(DispatchCallback callback) { base::DictValue response_dict; + + base::ListValue extension_schemes; + for (const std::string& scheme : url::GetExtensionSchemes()) + extension_schemes.Append(scheme + ":"); + response_dict.Set("devToolsExtensionSchemes", + base::Value(std::move(extension_schemes))); + base::Value response = base::Value(std::move(response_dict)); std::move(callback).Run(&response); } diff --git a/shell/common/options_switches.h b/shell/common/options_switches.h index a381c727d5..d362b26c4f 100644 --- a/shell/common/options_switches.h +++ b/shell/common/options_switches.h @@ -270,6 +270,9 @@ inline constexpr base::cstring_view kStreamingSchemes = "streaming-schemes"; // Register schemes as supporting V8 code cache. inline constexpr base::cstring_view kCodeCacheSchemes = "code-cache-schemes"; +// Register schemes as supporting extensions. +inline constexpr base::cstring_view kExtensionSchemes = "extension-schemes"; + // The browser process app model ID inline constexpr base::cstring_view kAppUserModelId = "app-user-model-id"; diff --git a/shell/renderer/renderer_client_base.cc b/shell/renderer/renderer_client_base.cc index 26341a9237..a5d9fe48e8 100644 --- a/shell/renderer/renderer_client_base.cc +++ b/shell/renderer/renderer_client_base.cc @@ -162,6 +162,11 @@ RendererClientBase::RendererClientBase() { ParseSchemesCLISwitch(command_line, switches::kSecureSchemes); for (const std::string& scheme : secure_schemes_list) url::AddSecureScheme(scheme.data()); + // Parse --extension-schemes=scheme1,scheme2 + std::vector extension_schemes_list = + ParseSchemesCLISwitch(command_line, switches::kExtensionSchemes); + for (const std::string& scheme : extension_schemes_list) + url::AddExtensionScheme(scheme.c_str()); // We rely on the unique process host id which is notified to the // renderer process via command line switch from the content layer, // if this switch is removed from the content layer for some reason, diff --git a/spec/api-protocol-spec.ts b/spec/api-protocol-spec.ts index e5f7e968ae..9702bb5be3 100644 --- a/spec/api-protocol-spec.ts +++ b/spec/api-protocol-spec.ts @@ -1123,6 +1123,8 @@ describe('protocol module', () => { }); }); + // protocol.registerSchemesAsPrivileged allowExtensions tests are in extensions-spec.ts. + describe('handle', () => { afterEach(closeAllWindows); diff --git a/spec/api-web-request-spec.ts b/spec/api-web-request-spec.ts index 6160129cfa..eb41873fed 100644 --- a/spec/api-web-request-spec.ts +++ b/spec/api-web-request-spec.ts @@ -13,7 +13,7 @@ import * as qs from 'node:querystring'; import { ReadableStream } from 'node:stream/web'; import * as url from 'node:url'; -import { listen, defer } from './lib/spec-helpers'; +import { listen, defer, startRemoteControlApp } from './lib/spec-helpers'; const fixturesPath = path.resolve(__dirname, 'fixtures'); @@ -173,6 +173,107 @@ describe('webRequest module', () => { expect((await ajax(`${defaultURL}exclude/test`)).data).to.equal('/exclude/test'); }); + // allowExtensions changes how URLPattern works, so we add extra tests that ensure that filters still work as expected. + describe('with protocol.registerSchemesAsPrivileged() and allowExtensions', () => { + it('will filter http URLs properly', async () => { + const rc = await startRemoteControlApp(['--boot-eval="protocol.registerSchemesAsPrivileged([{ scheme: \'custom\', privileges: { allowExtensions: true } }]);"']); + const called = await rc.remotely(async (url: string) => { + const { BrowserWindow, session } = require('electron/main'); + + let called = false; + + session.defaultSession.webRequest.onBeforeRequest({ urls: ['http://*/*'] }, (_: Electron.OnBeforeRequestListenerDetails, callback: (response: Electron.CallbackResponse) => void) => { + called = true; + callback({ cancel: true }); + }); + + const w = new BrowserWindow({ show: false }); + await w.loadURL('about:blank'); + + await w.webContents.executeJavaScript(`fetch("${url}").then(() => true, () => false)`); + + global.setTimeout(() => require('electron').app.quit()); + + return called; + }, defaultURL); + expect(called).to.be.true(); + }); + + it('will not call webRequest.onBeforeRequest for non-custom protocol URLs that do not match the filter', async () => { + const rc = await startRemoteControlApp(['--boot-eval="protocol.registerSchemesAsPrivileged([{ scheme: \'custom\', privileges: { allowExtensions: true } }]);"']); + const called = await rc.remotely(async (url: string) => { + const { BrowserWindow, session } = require('electron/main'); + + let called = false; + + session.defaultSession.webRequest.onBeforeRequest({ urls: ['https://*/*'] }, (_: Electron.OnBeforeRequestListenerDetails, callback: (response: Electron.CallbackResponse) => void) => { + called = true; + callback({ cancel: true }); + }); + + const w = new BrowserWindow({ show: false }); + await w.loadURL('about:blank'); + + await w.webContents.executeJavaScript(`fetch("${url}").then(() => true, () => false)`); + + global.setTimeout(() => require('electron').app.quit()); + + return called; + }, defaultURL); + expect(called).to.be.false(); + }); + + it('will call webRequest.onBeforeRequest for custom protocol URLs with filter', async () => { + const rc = await startRemoteControlApp(['--boot-eval="protocol.registerSchemesAsPrivileged([{ scheme: \'custom\', privileges: { allowExtensions: true } }]);"']); + const { called, responseText } = await rc.remotely(async () => { + const { net, protocol, session } = require('electron/main'); + + protocol.handle('custom', () => new Response('success')); + + let called = false; + + session.defaultSession.webRequest.onBeforeRequest({ urls: [''] }, (_: Electron.OnBeforeRequestListenerDetails, callback: (response: Electron.CallbackResponse) => void) => { + called = true; + callback({ cancel: false }); + }); + + const response = await net.fetch('custom://app/test'); + const responseText = await response.text(); + + global.setTimeout(() => require('electron').app.quit()); + + return { called, responseText }; + }); + expect(responseText).to.equal('success'); + expect(called).to.be.true(); + }); + + it('will not call webRequest.onBeforeRequest for custom protocol URLs that do not match the filter', async () => { + const rc = await startRemoteControlApp(['--boot-eval="protocol.registerSchemesAsPrivileged([{ scheme: \'custom\', privileges: { allowExtensions: true } }]);"']); + const { called, responseText } = await rc.remotely(async () => { + const { net, protocol, session } = require('electron/main'); + + protocol.handle('custom', () => new Response('success')); + + let called = false; + + session.defaultSession.webRequest.onBeforeRequest({ urls: ['http://*/*'] }, (_: Electron.OnBeforeRequestListenerDetails, callback: (response: Electron.CallbackResponse) => void) => { + called = true; + callback({ cancel: false }); + }); + + const response = await net.fetch('custom://app/test'); + const responseText = await response.text(); + + global.setTimeout(() => require('electron').app.quit()); + + return { called, responseText }; + }); + expect(responseText).to.equal('success'); + expect(called).to.be.false(); + }); + }); + it('receives details object', async () => { ses.webRequest.onBeforeRequest((details, callback) => { expect(details.id).to.be.a('number'); diff --git a/spec/extensions-spec.ts b/spec/extensions-spec.ts index a8962ed631..08d2a5a25b 100644 --- a/spec/extensions-spec.ts +++ b/spec/extensions-spec.ts @@ -3,6 +3,7 @@ import { app, session, webFrameMain, BrowserWindow, ipcMain, WebContents, Extens import { expect } from 'chai'; import * as WebSocket from 'ws'; +import { spawn } from 'node:child_process'; import { once } from 'node:events'; import * as fs from 'node:fs/promises'; import * as http from 'node:http'; @@ -1338,4 +1339,26 @@ describe('chrome extensions', () => { }); }); }); + + describe('custom protocol', () => { + async function runFixture (name: string) { + const appProcess = spawn(process.execPath, [(path.join(fixtures, 'extensions', name, 'main.js'))]); + + let output = ''; + appProcess.stdout.on('data', (data) => { output += data; }); + await once(appProcess.stdout, 'end'); + + return output.trim(); + }; + + it('loads DevTools extensions on custom protocols with allowExtensions privileges and runs content and background scripts', async () => { + const output = await runFixture('custom-protocol'); + expect(output).to.equal('Title: MESSAGE RECEIVED'); + }); + + it('loads DevTools panels on custom protocols with allowExtensions privileges', async () => { + const output = await runFixture('custom-protocol-panel'); + expect(output).to.equal('ELECTRON TEST PANEL created'); + }); + }); }); diff --git a/spec/fixtures/extensions/custom-protocol-panel/extension/devtools.html b/spec/fixtures/extensions/custom-protocol-panel/extension/devtools.html new file mode 100644 index 0000000000..c70108940c --- /dev/null +++ b/spec/fixtures/extensions/custom-protocol-panel/extension/devtools.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/spec/fixtures/extensions/custom-protocol-panel/extension/devtools.js b/spec/fixtures/extensions/custom-protocol-panel/extension/devtools.js new file mode 100644 index 0000000000..8083941686 --- /dev/null +++ b/spec/fixtures/extensions/custom-protocol-panel/extension/devtools.js @@ -0,0 +1,4 @@ +/* global chrome */ +chrome.devtools.panels.create('ELECTRON TEST PANEL', '', 'panel.html'); + +console.log('ELECTRON TEST PANEL created'); diff --git a/spec/fixtures/extensions/custom-protocol-panel/extension/manifest.json b/spec/fixtures/extensions/custom-protocol-panel/extension/manifest.json new file mode 100644 index 0000000000..db8f32aab9 --- /dev/null +++ b/spec/fixtures/extensions/custom-protocol-panel/extension/manifest.json @@ -0,0 +1,6 @@ +{ + "name": "custom-protocol-panel", + "version": "1.0", + "devtools_page": "devtools.html", + "manifest_version": 3 +} diff --git a/spec/fixtures/extensions/custom-protocol-panel/extension/panel.html b/spec/fixtures/extensions/custom-protocol-panel/extension/panel.html new file mode 100644 index 0000000000..6c34a03ff9 --- /dev/null +++ b/spec/fixtures/extensions/custom-protocol-panel/extension/panel.html @@ -0,0 +1,4 @@ + + + DevTools panel + diff --git a/spec/fixtures/extensions/custom-protocol-panel/main.js b/spec/fixtures/extensions/custom-protocol-panel/main.js new file mode 100644 index 0000000000..d7ccfa30bc --- /dev/null +++ b/spec/fixtures/extensions/custom-protocol-panel/main.js @@ -0,0 +1,41 @@ +const { app, BrowserWindow, protocol, session } = require('electron/main'); + +const { once } = require('node:events'); +const path = require('node:path'); + +const html = '

EMPTY PAGE

'; +const scheme = 'custom'; + +protocol.registerSchemesAsPrivileged([ + { + scheme, + privileges: { + standard: true, + allowExtensions: true + } + } +]); + +app.whenReady().then(async () => { + const ses = session.defaultSession; + + ses.protocol.handle(scheme, () => new Response(html, { + headers: { 'Content-Type': 'text/html' } + })); + + await ses.extensions.loadExtension(path.join(__dirname, 'extension')); + + const win = new BrowserWindow(); + + win.webContents.openDevTools(); + await once(win.webContents, 'devtools-opened'); + + win.devToolsWebContents.on('console-message', ({ message }) => { + if (message === 'ELECTRON TEST PANEL created') { + console.log(message); + app.quit(); + } + }); + + await win.loadURL(`${scheme}://app/`); +}); diff --git a/spec/fixtures/extensions/custom-protocol/extension/background.js b/spec/fixtures/extensions/custom-protocol/extension/background.js new file mode 100644 index 0000000000..11abd3d99c --- /dev/null +++ b/spec/fixtures/extensions/custom-protocol/extension/background.js @@ -0,0 +1,7 @@ +/* global chrome */ +chrome.runtime.onMessage.addListener((_message, sender, reply) => { + reply({ + text: 'MESSAGE RECEIVED', + senderTabId: sender.tab && sender.tab.id + }); +}); diff --git a/spec/fixtures/extensions/custom-protocol/extension/content_script.js b/spec/fixtures/extensions/custom-protocol/extension/content_script.js new file mode 100644 index 0000000000..60098e6a68 --- /dev/null +++ b/spec/fixtures/extensions/custom-protocol/extension/content_script.js @@ -0,0 +1,5 @@ +/* global chrome */ +chrome.runtime.sendMessage({ text: 'hello from content script' }, (response) => { + if (!response || !response.text) return; + document.title = response.text; +}); diff --git a/spec/fixtures/extensions/custom-protocol/extension/manifest.json b/spec/fixtures/extensions/custom-protocol/extension/manifest.json new file mode 100644 index 0000000000..25447fec07 --- /dev/null +++ b/spec/fixtures/extensions/custom-protocol/extension/manifest.json @@ -0,0 +1,15 @@ +{ + "name": "custom-protocol", + "version": "1.0", + "background": { + "service_worker": "background.js" + }, + "content_scripts": [ + { + "matches": [""], + "js": ["content_script.js"], + "run_at": "document_start" + } + ], + "manifest_version": 3 +} diff --git a/spec/fixtures/extensions/custom-protocol/main.js b/spec/fixtures/extensions/custom-protocol/main.js new file mode 100644 index 0000000000..bfdef6c21e --- /dev/null +++ b/spec/fixtures/extensions/custom-protocol/main.js @@ -0,0 +1,35 @@ +const { app, BrowserWindow, protocol, session } = require('electron/main'); + +const path = require('node:path'); + +const html = '

EMPTY PAGE

'; +const scheme = 'example'; + +protocol.registerSchemesAsPrivileged([ + { + scheme, + privileges: { + standard: true, + allowExtensions: true + } + } +]); + +app.whenReady().then(async () => { + const ses = session.defaultSession; + + ses.protocol.handle(scheme, () => new Response(html, { + headers: { 'Content-Type': 'text/html' } + })); + + await ses.extensions.loadExtension(path.join(__dirname, 'extension')); + + const win = new BrowserWindow(); + + win.on('page-title-updated', (_event, title) => { + console.log(`Title: ${title}`); + app.quit(); + }); + + await win.loadURL(`${scheme}://app/`); +});