Compare commits

..

1 Commits

Author SHA1 Message Date
Sam Attard
580a9668a1 fix: always emit executableWillLaunchAtLogin from getLoginItemSettings
`Converter<LoginItemSettings>::ToV8` only set `executableWillLaunchAtLogin`
inside the Windows build block, so calling `app.getLoginItemSettings()` on
macOS returned an object where the property was `undefined` rather than
the boolean its type implies.

Move the `executable_will_launch_at_login` field out of the Windows-only
section of `LoginItemSettings` (it keeps its `false` default everywhere)
and unconditionally emit the property in the V8 converter. The value is
still only meaningful on Windows; on other platforms it is always
`false`.
2026-04-30 18:18:42 +00:00
24 changed files with 130 additions and 1363 deletions

View File

@@ -2,7 +2,7 @@ is_electron_build = true
root_extra_deps = [ "//electron" ]
# Registry of NMVs --> https://github.com/nodejs/node/blob/main/doc/abi_version_registry.json
node_module_version = 148
node_module_version = 146
v8_promise_internal_field_count = 1
v8_embedder_string = "-electron.0"

View File

@@ -882,7 +882,7 @@ Returns `string` - Name of the application handling the protocol, or an empty
This method returns the application name of the default handler for the protocol
(aka URI scheme) of a URL.
### `app.getApplicationInfoForProtocol(url)`
### `app.getApplicationInfoForProtocol(url)` _macOS_ _Windows_
* `url` string - a URL with the protocol name to check. Unlike the other
methods in this family, this accepts an entire URL, including `://` at a

View File

