mirror of
https://github.com/electron/electron.git
synced 2026-05-02 03:00:22 -04:00
Compare commits
1 Commits
fix/esm-im
...
sattard/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
580a9668a1 |
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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_);
|
||||
}
|
||||
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()', () => {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
22
spec/fixtures/api/protocol-name/main.js
vendored
22
spec/fixtures/api/protocol-name/main.js
vendored
@@ -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();
|
||||
|
||||
84
spec/fixtures/api/utility-process/net-session.js
vendored
84
spec/fixtures/api/utility-process/net-session.js
vendored
@@ -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' });
|
||||
15
spec/fixtures/esm/import-meta/main.mjs
vendored
15
spec/fixtures/esm/import-meta/main.mjs
vendored
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
Reference in New Issue
Block a user