Compare commits

..

15 Commits

Author SHA1 Message Date
Electron Bot
a19e55a902 Bump v7.0.0-nightly.20190602 2019-06-02 08:31:21 -07:00
Shelley Vohr
9187759460 chore: fix logging to stderr (#18537) 2019-06-01 09:21:13 -07:00
Electron Bot
04dd52e4dc Bump v7.0.0-nightly.20190601 2019-06-01 08:31:36 -07:00
Jeremy Apthorp
90caedb552 chore: re-enable disabled time ticks dcheck (#18525) 2019-05-31 16:56:10 -07:00
Samuel Attard
87ae9324ac feat: Add option to conditionally disable site instance patches (#18396)
* chore: allow conditional disable of the site instance override patches at runtime

* feat: add app.allowRendererProcessReuse property to allow runtime disable of site instance overrides

spec: add tests for the new allowRendererProcessReuse property

feat: add console warnings / errors for loading non context-aware native modules
  * Only error if the patch is disabled
  * Warn all the time, this will ship in Electron 7
2019-05-31 15:47:18 -07:00
Samuel Attard
26155c8a00 fix: handle gzipped chrome WebUI resources (#18531)
Fixes: #18503
Refs: https://chromium-review.googlesource.com/c/chromium/src/+/1576232

For whatever reason Chromium made this change and then made the default
//content behavior to return false for all IsDataResourceGzipped
requests.  This PR updates our ContentClient impl to return the correct
values.

Notes: Fixed issues where some `chrome://*` URLs would not render
correctly
2019-05-31 13:05:35 -07:00
Jeremy Apthorp
81366b5bfb chore: remove upstreamed ffmpeg patch (#18524) 2019-05-31 14:08:11 -04:00
Jeremy Apthorp
c436997840 feat: add ipcRenderer.invoke() (#18449) 2019-05-31 10:25:19 -07:00
Electron Bot
b180fb376c Bump v7.0.0-nightly.20190531 2019-05-31 08:32:10 -07:00
Maya Wolf
ab70e854f8 fix: contractions handling in spellchecker (#18506)
This fixes #18459 by improving the handling of contractions in the spellcheck API. Specifically, it now accepts contraction words where the spellchecker recognizes the whole word, and not, as previously, just if it recognizes all of its parts.
2019-05-30 21:19:10 -07:00
John Kleinschmidt
a31faaae61 ci: add retries to downloads for arm testing (#18526) 2019-05-30 17:22:34 -07:00
Shelley Vohr
1e3e5a6619 refactor: set appLevelAppearance prop on systemPreferences (#18477)
* refactor: set appLevelAppearance prop on systemPreferences

* ensure backwards compat is tested
2019-05-30 17:12:46 -07:00
Jeremy Apthorp
ac35f41e8d test: move download-related session specs to main runner (#18508) 2019-05-30 15:05:02 -07:00
Shelley Vohr
554ee92b39 docs: specify use case for the 'services' role (#18484)
* docs: specify use case for the 'services' role

* update based on @caesar's recommendation

Co-Authored-By: Caesar Schinas <caesar@caesarschinas.com>
2019-05-30 14:33:48 -07:00
Samuel Attard
02dc1b266c docs: use | instead of 'or' for docs (#18512) 2019-05-30 10:32:46 -07:00
61 changed files with 1095 additions and 718 deletions

2
DEPS
View File

@@ -12,7 +12,7 @@ vars = {
'chromium_version':
'ab588d36191964c4bca8de5c320534d95606c861',
'node_version':
'a86a4a160dc520c61a602c949a32a1bc4c0fc633',
'dee0db9864a001ffc16440f725f4952a1a417069',
'nan_version':
'960dd6c70fc9eb136efdf37b4bef18fadbc3436f',

View File

@@ -1 +1 @@
7.0.0-nightly.20190530
7.0.0-nightly.20190602

View File

@@ -268,4 +268,8 @@ void AtomContentClient::AddContentDecryptionModules(
}
}
bool AtomContentClient::IsDataResourceGzipped(int resource_id) const {
return ui::ResourceBundle::GetSharedInstance().IsGzipped(resource_id);
}
} // namespace atom

View File

@@ -31,6 +31,7 @@ class AtomContentClient : public content::ContentClient {
void AddContentDecryptionModules(
std::vector<content::CdmInfo>* cdms,
std::vector<media::CdmHostFilePath>* cdm_host_file_paths) override;
bool IsDataResourceGzipped(int resource_id) const override;
private:
DISALLOW_COPY_AND_ASSIGN(AtomContentClient);

View File

@@ -149,10 +149,12 @@ bool AtomMainDelegate::BasicStartupComplete(int* exit_code) {
settings.lock_log = logging::LOCK_LOG_FILE;
settings.delete_old = logging::DELETE_OLD_LOG_FILE;
#else
settings.logging_dest = logging::LOG_TO_SYSTEM_DEBUG_LOG;
settings.logging_dest =
logging::LOG_TO_SYSTEM_DEBUG_LOG | logging::LOG_TO_STDERR;
#endif // defined(DEBUG)
#else // defined(OS_WIN)
settings.logging_dest = logging::LOG_TO_SYSTEM_DEBUG_LOG;
settings.logging_dest =
logging::LOG_TO_SYSTEM_DEBUG_LOG | logging::LOG_TO_STDERR;
#endif // !defined(OS_WIN)
// Only enable logging when --enable-logging is specified.

View File

@@ -1286,6 +1286,13 @@ std::string App::GetUserAgentFallback() {
return AtomBrowserClient::Get()->GetUserAgent();
}
void App::SetBrowserClientCanUseCustomSiteInstance(bool should_disable) {
AtomBrowserClient::Get()->SetCanUseCustomSiteInstance(should_disable);
}
bool App::CanBrowserClientUseCustomSiteInstance() {
return AtomBrowserClient::Get()->CanUseCustomSiteInstance();
}
#if defined(OS_MACOSX)
bool App::MoveToApplicationsFolder(mate::Arguments* args) {
return ui::cocoa::AtomBundleMover::Move(args);
@@ -1467,7 +1474,10 @@ void App::BuildPrototype(v8::Isolate* isolate,
#endif
.SetProperty("userAgentFallback", &App::GetUserAgentFallback,
&App::SetUserAgentFallback)
.SetMethod("enableSandbox", &App::EnableSandbox);
.SetMethod("enableSandbox", &App::EnableSandbox)
.SetProperty("allowRendererProcessReuse",
&App::CanBrowserClientUseCustomSiteInstance,
&App::SetBrowserClientCanUseCustomSiteInstance);
}
} // namespace api

View File

@@ -213,6 +213,8 @@ class App : public AtomBrowserClient::Delegate,
void EnableSandbox(mate::Arguments* args);
void SetUserAgentFallback(const std::string& user_agent);
std::string GetUserAgentFallback();
void SetBrowserClientCanUseCustomSiteInstance(bool should_disable);
bool CanBrowserClientUseCustomSiteInstance();
#if defined(OS_MACOSX)
bool MoveToApplicationsFolder(mate::Arguments* args);

View File

@@ -106,6 +106,9 @@ void SystemPreferences::BuildPrototype(
&SystemPreferences::GetAppLevelAppearance)
.SetMethod("setAppLevelAppearance",
&SystemPreferences::SetAppLevelAppearance)
.SetProperty("appLevelAppearance",
&SystemPreferences::GetAppLevelAppearance,
&SystemPreferences::SetAppLevelAppearance)
.SetMethod("getSystemColor", &SystemPreferences::GetSystemColor)
.SetMethod("canPromptTouchID", &SystemPreferences::CanPromptTouchID)
.SetMethod("promptTouchID", &SystemPreferences::PromptTouchID)

View File

@@ -924,6 +924,14 @@ void WebContents::Message(bool internal,
internal, channel, std::move(arguments));
}
void WebContents::Invoke(const std::string& channel,
base::Value arguments,
InvokeCallback callback) {
// webContents.emit('-ipc-invoke', new Event(), channel, arguments);
EmitWithSender("-ipc-invoke", bindings_.dispatch_context(),
std::move(callback), channel, std::move(arguments));
}
void WebContents::MessageSync(bool internal,
const std::string& channel,
base::Value arguments,

View File

@@ -492,6 +492,9 @@ class WebContents : public mate::TrackableObject<WebContents>,
void Message(bool internal,
const std::string& channel,
base::Value arguments) override;
void Invoke(const std::string& channel,
base::Value arguments,
InvokeCallback callback) override;
void MessageSync(bool internal,
const std::string& channel,
base::Value arguments,

View File

@@ -58,7 +58,7 @@ void Event::PreventDefault(v8::Isolate* isolate) {
.Check();
}
bool Event::SendReply(const base::ListValue& result) {
bool Event::SendReply(const base::Value& result) {
if (!callback_ || sender_ == nullptr)
return false;

View File

@@ -32,8 +32,9 @@ class Event : public Wrappable<Event>, public content::WebContentsObserver {
// event.PreventDefault().
void PreventDefault(v8::Isolate* isolate);
// event.sendReply(array), used for replying synchronous message.
bool SendReply(const base::ListValue& result);
// event.sendReply(value), used for replying to synchronous messages and
// `invoke` calls.
bool SendReply(const base::Value& result);
protected:
explicit Event(v8::Isolate* isolate);

View File

@@ -403,6 +403,14 @@ void AtomBrowserClient::OverrideWebkitPrefs(content::RenderViewHost* host,
web_preferences->OverrideWebkitPrefs(prefs);
}
void AtomBrowserClient::SetCanUseCustomSiteInstance(bool should_disable) {
disable_process_restart_tricks_ = should_disable;
}
bool AtomBrowserClient::CanUseCustomSiteInstance() {
return disable_process_restart_tricks_;
}
content::ContentBrowserClient::SiteInstanceForNavigationType
AtomBrowserClient::ShouldOverrideSiteInstanceForNavigation(
content::RenderFrameHost* current_rfh,
@@ -509,6 +517,10 @@ void AtomBrowserClient::AppendExtraCommandLineSwitches(
web_preferences->AppendCommandLineSwitches(command_line);
SessionPreferences::AppendExtraCommandLineSwitches(
web_contents->GetBrowserContext(), command_line);
if (CanUseCustomSiteInstance()) {
command_line->AppendSwitch(
switches::kDisableElectronSiteInstanceOverrides);
}
}
}

View File

@@ -67,6 +67,9 @@ class AtomBrowserClient : public content::ContentBrowserClient,
std::string GetUserAgent() const override;
void SetUserAgent(const std::string& user_agent);
void SetCanUseCustomSiteInstance(bool should_disable);
bool CanUseCustomSiteInstance() override;
protected:
void RenderProcessWillLaunch(
content::RenderProcessHost* host,
@@ -253,6 +256,8 @@ class AtomBrowserClient : public content::ContentBrowserClient,
std::string user_agent_override_ = "";
bool disable_process_restart_tricks_ = false;
DISALLOW_COPY_AND_ASSIGN(AtomBrowserClient);
};

View File

@@ -17,9 +17,9 @@
<key>CFBundleIconFile</key>
<string>electron.icns</string>
<key>CFBundleVersion</key>
<string>7.0.0-nightly.20190530</string>
<string>7.0.0-nightly.20190602</string>
<key>CFBundleShortVersionString</key>
<string>7.0.0-nightly.20190530</string>
<string>7.0.0-nightly.20190602</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.developer-tools</string>
<key>LSMinimumSystemVersion</key>

View File

@@ -50,8 +50,8 @@ END
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 7,0,0,20190530
PRODUCTVERSION 7,0,0,20190530
FILEVERSION 7,0,0,20190602
PRODUCTVERSION 7,0,0,20190602
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L

View File

@@ -21,6 +21,12 @@ interface ElectronBrowser {
string channel,
mojo_base.mojom.ListValue arguments);
// Emits an event on |channel| from the ipcMain JavaScript object in the main
// process, and returns the response.
Invoke(
string channel,
mojo_base.mojom.ListValue arguments) => (mojo_base.mojom.Value result);
// Emits an event on |channel| from the ipcMain JavaScript object in the main
// process, and waits synchronously for a response.
//

View File

@@ -9,7 +9,7 @@
#define ATOM_MINOR_VERSION 0
#define ATOM_PATCH_VERSION 0
// clang-format off
#define ATOM_PRE_RELEASE_VERSION -nightly.20190530
#define ATOM_PRE_RELEASE_VERSION -nightly.20190602
// clang-format on
#ifndef ATOM_STRINGIFY

View File

@@ -232,6 +232,8 @@ const char kScrollBounce[] = "scroll-bounce";
const char kHiddenPage[] = "hidden-page";
const char kNativeWindowOpen[] = "native-window-open";
const char kWebviewTag[] = "webview-tag";
const char kDisableElectronSiteInstanceOverrides[] =
"disable-electron-site-instance-overrides";
// Command switch passed to renderer process to control nodeIntegration.
const char kNodeIntegrationInWorker[] = "node-integration-in-worker";

View File

@@ -118,6 +118,7 @@ extern const char kNodeIntegrationInWorker[];
extern const char kWebviewTag[];
extern const char kNodeIntegrationInSubFrames[];
extern const char kDisableHtmlFullscreenWindowResize[];
extern const char kDisableElectronSiteInstanceOverrides[];
extern const char kWidevineCdmPath[];
extern const char kWidevineCdmVersion[];

View File

@@ -8,6 +8,7 @@
#include "atom/common/native_mate_converters/value_converter.h"
#include "atom/common/node_bindings.h"
#include "atom/common/node_includes.h"
#include "atom/common/promise_util.h"
#include "base/task/post_task.h"
#include "base/values.h"
#include "content/public/renderer/render_frame.h"
@@ -41,6 +42,8 @@ class IPCRenderer : public mate::Wrappable<IPCRenderer> {
DCHECK(render_frame);
render_frame->GetRemoteInterfaces()->GetInterface(
mojo::MakeRequest(&electron_browser_ptr_));
render_frame->GetRemoteInterfaces()->GetInterface(
mojo::MakeRequest(&electron_browser_sync_ptr_));
}
static void BuildPrototype(v8::Isolate* isolate,
v8::Local<v8::FunctionTemplate> prototype) {
@@ -49,7 +52,8 @@ class IPCRenderer : public mate::Wrappable<IPCRenderer> {
.SetMethod("send", &IPCRenderer::Send)
.SetMethod("sendSync", &IPCRenderer::SendSync)
.SetMethod("sendTo", &IPCRenderer::SendTo)
.SetMethod("sendToHost", &IPCRenderer::SendToHost);
.SetMethod("sendToHost", &IPCRenderer::SendToHost)
.SetMethod("invoke", &IPCRenderer::Invoke);
}
static mate::Handle<IPCRenderer> Create(v8::Isolate* isolate) {
return mate::CreateHandle(isolate, new IPCRenderer(isolate));
@@ -62,6 +66,20 @@ class IPCRenderer : public mate::Wrappable<IPCRenderer> {
electron_browser_ptr_->Message(internal, channel, arguments.Clone());
}
v8::Local<v8::Promise> Invoke(mate::Arguments* args,
const std::string& channel,
const base::Value& arguments) {
atom::util::Promise p(args->isolate());
auto handle = p.GetHandle();
electron_browser_ptr_->Invoke(
channel, arguments.Clone(),
base::BindOnce(
[](atom::util::Promise p, base::Value value) { p.Resolve(value); },
std::move(p)));
return handle;
}
void SendTo(mate::Arguments* args,
bool internal,
bool send_to_all,
@@ -82,6 +100,52 @@ class IPCRenderer : public mate::Wrappable<IPCRenderer> {
bool internal,
const std::string& channel,
const base::ListValue& arguments) {
// We aren't using a true synchronous mojo call here. We're calling an
// asynchronous method and blocking on the result. The reason we're doing
// this is a little complicated, so buckle up.
//
// Mojo has a concept of synchronous calls. However, synchronous calls are
// dangerous. In particular, it's quite possible for two processes to call
// synchronous methods on each other and cause a deadlock. Mojo has a
// mechanism to avoid this kind of deadlock: if a process is waiting on the
// result of a synchronous call, and it receives an incoming call for a
// synchronous method, it will process that request immediately, even
// though it's currently blocking. However, if it receives an incoming
// request for an _asynchronous_ method, that can't cause a deadlock, so it
// stashes the request on a queue to be processed once the synchronous
// thing it's waiting on returns.
//
// This behavior is useful for preventing deadlocks, but it is inconvenient
// here because it can result in messages being reordered. If the main
// process is awaiting the result of a synchronous call (which it does only
// very rarely, since it's bad to block the main process), and we send
// first an asynchronous message to the main process, followed by a
// synchronous message, then the main process will process the synchronous
// one first.
//
// It turns out, Electron has some dependency on message ordering,
// especially during window shutdown, and getting messages out of order can
// result in, for example, remote objects disappearing unexpectedly. To
// avoid these issues and guarantee consistent message ordering, we send
// all messages to the main process as asynchronous messages. This causes
// them to always be queued and processed in the same order they were
// received, even if they were received while the main process was waiting
// on a synchronous call.
//
// However, in the calling process, we still need to block on the result,
// because the caller is expecting a result synchronously. So we do a bit
// of a trick: we pass the Mojo handle over to a new thread, send the
// asynchronous message from that thread, and then block on the result.
// It's important that we pass the handle over to the new thread, because
// that allows Mojo to process incoming messages (most importantly, the
// response to our request) on the new thread. If we didn't pass it to a
// new thread, and instead sent the call from the main thread, we would
// never receive a response because Mojo wouldn't be able to run its
// message handling code, because the main thread would be tied up blocking
// on the WaitableEvent.
//
// Phew. If you got this far, here's a gold star: ⭐️
base::Value result;
// A task is posted to a separate thread to execute the request so that
@@ -96,7 +160,7 @@ class IPCRenderer : public mate::Wrappable<IPCRenderer> {
// We unbind the interface from this thread to pass it over to the worker
// thread temporarily. This requires that no callbacks be pending for this
// interface.
auto interface_info = electron_browser_ptr_.PassInterface();
auto interface_info = electron_browser_sync_ptr_.PassInterface();
task_runner->PostTask(
FROM_HERE, base::BindOnce(&IPCRenderer::SendMessageSyncOnWorkerThread,
base::Unretained(&interface_info),
@@ -104,7 +168,7 @@ class IPCRenderer : public mate::Wrappable<IPCRenderer> {
base::Unretained(&result), internal, channel,
base::Unretained(&arguments)));
response_received_event.Wait();
electron_browser_ptr_.Bind(std::move(interface_info));
electron_browser_sync_ptr_.Bind(std::move(interface_info));
return result;
}
@@ -135,6 +199,10 @@ class IPCRenderer : public mate::Wrappable<IPCRenderer> {
}
atom::mojom::ElectronBrowserPtr electron_browser_ptr_;
// We execute all synchronous calls on a separate mojo pipe, because
// of the way that we block on the result of synchronous calls.
atom::mojom::ElectronBrowserPtr electron_browser_sync_ptr_;
};
void Initialize(v8::Local<v8::Object> exports,

View File

@@ -5,6 +5,8 @@
#include "atom/renderer/api/atom_api_spell_check_client.h"
#include <map>
#include <set>
#include <unordered_set>
#include <utility>
#include <vector>
@@ -39,14 +41,16 @@ bool HasWordCharacters(const base::string16& text, int index) {
return false;
}
struct Word {
blink::WebTextCheckingResult result;
base::string16 text;
std::vector<base::string16> contraction_words;
};
} // namespace
class SpellCheckClient::SpellcheckRequest {
public:
// Map of individual words to list of occurrences in text
using WordMap =
std::map<base::string16, std::vector<blink::WebTextCheckingResult>>;
SpellcheckRequest(
const base::string16& text,
std::unique_ptr<blink::WebTextCheckingCompletion> completion)
@@ -55,11 +59,11 @@ class SpellCheckClient::SpellcheckRequest {
const base::string16& text() const { return text_; }
blink::WebTextCheckingCompletion* completion() { return completion_.get(); }
WordMap& wordmap() { return word_map_; }
std::vector<Word>& wordlist() { return word_list_; }
private:
base::string16 text_; // Text to be checked in this task.
WordMap word_map_; // WordMap to hold distinct words in text
base::string16 text_; // Text to be checked in this task.
std::vector<Word> word_list_; // List of Words found in text
// The interface to send the misspelled ranges to WebKit.
std::unique_ptr<blink::WebTextCheckingCompletion> completion_;
@@ -150,9 +154,9 @@ void SpellCheckClient::SpellCheckText() {
base::string16 word;
size_t word_start;
size_t word_length;
std::vector<base::string16> words;
auto& word_map = pending_request_param_->wordmap();
blink::WebTextCheckingResult result;
std::set<base::string16> words;
auto& word_list = pending_request_param_->wordlist();
Word word_entry;
for (;;) { // Run until end of text
const auto status =
text_iterator_.GetNextWord(&word, &word_start, &word_length);
@@ -161,23 +165,18 @@ void SpellCheckClient::SpellCheckText() {
if (status == SpellcheckWordIterator::IS_SKIPPABLE)
continue;
result.location = base::checked_cast<int>(word_start);
result.length = base::checked_cast<int>(word_length);
word_entry.result.location = base::checked_cast<int>(word_start);
word_entry.result.length = base::checked_cast<int>(word_length);
word_entry.text = word;
word_entry.contraction_words.clear();
word_list.push_back(word_entry);
words.insert(word);
// If the given word is a concatenated word of two or more valid words
// (e.g. "hello:hello"), we should treat it as a valid word.
std::vector<base::string16> contraction_words;
if (!IsContraction(scope, word, &contraction_words)) {
words.push_back(word);
word_map[word].push_back(result);
} else {
// For a contraction, we want check the spellings of each individual
// part, but mark the entire word incorrect if any part is misspelled
// Hence, we use the same word_start and word_length values for every
// part of the contraction.
for (const auto& w : contraction_words) {
words.push_back(w);
word_map[w].push_back(result);
if (IsContraction(scope, word, &word_entry.contraction_words)) {
for (const auto& w : word_entry.contraction_words) {
words.insert(w);
}
}
}
@@ -189,29 +188,35 @@ void SpellCheckClient::SpellCheckText() {
void SpellCheckClient::OnSpellCheckDone(
const std::vector<base::string16>& misspelled_words) {
std::vector<blink::WebTextCheckingResult> results;
std::unordered_set<base::string16> misspelled(misspelled_words.begin(),
misspelled_words.end());
auto& word_map = pending_request_param_->wordmap();
auto& word_list = pending_request_param_->wordlist();
// Take each word from the list of misspelled words received, find their
// corresponding WebTextCheckingResult that's stored in the map and pass
// all the results to blink through the completion callback.
for (const auto& word : misspelled_words) {
auto iter = word_map.find(word);
if (iter != word_map.end()) {
// Word found in map, now gather all the occurrences of the word
// from the map value
auto& words = iter->second;
results.insert(results.end(), words.begin(), words.end());
words.clear();
for (const auto& word : word_list) {
if (misspelled.find(word.text) != misspelled.end()) {
// If this is a contraction, iterate through parts and accept the word
// if none of them are misspelled
if (!word.contraction_words.empty()) {
auto all_correct = true;
for (const auto& contraction_word : word.contraction_words) {
if (misspelled.find(contraction_word) != misspelled.end()) {
all_correct = false;
break;
}
}
if (all_correct)
continue;
}
results.push_back(word.result);
}
}
pending_request_param_->completion()->DidFinishCheckingText(results);
pending_request_param_ = nullptr;
}
void SpellCheckClient::SpellCheckWords(
const SpellCheckScope& scope,
const std::vector<base::string16>& words) {
void SpellCheckClient::SpellCheckWords(const SpellCheckScope& scope,
const std::set<base::string16>& words) {
DCHECK(!scope.spell_check_.IsEmpty());
v8::Local<v8::FunctionTemplate> templ = mate::CreateFunctionTemplate(

View File

@@ -6,6 +6,7 @@
#define ATOM_RENDERER_API_ATOM_API_SPELL_CHECK_CLIENT_H_
#include <memory>
#include <set>
#include <string>
#include <vector>
@@ -68,7 +69,7 @@ class SpellCheckClient : public blink::WebSpellCheckPanelHostClient,
// The javascript function will callback OnSpellCheckDone
// with the results of all the misspelled words.
void SpellCheckWords(const SpellCheckScope& scope,
const std::vector<base::string16>& words);
const std::set<base::string16>& words);
// Returns whether or not the given word is a contraction of valid words
// (e.g. "word:word").

View File

@@ -104,6 +104,13 @@ void AtomRendererClient::DidCreateScriptContext(
// Setup node environment for each window.
node::Environment* env = node_bindings_->CreateEnvironment(context);
auto* command_line = base::CommandLine::ForCurrentProcess();
// If we have disabled the site instance overrides we should prevent loading
// any non-context aware native module
if (command_line->HasSwitch(switches::kDisableElectronSiteInstanceOverrides))
env->ForceOnlyContextAwareNativeModules();
env->WarnNonContextAwareNativeModules();
environments_.insert(env);
// Add Electron extended APIs.
@@ -145,9 +152,11 @@ void AtomRendererClient::WillReleaseScriptContext(
// Destroy the node environment. We only do this if node support has been
// enabled for sub-frames to avoid a change-of-behavior / introduce crashes
// for existing users.
// TODO(MarshallOfSOund): Free the environment regardless of this switch
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kNodeIntegrationInSubFrames))
// We also do this if we have disable electron site instance overrides to
// avoid memory leaks
auto* command_line = base::CommandLine::ForCurrentProcess();
if (command_line->HasSwitch(switches::kNodeIntegrationInSubFrames) ||
command_line->HasSwitch(switches::kDisableElectronSiteInstanceOverrides))
node::FreeEnvironment(env);
// ElectronBindings is tracking node environments.

View File

@@ -667,7 +667,7 @@ to the npm modules spec. You should usually also specify a `productName`
field, which is your application's full capitalized name, and which will be
preferred over `name` by Electron.
**[Deprecated Soon](modernization/property-updates.md)**
**[Deprecated](modernization/property-updates.md)**
### `app.setName(name)`
@@ -675,7 +675,7 @@ preferred over `name` by Electron.
Overrides the current application's name.
**[Deprecated Soon](modernization/property-updates.md)**
**[Deprecated](modernization/property-updates.md)**
### `app.getLocale()`
@@ -796,7 +796,7 @@ Returns `Object`:
### `app.setJumpList(categories)` _Windows_
* `categories` [JumpListCategory[]](structures/jump-list-category.md) or `null` - Array of `JumpListCategory` objects.
* `categories` [JumpListCategory[]](structures/jump-list-category.md) | `null` - Array of `JumpListCategory` objects.
Sets or removes a custom Jump List for the application, and returns one of the
following strings:
@@ -1070,13 +1070,13 @@ On macOS, it shows on the dock icon. On Linux, it only works for Unity launcher.
**Note:** Unity launcher requires the existence of a `.desktop` file to work,
for more information please read [Desktop Environment Integration][unity-requirement].
**[Deprecated Soon](modernization/property-updates.md)**
**[Deprecated](modernization/property-updates.md)**
### `app.getBadgeCount()` _Linux_ _macOS_
Returns `Integer` - The current value displayed in the counter badge.
**[Deprecated Soon](modernization/property-updates.md)**
**[Deprecated](modernization/property-updates.md)**
### `app.isUnityRunning()` _Linux_
@@ -1152,7 +1152,7 @@ technologies, such as screen readers, has been detected. See
https://www.chromium.org/developers/design-documents/accessibility for more
details.
**[Deprecated Soon](modernization/property-updates.md)**
**[Deprecated](modernization/property-updates.md)**
### `app.setAccessibilitySupportEnabled(enabled)` _macOS_ _Windows_
@@ -1165,7 +1165,7 @@ This API must be called after the `ready` event is emitted.
**Note:** Rendering accessibility tree can significantly affect the performance of your app. It should not be enabled by default.
**[Deprecated Soon](modernization/property-updates.md)**
**[Deprecated](modernization/property-updates.md)**
### `app.showAboutPanel()` _macOS_ _Linux_
@@ -1306,3 +1306,16 @@ This is the user agent that will be used when no user agent is set at the
`webContents` or `session` level. It is useful for ensuring that your entire
app has the same user agent. Set to a custom value as early as possible
in your app's initialization to ensure that your overridden value is used.
### `app.allowRendererProcessReuse`
A `Boolean` which when `true` disables the overrides that Electron has in place
to ensure renderer processes are restarted on every navigation. The current
default value for this property is `false`.
The intention is for these overrides to become disabled by default and then at
some point in the future this property will be removed. This property impacts
which native modules you can use in the renderer process. For more information
on the direction Electron is going with renderer process restarts and usage of
native modules in the renderer process please check out this
[Tracking Issue](https://github.com/electron/electron/issues/18397).

View File

@@ -88,7 +88,61 @@ Removes the specified `listener` from the listener array for the specified
Removes listeners of the specified `channel`.
## Event object
### `ipcMain.handle(channel, listener)`
* `channel` String
* `listener` Function<Promise> | Function<any>
* `event` IpcMainInvokeEvent
* `...args` any[]
Adds a handler for an `invoke`able IPC. This handler will be called whenever a
renderer calls `ipcRenderer.invoke(channel, ...args)`.
If `listener` returns a Promise, the eventual result of the promise will be
returned as a reply to the remote caller. Otherwise, the return value of the
listener will be used as the value of the reply.
```js
// Main process
ipcMain.handle('my-invokable-ipc', async (event, ...args) => {
const result = await somePromise(...args)
return result
})
// Renderer process
async () => {
const result = await ipcRenderer.invoke('my-invokable-ipc', arg1, arg2)
// ...
}
```
The `event` that is passed as the first argument to the handler is the same as
that passed to a regular event listener. It includes information about which
WebContents is the source of the invoke request.
### `ipcMain.handleOnce(channel, listener)`
* `channel` String
* `listener` Function<Promise> | Function<any>
* `event` IpcMainInvokeEvent
* `...args` any[]
Handles a single `invoke`able IPC message, then removes the listener. See
`ipcMain.handle(channel, listener)`.
### `ipcMain.removeHandler(channel)`
* `channel` String
Removes any handler for `channel`, if present.
## IpcMainEvent object
The documentation for the `event` object passed to the `callback` can be found
in the [`ipc-main-event`](structures/ipc-main-event.md) structure docs.
## IpcMainInvokeEvent object
The documentation for the `event` object passed to `handle` callbacks can be
found in the [`ipc-main-invoke-event`](structures/ipc-main-invoke-event.md)
structure docs.

View File

@@ -57,10 +57,39 @@ Removes all listeners, or those of the specified `channel`.
* `...args` any[]
Send a message to the main process asynchronously via `channel`, you can also
send arbitrary arguments. Arguments will be serialized in JSON internally and
send arbitrary arguments. Arguments will be serialized as JSON internally and
hence no functions or prototype chain will be included.
The main process handles it by listening for `channel` with [`ipcMain`](ipc-main.md) module.
The main process handles it by listening for `channel` with the
[`ipcMain`](ipc-main.md) module.
### `ipcRenderer.invoke(channel[, arg1][, arg2][, ...])`
* `channel` String
* `...args` any[]
Returns `Promise<any>` - Resolves with the response from the main process.
Send a message to the main process asynchronously via `channel` and expect an
asynchronous result. Arguments will be serialized as JSON internally and
hence no functions or prototype chain will be included.
The main process should listen for `channel` with
[`ipcMain.handle()`](ipc-main.md#ipcmainhandlechannel-listener).
For example:
```javascript
// Renderer process
ipcRenderer.invoke('some-name', someArgument).then((result) => {
// ...
})
// Main process
ipcMain.handle('some-name', async (event, someArgument) => {
const result = await doSomeWork(someArgument)
return result
})
```
### `ipcRenderer.sendSync(channel[, arg1][, arg2][, ...])`

View File

@@ -108,7 +108,7 @@ The following additional roles are available on _macOS_:
* `moveTabToNewWindow` - Map to the `moveTabToNewWindow` action.
* `window` - The submenu is a "Window" menu.
* `help` - The submenu is a "Help" menu.
* `services` - The submenu is a "Services" menu.
* `services` - The submenu is a ["Services"](https://developer.apple.com/documentation/appkit/nsapplication/1428608-servicesmenu?language=objc) menu. This is only intended for use in the Application Menu and is *not* the same as the "Services" submenu used in context menus in macOS apps, which is not implemented in Electron.
* `recentDocuments` - The submenu is an "Open Recent" menu.
* `clearRecentDocuments` - Map to the `clearRecentDocuments` action.

View File

@@ -277,7 +277,6 @@ window.addEventListener('contextmenu', (e) => {
</script>
```
## Notes on macOS Application Menu
macOS has a completely different style of application menu from Windows and
@@ -285,7 +284,7 @@ Linux. Here are some notes on making your app's menu more native-like.
### Standard Menus
On macOS there are many system-defined standard menus, like the `Services` and
On macOS there are many system-defined standard menus, like the [`Services`](https://developer.apple.com/documentation/appkit/nsapplication/1428608-servicesmenu?language=objc) and
`Windows` menus. To make your menu a standard menu, you should set your menu's
`role` to one of the following and Electron will recognize them and make them
become standard menus:

View File

@@ -32,8 +32,6 @@ The Electron team is currently undergoing an initiative to convert separate gett
* `paused`
* `Session` module
* `preloads`
* `SystemPreferences` module
* `appLevelAppearance`
* `webContents` module
* `zoomFactor`
* `zoomLevel`
@@ -58,3 +56,5 @@ The Electron team is currently undergoing an initiative to convert separate gett
* `name`
* `NativeImage`
* `isMacTemplateImage`
* `SystemPreferences` module
* `appLevelAppearance`

View File

@@ -275,13 +275,13 @@ Returns [`Size`](structures/size.md)
Marks the image as a template image.
**[Deprecated Soon](modernization/property-updates.md)**
**[Deprecated](modernization/property-updates.md)**
#### `image.isTemplateImage()`
Returns `Boolean` - Whether the image is a template image.
**[Deprecated Soon](modernization/property-updates.md)**
**[Deprecated](modernization/property-updates.md)**
#### `image.crop(rect)`

View File

@@ -5,4 +5,3 @@
* `sender` WebContents - Returns the `webContents` that sent the message
* `reply` Function - A function that will send an IPC message to the renderer frame that sent the original message that you are currently handling. You should use this method to "reply" to the sent message in order to guaruntee the reply will go to the correct process and frame.
* `...args` any[]
IpcRenderer

View File

@@ -0,0 +1,4 @@
# IpcMainInvokeEvent Object extends `Event`
* `frameId` Integer - The ID of the renderer frame that sent this message
* `sender` WebContents - Returns the `webContents` that sent the message

View File

@@ -380,12 +380,16 @@ You can use the `setAppLevelAppearance` API to set this value.
Sets the appearance setting for your application, this should override the
system default and override the value of `getEffectiveAppearance`.
**[Deprecated](modernization/property-updates.md)**
### `systemPreferences.canPromptTouchID()` _macOS_
Returns `Boolean` - whether or not this device has the ability to use Touch ID.
**NOTE:** This API will return `false` on macOS systems older than Sierra 10.12.2.
**[Deprecated](modernization/property-updates.md)**
### `systemPreferences.promptTouchID(reason)` _macOS_
* `reason` String - The reason you are asking for Touch ID authentication
@@ -439,3 +443,13 @@ Returns `Object`:
* `prefersReducedMotion` Boolean - Determines whether the user desires reduced motion based on platform APIs.
Returns an object with system animation settings.
## Properties
### `systemPreferences.appLevelAppearance` _macOS_
A `String` property that determines the macOS appearance setting for
your application. This maps to values in: [NSApplication.appearance](https://developer.apple.com/documentation/appkit/nsapplication/2967170-appearance?language=objc). Setting this will override the
system default as well as the value of `getEffectiveAppearance`.
Possible values that can be set are `dark` and `light`, and possible return values are `dark`, `light`, and `unknown`.

View File

@@ -78,6 +78,7 @@ auto_filenames = {
"docs/api/structures/gpu-feature-status.md",
"docs/api/structures/io-counters.md",
"docs/api/structures/ipc-main-event.md",
"docs/api/structures/ipc-main-invoke-event.md",
"docs/api/structures/ipc-renderer-event.md",
"docs/api/structures/jump-list-category.md",
"docs/api/structures/jump-list-item.md",

View File

@@ -1,8 +1,40 @@
import { EventEmitter } from 'events'
import { IpcMainInvokeEvent } from 'electron'
const emitter = new EventEmitter()
class IpcMain extends EventEmitter {
private _invokeHandlers: Map<string, (e: IpcMainInvokeEvent, ...args: any[]) => void> = new Map();
handle: Electron.IpcMain['handle'] = (method, fn) => {
if (this._invokeHandlers.has(method)) {
throw new Error(`Attempted to register a second handler for '${method}'`)
}
if (typeof fn !== 'function') {
throw new Error(`Expected handler to be a function, but found type '${typeof fn}'`)
}
this._invokeHandlers.set(method, async (e, ...args) => {
try {
(e as any)._reply(await Promise.resolve(fn(e, ...args)))
} catch (err) {
(e as any)._throw(err)
}
})
}
handleOnce: Electron.IpcMain['handleOnce'] = (method, fn) => {
this.handle(method, (e, ...args) => {
this.removeHandler(method)
return fn(e, ...args)
})
}
removeHandler (method: string) {
this._invokeHandlers.delete(method)
}
}
const ipcMain = new IpcMain()
// Do not throw exception when channel name is "error".
emitter.on('error', () => {})
ipcMain.on('error', () => {})
export default emitter
export default ipcMain

View File

@@ -1,10 +1,15 @@
'use strict'
const { EventEmitter } = require('events')
const { deprecate } = require('electron')
const { systemPreferences, SystemPreferences } = process.electronBinding('system_preferences')
// SystemPreferences is an EventEmitter.
Object.setPrototypeOf(SystemPreferences.prototype, EventEmitter.prototype)
EventEmitter.call(systemPreferences)
if ('appLevelAppearance' in systemPreferences) {
deprecate.fnToProperty(systemPreferences, 'appLevelAppearance', '_getAppLevelAppearance', '_setAppLevelAppearance')
}
module.exports = systemPreferences

View File

@@ -326,6 +326,19 @@ WebContents.prototype._init = function () {
}
})
this.on('-ipc-invoke', function (event, channel, args) {
event._reply = (result) => event.sendReply({ result })
event._throw = (error) => {
console.error(`Error occurred in handler for '${channel}':`, error)
event.sendReply({ error: error.toString() })
}
if (ipcMain._invokeHandlers.has(channel)) {
ipcMain._invokeHandlers.get(channel)(event, ...args)
} else {
event._throw(`No handler registered for '${channel}'`)
}
})
this.on('-ipc-message-sync', function (event, internal, channel, args) {
addReturnValueToEvent(event)
if (internal) {

View File

@@ -27,4 +27,11 @@ ipcRenderer.sendToAll = function (webContentsId, channel, ...args) {
return ipc.sendTo(internal, true, webContentsId, channel, args)
}
ipcRenderer.invoke = function (channel, ...args) {
return ipc.invoke(channel, args).then(({ error, result }) => {
if (error) { throw new Error(`Error invoking remote method '${channel}': ${error}`) }
return result
})
}
module.exports = ipcRenderer

View File

@@ -1,6 +1,6 @@
{
"name": "electron",
"version": "7.0.0-nightly.20190530",
"version": "7.0.0-nightly.20190602",
"repository": "https://github.com/electron/electron",
"description": "Build cross platform desktop apps with JavaScript, HTML, and CSS",
"devDependencies": {

View File

@@ -9,7 +9,6 @@ browser_compositor_mac.patch
can_create_window.patch
disable_hidden.patch
dom_storage_limits.patch
frame_host_manager.patch
out_of_process_instance.patch
render_widget_host_view_base.patch
render_widget_host_view_mac.patch
@@ -58,11 +57,9 @@ command-ismediakey.patch
tts.patch
printing.patch
verbose_generate_breakpad_symbols.patch
cross_site_document_resource_handler.patch
content_allow_embedder_to_prevent_locking_scheme_registry.patch
support_mixed_sandbox_with_zygote.patch
disable_color_correct_rendering.patch
disable_time_ticks_dcheck.patch
autofill_size_calculation.patch
revert_build_swiftshader_for_arm32.patch
fix_disable_usage_of_abort_report_np_in_mas_builds.patch
@@ -79,3 +76,5 @@ disable_custom_libcxx_on_windows.patch
feat_offscreen_rendering_with_viz_compositor.patch
worker_context_will_destroy.patch
fix_breakpad_symbol_generation_on_linux_arm.patch
cross_site_document_resource_handler.patch
frame_host_manager.patch

View File

@@ -22,31 +22,31 @@ index 2151c5b9698e9a2768875d04a2297956cc4c0d41..8a316a117ab367bcbac947894cbe1bc2
}
diff --git a/content/public/browser/content_browser_client.cc b/content/public/browser/content_browser_client.cc
index 2317ddf6a3c533f701fe44bf1b8114eb042c2189..9a823a98175c33c84b8d44a95c9d7d44568807ca 100644
index 3729dcc9ea3272c943754a92c6ed1d7a1fd8fcf3..787cd81b26508d3e04956721f0e7cec2f457aa60 100644
--- a/content/public/browser/content_browser_client.cc
+++ b/content/public/browser/content_browser_client.cc
@@ -61,6 +61,10 @@ ContentBrowserClient::SiteInstanceForNavigationType ContentBrowserClient::Should
return SiteInstanceForNavigationType::ASK_CHROMIUM;
@@ -56,6 +56,10 @@ BrowserMainParts* ContentBrowserClient::CreateBrowserMainParts(
return nullptr;
}
+bool ContentBrowserClient::ShouldBypassCORB(int render_process_id) const {
+ return false;
+}
+
BrowserMainParts* ContentBrowserClient::CreateBrowserMainParts(
const MainFunctionParams& parameters) {
return nullptr;
void ContentBrowserClient::PostAfterStartupTask(
const base::Location& from_here,
const scoped_refptr<base::TaskRunner>& task_runner,
diff --git a/content/public/browser/content_browser_client.h b/content/public/browser/content_browser_client.h
index a81f40507b2233c3bde03b940cccd6be0aaa4926..1a208d24bae80277e4a60f4180bb7f95c38561ce 100644
index 4c84fb3648b3de36015b325246559f8aefe2ebd5..bf9b3a601fc16d5070e4467a258a047f47b059f3 100644
--- a/content/public/browser/content_browser_client.h
+++ b/content/public/browser/content_browser_client.h
@@ -242,6 +242,9 @@ class CONTENT_EXPORT ContentBrowserClient {
content::RenderFrameHost* rfh,
content::SiteInstance* pending_site_instance) {}
@@ -219,6 +219,9 @@ class CONTENT_EXPORT ContentBrowserClient {
virtual BrowserMainParts* CreateBrowserMainParts(
const MainFunctionParams& parameters);
+ // Electron: Allows bypassing CORB checks for a renderer process.
+ virtual bool ShouldBypassCORB(int render_process_id) const;
+
// Allows the embedder to set any number of custom BrowserMainParts
// implementations for the browser startup code. See comments in
// browser_main_parts.h.
// Allows the embedder to change the default behavior of
// BrowserThread::PostAfterStartupTask to better match whatever
// definition of "startup" the embedder has in mind. This may be

View File

@@ -1,27 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Cheng Zhao <zcbenz@gmail.com>
Date: Tue, 15 Jan 2019 14:57:02 -0700
Subject: disable_time_ticks_dcheck.patch
The DCHECK is failing for some reason.
diff --git a/content/common/inter_process_time_ticks_converter.cc b/content/common/inter_process_time_ticks_converter.cc
index 128abab37eb8a96535ef92ebf11a463e863cc485..4d8e5e9c05b11083a69537d5badc85924b6fbae2 100644
--- a/content/common/inter_process_time_ticks_converter.cc
+++ b/content/common/inter_process_time_ticks_converter.cc
@@ -55,13 +55,13 @@ LocalTimeTicks InterProcessTimeTicksConverter::ToLocalTimeTicks(
RemoteTimeDelta remote_delta = remote_time_ticks - remote_lower_bound_;
- DCHECK_LE(remote_time_ticks, remote_upper_bound_);
+ // DCHECK_LE(remote_time_ticks, remote_upper_bound_);
return local_base_time_ + ToLocalTimeDelta(remote_delta);
}
LocalTimeDelta InterProcessTimeTicksConverter::ToLocalTimeDelta(
RemoteTimeDelta remote_delta) const {
- DCHECK_LE(remote_lower_bound_ + remote_delta, remote_upper_bound_);
+ // DCHECK_LE(remote_lower_bound_ + remote_delta, remote_upper_bound_);
// For remote times that come before remote time range, apply just time
// offset and ignore scaling, so as to avoid extrapolation error for values

View File

@@ -4,94 +4,104 @@ Date: Wed, 14 Nov 2018 20:38:46 +0530
Subject: frame_host_manager.patch
Allows embedder to intercept site instances chosen by chromium
and respond with custom instance.
and respond with custom instance. Also allows for us to at-runtime
enable or disable this patch.
diff --git a/content/browser/frame_host/render_frame_host_manager.cc b/content/browser/frame_host/render_frame_host_manager.cc
index b5301d164138f21ca8ae01abfb153efde51ec324..91efc5f94f61f7bccc2ff802851097df8eb52818 100644
index b5301d164138f21ca8ae01abfb153efde51ec324..886b6d3beb3e2d7b13a15af830bea6fb5a567cba 100644
--- a/content/browser/frame_host/render_frame_host_manager.cc
+++ b/content/browser/frame_host/render_frame_host_manager.cc
@@ -2127,6 +2127,16 @@ bool RenderFrameHostManager::InitRenderView(
@@ -2127,6 +2127,20 @@ bool RenderFrameHostManager::InitRenderView(
scoped_refptr<SiteInstance>
RenderFrameHostManager::GetSiteInstanceForNavigationRequest(
const NavigationRequest& request) {
+ BrowserContext* browser_context =
+ delegate_->GetControllerForRenderManager().GetBrowserContext();
+ // If the navigation can swap SiteInstances, compute the SiteInstance it
+ // should use.
+ // TODO(clamy): We should also consider as a candidate SiteInstance the
+ // speculative SiteInstance that was computed on redirects.
+ scoped_refptr<SiteInstance> candidate_site_instance =
+ speculative_render_frame_host_
+ ? speculative_render_frame_host_->GetSiteInstance()
+ : nullptr;
+ BrowserContext* browser_context = nullptr;
+ scoped_refptr<SiteInstance> candidate_site_instance;
+ if (!GetContentClient()->browser()->CanUseCustomSiteInstance()) {
+ browser_context =
+ delegate_->GetControllerForRenderManager().GetBrowserContext();
+ // If the navigation can swap SiteInstances, compute the SiteInstance it
+ // should use.
+ // TODO(clamy): We should also consider as a candidate SiteInstance the
+ // speculative SiteInstance that was computed on redirects.
+ candidate_site_instance =
+ speculative_render_frame_host_
+ ? speculative_render_frame_host_->GetSiteInstance()
+ : nullptr;
+ }
// First, check if the navigation can switch SiteInstances. If not, the
// navigation should use the current SiteInstance.
SiteInstance* current_site_instance = render_frame_host_->GetSiteInstance();
@@ -2159,6 +2169,51 @@ RenderFrameHostManager::GetSiteInstanceForNavigationRequest(
@@ -2159,6 +2173,53 @@ RenderFrameHostManager::GetSiteInstanceForNavigationRequest(
request.common_params().url);
no_renderer_swap_allowed |=
request.from_begin_navigation() && !can_renderer_initiate_transfer;
+
+ bool has_response_started =
+ (request.state() == NavigationRequest::RESPONSE_STARTED ||
+ request.state() == NavigationRequest::FAILED) &&
+ !speculative_render_frame_host_;
+ // Gives user a chance to choose a custom site instance.
+ SiteInstance* affinity_site_instance = nullptr;
+ scoped_refptr<SiteInstance> overriden_site_instance;
+ ContentBrowserClient::SiteInstanceForNavigationType siteInstanceType =
+ GetContentClient()->browser()->ShouldOverrideSiteInstanceForNavigation(
+ current_frame_host(), speculative_frame_host(), browser_context,
+ request.common_params().url, has_response_started,
+ &affinity_site_instance);
+ switch (siteInstanceType) {
+ case ContentBrowserClient::SiteInstanceForNavigationType::
+ FORCE_CANDIDATE_OR_NEW:
+ overriden_site_instance =
+ candidate_site_instance
+ ? candidate_site_instance
+ : SiteInstance::CreateForURL(browser_context,
+ request.common_params().url);
+ break;
+ case ContentBrowserClient::SiteInstanceForNavigationType::FORCE_CURRENT:
+ overriden_site_instance = render_frame_host_->GetSiteInstance();
+ break;
+ case ContentBrowserClient::SiteInstanceForNavigationType::FORCE_AFFINITY:
+ DCHECK(affinity_site_instance);
+ overriden_site_instance =
+ scoped_refptr<SiteInstance>(affinity_site_instance);
+ break;
+ case ContentBrowserClient::SiteInstanceForNavigationType::ASK_CHROMIUM:
+ DCHECK(!affinity_site_instance);
+ break;
+ default:
+ break;
+ }
+ if (overriden_site_instance) {
+ if (siteInstanceType ==
+ ContentBrowserClient::SiteInstanceForNavigationType::
+ FORCE_CANDIDATE_OR_NEW) {
+ GetContentClient()->browser()->RegisterPendingSiteInstance(
+ render_frame_host_.get(), overriden_site_instance.get());
+ if (!GetContentClient()->browser()->CanUseCustomSiteInstance()) {
+ bool has_response_started =
+ (request.state() == NavigationRequest::RESPONSE_STARTED ||
+ request.state() == NavigationRequest::FAILED) &&
+ !speculative_render_frame_host_;
+ // Gives user a chance to choose a custom site instance.
+ SiteInstance* affinity_site_instance = nullptr;
+ scoped_refptr<SiteInstance> overriden_site_instance;
+ ContentBrowserClient::SiteInstanceForNavigationType siteInstanceType =
+ GetContentClient()->browser()->ShouldOverrideSiteInstanceForNavigation(
+ current_frame_host(), speculative_frame_host(), browser_context,
+ request.common_params().url, has_response_started,
+ &affinity_site_instance);
+ switch (siteInstanceType) {
+ case ContentBrowserClient::SiteInstanceForNavigationType::
+ FORCE_CANDIDATE_OR_NEW:
+ overriden_site_instance =
+ candidate_site_instance
+ ? candidate_site_instance
+ : SiteInstance::CreateForURL(browser_context,
+ request.common_params().url);
+ break;
+ case ContentBrowserClient::SiteInstanceForNavigationType::FORCE_CURRENT:
+ overriden_site_instance = render_frame_host_->GetSiteInstance();
+ break;
+ case ContentBrowserClient::SiteInstanceForNavigationType::FORCE_AFFINITY:
+ DCHECK(affinity_site_instance);
+ overriden_site_instance =
+ scoped_refptr<SiteInstance>(affinity_site_instance);
+ break;
+ case ContentBrowserClient::SiteInstanceForNavigationType::ASK_CHROMIUM:
+ DCHECK(!affinity_site_instance);
+ break;
+ default:
+ break;
+ }
+ if (overriden_site_instance) {
+ if (siteInstanceType ==
+ ContentBrowserClient::SiteInstanceForNavigationType::
+ FORCE_CANDIDATE_OR_NEW) {
+ GetContentClient()->browser()->RegisterPendingSiteInstance(
+ render_frame_host_.get(), overriden_site_instance.get());
+ }
+ return overriden_site_instance;
+ }
+ return overriden_site_instance;
+ }
} else {
// Subframe navigations will use the current renderer, unless specifically
// allowed to swap processes.
@@ -2170,23 +2225,17 @@ RenderFrameHostManager::GetSiteInstanceForNavigationRequest(
@@ -2170,23 +2231,28 @@ RenderFrameHostManager::GetSiteInstanceForNavigationRequest(
if (no_renderer_swap_allowed && !should_swap_for_error_isolation)
return scoped_refptr<SiteInstance>(current_site_instance);
- // If the navigation can swap SiteInstances, compute the SiteInstance it
- // should use.
- // TODO(clamy): We should also consider as a candidate SiteInstance the
- // speculative SiteInstance that was computed on redirects.
+ if (GetContentClient()->browser()->CanUseCustomSiteInstance()) {
// If the navigation can swap SiteInstances, compute the SiteInstance it
// should use.
// TODO(clamy): We should also consider as a candidate SiteInstance the
// speculative SiteInstance that was computed on redirects.
- SiteInstance* candidate_site_instance =
- speculative_render_frame_host_
- ? speculative_render_frame_host_->GetSiteInstance()
- : nullptr;
-
+ candidate_site_instance =
speculative_render_frame_host_
? speculative_render_frame_host_->GetSiteInstance()
: nullptr;
+ }
scoped_refptr<SiteInstance> dest_site_instance = GetSiteInstanceForNavigation(
request.common_params().url, request.source_site_instance(),
- request.dest_site_instance(), candidate_site_instance,
@@ -108,13 +118,17 @@ index b5301d164138f21ca8ae01abfb153efde51ec324..91efc5f94f61f7bccc2ff802851097df
}
diff --git a/content/public/browser/content_browser_client.cc b/content/public/browser/content_browser_client.cc
index 3729dcc9ea3272c943754a92c6ed1d7a1fd8fcf3..2317ddf6a3c533f701fe44bf1b8114eb042c2189 100644
index 787cd81b26508d3e04956721f0e7cec2f457aa60..8e62a5dd26595411757e03078ed0e44646c47a52 100644
--- a/content/public/browser/content_browser_client.cc
+++ b/content/public/browser/content_browser_client.cc
@@ -51,6 +51,16 @@ void OverrideOnBindInterface(const service_manager::BindSourceInfo& remote_info,
@@ -51,6 +51,20 @@ void OverrideOnBindInterface(const service_manager::BindSourceInfo& remote_info,
handle);
}
+bool ContentBrowserClient::CanUseCustomSiteInstance() {
+ return false;
+}
+
+ContentBrowserClient::SiteInstanceForNavigationType ContentBrowserClient::ShouldOverrideSiteInstanceForNavigation(
+ content::RenderFrameHost* current_rfh,
+ content::RenderFrameHost* speculative_rfh,
@@ -129,10 +143,10 @@ index 3729dcc9ea3272c943754a92c6ed1d7a1fd8fcf3..2317ddf6a3c533f701fe44bf1b8114eb
const MainFunctionParams& parameters) {
return nullptr;
diff --git a/content/public/browser/content_browser_client.h b/content/public/browser/content_browser_client.h
index 4c84fb3648b3de36015b325246559f8aefe2ebd5..a81f40507b2233c3bde03b940cccd6be0aaa4926 100644
index bf9b3a601fc16d5070e4467a258a047f47b059f3..3c35eddc2498157c2b98bab55991d8aa195954f6 100644
--- a/content/public/browser/content_browser_client.h
+++ b/content/public/browser/content_browser_client.h
@@ -211,8 +211,37 @@ CONTENT_EXPORT void OverrideOnBindInterface(
@@ -211,8 +211,40 @@ CONTENT_EXPORT void OverrideOnBindInterface(
// the observer interfaces.)
class CONTENT_EXPORT ContentBrowserClient {
public:
@@ -153,6 +167,9 @@ index 4c84fb3648b3de36015b325246559f8aefe2ebd5..a81f40507b2233c3bde03b940cccd6be
+ };
virtual ~ContentBrowserClient() {}
+ // Electron: Allows disabling the below ShouldOverride patch
+ virtual bool CanUseCustomSiteInstance();
+
+ // Electron: Allows overriding the SiteInstance when navigating.
+ virtual SiteInstanceForNavigationType ShouldOverrideSiteInstanceForNavigation(
+ content::RenderFrameHost* current_rfh,

View File

@@ -3,7 +3,5 @@
"src/electron/patches/common/boringssl": "src/third_party/boringssl/src",
"src/electron/patches/common/ffmpeg": "src/third_party/ffmpeg",
"src/electron/patches/common/v8": "src/v8"
}

View File

@@ -1 +0,0 @@
build_gn.patch

View File

@@ -1,24 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Ales Pergl <alpergl@microsoft.com>
Date: Mon, 22 Oct 2018 10:45:25 -0700
Subject: build_gn.patch
Chromium's Mac toolchain sets the "install_name" linker parameter only
when "is_component_build" is true, but we want to set even if it's false,
because we are making a dylib which will be distributed inside a bundle.
diff --git a/BUILD.gn b/BUILD.gn
index fd79bc4eaba90ab5e8c6df9942dbfaeeebabbc73..1e1859d5aa1567da8a768c870d8a477b052f50ec 100755
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -397,6 +397,10 @@ if (is_component_ffmpeg) {
# So we can append below and assume they're defined.
ldflags = []
+ if (!is_component_build && is_mac) {
+ ldflags += [ "-Wl,-install_name,@rpath/libffmpeg.dylib" ]
+ }
+
if (is_fuchsia || (is_posix && !is_mac)) {
# Fixes warnings PIC relocation when building as component.
ldflags += [

View File

@@ -49,11 +49,30 @@ async function downloadArtifact (name, buildNum, dest) {
process.exit(1)
} else {
console.log(`Downloading ${artifactToDownload.url}.`)
await downloadFile(artifactToDownload.url, dest)
console.log(`Successfully downloaded ${name}.`)
let downloadError = false
await downloadWithRetry(artifactToDownload.url, dest).catch(err => {
console.log(`Error downnloading ${artifactToDownload.url} :`, err)
downloadError = true
})
if (!downloadError) {
console.log(`Successfully downloaded ${name}.`)
}
}
}
async function downloadWithRetry (url, directory) {
let lastError
for (let i = 0; i < 5; i++) {
console.log(`Attempting to download ${url} - attempt #${(i + 1)}`)
try {
return await downloadFile(url, directory)
} catch (err) {
lastError = err
}
}
throw lastError
}
function downloadFile (url, directory) {
return new Promise((resolve, reject) => {
const nuggetOpts = {

View File

@@ -1332,6 +1332,26 @@ describe('default behavior', () => {
expect(app.userAgentFallback).to.equal(initialValue)
})
})
describe('app.allowRendererProcessReuse', () => {
it('should default to false', () => {
expect(app.allowRendererProcessReuse).to.equal(false)
})
it('should cause renderer processes to get new PIDs when false', async () => {
const output = await runTestApp('site-instance-overrides', 'false')
expect(output[0]).to.be.a('number').that.is.greaterThan(0)
expect(output[1]).to.be.a('number').that.is.greaterThan(0)
expect(output[0]).to.not.equal(output[1])
})
it('should cause renderer processes to keep the same PID when true', async () => {
const output = await runTestApp('site-instance-overrides', 'true')
expect(output[0]).to.be.a('number').that.is.greaterThan(0)
expect(output[1]).to.be.a('number').that.is.greaterThan(0)
expect(output[0]).to.equal(output[1])
})
})
})
async function runTestApp (name: string, ...args: any[]) {

109
spec-main/api-ipc-spec.ts Normal file
View File

@@ -0,0 +1,109 @@
import * as chai from 'chai'
import * as chaiAsPromised from 'chai-as-promised'
import { BrowserWindow, ipcMain, IpcMainInvokeEvent } from 'electron'
const { expect } = chai
chai.use(chaiAsPromised)
describe('ipc module', () => {
describe('invoke', () => {
let w = (null as unknown as BrowserWindow);
before(async () => {
w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } })
await w.loadURL('about:blank')
})
after(async () => {
w.destroy()
})
async function rendererInvoke(...args: any[]) {
const {ipcRenderer} = require('electron')
try {
const result = await ipcRenderer.invoke('test', ...args)
ipcRenderer.send('result', {result})
} catch (e) {
ipcRenderer.send('result', {error: e.message})
}
}
it('receives a response from a synchronous handler', async () => {
ipcMain.handleOnce('test', (e: IpcMainInvokeEvent, arg: number) => {
expect(arg).to.equal(123)
return 3
})
const done = new Promise(resolve => ipcMain.once('result', (e, arg) => {
expect(arg).to.deep.equal({result: 3})
resolve()
}))
await w.webContents.executeJavaScript(`(${rendererInvoke})(123)`)
await done
})
it('receives a response from an asynchronous handler', async () => {
ipcMain.handleOnce('test', async (e: IpcMainInvokeEvent, arg: number) => {
expect(arg).to.equal(123)
await new Promise(resolve => setImmediate(resolve))
return 3
})
const done = new Promise(resolve => ipcMain.once('result', (e, arg) => {
expect(arg).to.deep.equal({result: 3})
resolve()
}))
await w.webContents.executeJavaScript(`(${rendererInvoke})(123)`)
await done
})
it('receives an error from a synchronous handler', async () => {
ipcMain.handleOnce('test', () => {
throw new Error('some error')
})
const done = new Promise(resolve => ipcMain.once('result', (e, arg) => {
expect(arg.error).to.match(/some error/)
resolve()
}))
await w.webContents.executeJavaScript(`(${rendererInvoke})()`)
await done
})
it('receives an error from an asynchronous handler', async () => {
ipcMain.handleOnce('test', async () => {
await new Promise(resolve => setImmediate(resolve))
throw new Error('some error')
})
const done = new Promise(resolve => ipcMain.once('result', (e, arg) => {
expect(arg.error).to.match(/some error/)
resolve()
}))
await w.webContents.executeJavaScript(`(${rendererInvoke})()`)
await done
})
it('throws an error if no handler is registered', async () => {
const done = new Promise(resolve => ipcMain.once('result', (e, arg) => {
expect(arg.error).to.match(/No handler registered/)
resolve()
}))
await w.webContents.executeJavaScript(`(${rendererInvoke})()`)
await done
})
it('throws an error when invoking a handler that was removed', async () => {
ipcMain.handle('test', () => {})
ipcMain.removeHandler('test')
const done = new Promise(resolve => ipcMain.once('result', (e, arg) => {
expect(arg.error).to.match(/No handler registered/)
resolve()
}))
await w.webContents.executeJavaScript(`(${rendererInvoke})()`)
await done
})
it('forbids multiple handlers', async () => {
ipcMain.handle('test', () => {})
expect(() => { ipcMain.handle('test', () => {}) }).to.throw(/second handler/)
ipcMain.removeHandler('test')
})
})
})

View File

@@ -9,7 +9,7 @@ const ChildProcess = require('child_process')
const { closeWindow } = require('./window-helpers')
const { emittedOnce } = require('./events-helpers')
const { session, BrowserWindow, net } = require('electron')
const { session, BrowserWindow, net, ipcMain } = require('electron')
const { expect } = chai
/* The whole session API doesn't use standard callbacks */
@@ -26,7 +26,8 @@ describe('session module', () => {
width: 400,
height: 400,
webPreferences: {
nodeIntegration: true
nodeIntegration: true,
webviewTag: true,
}
})
})
@@ -62,23 +63,18 @@ describe('session module', () => {
const name = '0'
const value = '0'
it('should get cookies', (done) => {
it('should get cookies', async () => {
const server = http.createServer((req, res) => {
res.setHeader('Set-Cookie', [`${name}=${value}`])
res.end('finished')
server.close()
})
server.listen(0, '127.0.0.1', () => {
w.webContents.once('did-finish-load', async () => {
const list = await w.webContents.session.cookies.get({ url })
const cookie = list.find(cookie => cookie.name === name)
expect(cookie).to.exist.and.to.have.property('value', value)
done()
})
const { port } = server.address()
w.loadURL(`${url}:${port}`)
})
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
const { port } = server.address()
await w.loadURL(`${url}:${port}`)
const list = await w.webContents.session.cookies.get({ url })
const cookie = list.find(cookie => cookie.name === name)
expect(cookie).to.exist.and.to.have.property('value', value)
})
it('sets cookies', async () => {
@@ -92,17 +88,12 @@ describe('session module', () => {
})
it('yields an error when setting a cookie with missing required fields', async () => {
let error
try {
await expect((async () => {
const { cookies } = session.defaultSession
const name = '1'
const value = '1'
await cookies.set({ url: '', name, value })
} catch (e) {
error = e
}
expect(error).is.an('Error')
expect(error).to.have.property('message').which.equals('Failed to get cookie domain')
})()).to.eventually.be.rejectedWith('Failed to get cookie domain')
})
it('should overwrite previous cookies', async () => {
@@ -173,7 +164,7 @@ describe('session module', () => {
})
describe('ses.cookies.flushStore()', async () => {
describe('flushes the cookies to disk', async () => {
it('flushes the cookies to disk', async () => {
const name = 'foo'
const value = 'bar'
const { cookies } = session.defaultSession
@@ -185,29 +176,26 @@ describe('session module', () => {
it('should survive an app restart for persistent partition', async () => {
const appPath = path.join(fixtures, 'api', 'cookie-app')
const electronPath = process.execPath
const test = (result, phase) => {
const runAppWithPhase = (phase) => {
return new Promise((resolve, reject) => {
let output = ''
const appProcess = ChildProcess.spawn(
electronPath,
process.execPath,
[appPath],
{ env: { PHASE: phase, ...process.env } }
)
appProcess.stdout.on('data', data => { output += data })
appProcess.stdout.on('end', () => {
output = output.replace(/(\r\n|\n|\r)/gm, '')
expect(output).to.equal(result)
resolve()
resolve(output.replace(/(\r\n|\n|\r)/gm, ''))
})
})
}
await test('011', 'one')
await test('110', 'two')
expect(await runAppWithPhase('one')).to.equal('011')
expect(await runAppWithPhase('two')).to.equal('110')
})
})
@@ -228,16 +216,7 @@ describe('session module', () => {
})
describe('will-download event', () => {
beforeEach(() => {
if (w != null) w.destroy()
w = new BrowserWindow({
show: false,
width: 400,
height: 400
})
})
it('can cancel default download behavior', (done) => {
it('can cancel default download behavior', async () => {
const mockFile = Buffer.alloc(1024)
const contentDisposition = 'inline; filename="mockFile.txt"'
const downloadServer = http.createServer((req, res) => {
@@ -249,22 +228,23 @@ describe('session module', () => {
res.end(mockFile)
downloadServer.close()
})
await new Promise(resolve => downloadServer.listen(0, '127.0.0.1', resolve))
downloadServer.listen(0, '127.0.0.1', () => {
const port = downloadServer.address().port
const url = `http://127.0.0.1:${port}/`
const port = downloadServer.address().port
const url = `http://127.0.0.1:${port}/`
const downloadPrevented = new Promise(resolve => {
w.webContents.session.once('will-download', function (e, item) {
e.preventDefault()
expect(item.getURL()).to.equal(url)
expect(item.getFilename()).to.equal('mockFile.txt')
setImmediate(() => {
expect(() => item.getURL()).to.throw('Object has been destroyed')
done()
})
resolve(item)
})
w.loadURL(url)
})
w.loadURL(url)
const item = await downloadPrevented
expect(item.getURL()).to.equal(url)
expect(item.getFilename()).to.equal('mockFile.txt')
await new Promise(setImmediate)
expect(() => item.getURL()).to.throw('Object has been destroyed')
})
})
@@ -277,7 +257,7 @@ describe('session module', () => {
callback({ data: 'test', mimeType: 'text/html' })
}
beforeEach((done) => {
beforeEach(async () => {
if (w != null) w.destroy()
w = new BrowserWindow({
show: false,
@@ -286,13 +266,11 @@ describe('session module', () => {
}
})
customSession = session.fromPartition(partitionName)
customSession.protocol.registerStringProtocol(protocolName, handler, (error) => {
done(error != null ? error : undefined)
})
await customSession.protocol.registerStringProtocol(protocolName, handler)
})
afterEach((done) => {
customSession.protocol.unregisterProtocol(protocolName, () => done())
afterEach(async () => {
await customSession.protocol.unregisterProtocol(protocolName)
customSession = null
})
@@ -314,13 +292,13 @@ describe('session module', () => {
let server = null
let customSession = null
beforeEach((done) => {
beforeEach(async () => {
customSession = session.fromPartition('proxyconfig')
// FIXME(deepak1556): This is just a hack to force
// creation of request context which in turn initializes
// the network context, can be removed with network
// service enabled.
customSession.clearHostResolverCache().then(() => done())
await customSession.clearHostResolverCache()
})
afterEach(() => {
@@ -362,19 +340,11 @@ describe('session module', () => {
})
res.end(pac)
})
return new Promise((resolve, reject) => {
server.listen(0, '127.0.0.1', async () => {
try {
const config = { pacScript: `http://127.0.0.1:${server.address().port}` }
await customSession.setProxy(config)
const proxy = await customSession.resolveProxy('https://google.com')
expect(proxy).to.equal('PROXY myproxy:8132')
resolve()
} catch (error) {
reject(error)
}
})
})
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
const config = { pacScript: `http://127.0.0.1:${server.address().port}` }
await customSession.setProxy(config)
const proxy = await customSession.resolveProxy('https://google.com')
expect(proxy).to.equal('PROXY myproxy:8132')
})
it('allows bypassing proxy settings', async () => {
@@ -389,24 +359,14 @@ describe('session module', () => {
})
describe('ses.getBlobData()', () => {
const scheme = 'cors-blob'
const protocol = session.defaultSession.protocol
const url = `${scheme}://host`
after(async () => {
await protocol.unregisterProtocol(scheme)
})
it('returns blob data for uuid', (done) => {
const scheme = 'cors-blob'
const protocol = session.defaultSession.protocol
const url = `${scheme}://host`
before(() => {
if (w != null) w.destroy()
w = new BrowserWindow({ show: false })
})
after((done) => {
protocol.unregisterProtocol(scheme, () => {
closeWindow(w).then(() => {
w = null
done()
})
})
})
const postData = JSON.stringify({
type: 'blob',
value: 'hello'
@@ -460,26 +420,23 @@ describe('session module', () => {
server.listen(0, '127.0.0.1', done)
})
afterEach(() => {
afterEach((done) => {
session.defaultSession.setCertificateVerifyProc(null)
server.close()
server.close(done)
})
it('accepts the request when the callback is called with 0', (done) => {
it('accepts the request when the callback is called with 0', async () => {
session.defaultSession.setCertificateVerifyProc(({ hostname, certificate, verificationResult, errorCode }, callback) => {
expect(['net::ERR_CERT_AUTHORITY_INVALID', 'net::ERR_CERT_COMMON_NAME_INVALID'].includes(verificationResult)).to.be.true
expect([-202, -200].includes(errorCode)).to.be.true
callback(0)
})
w.webContents.once('did-finish-load', () => {
expect(w.webContents.getTitle()).to.equal('hello')
done()
})
w.loadURL(`https://127.0.0.1:${server.address().port}`)
await w.loadURL(`https://127.0.0.1:${server.address().port}`)
expect(w.webContents.getTitle()).to.equal('hello')
})
it('rejects the request when the callback is called with -2', (done) => {
it('rejects the request when the callback is called with -2', async () => {
session.defaultSession.setCertificateVerifyProc(({ hostname, certificate, verificationResult }, callback) => {
expect(hostname).to.equal('127.0.0.1')
expect(certificate.issuerName).to.equal('Intermediate CA')
@@ -496,11 +453,8 @@ describe('session module', () => {
})
const url = `https://127.0.0.1:${server.address().port}`
w.webContents.once('did-finish-load', () => {
expect(w.webContents.getTitle()).to.equal(url)
done()
})
w.loadURL(url)
await expect(w.loadURL(url)).to.eventually.be.rejectedWith(/ERR_FAILED/)
expect(w.webContents.getTitle()).to.equal(url)
})
})
@@ -557,4 +511,292 @@ describe('session module', () => {
})
})
})
describe('DownloadItem', () => {
const mockPDF = Buffer.alloc(1024 * 1024 * 5)
const downloadFilePath = path.join(__dirname, '..', 'fixtures', 'mock.pdf')
const protocolName = 'custom-dl'
const contentDisposition = 'inline; filename="mock.pdf"'
let address = null
let downloadServer = null
before(async () => {
downloadServer = http.createServer((req, res) => {
address = downloadServer.address()
res.writeHead(200, {
'Content-Length': mockPDF.length,
'Content-Type': 'application/pdf',
'Content-Disposition': req.url === '/?testFilename' ? 'inline' : contentDisposition
})
res.end(mockPDF)
})
await new Promise(resolve => downloadServer.listen(0, '127.0.0.1', resolve))
})
after(async () => {
await new Promise(resolve => downloadServer.close(resolve))
})
const isPathEqual = (path1, path2) => {
return path.relative(path1, path2) === ''
}
const assertDownload = (state, item, isCustom = false) => {
expect(state).to.equal('completed')
expect(item.getFilename()).to.equal('mock.pdf')
expect(path.isAbsolute(item.getSavePath())).to.equal(true)
expect(isPathEqual(item.getSavePath(), downloadFilePath)).to.equal(true)
if (isCustom) {
expect(item.getURL()).to.equal(`${protocolName}://item`)
} else {
expect(item.getURL()).to.be.equal(`${url}:${address.port}/`)
}
expect(item.getMimeType()).to.equal('application/pdf')
expect(item.getReceivedBytes()).to.equal(mockPDF.length)
expect(item.getTotalBytes()).to.equal(mockPDF.length)
expect(item.getContentDisposition()).to.equal(contentDisposition)
expect(fs.existsSync(downloadFilePath)).to.equal(true)
fs.unlinkSync(downloadFilePath)
}
it('can download using WebContents.downloadURL', (done) => {
const port = downloadServer.address().port
w.webContents.session.once('will-download', function (e, item) {
item.setSavePath(downloadFilePath)
item.on('done', function (e, state) {
assertDownload(state, item)
done()
})
})
w.webContents.downloadURL(`${url}:${port}`)
})
it('can download from custom protocols using WebContents.downloadURL', (done) => {
const protocol = session.defaultSession.protocol
const port = downloadServer.address().port
const handler = (ignoredError, callback) => {
callback({ url: `${url}:${port}` })
}
protocol.registerHttpProtocol(protocolName, handler, (error) => {
if (error) return done(error)
w.webContents.session.once('will-download', function (e, item) {
item.setSavePath(downloadFilePath)
item.on('done', function (e, state) {
assertDownload(state, item, true)
done()
})
})
w.webContents.downloadURL(`${protocolName}://item`)
})
})
it('can download using WebView.downloadURL', async () => {
const port = downloadServer.address().port
await w.loadURL('about:blank')
function webviewDownload({fixtures, url, port}) {
const webview = new WebView()
webview.addEventListener('did-finish-load', () => {
webview.downloadURL(`${url}:${port}/`)
})
webview.src = `file://${fixtures}/api/blank.html`
document.body.appendChild(webview)
}
const done = new Promise(resolve => {
w.webContents.session.once('will-download', function (e, item) {
item.setSavePath(downloadFilePath)
item.on('done', function (e, state) {
resolve([state, item])
})
})
})
await w.webContents.executeJavaScript(`(${webviewDownload})(${JSON.stringify({fixtures, url, port})})`)
const [state, item] = await done
assertDownload(state, item)
})
it('can cancel download', (done) => {
const port = downloadServer.address().port
w.webContents.session.once('will-download', function (e, item) {
item.setSavePath(downloadFilePath)
item.on('done', function (e, state) {
expect(state).to.equal('cancelled')
expect(item.getFilename()).to.equal('mock.pdf')
expect(item.getMimeType()).to.equal('application/pdf')
expect(item.getReceivedBytes()).to.equal(0)
expect(item.getTotalBytes()).to.equal(mockPDF.length)
expect(item.getContentDisposition()).to.equal(contentDisposition)
done()
})
item.cancel()
})
w.webContents.downloadURL(`${url}:${port}/`)
})
it('can generate a default filename', function (done) {
if (process.env.APPVEYOR === 'True') {
// FIXME(alexeykuzmin): Skip the test.
// this.skip()
return done()
}
const port = downloadServer.address().port
w.webContents.session.once('will-download', function (e, item) {
item.setSavePath(downloadFilePath)
item.on('done', function (e, state) {
expect(item.getFilename()).to.equal('download.pdf')
done()
})
item.cancel()
})
w.webContents.downloadURL(`${url}:${port}/?testFilename`)
})
it('can set options for the save dialog', (done) => {
const filePath = path.join(__dirname, 'fixtures', 'mock.pdf')
const port = downloadServer.address().port
const options = {
window: null,
title: 'title',
message: 'message',
buttonLabel: 'buttonLabel',
nameFieldLabel: 'nameFieldLabel',
defaultPath: '/',
filters: [{
name: '1', extensions: ['.1', '.2']
}, {
name: '2', extensions: ['.3', '.4', '.5']
}],
showsTagField: true,
securityScopedBookmarks: true
}
w.webContents.session.once('will-download', function (e, item) {
item.setSavePath(filePath)
item.setSaveDialogOptions(options)
item.on('done', function (e, state) {
expect(item.getSaveDialogOptions()).to.deep.equal(options)
done()
})
item.cancel()
})
w.webContents.downloadURL(`${url}:${port}`)
})
describe('when a save path is specified and the URL is unavailable', () => {
it('does not display a save dialog and reports the done state as interrupted', (done) => {
w.webContents.session.once('will-download', function (e, item) {
item.setSavePath(downloadFilePath)
if (item.getState() === 'interrupted') {
item.resume()
}
item.on('done', function (e, state) {
expect(state).to.equal('interrupted')
done()
})
})
w.webContents.downloadURL(`file://${path.join(__dirname, 'does-not-exist.txt')}`)
})
})
})
describe('ses.createInterruptedDownload(options)', () => {
it('can create an interrupted download item', (done) => {
const downloadFilePath = path.join(__dirname, '..', 'fixtures', 'mock.pdf')
const options = {
path: downloadFilePath,
urlChain: ['http://127.0.0.1/'],
mimeType: 'application/pdf',
offset: 0,
length: 5242880
}
w.webContents.session.once('will-download', function (e, item) {
expect(item.getState()).to.equal('interrupted')
item.cancel()
expect(item.getURLChain()).to.deep.equal(options.urlChain)
expect(item.getMimeType()).to.equal(options.mimeType)
expect(item.getReceivedBytes()).to.equal(options.offset)
expect(item.getTotalBytes()).to.equal(options.length)
expect(item.getSavePath()).to.equal(downloadFilePath)
done()
})
w.webContents.session.createInterruptedDownload(options)
})
it('can be resumed', async () => {
const downloadFilePath = path.join(fixtures, 'logo.png')
const rangeServer = http.createServer((req, res) => {
const options = { root: fixtures }
send(req, req.url, options)
.on('error', (error) => { done(error) }).pipe(res)
})
try {
await new Promise(resolve => rangeServer.listen(0, '127.0.0.1', resolve))
const port = rangeServer.address().port
const downloadCancelled = new Promise((resolve) => {
w.webContents.session.once('will-download', function (e, item) {
item.setSavePath(downloadFilePath)
item.on('done', function (e, state) {
resolve(item)
})
item.cancel()
})
})
const downloadUrl = `http://127.0.0.1:${port}/assets/logo.png`
w.webContents.downloadURL(downloadUrl)
const item = await downloadCancelled
expect(item.getState()).to.equal('cancelled')
const options = {
path: item.getSavePath(),
urlChain: item.getURLChain(),
mimeType: item.getMimeType(),
offset: item.getReceivedBytes(),
length: item.getTotalBytes(),
lastModified: item.getLastModifiedTime(),
eTag: item.getETag(),
}
const downloadResumed = new Promise((resolve) => {
w.webContents.session.once('will-download', function (e, item) {
expect(item.getState()).to.equal('interrupted')
item.setSavePath(downloadFilePath)
item.resume()
item.on('done', function (e, state) {
resolve(item)
})
})
})
w.webContents.session.createInterruptedDownload(options)
const completedItem = await downloadResumed
expect(completedItem.getState()).to.equal('completed')
expect(completedItem.getFilename()).to.equal('logo.png')
expect(completedItem.getSavePath()).to.equal(downloadFilePath)
expect(completedItem.getURL()).to.equal(downloadUrl)
expect(completedItem.getMimeType()).to.equal('image/png')
expect(completedItem.getReceivedBytes()).to.equal(14022)
expect(completedItem.getTotalBytes()).to.equal(14022)
expect(fs.existsSync(downloadFilePath)).to.equal(true)
} finally {
rangeServer.close()
}
})
})
describe('ses.setPermissionRequestHandler(handler)', () => {
it('cancels any pending requests when cleared', async () => {
const ses = w.webContents.session
ses.setPermissionRequestHandler(() => {
ses.setPermissionRequestHandler(null)
})
const result = emittedOnce(require('electron').ipcMain, 'message')
function remote() {
navigator.requestMIDIAccess({sysex: true}).then(() => {}, (err) => {
require('electron').ipcRenderer.send('message', err.name);
});
}
await w.loadURL(`data:text/html,<script>(${remote})()</script>`)
const [,name] = await result
expect(name).to.deep.equal('SecurityError')
})
})
})

View File

@@ -1,331 +0,0 @@
const chai = require('chai')
const http = require('http')
const https = require('https')
const path = require('path')
const fs = require('fs')
const send = require('send')
const auth = require('basic-auth')
const ChildProcess = require('child_process')
const { closeWindow } = require('./window-helpers')
const { ipcRenderer, remote } = require('electron')
const { ipcMain, session, BrowserWindow, net } = remote
const { expect } = chai
/* The whole session API doesn't use standard callbacks */
/* eslint-disable standard/no-callback-literal */
describe('session module', () => {
const fixtures = path.resolve(__dirname, 'fixtures')
let w = null
let webview = null
const url = 'http://127.0.0.1'
beforeEach(() => {
w = new BrowserWindow({
show: false,
width: 400,
height: 400,
webPreferences: {
nodeIntegration: true
}
})
})
afterEach(() => {
if (webview != null) {
if (!document.body.contains(webview)) {
document.body.appendChild(webview)
}
webview.remove()
}
return closeWindow(w).then(() => { w = null })
})
describe('DownloadItem', () => {
const mockPDF = Buffer.alloc(1024 * 1024 * 5)
const protocolName = 'custom-dl'
let contentDisposition = 'inline; filename="mock.pdf"'
const downloadFilePath = path.join(fixtures, 'mock.pdf')
const downloadServer = http.createServer((req, res) => {
if (req.url === '/?testFilename') contentDisposition = 'inline'
res.writeHead(200, {
'Content-Length': mockPDF.length,
'Content-Type': 'application/pdf',
'Content-Disposition': contentDisposition
})
res.end(mockPDF)
downloadServer.close()
})
const isPathEqual = (path1, path2) => {
return path.relative(path1, path2) === ''
}
const assertDownload = (event, state, url, mimeType,
receivedBytes, totalBytes, disposition,
filename, port, savePath, isCustom) => {
expect(state).to.equal('completed')
expect(filename).to.equal('mock.pdf')
expect(path.isAbsolute(savePath)).to.be.true()
expect(isPathEqual(savePath, path.join(__dirname, 'fixtures', 'mock.pdf'))).to.be.true()
if (isCustom) {
expect(url).to.be.equal(`${protocolName}://item`)
} else {
expect(url).to.be.equal(`http://127.0.0.1:${port}/`)
}
expect(mimeType).to.equal('application/pdf')
expect(receivedBytes).to.equal(mockPDF.length)
expect(totalBytes).to.equal(mockPDF.length)
expect(disposition).to.equal(contentDisposition)
expect(fs.existsSync(downloadFilePath)).to.be.true()
fs.unlinkSync(downloadFilePath)
}
it('can download using WebContents.downloadURL', (done) => {
downloadServer.listen(0, '127.0.0.1', () => {
const port = downloadServer.address().port
ipcRenderer.sendSync('set-download-option', false, false)
w.webContents.downloadURL(`${url}:${port}`)
ipcRenderer.once('download-done', (event, state, url,
mimeType, receivedBytes,
totalBytes, disposition,
filename, savePath) => {
assertDownload(event, state, url, mimeType, receivedBytes,
totalBytes, disposition, filename, port, savePath)
done()
})
})
})
it('can download from custom protocols using WebContents.downloadURL', (done) => {
const protocol = session.defaultSession.protocol
downloadServer.listen(0, '127.0.0.1', () => {
const port = downloadServer.address().port
const handler = (ignoredError, callback) => {
callback({ url: `${url}:${port}` })
}
protocol.registerHttpProtocol(protocolName, handler, (error) => {
if (error) return done(error)
ipcRenderer.sendSync('set-download-option', false, false)
w.webContents.downloadURL(`${protocolName}://item`)
ipcRenderer.once('download-done', (event, state, url,
mimeType, receivedBytes,
totalBytes, disposition,
filename, savePath) => {
assertDownload(event, state, url, mimeType, receivedBytes,
totalBytes, disposition, filename, port, savePath,
true)
done()
})
})
})
})
it('can download using WebView.downloadURL', (done) => {
downloadServer.listen(0, '127.0.0.1', () => {
const port = downloadServer.address().port
ipcRenderer.sendSync('set-download-option', false, false)
webview = new WebView()
webview.addEventListener('did-finish-load', () => {
webview.downloadURL(`${url}:${port}/`)
})
webview.src = `file://${fixtures}/api/blank.html`
ipcRenderer.once('download-done', (event, state, url,
mimeType, receivedBytes,
totalBytes, disposition,
filename, savePath) => {
assertDownload(event, state, url, mimeType, receivedBytes,
totalBytes, disposition, filename, port, savePath)
document.body.removeChild(webview)
done()
})
document.body.appendChild(webview)
})
})
it('can cancel download', (done) => {
downloadServer.listen(0, '127.0.0.1', () => {
const port = downloadServer.address().port
ipcRenderer.sendSync('set-download-option', true, false)
w.webContents.downloadURL(`${url}:${port}/`)
ipcRenderer.once('download-done', (event, state, url,
mimeType, receivedBytes,
totalBytes, disposition,
filename) => {
expect(state).to.equal('cancelled')
expect(filename).to.equal('mock.pdf')
expect(mimeType).to.equal('application/pdf')
expect(receivedBytes).to.equal(0)
expect(totalBytes).to.equal(mockPDF.length)
expect(disposition).to.equal(contentDisposition)
done()
})
})
})
it('can generate a default filename', function (done) {
if (process.env.APPVEYOR === 'True') {
// FIXME(alexeykuzmin): Skip the test.
// this.skip()
return done()
}
downloadServer.listen(0, '127.0.0.1', () => {
const port = downloadServer.address().port
ipcRenderer.sendSync('set-download-option', true, false)
w.webContents.downloadURL(`${url}:${port}/?testFilename`)
ipcRenderer.once('download-done', (event, state, url,
mimeType, receivedBytes,
totalBytes, disposition,
filename) => {
expect(state).to.equal('cancelled')
expect(filename).to.equal('download.pdf')
expect(mimeType).to.equal('application/pdf')
expect(receivedBytes).to.equal(0)
expect(totalBytes).to.equal(mockPDF.length)
expect(disposition).to.equal(contentDisposition)
done()
})
})
})
it('can set options for the save dialog', (done) => {
downloadServer.listen(0, '127.0.0.1', () => {
const filePath = path.join(__dirname, 'fixtures', 'mock.pdf')
const port = downloadServer.address().port
const options = {
window: null,
title: 'title',
message: 'message',
buttonLabel: 'buttonLabel',
nameFieldLabel: 'nameFieldLabel',
defaultPath: '/',
filters: [{
name: '1', extensions: ['.1', '.2']
}, {
name: '2', extensions: ['.3', '.4', '.5']
}],
showsTagField: true,
securityScopedBookmarks: true
}
ipcRenderer.sendSync('set-download-option', true, false, filePath, options)
w.webContents.downloadURL(`${url}:${port}`)
ipcRenderer.once('download-done', (event, state, url,
mimeType, receivedBytes,
totalBytes, disposition,
filename, savePath, dialogOptions) => {
expect(dialogOptions).to.deep.equal(options)
done()
})
})
})
describe('when a save path is specified and the URL is unavailable', () => {
it('does not display a save dialog and reports the done state as interrupted', (done) => {
ipcRenderer.sendSync('set-download-option', false, false)
ipcRenderer.once('download-done', (event, state) => {
expect(state).to.equal('interrupted')
done()
})
w.webContents.downloadURL(`file://${path.join(__dirname, 'does-not-exist.txt')}`)
})
})
})
describe('ses.createInterruptedDownload(options)', () => {
it('can create an interrupted download item', (done) => {
ipcRenderer.sendSync('set-download-option', true, false)
const filePath = path.join(__dirname, 'fixtures', 'mock.pdf')
const options = {
path: filePath,
urlChain: ['http://127.0.0.1/'],
mimeType: 'application/pdf',
offset: 0,
length: 5242880
}
w.webContents.session.createInterruptedDownload(options)
ipcRenderer.once('download-created', (event, state, urlChain,
mimeType, receivedBytes,
totalBytes, filename,
savePath) => {
expect(state).to.equal('interrupted')
expect(urlChain).to.deep.equal(['http://127.0.0.1/'])
expect(mimeType).to.equal('application/pdf')
expect(receivedBytes).to.equal(0)
expect(totalBytes).to.equal(5242880)
expect(savePath).to.equal(filePath)
done()
})
})
it('can be resumed', (done) => {
const fixtures = path.join(__dirname, 'fixtures')
const downloadFilePath = path.join(fixtures, 'logo.png')
const rangeServer = http.createServer((req, res) => {
const options = { root: fixtures }
send(req, req.url, options)
.on('error', (error) => { done(error) }).pipe(res)
})
ipcRenderer.sendSync('set-download-option', true, false, downloadFilePath)
rangeServer.listen(0, '127.0.0.1', () => {
const port = rangeServer.address().port
const downloadUrl = `http://127.0.0.1:${port}/assets/logo.png`
const callback = (event, state, url, mimeType,
receivedBytes, totalBytes, disposition,
filename, savePath, dialogOptions, urlChain,
lastModifiedTime, eTag) => {
if (state === 'cancelled') {
const options = {
path: savePath,
urlChain: urlChain,
mimeType: mimeType,
offset: receivedBytes,
length: totalBytes,
lastModified: lastModifiedTime,
eTag: eTag
}
ipcRenderer.sendSync('set-download-option', false, false, downloadFilePath)
w.webContents.session.createInterruptedDownload(options)
} else {
expect(state).to.equal('completed')
expect(filename).to.equal('logo.png')
expect(savePath).to.equal(downloadFilePath)
expect(url).to.equal(downloadUrl)
expect(mimeType).to.equal('image/png')
expect(receivedBytes).to.equal(14022)
expect(totalBytes).to.equal(14022)
expect(fs.existsSync(downloadFilePath)).to.be.true()
fs.unlinkSync(downloadFilePath)
rangeServer.close()
ipcRenderer.removeListener('download-done', callback)
done()
}
}
ipcRenderer.on('download-done', callback)
w.webContents.downloadURL(downloadUrl)
})
})
})
describe('ses.setPermissionRequestHandler(handler)', () => {
it('cancels any pending requests when cleared', (done) => {
const ses = session.fromPartition('permissionTest')
ses.setPermissionRequestHandler(() => {
ses.setPermissionRequestHandler(null)
})
webview = new WebView()
webview.addEventListener('ipc-message', (e) => {
expect(e.channel).to.equal('message')
expect(e.args).to.deep.equal(['SecurityError'])
done()
})
webview.src = `file://${fixtures}/pages/permissions/midi-sysex.html`
webview.partition = 'permissionTest'
webview.setAttribute('nodeintegration', 'on')
document.body.appendChild(webview)
})
})
})

View File

@@ -151,6 +151,20 @@ describe('systemPreferences module', () => {
})
})
describe('systemPreferences.appLevelAppearance', () => {
before(function () {
if (process.platform !== 'darwin') this.skip()
})
it('has an appLevelAppearance property', () => {
expect(systemPreferences).to.have.a.property('appLevelAppearance')
// TODO(codebytere): remove when propertyification is complete
expect(systemPreferences.getAppLevelAppearance).to.be.a('function')
expect(systemPreferences.setAppLevelAppearance).to.be.a('function')
})
})
describe('systemPreferences.setUserDefault(key, type, value)', () => {
before(function () {
if (process.platform !== 'darwin') {

View File

@@ -40,19 +40,19 @@ describe('webFrame module', function () {
const spellCheckerFeedback =
new Promise(resolve => {
ipcMain.on('spec-spell-check', (e, words, callback) => {
if (words.length === 2) {
// The promise is resolved only after this event is received twice
// Array contains only 1 word first time and 2 the next time
if (words.length === 5) {
// The API calls the provider after every completed word.
// The promise is resolved only after this event is received with all words.
resolve([words, callback])
}
})
})
const inputText = 'spleling test '
const inputText = `spleling test you're `
for (const keyCode of inputText) {
w.webContents.sendInputEvent({ type: 'char', keyCode })
}
const [words, callback] = await spellCheckerFeedback
expect(words).to.deep.equal(['spleling', 'test'])
expect(words.sort()).to.deep.equal(['spleling', 'test', `you're`, 'you', 're'].sort())
expect(callback).to.be.true()
})

View File

@@ -1278,6 +1278,11 @@ describe('chromium feature', () => {
it('should download a pdf when plugins are disabled', (done) => {
this.createBrowserWindow({ plugins: false, preload: 'preload-pdf-loaded.js' })
// NOTE(nornagon): this test has been skipped for ages, so there's no way
// to refactor it confidently. The 'set-download-option' ipc was removed
// around May 2019, so if you're working on the pdf viewer and arrive at
// this test and want to know what 'set-download-option' did, look here:
// https://github.com/electron/electron/blob/d87b3ead760ae2d20f2401a8dac4ce548f8cd5f5/spec/static/main.js#L164
ipcRenderer.sendSync('set-download-option', false, false)
ipcRenderer.once('download-done', (event, state, url, mimeType, receivedBytes, totalBytes, disposition, filename) => {
expect(state).to.equal('completed')

View File

@@ -0,0 +1 @@
<html></html>

View File

@@ -0,0 +1,33 @@
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
process.on('uncaughtException', (e) => {
console.error(e)
process.exit(1)
})
app.allowRendererProcessReuse = JSON.parse(process.argv[2])
const pids = []
let win
ipcMain.on('pid', (event, pid) => {
pids.push(pid)
if (pids.length === 2) {
console.log(JSON.stringify(pids))
if (win) win.close()
app.quit()
} else {
if (win) win.reload()
}
})
app.whenReady().then(() => {
win = new BrowserWindow({
show: false,
webPreferences: {
preload: path.resolve(__dirname, 'preload.js')
}
})
win.loadFile('index.html')
})

View File

@@ -0,0 +1,4 @@
{
"name": "site-instance-overrides",
"main": "main.js"
}

View File

@@ -0,0 +1,3 @@
const { ipcRenderer } = require('electron')
ipcRenderer.send('pid', process.pid)

View File

@@ -158,58 +158,6 @@ app.on('ready', function () {
process.exit(1)
})
// For session's download test, listen 'will-download' event in browser, and
// reply the result to renderer for verifying
const downloadFilePath = path.join(__dirname, '..', 'fixtures', 'mock.pdf')
ipcMain.on('set-download-option', function (event, needCancel, preventDefault, filePath = downloadFilePath, dialogOptions = {}) {
window.webContents.session.once('will-download', function (e, item) {
window.webContents.send('download-created',
item.getState(),
item.getURLChain(),
item.getMimeType(),
item.getReceivedBytes(),
item.getTotalBytes(),
item.getFilename(),
item.getSavePath())
if (preventDefault) {
e.preventDefault()
const url = item.getURL()
const filename = item.getFilename()
setImmediate(function () {
try {
item.getURL()
} catch (err) {
window.webContents.send('download-error', url, filename, err.message)
}
})
} else {
if (item.getState() === 'interrupted' && !needCancel) {
item.resume()
} else {
item.setSavePath(filePath)
item.setSaveDialogOptions(dialogOptions)
}
item.on('done', function (e, state) {
window.webContents.send('download-done',
state,
item.getURL(),
item.getMimeType(),
item.getReceivedBytes(),
item.getTotalBytes(),
item.getContentDisposition(),
item.getFilename(),
item.getSavePath(),
item.getSaveDialogOptions(),
item.getURLChain(),
item.getLastModifiedTime(),
item.getETag())
})
if (needCancel) item.cancel()
}
})
event.returnValue = 'done'
})
ipcMain.on('prevent-next-input-event', (event, key, id) => {
webContents.fromId(id).once('before-input-event', (event, input) => {
if (key === input.key) event.preventDefault()

View File

@@ -23,8 +23,9 @@
regenerator-runtime "^0.12.0"
"@electron/docs-parser@^0.1.1":
version "0.1.5"
resolved "https://registry.yarnpkg.com/@electron/docs-parser/-/docs-parser-0.1.5.tgz#e6c05ed200b4c155986fb0f46178dede71a5e078"
version "0.1.6"
resolved "https://registry.yarnpkg.com/@electron/docs-parser/-/docs-parser-0.1.6.tgz#7ed86586e0ebc4a5c63e7c112264357adc61d334"
integrity sha512-WAV0xHx1HIflqvb6G01LLnpS9n3VzNF0vyfxYhbP3Ev2p+m8CODc2+9pCdzCmH457UYi8GsX/jnB23djTCxt7Q==
dependencies:
"@types/markdown-it" "^0.0.7"
chai "^4.2.0"
@@ -156,10 +157,12 @@
"@types/linkify-it@*":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-2.1.0.tgz#ea3dd64c4805597311790b61e872cbd1ed2cd806"
integrity sha512-Q7DYAOi9O/+cLLhdaSvKdaumWyHbm7HAk/bFwwyTuU0arR5yyCeW5GOoqt4tJTpDRxhpx9Q8kQL6vMpuw9hDSw==
"@types/markdown-it@^0.0.7":
version "0.0.7"
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.7.tgz#75070485a3d8ad11e7deb8287f4430be15bf4d39"
integrity sha512-WyL6pa76ollQFQNEaLVa41ZUUvDvPY+qAUmlsphnrpL6I9p1m868b26FyeoOmo7X3/Ta/S9WKXcEYXUSHnxoVQ==
dependencies:
"@types/linkify-it" "*"
@@ -319,6 +322,7 @@ ansi-regex@^3.0.0:
ansi-regex@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
ansi-styles@^2.2.1:
version "2.2.1"
@@ -327,6 +331,7 @@ ansi-styles@^2.2.1:
ansi-styles@^3.2.0, ansi-styles@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
dependencies:
color-convert "^1.9.0"
@@ -359,6 +364,7 @@ are-we-there-yet@~1.1.2:
argparse@^1.0.7:
version "1.0.10"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
dependencies:
sprintf-js "~1.0.2"
@@ -469,6 +475,7 @@ assert@^1.4.0:
assertion-error@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==
assign-symbols@^1.0.0:
version "1.0.0"
@@ -876,6 +883,7 @@ ccount@^1.0.0:
chai@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/chai/-/chai-4.2.0.tgz#760aa72cf20e3795e84b12877ce0e83737aa29e5"
integrity sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==
dependencies:
assertion-error "^1.1.0"
check-error "^1.0.2"
@@ -897,6 +905,7 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.1, chalk@^2.4.1, chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
dependencies:
ansi-styles "^3.2.1"
escape-string-regexp "^1.0.5"
@@ -929,6 +938,7 @@ chardet@^0.7.0:
check-error@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=
check-for-leaks@^1.0.2:
version "1.2.0"
@@ -1010,6 +1020,7 @@ cli-cursor@^1.0.2:
cli-cursor@^2.0.0, cli-cursor@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=
dependencies:
restore-cursor "^2.0.0"
@@ -1020,6 +1031,7 @@ cli-spinners@^0.1.2:
cli-spinners@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.1.0.tgz#22c34b4d51f573240885b201efda4e4ec9fff3c7"
integrity sha512-8B00fJOEh1HPrx4fo5eW16XmE1PcL1tGpGrxy63CXGP9nHdPBN63X75hA1zhvQuhVztJWLqV58Roj2qlNM7cAA==
cli-truncate@^0.2.1:
version "0.2.1"
@@ -1035,6 +1047,7 @@ cli-width@^2.0.0:
clone@^1.0.2:
version "1.0.4"
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
co@3.1.0:
version "3.1.0"
@@ -1058,12 +1071,14 @@ collection-visit@^1.0.0:
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
dependencies:
color-name "1.1.3"
color-name@1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
colors@^1.1.2:
version "1.3.3"
@@ -1291,6 +1306,7 @@ dedent@^0.7.0:
deep-eql@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df"
integrity sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==
dependencies:
type-detect "^4.0.0"
@@ -1309,6 +1325,7 @@ deepmerge@3.2.0:
defaults@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d"
integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=
dependencies:
clone "^1.0.2"
@@ -1554,6 +1571,7 @@ ensure-posix-path@^1.0.0:
entities@~1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
error-ex@^1.2.0, error-ex@^1.3.1:
version "1.3.2"
@@ -2262,6 +2280,7 @@ get-assigned-identifiers@^1.2.0:
get-func-name@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41"
integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=
get-own-enumerable-property-symbols@^3.0.0:
version "3.0.0"
@@ -2416,6 +2435,7 @@ has-flag@^2.0.0:
has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
has-symbols@^1.0.0:
version "1.0.0"
@@ -3117,6 +3137,7 @@ levn@^0.3.0, levn@~0.3.0:
linkify-it@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.1.0.tgz#c4caf38a6cd7ac2212ef3c7d2bde30a91561f9db"
integrity sha512-4REs8/062kV2DSHxNfq5183zrqXMl7WP0WzABH9IeJI+NLm429FgE1PDecltYfnOoFDFlZGh2T8PfZn0r+GTRg==
dependencies:
uc.micro "^1.0.1"
@@ -3247,6 +3268,7 @@ locate-path@^3.0.0:
lodash.camelcase@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY=
lodash.flatten@^4.4.0:
version "4.4.0"
@@ -3289,6 +3311,7 @@ log-symbols@^1.0.2:
log-symbols@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"
integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==
dependencies:
chalk "^2.0.1"
@@ -3354,6 +3377,7 @@ markdown-extensions@^1.1.0:
markdown-it@^8.4.2:
version "8.4.2"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.4.2.tgz#386f98998dc15a37722aa7722084f4020bdd9b54"
integrity sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==
dependencies:
argparse "^1.0.7"
entities "~1.1.1"
@@ -3410,6 +3434,7 @@ mdast-util-to-string@^1.0.2:
mdurl@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=
media-typer@0.3.0:
version "0.3.0"
@@ -3502,6 +3527,7 @@ mime@1.4.1:
mimic-fn@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
version "1.0.1"
@@ -3806,6 +3832,7 @@ onetime@^1.0.0:
onetime@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=
dependencies:
mimic-fn "^1.0.0"
@@ -3838,6 +3865,7 @@ ora@^0.2.3:
ora@^3.0.0, ora@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/ora/-/ora-3.4.0.tgz#bf0752491059a3ef3ed4c85097531de9fdbcd318"
integrity sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==
dependencies:
chalk "^2.4.2"
cli-cursor "^2.1.0"
@@ -3993,6 +4021,7 @@ parse-json@^4.0.0:
parse-ms@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d"
integrity sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==
parseurl@~1.3.2:
version "1.3.3"
@@ -4067,6 +4096,7 @@ path-type@^3.0.0:
pathval@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0"
integrity sha1-uULm1L3mUwBe9rcTYd74cn0GReA=
pbkdf2@^3.0.3:
version "3.0.17"
@@ -4198,6 +4228,7 @@ pretty-bytes@^1.0.2:
pretty-ms@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-5.0.0.tgz#6133a8f55804b208e4728f6aa7bf01085e951e24"
integrity sha512-94VRYjL9k33RzfKiGokPBPpsmloBYSf5Ri+Pq19zlsEcUKFob+admeXr5eFDRuPjFmEOcjJvPGdillYOJyvZ7Q==
dependencies:
parse-ms "^2.1.0"
@@ -5069,6 +5100,7 @@ restore-cursor@^1.0.1:
restore-cursor@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368=
dependencies:
onetime "^2.0.0"
signal-exit "^3.0.2"
@@ -5260,6 +5292,7 @@ shx@^0.3.2:
signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
simple-concat@^1.0.0:
version "1.0.0"
@@ -5399,6 +5432,7 @@ split-string@^3.0.1, split-string@^3.0.2:
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
sshpk@^1.7.0:
version "1.16.1"
@@ -5581,6 +5615,7 @@ strip-ansi@^4.0.0:
strip-ansi@^5.1.0, strip-ansi@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
dependencies:
ansi-regex "^4.1.0"
@@ -5604,10 +5639,6 @@ strip-indent@^1.0.1:
dependencies:
get-stdin "^4.0.1"
strip-indent@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
strip-json-comments@^2.0.0, strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
@@ -5637,6 +5668,7 @@ supports-color@^4.1.0:
supports-color@^5.3.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
dependencies:
has-flag "^3.0.0"
@@ -5898,6 +5930,7 @@ type-check@~0.3.2:
type-detect@^4.0.0, type-detect@^4.0.5:
version "4.0.8"
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
type-fest@^0.4.1:
version "0.4.1"
@@ -5926,6 +5959,7 @@ typescript@~3.3.3333:
uc.micro@^1.0.1, uc.micro@^1.0.5:
version "1.0.6"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
umd@^3.0.0:
version "3.0.3"
@@ -6207,6 +6241,7 @@ walk-sync@^0.3.2:
wcwidth@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=
dependencies:
defaults "^1.0.3"