@@ -17,16 +17,6 @@ Process: [Main](../glossary.md#main-process)<br />
* `env` Object (optional) - Environment key-value pairs. Default is `process.env`.
* `execArgv` string[] (optional) - List of string arguments passed to the executable.
* `cwd` string (optional) - Current working directory of the child process.
* `session` [Session](session.md) (optional) - Sets the session used by the process for network
requests. By default, network requests from the utility process will use the system network
context which does not have HTTP cache support. Setting a session enables HTTP caching and
other session-specific network features. See [session](session.md) for more information.
* `partition` string (optional) - Sets the session used by the process according to the
session's partition string. If `partition` starts with `persist:`, the process will use a
persistent session available to all pages in the app with the same `partition`. If there is
no `persist:` prefix, the process will use an in-memory session. By assigning the same
`partition`, multiple processes can share the same session. If the `session` option is set,
this option is ignored.
* `stdio` (string[] | string) (optional) - Allows configuring the mode for `stdout` and `stderr`
of the child process. Default is `inherit`.
String value can be one of `pipe`, `ignore`, `inherit`, for more details on these values you can refer to
@@ -54,9 +44,7 @@ Process: [Main](../glossary.md#main-process)<br />
that run third-party or otherwise untrusted code. Default is `false`.
* `respondToAuthRequestsFromMainProcess` boolean (optional) - With this flag, all HTTP 401 and 407 network
requests created via the [net module](net.md) will allow responding to them via the
[`login`](#event-login) event on the `UtilityProcess` instance when a `session` is provided, or via
the [`app#login`](app.md#event-login) event in the main process when using the default system network
context. Without this flag, auth challenges are handled by the default
[`app#login`](app.md#event-login) event in the main process instead of the default
[`login`](client-request.md#event-login) event on the [`ClientRequest`](client-request.md) object. Default is
`false`.
@@ -188,45 +176,6 @@ Returns:
Emitted when the child process sends a message using [`process.parentPort.postMessage()`](process.md#processparentport).
#### Event: 'login'
Returns:
* `authenticationResponseDetails` Object
* `url` URL
* `pid` number
* `authInfo` Object
* `isProxy` boolean
* `scheme` string
* `host` string
* `port` Integer
* `realm` string
* `callback` Function
* `username` string (optional)
* `password` string (optional)
Emitted when the utility process encounters an HTTP 401 or 407 authentication challenge, if the
process was created with both `respondToAuthRequestsFromMainProcess: true` and a `session` option.
The `callback` should be called with credentials to respond to the challenge. Calling `callback`
without arguments will cancel the request.
This behaves the same as the [`login` event on `app`](app.md#event-login) but is scoped to the
individual utility process instance.
```js
const { session, utilityProcess } = require('electron')
const ses = session.defaultSession
const child = utilityProcess.fork('./worker.js', [], {
session: ses,
respondToAuthRequestsFromMainProcess: true
})
child.on('login', (authenticationResponseDetails, authInfo, callback) => {
callback('username', 'password')
})
```
[`child_process.fork`]: https://nodejs.org/dist/latest-v16.x/docs/api/child_process.html#child_processforkmodulepath-args-options
[Services API]: https://chromium.googlesource.com/chromium/src/+/main/docs/mojo_and_services.md
[stdio]: https://nodejs.org/dist/latest/docs/api/child_process.html#optionsstdio

View File

@@ -134,12 +134,6 @@ When a cookie is deleted, the change cause remains `explicit`.
When the cookie being set is identical to an existing one (same name, domain, path, and value, with no actual changes), the change cause is `inserted-no-change-overwrite`.
When the value of the cookie being set remains unchanged but some of its attributes are updated, such as the expiration attribute, the change cause will be `inserted-no-value-change-overwrite`.
### Deprecated: `showHiddenFiles` in Dialogs on Linux
This property will still be honored on macOS and Windows, but support on Linux
will be removed in Electron 42. GTK intends for this to be a user choice rather
than an app choice and has removed the API to do this programmatically.
## Planned Breaking API Changes (40.0)
### Deprecated: `clipboard` API access from renderer processes
@@ -153,6 +147,12 @@ your preload script and expose it using the [contextBridge](https://www.electron
Debug symbols for MacOS (dSYM) now use xz compression in order to handle larger file sizes. `dsym.zip` files are now
`dsym.tar.xz` files. End users using debug symbols may need to update their zip utilities.
### Deprecated: `showHiddenFiles` in Dialogs on Linux
This property will still be honored on macOS and Windows, but support on Linux
will be removed in Electron 42. GTK intends for this to be a user choice rather
than an app choice and has removed the API to do this programmatically.
## Planned Breaking API Changes (39.0)
### Deprecated: `--host-rules` command line switch

View File

@@ -89,13 +89,6 @@ async function uploadObjectChangeStats(stats) {
unit: 'byte',
tags
},
{
metric: 'electron.build.object-total-size',
points: [{ timestamp, value: stats['total-size'] }],
type: 3, // GAUGE
unit: 'byte',
tags
},
{
metric: 'electron.build.new-object-count',
points: [{ timestamp, value: stats['new-object-count'] }],
@@ -181,10 +174,7 @@ async function main() {
const checksums = {};
for (const file of objectFiles) {
const content = await fs.readFile(resolve(outDir, file));
checksums[file] = {
size: content.byteLength,
checksum: createHash('sha256').update(content).digest('hex')
};
checksums[file] = createHash('sha256').update(content).digest('hex');
}
if (outputObjectChecksums) {
@@ -203,20 +193,13 @@ async function main() {
let newObjectCount = 0;
let changedSize = 0;
// Previously filenames mapped directly to checksum values, but now
// they map to objects containing both checksum and size. Handle both
// formats for backwards compatibility.
const getInputChecksum = (file) => {
const value = inputData.checksums[file];
return typeof value === 'string' ? value : value.checksum;
};
// Count changed files (only those present in both input and current)
for (const file of inputFiles) {
if (!(file in checksums)) continue; // Skip deleted files
if (getInputChecksum(file) !== checksums[file].checksum) {
if (inputData.checksums[file] !== checksums[file]) {
changedCount++;
changedSize += checksums[file].size;
const stat = await fs.stat(resolve(outDir, file));
changedSize += stat.size;
}
}
@@ -224,22 +207,20 @@ async function main() {
for (const file of Object.keys(checksums)) {
if (!(file in inputData.checksums)) {
newObjectCount++;
changedSize += checksums[file].size;
const stat = await fs.stat(resolve(outDir, file));
changedSize += stat.size;
}
}
const changeRate = inputFiles.length > 0 ? changedCount / inputFiles.length : 0;
const totalSize = Object.values(checksums).reduce((sum, { size }) => sum + size, 0);
console.log(`${messagePrefix}Object change rate: ${(changeRate * 100).toFixed(2)}%`);
if (newObjectCount > 0) {
console.log(`${messagePrefix}New object count: ${newObjectCount}`);
}
console.log(`${messagePrefix}Cumulative changed object sizes: ${changedSize.toLocaleString()} bytes`);
console.log(`${messagePrefix}Total object sizes: ${totalSize.toLocaleString()} bytes`);
objectChangeStats['change-rate'] = changeRate;
objectChangeStats['change-size'] = changedSize;
objectChangeStats['total-size'] = totalSize;
objectChangeStats['new-object-count'] = newObjectCount;
objectChangeStats['previous-chromium-version'] = inputData.chromiumVersion;
objectChangeStats['chromium-version'] = currentVersion;

View File

@@ -1881,9 +1881,11 @@ gin::ObjectTemplateBuilder App::GetObjectTemplateBuilder(v8::Isolate* isolate) {
.SetMethod(
"removeAsDefaultProtocolClient",
base::BindRepeating(&Browser::RemoveAsDefaultProtocolClient, browser))
#if !BUILDFLAG(IS_LINUX)
.SetMethod(
"getApplicationInfoForProtocol",
base::BindRepeating(&Browser::GetApplicationInfoForProtocol, browser))
#endif
.SetMethod(
"getApplicationNameForProtocol",
base::BindRepeating(&Browser::GetApplicationNameForProtocol, browser))

View File

@@ -240,10 +240,7 @@ class DesktopCapturer::ListObserver : public DesktopMediaListObserver {
ListObserver(DesktopCapturer* capturer,
DesktopMediaList* list,
bool need_thumbnails)
: capturer_{capturer},
list_{list},
list_type_{list->GetMediaListType()},
need_thumbnails_{need_thumbnails} {}
: capturer_{capturer}, list_{list}, need_thumbnails_{need_thumbnails} {}
~ListObserver() override = default;
[[nodiscard]] bool IsReady() const {
@@ -270,7 +267,7 @@ class DesktopCapturer::ListObserver : public DesktopMediaListObserver {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&DesktopCapturer::OnListReady,
capturer_->weak_ptr_factory_.GetWeakPtr(), list_type_));
capturer_->weak_ptr_factory_.GetWeakPtr(), list_.get()));
}
// DesktopMediaListObserver:
@@ -297,7 +294,6 @@ class DesktopCapturer::ListObserver : public DesktopMediaListObserver {
raw_ptr<DesktopCapturer> capturer_;
raw_ptr<DesktopMediaList> list_;
DesktopMediaList::Type list_type_;
bool need_thumbnails_ = false;
bool has_sources_ = false;
bool notified_ = false;
@@ -414,21 +410,16 @@ void DesktopCapturer::StartHandling(bool capture_window,
weak_ptr_factory_.GetWeakPtr()));
}
void DesktopCapturer::OnListReady(const DesktopMediaList::Type type) {
void DesktopCapturer::OnListReady(DesktopMediaList* list) {
if (finished_)
return;
switch (type) {
case DesktopMediaList::Type::kWindow:
if (window_capturer_)
FinalizeList(window_observer_, window_capturer_);
break;
case DesktopMediaList::Type::kScreen:
if (screen_capturer_)
FinalizeList(screen_observer_, screen_capturer_);
break;
default:
NOTREACHED();
if (list == window_capturer_.get()) {
FinalizeList(window_observer_, window_capturer_);
} else if (list == screen_capturer_.get()) {
FinalizeList(screen_observer_, screen_capturer_);
} else {
NOTREACHED();
}
}

View File

@@ -63,7 +63,7 @@ class DesktopCapturer final
void FinalizeList(std::unique_ptr<ListObserver>& observer,
std::unique_ptr<DesktopMediaList>& list);
void OnListReady(DesktopMediaList::Type type);
void OnListReady(DesktopMediaList* list);
void OnReadyTimeout();
void CollectSourcesFrom(DesktopMediaList* list);
void HandleFailure();

View File

@@ -18,16 +18,13 @@
#include "content/browser/network_service_instance_impl.h" // nogncheck
#include "content/public/browser/child_process_host.h"
#include "content/public/browser/service_process_host.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/common/result_codes.h"
#include "gin/object_template_builder.h"
#include "gin/persistent.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "services/network/public/cpp/originating_process_id.h"
#include "shell/browser/api/electron_api_session.h"
#include "shell/browser/api/message_port.h"
#include "shell/browser/browser.h"
#include "shell/browser/electron_browser_context.h"
#include "shell/browser/electron_child_process_host_flags.h"
#include "shell/browser/javascript_environment.h"
#include "shell/browser/net/system_network_context_manager.h"
@@ -95,9 +92,8 @@ UtilityProcessWrapper::UtilityProcessWrapper(
base::FilePath current_working_directory,
bool use_plugin_helper,
bool create_network_observer,
bool disclaim_responsibility,
Session* session)
: create_network_observer_(create_network_observer), session_(session) {
bool disclaim_responsibility)
: create_network_observer_(create_network_observer) {
auto& allocation_handle =
JavascriptEnvironment::GetIsolate()->GetCppHeap()->GetAllocationHandle();
#if BUILDFLAG(IS_WIN)
@@ -455,7 +451,6 @@ UtilityProcessWrapper::CreateURLLoaderFactoryParams() {
node::mojom::URLLoaderFactoryParamsPtr params =
node::mojom::URLLoaderFactoryParams::New();
mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory;
network::mojom::NetworkContext* network_context;
network::mojom::URLLoaderFactoryParamsPtr loader_params =
network::mojom::URLLoaderFactoryParams::New();
loader_params->process_id = network::OriginatingProcessId::browser();
@@ -467,27 +462,11 @@ UtilityProcessWrapper::CreateURLLoaderFactoryParams() {
url_loader_network_observer_->Bind();
}
if (session_) {
auto* browser_context = session_->browser_context();
network_context =
browser_context->GetDefaultStoragePartition()->GetNetworkContext();
// Build a factory through CreateURLLoaderFactoryBuilder so requests go
// through ProxyingURLLoaderFactory (enabling webRequest interception).
auto [factory_builder, header_client] =
browser_context->CreateURLLoaderFactoryBuilder();
loader_params->header_client = std::move(header_client);
url_loader_factory =
std::move(factory_builder)
.Finish<mojo::PendingRemote<network::mojom::URLLoaderFactory>>(
network_context, std::move(loader_params));
} else {
network_context =
g_browser_process->system_network_context_manager()->GetContext();
network_context->CreateURLLoaderFactory(
url_loader_factory.InitWithNewPipeAndPassReceiver(),
std::move(loader_params));
}
network::mojom::NetworkContext* network_context =
g_browser_process->system_network_context_manager()->GetContext();
network_context->CreateURLLoaderFactory(
url_loader_factory.InitWithNewPipeAndPassReceiver(),
std::move(loader_params));
params->url_loader_factory = std::move(url_loader_factory);
mojo::PendingRemote<network::mojom::HostResolver> host_resolver;
network_context->CreateHostResolver(
@@ -524,7 +503,6 @@ UtilityProcessWrapper* UtilityProcessWrapper::Create(
bool use_plugin_helper = false;
bool create_network_observer = false;
bool disclaim_responsibility = false;
api::Session* session = nullptr;
std::map<IOHandle, IOType> stdio;
base::FilePath current_working_directory;
base::EnvironmentMap env_map;
@@ -570,19 +548,12 @@ UtilityProcessWrapper* UtilityProcessWrapper::Create(
opts.Get("allowLoadingUnsignedLibraries", &use_plugin_helper);
opts.Get("disclaim", &disclaim_responsibility);
#endif
std::string partition;
if (opts.Get("session", &session) && session) {
} else if (opts.Get("partition", &partition)) {
session = Session::FromPartition(args->isolate(), partition);
}
}
v8::Isolate* isolate = args->isolate();
return cppgc::MakeGarbageCollected<UtilityProcessWrapper>(
isolate->GetCppHeap()->GetAllocationHandle(), std::move(params),
display_name, std::move(stdio), env_map, current_working_directory,
use_plugin_helper, create_network_observer, disclaim_responsibility,
session);
use_plugin_helper, create_network_observer, disclaim_responsibility);
}
gin::ObjectTemplateBuilder UtilityProcessWrapper::GetObjectTemplateBuilder(
@@ -596,7 +567,6 @@ gin::ObjectTemplateBuilder UtilityProcessWrapper::GetObjectTemplateBuilder(
void UtilityProcessWrapper::Trace(cppgc::Visitor* visitor) const {
gin::Wrappable<UtilityProcessWrapper>::Trace(visitor);
visitor->Trace(session_);
visitor->Trace(weak_factory_);
}

View File

@@ -38,8 +38,6 @@ class Connector;
namespace electron::api {
class Session;
class UtilityProcessWrapper final
: public gin::Wrappable<UtilityProcessWrapper>,
public gin_helper::EventEmitterMixin<UtilityProcessWrapper>,
@@ -57,8 +55,7 @@ class UtilityProcessWrapper final
base::FilePath current_working_directory,
bool use_plugin_helper,
bool create_network_observer,
bool disclaim_responsibility,
Session* session);
bool disclaim_responsibility);
~UtilityProcessWrapper() override;
static UtilityProcessWrapper* Create(gin::Arguments* args);
@@ -66,8 +63,6 @@ class UtilityProcessWrapper final
void Shutdown(uint32_t exit_code);
bool has_session() const { return session_.Get(); }
// gin::Wrappable
static const gin::WrapperInfo kWrapperInfo;
static const char* GetClassName() { return "UtilityProcess"; }
@@ -131,7 +126,6 @@ class UtilityProcessWrapper final
GC_PLUGIN_IGNORE(
"Context tracking of remote is not needed in the browser process.")
mojo::Remote<node::mojom::NodeService> node_service_remote_;
cppgc::Member<Session> session_;
std::optional<electron::URLLoaderNetworkObserver>
url_loader_network_observer_;
base::CallbackListSubscription network_service_gone_subscription_;

View File

@@ -88,8 +88,6 @@
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/system/platform_handle.h"
#include "printing/buildflags/buildflags.h"
#include "services/network/public/cpp/web_sandbox_flags.h"
#include "services/network/public/mojom/web_sandbox_flags.mojom-shared.h"
#include "services/resource_coordinator/public/cpp/memory_instrumentation/memory_instrumentation.h"
#include "services/service_manager/public/cpp/interface_provider.h"
#include "shell/browser/api/electron_api_browser_window.h"
@@ -158,7 +156,6 @@
#include "third_party/blink/public/common/page/page_zoom.h"
#include "third_party/blink/public/common/peerconnection/webrtc_ip_handling_policy.h"
#include "third_party/blink/public/common/tokens/tokens.h"
#include "third_party/blink/public/mojom/devtools/console_message.mojom.h"
#include "third_party/blink/public/mojom/frame/find_in_page.mojom.h"
#include "third_party/blink/public/mojom/frame/fullscreen.mojom.h"
#include "third_party/blink/public/mojom/messaging/transferable_message.mojom.h"
@@ -1400,26 +1397,6 @@ content::WebContents* WebContents::OpenURLFromTab(
navigation_handle_callback) {
auto weak_this = GetWeakPtr();
if (params.disposition != WindowOpenDisposition::CURRENT_TAB) {
content::FrameTreeNode* initiator =
params.frame_tree_node_id ? content::FrameTreeNode::GloballyFindByID(
params.frame_tree_node_id)
: nullptr;
if (initiator && !initiator->IsMainFrame()) {
using SandboxFlags = network::mojom::WebSandboxFlags;
const SandboxFlags flags = initiator->active_sandbox_flags();
auto allow = [flags](SandboxFlags flag) {
return (flags & flag) == SandboxFlags::kNone;
};
if (!allow(SandboxFlags::kPopups)) {
if (auto* rfh = initiator->current_frame_host()) {
rfh->AddMessageToConsole(
blink::mojom::ConsoleMessageLevel::kError,
"Blocked opening a new window because the iframe is sandboxed "
"and the 'allow-popups' keyword is not set.");
}
return nullptr;
}
}
Emit("-new-window", params.url, "", params.disposition, "", params.referrer,
params.post_data);
return nullptr;

View File

@@ -84,10 +84,13 @@ struct LoginItemSettings {
std::wstring name;
// used in browser::getLoginItemSettings
bool executable_will_launch_at_login = false;
std::vector<LaunchItem> launch_items;
#endif
// used in browser::getLoginItemSettings; only meaningful on Windows but
// always emitted so consumers don't observe `undefined`.
bool executable_will_launch_at_login = false;
LoginItemSettings();
~LoginItemSettings();
LoginItemSettings(const LoginItemSettings&);
@@ -161,9 +164,11 @@ class Browser : private WindowListObserver {
std::u16string GetApplicationNameForProtocol(const GURL& url);
#if !BUILDFLAG(IS_LINUX)
// get the name, icon and path for an application
v8::Local<v8::Promise> GetApplicationInfoForProtocol(v8::Isolate* isolate,
const GURL& url);
#endif
// Set/Get the badge count.
bool SetBadgeCount(std::optional<int> count);

View File

@@ -4,11 +4,6 @@
#include "shell/browser/browser.h"
#include <fcntl.h>
#include <stdlib.h>
#include <string_view>
#if BUILDFLAG(IS_LINUX)
#include <gio/gdesktopappinfo.h>
#include <gio/gio.h>
@@ -16,7 +11,6 @@
#endif
#include "base/environment.h"
#include "base/files/file_path.h"
#include "base/logging.h"
#include "base/strings/utf_string_conversions.h"
#include "electron/electron_version.h"
@@ -24,18 +18,7 @@
#include "shell/browser/native_window.h"
#include "shell/browser/window_list.h"
#include "shell/common/application_info.h"
#include "shell/common/gin_converters/image_converter.h"
#include "shell/common/gin_converters/login_item_settings_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/promise.h"
#include "shell/common/thread_restrictions.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/glib/glib_cast.h"
#include "ui/base/glib/scoped_gobject.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gtk/gtk_compat.h" // nogncheck
#include "ui/gtk/gtk_util.h" // nogncheck
#if BUILDFLAG(IS_LINUX)
#include "shell/browser/linux/unity_service.h"
@@ -67,101 +50,6 @@ bool SetDefaultWebClient(const std::string& protocol) {
return success;
}
[[nodiscard]] ScopedGObject<GAppInfo> GetAppInfoForProtocol(const GURL& url) {
const auto scheme = std::string{url.scheme()}; // gio can't use string_views
return TakeGObject(g_app_info_get_default_for_uri_scheme(scheme.c_str()));
}
[[nodiscard]] std::optional<base::FilePath> ResolveExecutablePath(
const char* const executable) {
if (executable == nullptr || *executable == '\0')
return {};
if (auto path = base::FilePath::FromUTF8Unsafe(executable); path.IsAbsolute())
return path;
gchar* const found = g_find_program_in_path(executable);
if (!found)
return {};
const auto path = base::FilePath::FromUTF8Unsafe(found);
g_free(found);
return path;
}
[[nodiscard]] gfx::Image GetApplicationIcon(GAppInfo* const app_info) {
constexpr int kIconSize = 32;
GIcon* const icon = g_app_info_get_icon(app_info);
if (!icon)
return {};
// Note: this gtk3/gtk4 + icon theme lookup + snapshot control flow
// is copied from GtkUi::GetIconForContentType(). We couldn't use it
// here because it gets its GIcon from a different place than us.
SkBitmap bitmap;
if (gtk::GtkCheckVersion(4)) {
const auto icon_paintable = gtk::Gtk4IconThemeLookupByGicon(
gtk::GetDefaultIconTheme(), icon, kIconSize, 1, GTK_TEXT_DIR_NONE,
static_cast<GtkIconLookupFlags>(0));
if (!icon_paintable)
return {};
auto* const paintable =
GlibCast<GdkPaintable>(icon_paintable.get(), gdk_paintable_get_type());
auto* const snapshot = gtk_snapshot_new();
gdk_paintable_snapshot(paintable, snapshot, kIconSize, kIconSize);
auto* const node = gtk_snapshot_free_to_node(snapshot);
GdkTexture* const texture = gtk::GetTextureFromRenderNode(node);
if (!texture) {
gsk_render_node_unref(node);
return {};
}
bitmap.allocN32Pixels(gdk_texture_get_width(texture),
gdk_texture_get_height(texture));
gdk_texture_download(texture, static_cast<guchar*>(bitmap.getAddr(0, 0)),
bitmap.rowBytes());
gsk_render_node_unref(node);
} else {
const auto icon_info = gtk::Gtk3IconThemeLookupByGiconForScale(
gtk::GetDefaultIconTheme(), icon, kIconSize, 1,
static_cast<GtkIconLookupFlags>(GTK_ICON_LOOKUP_FORCE_SIZE));
if (!icon_info)
return {};
auto* const surface =
gtk_icon_info_load_surface(icon_info.get(), nullptr, nullptr);
if (!surface)
return {};
DCHECK_EQ(cairo_surface_get_type(surface), CAIRO_SURFACE_TYPE_IMAGE);
DCHECK_EQ(cairo_image_surface_get_format(surface), CAIRO_FORMAT_ARGB32);
const SkImageInfo image_info =
SkImageInfo::Make(cairo_image_surface_get_width(surface),
cairo_image_surface_get_height(surface),
kBGRA_8888_SkColorType, kUnpremul_SkAlphaType);
if (!bitmap.installPixels(
image_info, cairo_image_surface_get_data(surface),
image_info.minRowBytes(),
[](void*, void* surface_ptr) {
cairo_surface_destroy(
reinterpret_cast<cairo_surface_t*>(surface_ptr));
},
surface)) {
return {};
}
}
gfx::ImageSkia image_skia = gfx::ImageSkia::CreateFrom1xBitmap(bitmap);
if (image_skia.isNull())
return {};
image_skia.MakeThreadSafe();
return gfx::Image(image_skia);
}
} // namespace
void Browser::AddRecentDocument(const base::FilePath& path) {}
@@ -207,53 +95,15 @@ bool Browser::RemoveAsDefaultProtocolClient(const std::string& protocol,
}
std::u16string Browser::GetApplicationNameForProtocol(const GURL& url) {
auto app_info = GetAppInfoForProtocol(url);
const auto scheme = std::string{url.scheme()}; // gio can't use string_view
auto* app_info = g_app_info_get_default_for_uri_scheme(scheme.c_str());
if (!app_info)
return {};
const char* const name = g_app_info_get_display_name(app_info);
return base::UTF8ToUTF16(name);
}
v8::Local<v8::Promise> Browser::GetApplicationInfoForProtocol(
v8::Isolate* const isolate,
const GURL& url) {
gin_helper::Promise<gin_helper::Dictionary> promise(isolate);
const v8::Local<v8::Promise> handle = promise.GetHandle();
auto app_info = GetAppInfoForProtocol(url);
if (!app_info) {
promise.RejectWithErrorMessage(
"Unable to retrieve installation path to app");
return handle;
}
const char* const executable = g_app_info_get_executable(app_info);
const auto app_path = ResolveExecutablePath(executable);
if (!app_path) {
promise.RejectWithErrorMessage(
"Unable to retrieve installation path to app");
return handle;
}
const char* const app_display_name = g_app_info_get_display_name(app_info);
if (!app_display_name || app_display_name[0] == '\0') {
promise.RejectWithErrorMessage("Unable to retrieve display name of app");
return handle;
}
const gfx::Image app_icon = GetApplicationIcon(app_info);
if (app_icon.IsEmpty()) {
promise.RejectWithErrorMessage("Failed to get file icon.");
return handle;
}
auto dict = gin_helper::Dictionary::CreateEmpty(isolate);
dict.Set("name", base::UTF8ToUTF16(app_display_name));
dict.Set("path", app_path->value());
dict.Set("icon", app_icon);
promise.Resolve(dict);
return handle;
const std::u16string u16name = base::UTF8ToUTF16(name);
g_object_unref(app_info);
return u16name;
}
bool Browser::SetBadgeCount(std::optional<int> count) {

View File

@@ -98,10 +98,6 @@ class ElectronBrowserContext : public content::BrowserContext {
scoped_refptr<network::SharedURLLoaderFactory> InterceptURLLoaderFactory(
scoped_refptr<network::SharedURLLoaderFactory> factory);
std::pair<network::URLLoaderFactoryBuilder,
mojo::PendingRemote<network::mojom::TrustedURLLoaderHeaderClient>>
CreateURLLoaderFactoryBuilder();
std::string GetMediaDeviceIDSalt();
// content::BrowserContext:
@@ -191,6 +187,10 @@ class ElectronBrowserContext : public content::BrowserContext {
content::MediaResponseCallback callback,
gin::Arguments* args);
std::pair<network::URLLoaderFactoryBuilder,
mojo::PendingRemote<network::mojom::TrustedURLLoaderHeaderClient>>
CreateURLLoaderFactoryBuilder();
// Initialize pref registry.
void InitPrefs();

View File

@@ -11,7 +11,6 @@
#include "gin/arguments.h"
#include "gin/dictionary.h"
#include "shell/browser/api/electron_api_app.h"
#include "shell/browser/api/electron_api_utility_process.h"
#include "shell/browser/api/electron_api_web_contents.h"
#include "shell/browser/javascript_environment.h"
#include "shell/common/gin_converters/callback_converter.h"
@@ -101,17 +100,6 @@ void LoginHandler::EmitEvent(
api_web_contents->Emit("login", std::move(details), auth_info,
base::BindOnce(&LoginHandler::CallbackFromJS,
weak_factory_.GetWeakPtr()));
} else if (auto* utility_process =
api::UtilityProcessWrapper::FromProcessId(process_id);
utility_process && utility_process->has_session()) {
// Route auth to the utility process wrapper when the request originated
// from a utility process with a session and
// respondToAuthRequestsFromMainProcess. Without a session, auth falls
// through to app.on('login') for backward compatibility.
default_prevented =
utility_process->Emit("login", std::move(details), auth_info,
base::BindOnce(&LoginHandler::CallbackFromJS,
weak_factory_.GetWeakPtr()));
} else {
default_prevented =
api::App::Get()->Emit("login", nullptr, std::move(details), auth_info,

View File

@@ -6,7 +6,6 @@
#include <shellapi.h>
#include <wrl/client.h>
#include "base/logging.h"
#include "base/win/atl.h" // Must be before UIAutomationCore.h
#include "base/win/registry.h"
#include "base/win/scoped_handle.h"
@@ -678,21 +677,8 @@ void NativeWindowViews::SetForwardMouseMessages(bool forward) {
RemoveWindowSubclass(legacy_window_, SubclassProc, 1);
if (forwarding_windows_->empty()) {
// If UnhookWindowsHookEx fails, the hook is still installed in the
// system. Leave |mouse_hook_| pointing at the existing hook so that a
// subsequent SetForwardMouseMessages(true) reuses it instead of
// installing a duplicate hook.
if (UnhookWindowsHookEx(mouse_hook_)) {
mouse_hook_ = nullptr;
} else {
const DWORD error = ::GetLastError();
if (error == ERROR_INVALID_HOOK_HANDLE) {
mouse_hook_ = nullptr;
} else {
LOG(WARNING) << "Failed to unhook low-level mouse hook: "
<< logging::SystemErrorCodeToString(error);
}
}
UnhookWindowsHookEx(mouse_hook_);
mouse_hook_ = nullptr;
}
}
}

View File

@@ -70,11 +70,13 @@ v8::Local<v8::Value> Converter<electron::LoginItemSettings>::ToV8(
auto dict = gin_helper::Dictionary::CreateEmpty(isolate);
#if BUILDFLAG(IS_WIN)
dict.Set("launchItems", val.launch_items);
dict.Set("executableWillLaunchAtLogin", val.executable_will_launch_at_login);
#elif BUILDFLAG(IS_MAC)
if (base::mac::MacOSMajorVersion() >= 13)
dict.Set("status", val.status);
#endif
// Always emit `executableWillLaunchAtLogin` so callers don't see undefined
// on non-Windows platforms; the value is only meaningful on Windows.
dict.Set("executableWillLaunchAtLogin", val.executable_will_launch_at_login);
dict.Set("openAtLogin", val.open_at_login);
dict.Set("openAsHidden", val.open_as_hidden);
dict.Set("restoreState", val.restore_state);

View File

@@ -18,12 +18,6 @@ import { promisify } from 'node:util';
import { collectStreamBody, getResponse } from './lib/net-helpers';
import { defer, ifdescribe, ifit, isWayland, listen, waitUntil } from './lib/spec-helpers';
import { closeWindow, closeAllWindows } from './lib/window-helpers';
import {
makeXdgMockDirectories,
spawnProtocolInfoWithXdgMock,
spawnProtocolNameWithXdgMock,
writeProtocolAssociation
} from './lib/xdg-helpers';
const fixturesPath = path.resolve(__dirname, 'fixtures');
@@ -1519,23 +1513,73 @@ describe('app module', () => {
});
ifdescribe(process.platform === 'linux')('on Linux with mocked XDG dirs', () => {
const fixtureApp = path.join(fixturesPath, 'api', 'protocol-name');
const desktopFileId = 'mock-browser.desktop';
const mockDisplayName = 'Mock Browser';
const mockScheme = 'mockproto';
const mockMimeType = `x-scheme-handler/${mockScheme}`;
function spawnWithXdgMock(url: string, xdgDataHome: string, xdgConfigHome: string): Promise<string> {
return new Promise((resolve, reject) => {
const child = cp.spawn(process.execPath, [fixtureApp, url], {
env: {
...process.env,
XDG_DATA_HOME: xdgDataHome,
XDG_DATA_DIRS: xdgDataHome,
XDG_CONFIG_HOME: xdgConfigHome
},
stdio: ['ignore', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (d: Buffer) => {
stdout += d;
});
child.stderr.on('data', (d: Buffer) => {
stderr += d;
});
child.on('close', (code) => {
if (code !== 0) {
reject(new Error(`Fixture exited with code ${code}: ${stderr}`));
return;
}
try {
const parsed = JSON.parse(stdout);
resolve(parsed.name);
} catch {
reject(new Error(`Failed to parse output: ${stdout}\nstderr: ${stderr}`));
}
});
child.on('error', reject);
});
}
let xdgDir: string;
let xdgDataHome: string;
let xdgConfigHome: string;
before(() => {
({ xdgDir, xdgDataHome, xdgConfigHome } = makeXdgMockDirectories('electron-xdg-name-'));
writeProtocolAssociation(
xdgDataHome,
xdgConfigHome,
desktopFileId,
mockDisplayName,
'/usr/bin/true %u',
mockMimeType
xdgDir = fs.mkdtempSync(path.join(os.tmpdir(), 'electron-xdg-'));
xdgDataHome = path.join(xdgDir, 'data');
xdgConfigHome = path.join(xdgDir, 'config');
const appsDir = path.join(xdgDataHome, 'applications');
fs.mkdirSync(appsDir, { recursive: true });
fs.mkdirSync(xdgConfigHome, { recursive: true });
fs.writeFileSync(
path.join(appsDir, desktopFileId),
[
'[Desktop Entry]',
`Name=${mockDisplayName}`,
'Exec=/usr/bin/true %u',
'Type=Application',
`MimeType=${mockMimeType};`
].join('\n')
);
fs.writeFileSync(
path.join(xdgConfigHome, 'mimeapps.list'),
['[Default Applications]', `${mockMimeType}=${desktopFileId}`].join('\n')
);
});
@@ -1544,52 +1588,18 @@ describe('app module', () => {
});
it('returns the display name for a registered protocol', async () => {
const name = await spawnProtocolNameWithXdgMock(`${mockScheme}://`, xdgDataHome, xdgConfigHome);
const name = await spawnWithXdgMock(`${mockScheme}://`, xdgDataHome, xdgConfigHome);
expect(name).to.equal(mockDisplayName);
});
it('returns an empty string for an unregistered protocol', async () => {
const name = await spawnProtocolNameWithXdgMock('unregistered-proto://', xdgDataHome, xdgConfigHome);
const name = await spawnWithXdgMock('unregistered-proto://', xdgDataHome, xdgConfigHome);
expect(name).to.equal('');
});
});
});
describe('getApplicationInfoForProtocol()', () => {
const fixtureIcon = path.join(fixturesPath, 'assets', '1x1.png');
const desktopFileId = 'mock-browser.desktop';
const mockDisplayName = 'Mock Browser';
const mockScheme = 'mockproto';
const mockMimeType = `x-scheme-handler/${mockScheme}`;
let xdgDir: string;
let xdgDataHome: string;
let xdgConfigHome: string;
let xdgBinDir: string;
before(() => {
if (process.platform !== 'linux') {
return;
}
({ xdgDir, xdgDataHome, xdgConfigHome, xdgBinDir } = makeXdgMockDirectories('electron-xdg-app-info-'));
writeProtocolAssociation(
xdgDataHome,
xdgConfigHome,
desktopFileId,
mockDisplayName,
'/usr/bin/true %u',
mockMimeType,
fixtureIcon
);
});
after(() => {
if (process.platform === 'linux') {
fs.rmSync(xdgDir, { recursive: true, force: true });
}
});
ifdescribe(process.platform !== 'linux')('getApplicationInfoForProtocol()', () => {
it('returns promise rejection for a bogus protocol', async function () {
await expect(app.getApplicationInfoForProtocol('bogus-protocol://')).to.eventually.be.rejectedWith(
'Unable to retrieve installation path to app'
@@ -1597,46 +1607,11 @@ describe('app module', () => {
});
it('returns resolved promise with appPath, displayName and icon', async function () {
if (process.platform === 'linux') {
const appInfo = await spawnProtocolInfoWithXdgMock(`${mockScheme}://`, xdgDataHome, xdgConfigHome);
expect(appInfo.name).to.equal(mockDisplayName);
expect(appInfo.path).to.equal('/usr/bin/true');
expect(appInfo.hasIcon).to.equal(true);
return;
}
const appInfo = await app.getApplicationInfoForProtocol('https://');
expect(appInfo.path).not.to.be.undefined();
expect(appInfo.name).not.to.be.undefined();
expect(appInfo.icon).not.to.be.undefined();
});
ifit(process.platform === 'linux')('resolves an executable name via PATH', async () => {
const pathLookupExecutable = 'mock-browser';
const pathLookupExecutablePath = path.join(xdgBinDir, pathLookupExecutable);
const pathLookupDisplayName = 'Mock Browser PATH';
const pathLookupScheme = 'mockproto-path';
const pathLookupMimeType = `x-scheme-handler/${pathLookupScheme}`;
fs.writeFileSync(pathLookupExecutablePath, '#!/bin/sh\nexit 0\n');
fs.chmodSync(pathLookupExecutablePath, 0o755);
writeProtocolAssociation(
xdgDataHome,
xdgConfigHome,
'mock-browser-path.desktop',
pathLookupDisplayName,
`${pathLookupExecutable} %u`,
pathLookupMimeType,
fixtureIcon
);
const appInfo = await spawnProtocolInfoWithXdgMock(`${pathLookupScheme}://`, xdgDataHome, xdgConfigHome, {
PATH: [xdgBinDir, process.env.PATH].filter(Boolean).join(':')
});
expect(appInfo.name).to.equal(pathLookupDisplayName);
expect(appInfo.path).to.equal(pathLookupExecutablePath);
expect(appInfo.hasIcon).to.equal(true);
});
});
describe('isDefaultProtocolClient()', () => {

View File

@@ -1,19 +1,18 @@
import { systemPreferences } from 'electron';
import { BrowserWindow, MessageChannelMain, utilityProcess, app, session } from 'electron/main';
import { BrowserWindow, MessageChannelMain, utilityProcess, app } from 'electron/main';
import { expect } from 'chai';
import * as childProcess from 'node:child_process';
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 { setImmediate } from 'node:timers/promises';
import { pathToFileURL } from 'node:url';
import { respondOnce, randomString, kOneKiloByte } from './lib/net-helpers';
import { ifit, listen, startRemoteControlApp } from './lib/spec-helpers';
import { ifit, startRemoteControlApp } from './lib/spec-helpers';
import { closeWindow } from './lib/window-helpers';
const fixturesPath = path.resolve(__dirname, 'fixtures', 'api', 'utility-process');
@@ -994,596 +993,4 @@ describe('utilityProcess module', () => {
await exit;
});
});
describe('session', () => {
it('can use a session object for network requests', async () => {
const customSession = session.fromPartition(`utility-session-test-${Math.random()}`);
const serverUrl = await respondOnce.toSingleURL((request, response) => {
response.end('session-response');
});
const child = utilityProcess.fork(path.join(fixturesPath, 'net-session.js'), [], {
session: customSession
});
await once(child, 'spawn');
await once(child, 'message');
child.postMessage({ type: 'fetch', url: serverUrl });
const [data] = await once(child, 'message');
expect(data.ok).to.be.true();
expect(data.body).to.equal('session-response');
const exit = once(child, 'exit');
child.kill();
await exit;
});
it('can use a partition string for network requests', async () => {
const serverUrl = await respondOnce.toSingleURL((request, response) => {
response.end('partition-response');
});
const child = utilityProcess.fork(path.join(fixturesPath, 'net-session.js'), [], {
partition: `utility-partition-test-${Math.random()}`
});
await once(child, 'spawn');
await once(child, 'message');
child.postMessage({ type: 'fetch', url: serverUrl });
const [data] = await once(child, 'message');
expect(data.ok).to.be.true();
expect(data.body).to.equal('partition-response');
const exit = once(child, 'exit');
child.kill();
await exit;
});
it('uses HTTP cache when session is provided', async () => {
let requestCount = 0;
const server = http.createServer((request, response) => {
requestCount++;
response.writeHead(200, {
'Cache-Control': 'max-age=3600',
'Content-Type': 'text/plain',
'X-Request-Count': String(requestCount)
});
response.end(`response-${requestCount}`);
});
const { url } = await listen(server);
const customSession = session.fromPartition(`utility-cache-test-${Math.random()}`);
const cacheFlags: boolean[] = [];
customSession.webRequest.onResponseStarted((details) => {
cacheFlags.push(details.fromCache);
});
try {
const child = utilityProcess.fork(path.join(fixturesPath, 'net-session.js'), [], {
session: customSession
});
await once(child, 'spawn');
await once(child, 'message');
child.postMessage({ type: 'fetch-cached', url: `${url}/cached` });
const [data] = await once(child, 'message');
expect(data.ok).to.be.true();
expect(data.first.body).to.equal('response-1');
expect(data.second.body).to.equal('response-1');
expect(requestCount).to.equal(1);
// Verify cache flags: first request from network, second from cache
expect(cacheFlags).to.deep.equal([false, true]);
const exit = once(child, 'exit');
child.kill();
await exit;
} finally {
customSession.webRequest.onResponseStarted(null);
await customSession.clearCache();
server.close();
}
});
it('does not use HTTP cache when using the system network context (no session)', async () => {
let requestCount = 0;
const server = http.createServer((request, response) => {
requestCount++;
response.writeHead(200, {
'Cache-Control': 'max-age=3600',
'Content-Type': 'text/plain'
});
response.end(`response-${requestCount}`);
});
const { url } = await listen(server);
try {
const child = utilityProcess.fork(path.join(fixturesPath, 'net-session.js'));
await once(child, 'spawn');
await once(child, 'message');
child.postMessage({ type: 'fetch-cached', url: `${url}/no-cache` });
const [data] = await once(child, 'message');
expect(data.ok).to.be.true();
expect(data.first.body).to.equal('response-1');
expect(data.second.body).to.equal('response-2');
expect(requestCount).to.equal(2);
const exit = once(child, 'exit');
child.kill();
await exit;
} finally {
server.close();
}
});
it('respects cache: "no-store" fetch option to bypass cache', async () => {
let requestCount = 0;
const server = http.createServer((request, response) => {
requestCount++;
response.writeHead(200, {
'Cache-Control': 'max-age=3600',
'Content-Type': 'text/plain'
});
response.end(`response-${requestCount}`);
});
const { url } = await listen(server);
const customSession = session.fromPartition(`utility-cache-nostore-${Math.random()}`);
const cacheFlags: boolean[] = [];
customSession.webRequest.onResponseStarted((details) => {
cacheFlags.push(details.fromCache);
});
try {
const child = utilityProcess.fork(path.join(fixturesPath, 'net-session.js'), [], {
session: customSession
});
await once(child, 'spawn');
await once(child, 'message');
child.postMessage({ type: 'fetch-cached', url: `${url}/nostore`, cacheMode: 'no-store' });
const [data] = await once(child, 'message');
expect(data.ok).to.be.true();
expect(data.first.body).to.equal('response-1');
expect(data.second.body).to.equal('response-2');
expect(requestCount).to.equal(2);
// Neither response should be from cache
expect(cacheFlags).to.deep.equal([false, false]);
const exit = once(child, 'exit');
child.kill();
await exit;
} finally {
customSession.webRequest.onResponseStarted(null);
await customSession.clearCache();
server.close();
}
});
it('respects cache: "no-cache" fetch option to revalidate', async () => {
let requestCount = 0;
const server = http.createServer((request, response) => {
requestCount++;
response.writeHead(200, {
'Cache-Control': 'max-age=3600',
'Content-Type': 'text/plain',
ETag: '"test-etag"'
});
response.end(`response-${requestCount}`);
});
const { url } = await listen(server);
const customSession = session.fromPartition(`utility-cache-nocache-${Math.random()}`);
const cacheFlags: boolean[] = [];
customSession.webRequest.onResponseStarted((details) => {
cacheFlags.push(details.fromCache);
});
try {
const child = utilityProcess.fork(path.join(fixturesPath, 'net-session.js'), [], {
session: customSession
});
await once(child, 'spawn');
await once(child, 'message');
child.postMessage({ type: 'fetch-cached', url: `${url}/nocache`, cacheMode: 'no-cache' });
const [data] = await once(child, 'message');
expect(data.ok).to.be.true();
expect(data.first.body).to.equal('response-1');
expect(requestCount).to.equal(2);
// First from network, second revalidated (not from cache)
expect(cacheFlags).to.deep.equal([false, false]);
const exit = once(child, 'exit');
child.kill();
await exit;
} finally {
customSession.webRequest.onResponseStarted(null);
await customSession.clearCache();
server.close();
}
});
it('respects cache: "force-cache" fetch option to use stale cache', async () => {
let requestCount = 0;
const server = http.createServer((request, response) => {
requestCount++;
response.writeHead(200, {
'Cache-Control': 'max-age=3600',
'Content-Type': 'text/plain'
});
response.end(`response-${requestCount}`);
});
const { url } = await listen(server);
const customSession = session.fromPartition(`utility-cache-forcecache-${Math.random()}`);
const cacheFlags: boolean[] = [];
customSession.webRequest.onResponseStarted((details) => {
cacheFlags.push(details.fromCache);
});
try {
const child = utilityProcess.fork(path.join(fixturesPath, 'net-session.js'), [], {
session: customSession
});
await once(child, 'spawn');
await once(child, 'message');
child.postMessage({ type: 'fetch-cached', url: `${url}/forcecache`, cacheMode: 'force-cache' });
const [data] = await once(child, 'message');
expect(data.ok).to.be.true();
expect(data.first.body).to.equal('response-1');
expect(data.second.body).to.equal('response-1');
expect(requestCount).to.equal(1);
// First from network, second from cache
expect(cacheFlags).to.deep.equal([false, true]);
const exit = once(child, 'exit');
child.kill();
await exit;
} finally {
customSession.webRequest.onResponseStarted(null);
await customSession.clearCache();
server.close();
}
});
it('respects cache: "reload" fetch option to bypass cache entirely', async () => {
let requestCount = 0;
const server = http.createServer((request, response) => {
requestCount++;
response.writeHead(200, {
'Cache-Control': 'max-age=3600',
'Content-Type': 'text/plain'
});
response.end(`response-${requestCount}`);
});
const { url } = await listen(server);
const customSession = session.fromPartition(`utility-cache-reload-${Math.random()}`);
const cacheFlags: boolean[] = [];
customSession.webRequest.onResponseStarted((details) => {
cacheFlags.push(details.fromCache);
});
try {
const child = utilityProcess.fork(path.join(fixturesPath, 'net-session.js'), [], {
session: customSession
});
await once(child, 'spawn');
await once(child, 'message');
child.postMessage({ type: 'fetch-cached', url: `${url}/reload`, cacheMode: 'reload' });
const [data] = await once(child, 'message');
expect(data.ok).to.be.true();
expect(data.first.body).to.equal('response-1');
expect(data.second.body).to.equal('response-2');
expect(requestCount).to.equal(2);
// Neither response should be from cache
expect(cacheFlags).to.deep.equal([false, false]);
const exit = once(child, 'exit');
child.kill();
await exit;
} finally {
customSession.webRequest.onResponseStarted(null);
await customSession.clearCache();
server.close();
}
});
it('isolates cookies between different sessions', async () => {
const sess1 = session.fromPartition(`utility-cookie-test-1-${Math.random()}`);
const sess2 = session.fromPartition(`utility-cookie-test-2-${Math.random()}`);
const server = http.createServer((request, response) => {
const cookie = request.headers.cookie || 'none';
response.writeHead(200, {
'Content-Type': 'text/plain',
'Set-Cookie': 'testcookie=value; Path=/'
});
response.end(cookie);
});
const { url } = await listen(server);
try {
await sess1.cookies.set({ url, name: 'testcookie', value: 'sess1value' });
const child1 = utilityProcess.fork(path.join(fixturesPath, 'net-session.js'), [], {
session: sess1
});
await once(child1, 'spawn');
await once(child1, 'message');
child1.postMessage({ type: 'fetch', url: `${url}/cookies`, options: { credentials: 'include' } });
const [data1] = await once(child1, 'message');
expect(data1.ok).to.be.true();
expect(data1.body).to.include('testcookie=sess1value');
const exit1 = once(child1, 'exit');
child1.kill();
await exit1;
const child2 = utilityProcess.fork(path.join(fixturesPath, 'net-session.js'), [], {
session: sess2
});
await once(child2, 'spawn');
await once(child2, 'message');
child2.postMessage({ type: 'fetch', url: `${url}/cookies`, options: { credentials: 'include' } });
const [data2] = await once(child2, 'message');
expect(data2.ok).to.be.true();
expect(data2.body).to.equal('none');
const exit2 = once(child2, 'exit');
child2.kill();
await exit2;
} finally {
await sess1.clearStorageData();
await sess2.clearStorageData();
server.close();
}
});
it('shares cookies when utility processes use the same session', async () => {
const sharedSession = session.fromPartition(`utility-shared-session-${Math.random()}`);
const server = http.createServer((request, response) => {
const cookie = request.headers.cookie || 'none';
response.writeHead(200, { 'Content-Type': 'text/plain' });
response.end(cookie);
});
const { url } = await listen(server);
try {
await sharedSession.cookies.set({ url, name: 'shared', value: 'cookie123' });
const child1 = utilityProcess.fork(path.join(fixturesPath, 'net-session.js'), [], {
session: sharedSession
});
await once(child1, 'spawn');
await once(child1, 'message');
child1.postMessage({ type: 'fetch', url: `${url}/shared`, options: { credentials: 'include' } });
const [data1] = await once(child1, 'message');
expect(data1.ok).to.be.true();
expect(data1.body).to.include('shared=cookie123');
const exit1 = once(child1, 'exit');
child1.kill();
await exit1;
const child2 = utilityProcess.fork(path.join(fixturesPath, 'net-session.js'), [], {
session: sharedSession
});
await once(child2, 'spawn');
await once(child2, 'message');
child2.postMessage({ type: 'fetch', url: `${url}/shared`, options: { credentials: 'include' } });
const [data2] = await once(child2, 'message');
expect(data2.ok).to.be.true();
expect(data2.body).to.include('shared=cookie123');
const exit2 = once(child2, 'exit');
child2.kill();
await exit2;
} finally {
await sharedSession.clearStorageData();
server.close();
}
});
it('session option takes precedence over partition', async () => {
const customSession = session.fromPartition(`utility-precedence-session-${Math.random()}`);
const serverUrl = await respondOnce.toSingleURL((request, response) => {
response.end('precedence-ok');
});
const child = utilityProcess.fork(path.join(fixturesPath, 'net-session.js'), [], {
session: customSession,
partition: 'this-should-be-ignored'
} as any);
await once(child, 'spawn');
await once(child, 'message');
child.postMessage({ type: 'fetch', url: serverUrl });
const [data] = await once(child, 'message');
expect(data.ok).to.be.true();
expect(data.body).to.equal('precedence-ok');
const exit = once(child, 'exit');
child.kill();
await exit;
});
it('session webRequest handlers intercept utility process requests', async () => {
const customSession = session.fromPartition(`utility-webrequest-test-${Math.random()}`);
const server = http.createServer((request, response) => {
response.writeHead(200, { 'Content-Type': 'text/plain' });
response.end(`header: ${request.headers['x-custom-header'] || 'missing'}`);
});
const { url } = await listen(server);
try {
customSession.webRequest.onBeforeSendHeaders((details, callback) => {
details.requestHeaders['X-Custom-Header'] = 'intercepted';
callback({ requestHeaders: details.requestHeaders });
});
const child = utilityProcess.fork(path.join(fixturesPath, 'net-session.js'), [], {
session: customSession
});
await once(child, 'spawn');
await once(child, 'message');
child.postMessage({ type: 'fetch', url: `${url}/webrequest` });
const [data] = await once(child, 'message');
expect(data.ok).to.be.true();
expect(data.body).to.equal('header: intercepted');
const exit = once(child, 'exit');
child.kill();
await exit;
} finally {
customSession.webRequest.onBeforeSendHeaders(null);
server.close();
}
});
it('fires ClientRequest login event when respondToAuthRequestsFromMainProcess is false', async () => {
const customSession = session.fromPartition(`utility-login-client-${Math.random()}`);
const serverUrl = await respondOnce.toSingleURL((request, response) => {
if (!request.headers.authorization) {
return response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Foo"' }).end();
}
response.writeHead(200).end('authenticated');
});
const child = utilityProcess.fork(path.join(fixturesPath, 'net-session.js'), [], {
session: customSession
});
await once(child, 'spawn');
await once(child, 'message'); // ready
child.postMessage({ type: 'net-request-login', url: serverUrl });
const [loginMsg] = await once(child, 'message');
expect(loginMsg.event).to.equal('login');
expect(loginMsg.authInfo.realm).to.equal('Foo');
expect(loginMsg.authInfo.scheme).to.equal('basic');
const [responseMsg] = await once(child, 'message');
expect(responseMsg.event).to.equal('response');
expect(responseMsg.statusCode).to.equal(200);
const exit = once(child, 'exit');
child.kill();
await exit;
});
it('fires app login event when respondToAuthRequestsFromMainProcess is true without session', async () => {
const { remotely } = await startRemoteControlApp();
const serverUrl = await respondOnce.toSingleURL((request, response) => {
if (!request.headers.authorization) {
return response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Foo"' }).end();
}
response.writeHead(200).end('authenticated');
});
const [loginAuthInfo, statusCode] = await remotely(
async (serverUrl: string, fixture: string) => {
const { app, utilityProcess } = require('electron');
const { once } = require('node:events');
const child = utilityProcess.fork(fixture, [], {
stdio: 'ignore',
respondToAuthRequestsFromMainProcess: true
});
await once(child, 'spawn');
await once(child, 'message');
child.postMessage({ type: 'fetch-auth', url: serverUrl });
const [ev, , , authInfo, cb] = await once(app, 'login');
ev.preventDefault();
cb('user', 'pass');
const [result] = await once(child, 'message');
return [authInfo, result.status];
},
serverUrl,
path.join(fixturesPath, 'net-session.js')
);
expect(statusCode).to.equal(200);
expect(loginAuthInfo!.realm).to.equal('Foo');
expect(loginAuthInfo!.scheme).to.equal('basic');
});
it('fires utility process login event when respondToAuthRequestsFromMainProcess is true with session', async () => {
const { remotely } = await startRemoteControlApp();
const serverUrl = await respondOnce.toSingleURL((request, response) => {
if (!request.headers.authorization) {
return response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Foo"' }).end();
}
response.writeHead(200).end('authenticated');
});
const [loginAuthInfo, statusCode, appLoginFired] = await remotely(
async (serverUrl: string, fixture: string) => {
const { app, session, utilityProcess } = require('electron');
const { once } = require('node:events');
const customSession = session.fromPartition(`utility-login-session-${Math.random()}`);
let appLoginFired = false;
app.on('login', () => {
appLoginFired = true;
});
const child = utilityProcess.fork(fixture, [], {
stdio: 'ignore',
respondToAuthRequestsFromMainProcess: true,
session: customSession
});
await once(child, 'spawn');
await once(child, 'message');
child.postMessage({ type: 'fetch-auth', url: serverUrl });
const [ev, , authInfo, cb] = await once(child, 'login');
ev.preventDefault();
cb('user', 'pass');
const [result] = await once(child, 'message');
return [authInfo, result.status, appLoginFired];
},
serverUrl,
path.join(fixturesPath, 'net-session.js')
);
expect(statusCode).to.equal(200);
expect(loginAuthInfo!.realm).to.equal('Foo');
expect(loginAuthInfo!.scheme).to.equal('basic');
expect(appLoginFired).to.be.false();
});
it('resolves hosts using the session network context, not the default', async () => {
const proxyServer = http.createServer((request, response) => {
response.writeHead(200, { 'Content-Type': 'text/plain' });
response.end(`proxied:${request.headers.host}`);
});
const { port: proxyPort } = await listen(proxyServer);
const customSession = session.fromPartition(`utility-resolver-test-${Math.random()}`);
try {
await customSession.setProxy({
proxyRules: `http=127.0.0.1:${proxyPort}`,
proxyBypassRules: '<-loopback>'
});
await session.defaultSession.setProxy({});
const child = utilityProcess.fork(path.join(fixturesPath, 'net-session.js'), [], {
session: customSession
});
await once(child, 'spawn');
await once(child, 'message');
child.postMessage({ type: 'fetch', url: 'http://non-existent-host.test:12345/path' });
const [data] = await once(child, 'message');
expect(data.ok).to.be.true();
expect(data.body).to.equal('proxied:non-existent-host.test:12345');
const exit = once(child, 'exit');
child.kill();
await exit;
} finally {
await session.defaultSession.setProxy({});
proxyServer.close();
}
});
it('does not use system network proxy set via app.setProxy when session is provided', async () => {
const { remotely } = await startRemoteControlApp();
const systemProxyServer = http.createServer((request, response) => {
response.writeHead(200, { 'Content-Type': 'text/plain' });
response.end('system-proxy');
});
const { port: systemProxyPort } = await listen(systemProxyServer);
const targetServer = http.createServer((request, response) => {
response.writeHead(200, { 'Content-Type': 'text/plain' });
response.end('direct-response');
});
const { url: targetUrl } = await listen(targetServer);
try {
const body = await remotely(
async (targetUrl: string, systemProxyPort: number, fixture: string) => {
const { app, session, utilityProcess } = require('electron');
const { once } = require('node:events');
await app.setProxy({
proxyRules: `http=127.0.0.1:${systemProxyPort}`,
proxyBypassRules: '<-loopback>'
});
const customSession = session.fromPartition(`utility-app-proxy-test-${Math.random()}`);
await customSession.setProxy({});
const child = utilityProcess.fork(fixture, [], {
stdio: 'ignore',
session: customSession
});
await once(child, 'spawn');
await once(child, 'message');
child.postMessage({ type: 'fetch', url: targetUrl });
const [data] = await once(child, 'message');
const exit = once(child, 'exit');
child.kill();
await exit;
return data.body;
},
targetUrl,
systemProxyPort,
path.join(fixturesPath, 'net-session.js')
);
expect(body).to.equal('direct-response');
} finally {
targetServer.close();
systemProxyServer.close();
}
});
});
});

View File

@@ -5061,76 +5061,3 @@ describe('iframe sandbox external protocols', () => {
expect(openExternalRequests).to.deep.equal(['magnet:sandbox-test']);
});
});
describe('iframe sandbox popups', () => {
let server: http.Server;
let serverUrl: string;
let w: BrowserWindow;
const childHtml = `
<script>
addEventListener('DOMContentLoaded', () => {
const a = document.createElement('a');
a.href = 'https://example.com/sandbox-bypassed';
document.body.appendChild(a);
a.dispatchEvent(new MouseEvent('click', {
ctrlKey: true, metaKey: true, bubbles: true, cancelable: true, view: window
}));
});
</script>`;
before(async () => {
server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html');
if (req.url === '/child') {
res.end(childHtml);
} else {
const sandbox = new URL(req.url!, serverUrl).searchParams.get('sandbox') ?? '';
res.end(`<iframe sandbox="${sandbox}" src="/child"></iframe>`);
}
});
serverUrl = (await listen(server)).url;
});
after(() => {
server.close();
});
beforeEach(() => {
w = new BrowserWindow({ show: false });
});
afterEach(() => closeAllWindows());
it('does not invoke setWindowOpenHandler from a sandboxed iframe without allow-popups', async () => {
let handlerCalls = 0;
w.webContents.setWindowOpenHandler(() => {
handlerCalls++;
return { action: 'deny' };
});
await w.loadURL(`${serverUrl}/?sandbox=${encodeURIComponent('allow-scripts')}`);
await setTimeout(200);
expect(handlerCalls).to.equal(0);
});
it('does not create a BrowserWindow from a sandboxed iframe without allow-popups (no handler installed)', async () => {
let created = false;
w.webContents.on('did-create-window', () => {
created = true;
});
await w.loadURL(`${serverUrl}/?sandbox=${encodeURIComponent('allow-scripts')}`);
await setTimeout(200);
expect(created).to.be.false();
});
it('invokes setWindowOpenHandler when allow-popups is set', async () => {
let handlerCalls = 0;
w.webContents.setWindowOpenHandler(() => {
handlerCalls++;
return { action: 'deny' };
});
await w.loadURL(`${serverUrl}/?sandbox=${encodeURIComponent('allow-scripts allow-popups')}`);
await setTimeout(200);
expect(handlerCalls).to.equal(1);
});
});

View File

@@ -1,27 +1,7 @@
const { app } = require('electron');
app.whenReady().then(async () => {
app.whenReady().then(() => {
const url = process.argv[2];
if (process.env.ELECTRON_PROTOCOL_LOOKUP_MODE === 'info') {
try {
const info = await app.getApplicationInfoForProtocol(url);
process.stdout.write(
JSON.stringify({
name: info.name,
path: info.path,
hasIcon: !info.icon.isEmpty()
})
);
} catch (error) {
process.stderr.write(`${error.message}\n`);
app.exit(1);
return;
}
app.quit();
return;
}
const name = app.getApplicationNameForProtocol(url);
process.stdout.write(JSON.stringify({ name }));
app.quit();

View File

@@ -1,84 +0,0 @@
const { net } = require('electron');
process.parentPort.on('message', async (e) => {
const { type, url, options } = e.data;
if (type === 'fetch') {
try {
const response = await net.fetch(url, options || {});
const body = await response.text();
const headers = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});
process.parentPort.postMessage({
ok: response.ok,
status: response.status,
body,
headers
});
} catch (error) {
process.parentPort.postMessage({
ok: false,
error: error.message
});
}
} else if (type === 'fetch-cached') {
try {
const response1 = await net.fetch(url);
const body1 = await response1.text();
const fetchOpts = {};
if (e.data.cacheMode) {
fetchOpts.cache = e.data.cacheMode;
}
const response2 = await net.fetch(url, fetchOpts);
const body2 = await response2.text();
process.parentPort.postMessage({
ok: true,
first: { status: response1.status, body: body1 },
second: { status: response2.status, body: body2 }
});
} catch (error) {
process.parentPort.postMessage({
ok: false,
error: error.message
});
}
} else if (type === 'net-request-login') {
const request = net.request({ method: 'GET', url });
request.on('login', (authInfo, callback) => {
process.parentPort.postMessage({ event: 'login', authInfo });
callback('user', 'pass');
});
request.on('response', (response) => {
const data = [];
response.on('data', (chunk) => data.push(chunk));
response.on('end', () => {
process.parentPort.postMessage({
event: 'response',
statusCode: response.statusCode
});
});
});
request.end();
} else if (type === 'fetch-auth') {
try {
const response = await net.fetch(url);
const body = await response.text();
process.parentPort.postMessage({
event: 'response',
status: response.status,
body
});
} catch (error) {
process.parentPort.postMessage({
event: 'error',
error: error.message
});
}
}
});
process.parentPort.postMessage({ type: 'ready' });

View File

@@ -21,9 +21,14 @@ async function createWindow() {
process.exit(importMetaPreload === expected ? 0 : 1);
}
app
.whenReady()
.then(() => createWindow())
.catch(() => process.exit(1));
app.whenReady().then(() => {
createWindow();
app.on('window-all-closed', app.quit);
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit();
});

View File

@@ -1,138 +0,0 @@
import * as cp from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
const fixturesPath = path.resolve(__dirname, '..', 'fixtures');
const xdgMockFixturePath = path.join(fixturesPath, 'api', 'xdg-mock');
const protocolLookupFixturePath = path.join(fixturesPath, 'api', 'protocol-name');
const kDefaultXdgDataDirs = '/usr/local/share:/usr/share';
type ProtocolNameLookupResult = {
name: string;
};
export type ProtocolInfoLookupResult = ProtocolNameLookupResult & {
path: string;
hasIcon: boolean;
};
export function getXdgDataDirsWithFallback(xdgDataHome: string, xdgDataDirs = process.env.XDG_DATA_DIRS) {
// Match Chromium's XDG fallback when XDG_DATA_DIRS is unset.
return [xdgDataHome, xdgDataDirs || kDefaultXdgDataDirs].join(':');
}
export function makeXdgMockDirectories(prefix: string) {
const xdgDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
fs.cpSync(xdgMockFixturePath, xdgDir, { recursive: true });
const xdgDataHome = path.join(xdgDir, 'data');
const xdgConfigHome = path.join(xdgDir, 'config');
const xdgBinDir = path.join(xdgDir, 'bin');
fs.chmodSync(path.join(xdgBinDir, 'xdg-mime'), 0o755);
fs.chmodSync(path.join(xdgBinDir, 'xdg-settings'), 0o755);
return { xdgDir, xdgDataHome, xdgConfigHome, xdgBinDir };
}
export function writeProtocolAssociation(
xdgDataHome: string,
xdgConfigHome: string,
desktopFileId: string,
displayName: string,
execCommand: string,
mimeType: string,
iconPath?: string
) {
const appsDir = path.join(xdgDataHome, 'applications');
fs.mkdirSync(appsDir, { recursive: true });
fs.mkdirSync(xdgConfigHome, { recursive: true });
const desktopEntry = ['[Desktop Entry]', `Name=${displayName}`, `Exec=${execCommand}`];
if (iconPath) {
desktopEntry.push(`Icon=${iconPath}`);
}
desktopEntry.push('Type=Application', `MimeType=${mimeType};`);
fs.writeFileSync(path.join(appsDir, desktopFileId), desktopEntry.join('\n'));
fs.writeFileSync(
path.join(xdgConfigHome, 'mimeapps.list'),
['[Default Applications]', `${mimeType}=${desktopFileId}`].join('\n')
);
}
function spawnProtocolLookupWithXdgMock(
url: string,
xdgDataHome: string,
xdgConfigHome: string,
options: {
lookupMode?: 'info';
extraEnv?: Record<string, string | undefined>;
} = {}
): Promise<ProtocolNameLookupResult | ProtocolInfoLookupResult> {
const { lookupMode, extraEnv = {} } = options;
return new Promise((resolve, reject) => {
const env: NodeJS.ProcessEnv = {
...process.env,
...extraEnv,
XDG_DATA_HOME: xdgDataHome,
XDG_DATA_DIRS: getXdgDataDirsWithFallback(xdgDataHome),
XDG_CONFIG_HOME: xdgConfigHome
};
if (lookupMode === 'info') {
env.ELECTRON_PROTOCOL_LOOKUP_MODE = 'info';
}
const child = cp.spawn(process.execPath, [protocolLookupFixturePath, url], {
env,
stdio: ['ignore', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (data: Buffer) => {
stdout += data;
});
child.stderr.on('data', (data: Buffer) => {
stderr += data;
});
child.on('close', (code) => {
if (code !== 0) {
reject(new Error(`Fixture exited with code ${code}: ${stderr}`));
return;
}
try {
resolve(JSON.parse(stdout));
} catch {
reject(new Error(`Failed to parse output: ${stdout}\nstderr: ${stderr}`));
}
});
child.on('error', reject);
});
}
export async function spawnProtocolNameWithXdgMock(
url: string,
xdgDataHome: string,
xdgConfigHome: string,
extraEnv: Record<string, string | undefined> = {}
) {
const parsed = (await spawnProtocolLookupWithXdgMock(url, xdgDataHome, xdgConfigHome, {
extraEnv
})) as ProtocolNameLookupResult;
return parsed.name;
}
export function spawnProtocolInfoWithXdgMock(
url: string,
xdgDataHome: string,
xdgConfigHome: string,
extraEnv: Record<string, string | undefined> = {}
) {
return spawnProtocolLookupWithXdgMock(url, xdgDataHome, xdgConfigHome, {
lookupMode: 'info',
extraEnv
}) as Promise<ProtocolInfoLookupResult>;
}