From 9d696ceffe9600d73cc395dff4bffc771541397b Mon Sep 17 00:00:00 2001 From: "trop[bot]" <37223003+trop[bot]@users.noreply.github.com> Date: Fri, 31 Jan 2025 09:46:17 -0500 Subject: [PATCH] feat: redesign preload APIs (#45329) * feat: redesign preload APIs Co-authored-by: Samuel Maddock * docs: remove service-worker mentions for now Co-authored-by: Samuel Maddock * fix lint Co-authored-by: Samuel Maddock * remove service-worker ipc code Co-authored-by: Samuel Maddock * add filename Co-authored-by: Samuel Maddock * fix: web preferences preload not included Co-authored-by: Samuel Maddock * fix: missing common init Co-authored-by: Samuel Maddock * fix: preload bundle script error Co-authored-by: Samuel Maddock --------- Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com> Co-authored-by: Samuel Maddock --- docs/api/session.md | 29 ++++- .../structures/preload-script-registration.md | 6 + docs/api/structures/preload-script.md | 6 + docs/breaking-changes.md | 19 +++ filenames.auto.gni | 4 + filenames.gni | 1 + lib/browser/api/session.ts | 26 +++++ lib/browser/rpc-server.ts | 42 +++++-- lib/sandboxed_renderer/init.ts | 110 +++--------------- lib/sandboxed_renderer/pre-init.ts | 30 +++++ lib/sandboxed_renderer/preload.ts | 101 ++++++++++++++++ shell/browser/api/electron_api_session.cc | 69 ++++++++++- shell/browser/api/electron_api_session.h | 8 +- .../browser/api/electron_api_web_contents.cc | 13 +-- shell/browser/api/electron_api_web_contents.h | 5 +- shell/browser/preload_script.h | 104 +++++++++++++++++ shell/browser/session_preferences.cc | 18 --- shell/browser/session_preferences.h | 10 +- typings/internal-electron.d.ts | 7 +- 19 files changed, 459 insertions(+), 149 deletions(-) create mode 100644 docs/api/structures/preload-script-registration.md create mode 100644 docs/api/structures/preload-script.md create mode 100644 lib/sandboxed_renderer/pre-init.ts create mode 100644 lib/sandboxed_renderer/preload.ts create mode 100644 shell/browser/preload_script.h diff --git a/docs/api/session.md b/docs/api/session.md index 41126bb92521..cc6a22e59b06 100644 --- a/docs/api/session.md +++ b/docs/api/session.md @@ -1330,18 +1330,43 @@ the initial state will be `interrupted`. The download will start only when the Returns `Promise` - resolves when the session’s HTTP authentication cache has been cleared. -#### `ses.setPreloads(preloads)` +#### `ses.setPreloads(preloads)` _Deprecated_ * `preloads` string[] - An array of absolute path to preload scripts Adds scripts that will be executed on ALL web contents that are associated with this session just before normal `preload` scripts run. -#### `ses.getPreloads()` +**Deprecated:** Use the new `ses.registerPreloadScript` API. + +#### `ses.getPreloads()` _Deprecated_ Returns `string[]` an array of paths to preload scripts that have been registered. +**Deprecated:** Use the new `ses.getPreloadScripts` API. This will only return preload script paths +for `frame` context types. + +#### `ses.registerPreloadScript(script)` + +* `script` [PreloadScriptRegistration](structures/preload-script-registration.md) - Preload script + +Registers preload script that will be executed in its associated context type in this session. For +`frame` contexts, this will run prior to any preload defined in the web preferences of a +WebContents. + +Returns `string` - The ID of the registered preload script. + +#### `ses.unregisterPreloadScript(id)` + +* `id` string - Preload script ID + +Unregisters script. + +#### `ses.getPreloadScripts()` + +Returns [`PreloadScript[]`](structures/preload-script.md): An array of paths to preload scripts that have been registered. + #### `ses.setCodeCachePath(path)` * `path` String - Absolute path to store the v8 generated JS code cache from the renderer. diff --git a/docs/api/structures/preload-script-registration.md b/docs/api/structures/preload-script-registration.md new file mode 100644 index 000000000000..011d034f0884 --- /dev/null +++ b/docs/api/structures/preload-script-registration.md @@ -0,0 +1,6 @@ +# PreloadScriptRegistration Object + +* `type` string - Context type where the preload script will be executed. + Possible values include `frame`. +* `id` string (optional) - Unique ID of preload script. Defaults to a random UUID. +* `filePath` string - Path of the script file. Must be an absolute path. diff --git a/docs/api/structures/preload-script.md b/docs/api/structures/preload-script.md new file mode 100644 index 000000000000..c75ead5217f9 --- /dev/null +++ b/docs/api/structures/preload-script.md @@ -0,0 +1,6 @@ +# PreloadScript Object + +* `type` string - Context type where the preload script will be executed. + Possible values include `frame`. +* `id` string - Unique ID of preload script. +* `filePath` string - Path of the script file. Must be an absolute path. diff --git a/docs/breaking-changes.md b/docs/breaking-changes.md index 7add55a43a96..82c11f63e3be 100644 --- a/docs/breaking-changes.md +++ b/docs/breaking-changes.md @@ -14,6 +14,25 @@ This document uses the following convention to categorize breaking changes: ## Planned Breaking API Changes (35.0) +### Deprecated: `setPreloads`, `getPreloads` on `Session` + +`registerPreloadScript`, `unregisterPreloadScript`, and `getPreloadScripts` are introduced as a +replacement for the deprecated methods. These new APIs allow third-party libraries to register +preload scripts without replacing existing scripts. Also, the new `type` option allows for +additional preload targets beyond `frame`. + +```ts +// Deprecated +session.setPreloads([path.join(__dirname, 'preload.js')]) + +// Replace with: +session.registerPreloadScript({ + type: 'frame', + id: 'app-preload', + filePath: path.join(__dirname, 'preload.js') +}) +``` + ### Deprecated: `level`, `message`, `line`, and `sourceId` arguments in `console-message` event on `WebContents` The `console-message` event on `WebContents` has been updated to provide details on the `Event` diff --git a/filenames.auto.gni b/filenames.auto.gni index d937bcd9e745..cfc4e6484b00 100644 --- a/filenames.auto.gni +++ b/filenames.auto.gni @@ -114,6 +114,8 @@ auto_filenames = { "docs/api/structures/permission-request.md", "docs/api/structures/point.md", "docs/api/structures/post-body.md", + "docs/api/structures/preload-script-registration.md", + "docs/api/structures/preload-script.md", "docs/api/structures/printer-info.md", "docs/api/structures/process-memory-info.md", "docs/api/structures/process-metric.md", @@ -183,6 +185,8 @@ auto_filenames = { "lib/sandboxed_renderer/api/exports/electron.ts", "lib/sandboxed_renderer/api/module-list.ts", "lib/sandboxed_renderer/init.ts", + "lib/sandboxed_renderer/pre-init.ts", + "lib/sandboxed_renderer/preload.ts", "package.json", "tsconfig.electron.json", "tsconfig.json", diff --git a/filenames.gni b/filenames.gni index 9a950d0e1955..8dd8b4d346e9 100644 --- a/filenames.gni +++ b/filenames.gni @@ -474,6 +474,7 @@ filenames = { "shell/browser/osr/osr_web_contents_view.h", "shell/browser/plugins/plugin_utils.cc", "shell/browser/plugins/plugin_utils.h", + "shell/browser/preload_script.h", "shell/browser/protocol_registry.cc", "shell/browser/protocol_registry.h", "shell/browser/relauncher.cc", diff --git a/lib/browser/api/session.ts b/lib/browser/api/session.ts index 169f1d1eeab5..0daf56291af3 100644 --- a/lib/browser/api/session.ts +++ b/lib/browser/api/session.ts @@ -1,4 +1,5 @@ import { fetchWithSession } from '@electron/internal/browser/api/net-fetch'; +import * as deprecate from '@electron/internal/common/deprecate'; import { net } from 'electron/main'; @@ -36,6 +37,31 @@ Session.prototype.setDisplayMediaRequestHandler = function (handler, opts) { }, opts); }; +const getPreloadsDeprecated = deprecate.warnOnce('session.getPreloads', 'session.getPreloadScripts'); +Session.prototype.getPreloads = function () { + getPreloadsDeprecated(); + return this.getPreloadScripts() + .filter((script) => script.type === 'frame') + .map((script) => script.filePath); +}; + +const setPreloadsDeprecated = deprecate.warnOnce('session.setPreloads', 'session.registerPreloadScript'); +Session.prototype.setPreloads = function (preloads) { + setPreloadsDeprecated(); + this.getPreloadScripts() + .filter((script) => script.type === 'frame') + .forEach((script) => { + this.unregisterPreloadScript(script.id); + }); + preloads.map(filePath => ({ + type: 'frame', + filePath, + _deprecated: true + }) as Electron.PreloadScriptRegistration).forEach(script => { + this.registerPreloadScript(script); + }); +}; + export default { fromPartition, fromPath, diff --git a/lib/browser/rpc-server.ts b/lib/browser/rpc-server.ts index 4eef7baa1f5d..b305d09f119b 100644 --- a/lib/browser/rpc-server.ts +++ b/lib/browser/rpc-server.ts @@ -5,6 +5,7 @@ import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages'; import { clipboard } from 'electron/common'; import * as fs from 'fs'; +import * as path from 'path'; // Implements window.close() ipcMainInternal.on(IPC_MESSAGES.BROWSER_WINDOW_CLOSE, function (event) { @@ -43,22 +44,40 @@ ipcMainUtils.handleSync(IPC_MESSAGES.BROWSER_CLIPBOARD_SYNC, function (event, me return (clipboard as any)[method](...args); }); -const getPreloadScript = async function (preloadPath: string) { - let preloadSrc = null; - let preloadError = null; +const getPreloadScriptsFromEvent = (event: ElectronInternal.IpcMainInternalEvent) => { + const session: Electron.Session = event.sender.session; + const preloadScripts = session.getPreloadScripts(); + const framePreloads = preloadScripts.filter(script => script.type === 'frame'); + + const webPrefPreload = event.sender._getPreloadScript(); + if (webPrefPreload) framePreloads.push(webPrefPreload); + + // TODO(samuelmaddock): Remove filter after Session.setPreloads is fully + // deprecated. The new API will prevent relative paths from being registered. + return framePreloads.filter(script => path.isAbsolute(script.filePath)); +}; + +const readPreloadScript = async function (script: Electron.PreloadScript): Promise { + let contents; + let error; try { - preloadSrc = await fs.promises.readFile(preloadPath, 'utf8'); - } catch (error) { - preloadError = error; + contents = await fs.promises.readFile(script.filePath, 'utf8'); + } catch (err) { + if (err instanceof Error) { + error = err; + } } - return { preloadPath, preloadSrc, preloadError }; + return { + ...script, + contents, + error + }; }; ipcMainUtils.handleSync(IPC_MESSAGES.BROWSER_SANDBOX_LOAD, async function (event) { - const preloadPaths = event.sender._getPreloadPaths(); - + const preloadScripts = getPreloadScriptsFromEvent(event); return { - preloadScripts: await Promise.all(preloadPaths.map(path => getPreloadScript(path))), + preloadScripts: await Promise.all(preloadScripts.map(readPreloadScript)), process: { arch: process.arch, platform: process.platform, @@ -71,7 +90,8 @@ ipcMainUtils.handleSync(IPC_MESSAGES.BROWSER_SANDBOX_LOAD, async function (event }); ipcMainUtils.handleSync(IPC_MESSAGES.BROWSER_NONSANDBOX_LOAD, function (event) { - return { preloadPaths: event.sender._getPreloadPaths() }; + const preloadScripts = getPreloadScriptsFromEvent(event); + return { preloadPaths: preloadScripts.map(script => script.filePath) }; }); ipcMainInternal.on(IPC_MESSAGES.BROWSER_PRELOAD_ERROR, function (event, preloadPath: string, error: Error) { diff --git a/lib/sandboxed_renderer/init.ts b/lib/sandboxed_renderer/init.ts index 6762945a628e..6bb1374a3905 100644 --- a/lib/sandboxed_renderer/init.ts +++ b/lib/sandboxed_renderer/init.ts @@ -1,6 +1,7 @@ +import '@electron/internal/sandboxed_renderer/pre-init'; import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages'; -import type * as ipcRendererInternalModule from '@electron/internal/renderer/ipc-renderer-internal'; import type * as ipcRendererUtilsModule from '@electron/internal/renderer/ipc-renderer-internal-utils'; +import { createPreloadProcessObject, executeSandboxedPreloadScripts } from '@electron/internal/sandboxed_renderer/preload'; import * as events from 'events'; import { setImmediate, clearImmediate } from 'timers'; @@ -11,35 +12,14 @@ declare const binding: { createPreloadScript: (src: string) => Function }; -const { EventEmitter } = events; - -process._linkedBinding = binding.get; - const v8Util = process._linkedBinding('electron_common_v8_util'); -// Expose Buffer shim as a hidden value. This is used by C++ code to -// deserialize Buffer instances sent from browser process. -v8Util.setHiddenValue(global, 'Buffer', Buffer); -// The process object created by webpack is not an event emitter, fix it so -// the API is more compatible with non-sandboxed renderers. -for (const prop of Object.keys(EventEmitter.prototype) as (keyof typeof process)[]) { - if (Object.hasOwn(process, prop)) { - delete process[prop]; - } -} -Object.setPrototypeOf(process, EventEmitter.prototype); - -const { ipcRendererInternal } = require('@electron/internal/renderer/ipc-renderer-internal') as typeof ipcRendererInternalModule; const ipcRendererUtils = require('@electron/internal/renderer/ipc-renderer-internal-utils') as typeof ipcRendererUtilsModule; const { preloadScripts, process: processProps } = ipcRendererUtils.invokeSync<{ - preloadScripts: { - preloadPath: string; - preloadSrc: string | null; - preloadError: null | Error; - }[]; + preloadScripts: ElectronInternal.PreloadScript[]; process: NodeJS.Process; }>(IPC_MESSAGES.BROWSER_SANDBOX_LOAD); @@ -60,8 +40,7 @@ const loadableModules = new Map([ ['node:url', () => require('url')] ]); -// Pass different process object to the preload script. -const preloadProcess: NodeJS.Process = new EventEmitter() as any; +const preloadProcess = createPreloadProcessObject(); // InvokeEmitProcessEvent in ElectronSandboxedRendererClient will look for this v8Util.setHiddenValue(global, 'emit-process-event', (event: string) => { @@ -72,77 +51,22 @@ v8Util.setHiddenValue(global, 'emit-process-event', (event: string) => { Object.assign(preloadProcess, binding.process); Object.assign(preloadProcess, processProps); -Object.assign(process, binding.process); Object.assign(process, processProps); -process.getProcessMemoryInfo = preloadProcess.getProcessMemoryInfo = () => { - return ipcRendererInternal.invoke(IPC_MESSAGES.BROWSER_GET_PROCESS_MEMORY_INFO); -}; - -Object.defineProperty(preloadProcess, 'noDeprecation', { - get () { - return process.noDeprecation; - }, - set (value) { - process.noDeprecation = value; - } -}); - -// This is the `require` function that will be visible to the preload script -function preloadRequire (module: string) { - if (loadedModules.has(module)) { - return loadedModules.get(module); - } - if (loadableModules.has(module)) { - const loadedModule = loadableModules.get(module)!(); - loadedModules.set(module, loadedModule); - return loadedModule; - } - throw new Error(`module not found: ${module}`); -} - -// Process command line arguments. -const { hasSwitch } = process._linkedBinding('electron_common_command_line'); - -// Similar to nodes --expose-internals flag, this exposes _linkedBinding so -// that tests can call it to get access to some test only bindings -if (hasSwitch('unsafely-expose-electron-internals-for-testing')) { - preloadProcess._linkedBinding = process._linkedBinding; -} - // Common renderer initialization require('@electron/internal/renderer/common-init'); -// Wrap the script into a function executed in global scope. It won't have -// access to the current scope, so we'll expose a few objects as arguments: -// -// - `require`: The `preloadRequire` function -// - `process`: The `preloadProcess` object -// - `Buffer`: Shim of `Buffer` implementation -// - `global`: The window object, which is aliased to `global` by webpack. -function runPreloadScript (preloadSrc: string) { - const preloadWrapperSrc = `(function(require, process, Buffer, global, setImmediate, clearImmediate, exports, module) { - ${preloadSrc} - })`; - - // eval in window scope - const preloadFn = binding.createPreloadScript(preloadWrapperSrc); - const exports = {}; - - preloadFn(preloadRequire, preloadProcess, Buffer, global, setImmediate, clearImmediate, exports, { exports }); -} - -for (const { preloadPath, preloadSrc, preloadError } of preloadScripts) { - try { - if (preloadSrc) { - runPreloadScript(preloadSrc); - } else if (preloadError) { - throw preloadError; - } - } catch (error) { - console.error(`Unable to load preload script: ${preloadPath}`); - console.error(error); - - ipcRendererInternal.send(IPC_MESSAGES.BROWSER_PRELOAD_ERROR, preloadPath, error); +executeSandboxedPreloadScripts({ + loadedModules, + loadableModules, + process: preloadProcess, + createPreloadScript: binding.createPreloadScript, + exposeGlobals: { + Buffer, + // FIXME(samuelmaddock): workaround webpack bug replacing this with just + // `__webpack_require__.g,` which causes script error + global: globalThis, + setImmediate, + clearImmediate } -} +}, preloadScripts); diff --git a/lib/sandboxed_renderer/pre-init.ts b/lib/sandboxed_renderer/pre-init.ts new file mode 100644 index 000000000000..dd5b7b9718ba --- /dev/null +++ b/lib/sandboxed_renderer/pre-init.ts @@ -0,0 +1,30 @@ +// Pre-initialization code for sandboxed renderers. + +import * as events from 'events'; + +declare const binding: { + get: (name: string) => any; + process: NodeJS.Process; +}; + +// Expose internal binding getter. +process._linkedBinding = binding.get; + +const { EventEmitter } = events; +const v8Util = process._linkedBinding('electron_common_v8_util'); + +// Include properties from script 'binding' parameter. +Object.assign(process, binding.process); + +// Expose Buffer shim as a hidden value. This is used by C++ code to +// deserialize Buffer instances sent from browser process. +v8Util.setHiddenValue(global, 'Buffer', Buffer); + +// The process object created by webpack is not an event emitter, fix it so +// the API is more compatible with non-sandboxed renderers. +for (const prop of Object.keys(EventEmitter.prototype) as (keyof typeof process)[]) { + if (Object.hasOwn(process, prop)) { + delete process[prop]; + } +} +Object.setPrototypeOf(process, EventEmitter.prototype); diff --git a/lib/sandboxed_renderer/preload.ts b/lib/sandboxed_renderer/preload.ts new file mode 100644 index 000000000000..a83aeb6599ee --- /dev/null +++ b/lib/sandboxed_renderer/preload.ts @@ -0,0 +1,101 @@ +import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages'; +import { ipcRendererInternal } from '@electron/internal/renderer/ipc-renderer-internal'; + +import { EventEmitter } from 'events'; + +interface PreloadContext { + loadedModules: Map; + loadableModules: Map; + + /** Process object to pass into preloads. */ + process: NodeJS.Process; + + createPreloadScript: (src: string) => Function + + /** Globals to be exposed to preload context. */ + exposeGlobals: any; +} + +export function createPreloadProcessObject (): NodeJS.Process { + const preloadProcess: NodeJS.Process = new EventEmitter() as any; + + preloadProcess.getProcessMemoryInfo = () => { + return ipcRendererInternal.invoke(IPC_MESSAGES.BROWSER_GET_PROCESS_MEMORY_INFO); + }; + + Object.defineProperty(preloadProcess, 'noDeprecation', { + get () { + return process.noDeprecation; + }, + set (value) { + process.noDeprecation = value; + } + }); + + const { hasSwitch } = process._linkedBinding('electron_common_command_line'); + + // Similar to nodes --expose-internals flag, this exposes _linkedBinding so + // that tests can call it to get access to some test only bindings + if (hasSwitch('unsafely-expose-electron-internals-for-testing')) { + preloadProcess._linkedBinding = process._linkedBinding; + } + + return preloadProcess; +} + +// This is the `require` function that will be visible to the preload script +function preloadRequire (context: PreloadContext, module: string) { + if (context.loadedModules.has(module)) { + return context.loadedModules.get(module); + } + if (context.loadableModules.has(module)) { + const loadedModule = context.loadableModules.get(module)!(); + context.loadedModules.set(module, loadedModule); + return loadedModule; + } + throw new Error(`module not found: ${module}`); +} + +// Wrap the script into a function executed in global scope. It won't have +// access to the current scope, so we'll expose a few objects as arguments: +// +// - `require`: The `preloadRequire` function +// - `process`: The `preloadProcess` object +// - `Buffer`: Shim of `Buffer` implementation +// - `global`: The window object, which is aliased to `global` by webpack. +function runPreloadScript (context: PreloadContext, preloadSrc: string) { + const globalVariables = []; + const fnParameters = []; + for (const [key, value] of Object.entries(context.exposeGlobals)) { + globalVariables.push(key); + fnParameters.push(value); + } + const preloadWrapperSrc = `(function(require, process, exports, module, ${globalVariables.join(', ')}) { + ${preloadSrc} + })`; + + // eval in window scope + const preloadFn = context.createPreloadScript(preloadWrapperSrc); + const exports = {}; + + preloadFn(preloadRequire.bind(null, context), context.process, exports, { exports }, ...fnParameters); +} + +/** + * Execute preload scripts within a sandboxed process. + */ +export function executeSandboxedPreloadScripts (context: PreloadContext, preloadScripts: ElectronInternal.PreloadScript[]) { + for (const { filePath, contents, error } of preloadScripts) { + try { + if (contents) { + runPreloadScript(context, contents); + } else if (error) { + throw error; + } + } catch (error) { + console.error(`Unable to load preload script: ${filePath}`); + console.error(error); + ipcRendererInternal.send(IPC_MESSAGES.BROWSER_PRELOAD_ERROR, filePath, error); + } + } +} diff --git a/shell/browser/api/electron_api_session.cc b/shell/browser/api/electron_api_session.cc index e3e756750dd3..9f9c21278b91 100644 --- a/shell/browser/api/electron_api_session.cc +++ b/shell/browser/api/electron_api_session.cc @@ -1065,16 +1065,72 @@ void Session::CreateInterruptedDownload(const gin_helper::Dictionary& options) { base::Time::FromSecondsSinceUnixEpoch(start_time))); } -void Session::SetPreloads(const std::vector& preloads) { +std::string Session::RegisterPreloadScript( + gin_helper::ErrorThrower thrower, + const PreloadScript& new_preload_script) { auto* prefs = SessionPreferences::FromBrowserContext(browser_context()); DCHECK(prefs); - prefs->set_preloads(preloads); + + auto& preload_scripts = prefs->preload_scripts(); + + auto it = std::find_if(preload_scripts.begin(), preload_scripts.end(), + [&new_preload_script](const PreloadScript& script) { + return script.id == new_preload_script.id; + }); + + if (it != preload_scripts.end()) { + thrower.ThrowError(base::StringPrintf( + "Cannot register preload script with existing ID '%s'", + new_preload_script.id.c_str())); + return ""; + } + + if (!new_preload_script.file_path.IsAbsolute()) { + // Deprecated preload scripts logged error without throwing. + if (new_preload_script.deprecated) { + LOG(ERROR) << "preload script must have absolute path: " + << new_preload_script.file_path; + } else { + thrower.ThrowError( + base::StringPrintf("Preload script must have absolute path: %s", + new_preload_script.file_path.value().c_str())); + return ""; + } + } + + preload_scripts.push_back(new_preload_script); + return new_preload_script.id; } -std::vector Session::GetPreloads() const { +void Session::UnregisterPreloadScript(gin_helper::ErrorThrower thrower, + const std::string& script_id) { auto* prefs = SessionPreferences::FromBrowserContext(browser_context()); DCHECK(prefs); - return prefs->preloads(); + + auto& preload_scripts = prefs->preload_scripts(); + + // Find the preload script by its ID + auto it = std::find_if(preload_scripts.begin(), preload_scripts.end(), + [&script_id](const PreloadScript& script) { + return script.id == script_id; + }); + + // If the script is found, erase it from the vector + if (it != preload_scripts.end()) { + preload_scripts.erase(it); + return; + } + + // If the script is not found, throw an error + thrower.ThrowError(base::StringPrintf( + "Cannot unregister preload script with non-existing ID '%s'", + script_id.c_str())); +} + +std::vector Session::GetPreloadScripts() const { + auto* prefs = SessionPreferences::FromBrowserContext(browser_context()); + DCHECK(prefs); + return prefs->preload_scripts(); } /** @@ -1800,8 +1856,9 @@ void Session::FillObjectTemplate(v8::Isolate* isolate, .SetMethod("downloadURL", &Session::DownloadURL) .SetMethod("createInterruptedDownload", &Session::CreateInterruptedDownload) - .SetMethod("setPreloads", &Session::SetPreloads) - .SetMethod("getPreloads", &Session::GetPreloads) + .SetMethod("registerPreloadScript", &Session::RegisterPreloadScript) + .SetMethod("unregisterPreloadScript", &Session::UnregisterPreloadScript) + .SetMethod("getPreloadScripts", &Session::GetPreloadScripts) .SetMethod("getSharedDictionaryUsageInfo", &Session::GetSharedDictionaryUsageInfo) .SetMethod("getSharedDictionaryInfo", &Session::GetSharedDictionaryInfo) diff --git a/shell/browser/api/electron_api_session.h b/shell/browser/api/electron_api_session.h index 327e9a7552c5..0ffaabcd2d35 100644 --- a/shell/browser/api/electron_api_session.h +++ b/shell/browser/api/electron_api_session.h @@ -57,6 +57,7 @@ class ProxyConfig; namespace electron { class ElectronBrowserContext; +struct PreloadScript; namespace api { @@ -141,8 +142,11 @@ class Session final : public gin::Wrappable, const std::string& uuid); void DownloadURL(const GURL& url, gin::Arguments* args); void CreateInterruptedDownload(const gin_helper::Dictionary& options); - void SetPreloads(const std::vector& preloads); - std::vector GetPreloads() const; + std::string RegisterPreloadScript(gin_helper::ErrorThrower thrower, + const PreloadScript& new_preload_script); + void UnregisterPreloadScript(gin_helper::ErrorThrower thrower, + const std::string& script_id); + std::vector GetPreloadScripts() const; v8::Local GetSharedDictionaryInfo( const gin_helper::Dictionary& options); v8::Local GetSharedDictionaryUsageInfo(); diff --git a/shell/browser/api/electron_api_web_contents.cc b/shell/browser/api/electron_api_web_contents.cc index 710f1d9878dc..751baa8d66df 100644 --- a/shell/browser/api/electron_api_web_contents.cc +++ b/shell/browser/api/electron_api_web_contents.cc @@ -3757,16 +3757,15 @@ void WebContents::DoGetZoomLevel( std::move(callback).Run(GetZoomLevel()); } -std::vector WebContents::GetPreloadPaths() const { - auto result = SessionPreferences::GetValidPreloads(GetBrowserContext()); - +std::optional WebContents::GetPreloadScript() const { if (auto* web_preferences = WebContentsPreferences::From(web_contents())) { if (auto preload = web_preferences->GetPreloadPath()) { - result.emplace_back(*preload); + auto preload_script = PreloadScript{ + "", PreloadScript::ScriptType::kWebFrame, preload.value()}; + return preload_script; } } - - return result; + return std::nullopt; } v8::Local WebContents::GetLastWebPreferences( @@ -4520,7 +4519,7 @@ void WebContents::FillObjectTemplate(v8::Isolate* isolate, .SetMethod("setZoomFactor", &WebContents::SetZoomFactor) .SetMethod("getZoomFactor", &WebContents::GetZoomFactor) .SetMethod("getType", &WebContents::type) - .SetMethod("_getPreloadPaths", &WebContents::GetPreloadPaths) + .SetMethod("_getPreloadScript", &WebContents::GetPreloadScript) .SetMethod("getLastWebPreferences", &WebContents::GetLastWebPreferences) .SetMethod("getOwnerBrowserWindow", &WebContents::GetOwnerBrowserWindow) .SetMethod("inspectServiceWorker", &WebContents::InspectServiceWorker) diff --git a/shell/browser/api/electron_api_web_contents.h b/shell/browser/api/electron_api_web_contents.h index e453618c4f70..2275f9017106 100644 --- a/shell/browser/api/electron_api_web_contents.h +++ b/shell/browser/api/electron_api_web_contents.h @@ -40,6 +40,7 @@ #include "shell/browser/event_emitter_mixin.h" #include "shell/browser/extended_web_contents_observer.h" #include "shell/browser/osr/osr_paint_event.h" +#include "shell/browser/preload_script.h" #include "shell/browser/ui/inspectable_web_contents_delegate.h" #include "shell/browser/ui/inspectable_web_contents_view_delegate.h" #include "shell/common/gin_helper/cleaned_up_at_exit.h" @@ -344,8 +345,8 @@ class WebContents final : public ExclusiveAccessContext, const std::string& features, const scoped_refptr& body); - // Returns the preload script path of current WebContents. - std::vector GetPreloadPaths() const; + // Returns the preload script of current WebContents. + std::optional GetPreloadScript() const; // Returns the web preferences of current WebContents. v8::Local GetLastWebPreferences(v8::Isolate* isolate) const; diff --git a/shell/browser/preload_script.h b/shell/browser/preload_script.h new file mode 100644 index 000000000000..bae243c7cafc --- /dev/null +++ b/shell/browser/preload_script.h @@ -0,0 +1,104 @@ +// Copyright (c) 2025 Salesforce, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ELECTRON_SHELL_BROWSER_PRELOAD_SCRIPT_H_ +#define ELECTRON_SHELL_BROWSER_PRELOAD_SCRIPT_H_ + +#include + +#include "base/containers/fixed_flat_map.h" +#include "base/files/file_path.h" +#include "base/uuid.h" +#include "gin/converter.h" +#include "shell/common/gin_converters/file_path_converter.h" +#include "shell/common/gin_helper/dictionary.h" + +namespace electron { + +struct PreloadScript { + enum class ScriptType { kWebFrame, kServiceWorker }; + + std::string id; + ScriptType script_type; + base::FilePath file_path; + + // If set, use the deprecated validation behavior of Session.setPreloads + bool deprecated = false; +}; + +} // namespace electron + +namespace gin { + +using electron::PreloadScript; + +template <> +struct Converter { + static v8::Local ToV8(v8::Isolate* isolate, + const PreloadScript::ScriptType& in) { + using Val = PreloadScript::ScriptType; + static constexpr auto Lookup = + base::MakeFixedFlatMap({ + {Val::kWebFrame, "frame"}, + {Val::kServiceWorker, "service-worker"}, + }); + return StringToV8(isolate, Lookup.at(in)); + } + + static bool FromV8(v8::Isolate* isolate, + v8::Local val, + PreloadScript::ScriptType* out) { + using Val = PreloadScript::ScriptType; + static constexpr auto Lookup = + base::MakeFixedFlatMap({ + {"frame", Val::kWebFrame}, + {"service-worker", Val::kServiceWorker}, + }); + return FromV8WithLookup(isolate, val, Lookup, out); + } +}; + +template <> +struct Converter { + static v8::Local ToV8(v8::Isolate* isolate, + const PreloadScript& script) { + gin::Dictionary dict(isolate, v8::Object::New(isolate)); + dict.Set("filePath", script.file_path.AsUTF8Unsafe()); + dict.Set("id", script.id); + dict.Set("type", script.script_type); + return ConvertToV8(isolate, dict).As(); + } + + static bool FromV8(v8::Isolate* isolate, + v8::Local val, + PreloadScript* out) { + gin_helper::Dictionary options; + if (!ConvertFromV8(isolate, val, &options)) + return false; + if (PreloadScript::ScriptType script_type; + options.Get("type", &script_type)) { + out->script_type = script_type; + } else { + return false; + } + if (base::FilePath file_path; options.Get("filePath", &file_path)) { + out->file_path = file_path; + } else { + return false; + } + if (std::string id; options.Get("id", &id)) { + out->id = id; + } else { + out->id = base::Uuid::GenerateRandomV4().AsLowercaseString(); + } + if (bool deprecated; options.Get("_deprecated", &deprecated)) { + out->deprecated = deprecated; + } + return true; + } +}; + +} // namespace gin + +#endif // ELECTRON_SHELL_BROWSER_PRELOAD_SCRIPT_H_ diff --git a/shell/browser/session_preferences.cc b/shell/browser/session_preferences.cc index af2e40d302bd..89acb11410a6 100644 --- a/shell/browser/session_preferences.cc +++ b/shell/browser/session_preferences.cc @@ -30,22 +30,4 @@ SessionPreferences* SessionPreferences::FromBrowserContext( return static_cast(context->GetUserData(&kLocatorKey)); } -// static -std::vector SessionPreferences::GetValidPreloads( - content::BrowserContext* context) { - std::vector result; - - if (auto* self = FromBrowserContext(context)) { - for (const auto& preload : self->preloads()) { - if (preload.IsAbsolute()) { - result.emplace_back(preload); - } else { - LOG(ERROR) << "preload script must have absolute path: " << preload; - } - } - } - - return result; -} - } // namespace electron diff --git a/shell/browser/session_preferences.h b/shell/browser/session_preferences.h index 191a8dd8d056..8ddac4e6b3d4 100644 --- a/shell/browser/session_preferences.h +++ b/shell/browser/session_preferences.h @@ -9,6 +9,7 @@ #include "base/files/file_path.h" #include "base/supports_user_data.h" +#include "shell/browser/preload_script.h" namespace content { class BrowserContext; @@ -20,17 +21,12 @@ class SessionPreferences : public base::SupportsUserData::Data { public: static SessionPreferences* FromBrowserContext( content::BrowserContext* context); - static std::vector GetValidPreloads( - content::BrowserContext* context); static void CreateForBrowserContext(content::BrowserContext* context); ~SessionPreferences() override; - void set_preloads(const std::vector& preloads) { - preloads_ = preloads; - } - const std::vector& preloads() const { return preloads_; } + std::vector& preload_scripts() { return preload_scripts_; } private: SessionPreferences(); @@ -38,7 +34,7 @@ class SessionPreferences : public base::SupportsUserData::Data { // The user data key. static int kLocatorKey; - std::vector preloads_; + std::vector preload_scripts_; }; } // namespace electron diff --git a/typings/internal-electron.d.ts b/typings/internal-electron.d.ts index 0f2cc17c2815..41da29713f3d 100644 --- a/typings/internal-electron.d.ts +++ b/typings/internal-electron.d.ts @@ -76,7 +76,7 @@ declare namespace Electron { getOwnerBrowserWindow(): Electron.BrowserWindow | null; getLastWebPreferences(): Electron.WebPreferences | null; _getProcessMemoryInfo(): Electron.ProcessMemoryInfo; - _getPreloadPaths(): string[]; + _getPreloadScript(): Electron.PreloadScript | null; equal(other: WebContents): boolean; browserWindowOptions: BrowserWindowConstructorOptions; _windowOpenHandler: ((details: Electron.HandlerDetails) => any) | null; @@ -330,6 +330,11 @@ declare namespace ElectronInternal { class WebContents extends Electron.WebContents { static create(opts?: Electron.WebPreferences): Electron.WebContents; } + + interface PreloadScript extends Electron.PreloadScript { + contents?: string; + error?: Error; + } } declare namespace Chrome {