diff --git a/atom/browser/api/atom_api_render_process_preferences.cc b/atom/browser/api/atom_api_render_process_preferences.cc new file mode 100644 index 000000000000..59ae07b45c75 --- /dev/null +++ b/atom/browser/api/atom_api_render_process_preferences.cc @@ -0,0 +1,88 @@ +// Copyright (c) 2016 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/browser/api/atom_api_render_process_preferences.h" + +#include "atom/browser/atom_browser_client.h" +#include "atom/browser/native_window.h" +#include "atom/browser/window_list.h" +#include "atom/common/native_mate_converters/value_converter.h" +#include "atom/common/node_includes.h" +#include "content/public/browser/render_process_host.h" +#include "native_mate/dictionary.h" +#include "native_mate/object_template_builder.h" + +namespace atom { + +namespace api { + +namespace { + +bool IsBrowserWindow(content::RenderProcessHost* process) { + content::WebContents* web_contents = + static_cast(AtomBrowserClient::Get())-> + GetWebContentsFromProcessID(process->GetID()); + if (!web_contents) + return false; + + NativeWindow* window = NativeWindow::FromWebContents(web_contents); + if (!window) + return false; + + return true; +} + +} // namespace + +RenderProcessPreferences::RenderProcessPreferences( + v8::Isolate* isolate, + const atom::RenderProcessPreferences::Predicate& predicate) + : preferences_(predicate) { + Init(isolate); +} + +RenderProcessPreferences::~RenderProcessPreferences() { +} + +int RenderProcessPreferences::AddEntry(const base::DictionaryValue& entry) { + return preferences_.AddEntry(entry); +} + +void RenderProcessPreferences::RemoveEntry(int id) { + preferences_.RemoveEntry(id); +} + +// static +void RenderProcessPreferences::BuildPrototype( + v8::Isolate* isolate, v8::Local prototype) { + mate::ObjectTemplateBuilder(isolate, prototype) + .SetMethod("addEntry", &RenderProcessPreferences::AddEntry) + .SetMethod("removeEntry", &RenderProcessPreferences::RemoveEntry); +} + +// static +mate::Handle +RenderProcessPreferences::ForAllBrowserWindow(v8::Isolate* isolate) { + return mate::CreateHandle( + isolate, + new RenderProcessPreferences(isolate, base::Bind(&IsBrowserWindow))); +} + +} // namespace api + +} // namespace atom + +namespace { + +void Initialize(v8::Local exports, v8::Local unused, + v8::Local context, void* priv) { + mate::Dictionary dict(context->GetIsolate(), exports); + dict.SetMethod("forAllBrowserWindow", + &atom::api::RenderProcessPreferences::ForAllBrowserWindow); +} + +} // namespace + +NODE_MODULE_CONTEXT_AWARE_BUILTIN(atom_browser_render_process_preferences, + Initialize) diff --git a/atom/browser/api/atom_api_render_process_preferences.h b/atom/browser/api/atom_api_render_process_preferences.h new file mode 100644 index 000000000000..a305f1361b2d --- /dev/null +++ b/atom/browser/api/atom_api_render_process_preferences.h @@ -0,0 +1,44 @@ +// Copyright (c) 2016 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_BROWSER_API_ATOM_API_RENDER_PROCESS_PREFERENCES_H_ +#define ATOM_BROWSER_API_ATOM_API_RENDER_PROCESS_PREFERENCES_H_ + +#include "atom/browser/render_process_preferences.h" +#include "native_mate/handle.h" +#include "native_mate/wrappable.h" + +namespace atom { + +namespace api { + +class RenderProcessPreferences + : public mate::Wrappable { + public: + static mate::Handle + ForAllBrowserWindow(v8::Isolate* isolate); + + static void BuildPrototype(v8::Isolate* isolate, + v8::Local prototype); + + int AddEntry(const base::DictionaryValue& entry); + void RemoveEntry(int id); + + protected: + RenderProcessPreferences( + v8::Isolate* isolate, + const atom::RenderProcessPreferences::Predicate& predicate); + ~RenderProcessPreferences() override; + + private: + atom::RenderProcessPreferences preferences_; + + DISALLOW_COPY_AND_ASSIGN(RenderProcessPreferences); +}; + +} // namespace api + +} // namespace atom + +#endif // ATOM_BROWSER_API_ATOM_API_RENDER_PROCESS_PREFERENCES_H_ diff --git a/atom/browser/api/atom_api_web_contents.cc b/atom/browser/api/atom_api_web_contents.cc index 72e87c30a8c7..736fb7ca0fb1 100644 --- a/atom/browser/api/atom_api_web_contents.cc +++ b/atom/browser/api/atom_api_web_contents.cc @@ -655,6 +655,11 @@ void WebContents::DevToolsOpened() { isolate(), managed_web_contents()->GetDevToolsWebContents()); devtools_web_contents_.Reset(isolate(), handle.ToV8()); + // Set inspected tabID. + base::FundamentalValue tab_id(ID()); + managed_web_contents()->CallClientFunction( + "DevToolsAPI.setInspectedTabId", &tab_id, nullptr, nullptr); + // Inherit owner window in devtools. if (owner_window()) handle->SetOwnerWindow(managed_web_contents()->GetDevToolsWebContents(), @@ -1083,9 +1088,10 @@ void WebContents::TabTraverse(bool reverse) { web_contents()->FocusThroughTabTraversal(reverse); } -bool WebContents::SendIPCMessage(const base::string16& channel, +bool WebContents::SendIPCMessage(bool all_frames, + const base::string16& channel, const base::ListValue& args) { - return Send(new AtomViewMsg_Message(routing_id(), channel, args)); + return Send(new AtomViewMsg_Message(routing_id(), all_frames, channel, args)); } void WebContents::SendInputEvent(v8::Isolate* isolate, @@ -1333,6 +1339,8 @@ void Initialize(v8::Local exports, v8::Local unused, mate::Dictionary dict(isolate, exports); dict.SetMethod("create", &atom::api::WebContents::Create); dict.SetMethod("_setWrapWebContents", &atom::api::SetWrapWebContents); + dict.SetMethod("fromId", + &mate::TrackableObject::FromWeakMapID); } } // namespace diff --git a/atom/browser/api/atom_api_web_contents.h b/atom/browser/api/atom_api_web_contents.h index f3661bedd253..e03ab653a8ce 100644 --- a/atom/browser/api/atom_api_web_contents.h +++ b/atom/browser/api/atom_api_web_contents.h @@ -122,7 +122,8 @@ class WebContents : public mate::TrackableObject, void TabTraverse(bool reverse); // Send messages to browser. - bool SendIPCMessage(const base::string16& channel, + bool SendIPCMessage(bool all_frames, + const base::string16& channel, const base::ListValue& args); // Send WebInputEvent to the page. diff --git a/atom/browser/atom_browser_client.cc b/atom/browser/atom_browser_client.cc index 5a6b49483bcb..e3cb9c5c8c89 100644 --- a/atom/browser/atom_browser_client.cc +++ b/atom/browser/atom_browser_client.cc @@ -73,6 +73,17 @@ AtomBrowserClient::AtomBrowserClient() : delegate_(nullptr) { AtomBrowserClient::~AtomBrowserClient() { } +content::WebContents* AtomBrowserClient::GetWebContentsFromProcessID( + int process_id) { + // If the process is a pending process, we should use the old one. + if (ContainsKey(pending_processes_, process_id)) + process_id = pending_processes_[process_id]; + + // Certain render process will be created with no associated render view, + // for example: ServiceWorker. + return WebContentsPreferences::GetWebContentsFromProcessID(process_id); +} + void AtomBrowserClient::RenderProcessWillLaunch( content::RenderProcessHost* host) { int process_id = host->GetID(); @@ -172,14 +183,7 @@ void AtomBrowserClient::AppendExtraCommandLineSwitches( } #endif - // If the process is a pending process, we should use the old one. - if (ContainsKey(pending_processes_, process_id)) - process_id = pending_processes_[process_id]; - - // Certain render process will be created with no associated render view, - // for example: ServiceWorker. - content::WebContents* web_contents = - WebContentsPreferences::GetWebContentsFromProcessID(process_id); + content::WebContents* web_contents = GetWebContentsFromProcessID(process_id); if (!web_contents) return; diff --git a/atom/browser/atom_browser_client.h b/atom/browser/atom_browser_client.h index 5588f0435afa..cf1a4cc438b6 100644 --- a/atom/browser/atom_browser_client.h +++ b/atom/browser/atom_browser_client.h @@ -34,6 +34,9 @@ class AtomBrowserClient : public brightray::BrowserClient, using Delegate = content::ContentBrowserClient; void set_delegate(Delegate* delegate) { delegate_ = delegate; } + // Returns the WebContents for pending render processes. + content::WebContents* GetWebContentsFromProcessID(int process_id); + // Don't force renderer process to restart for once. static void SuppressRendererProcessRestartForOnce(); diff --git a/atom/browser/net/url_request_buffer_job.cc b/atom/browser/net/url_request_buffer_job.cc index c0c8e1584e69..c713099c76a1 100644 --- a/atom/browser/net/url_request_buffer_job.cc +++ b/atom/browser/net/url_request_buffer_job.cc @@ -8,10 +8,24 @@ #include "atom/common/atom_constants.h" #include "base/strings/string_number_conversions.h" +#include "base/strings/utf_string_conversions.h" +#include "net/base/mime_util.h" #include "net/base/net_errors.h" namespace atom { +namespace { + +std::string GetExtFromURL(const GURL& url) { + std::string spec = url.spec(); + size_t index = spec.find_last_of('.'); + if (index == std::string::npos || index == spec.size()) + return std::string(); + return spec.substr(index + 1, spec.size() - index - 1); +} + +} // namespace + URLRequestBufferJob::URLRequestBufferJob( net::URLRequest* request, net::NetworkDelegate* network_delegate) : JsAsker(request, network_delegate), @@ -30,6 +44,15 @@ void URLRequestBufferJob::StartAsync(std::unique_ptr options) { options->GetAsBinary(&binary); } + if (mime_type_.empty()) { + std::string ext = GetExtFromURL(request()->url()); +#if defined(OS_WIN) + net::GetWellKnownMimeTypeFromExtension(base::UTF8ToUTF16(ext), &mime_type_); +#else + net::GetWellKnownMimeTypeFromExtension(ext, &mime_type_); +#endif + } + if (!binary) { NotifyStartError(net::URLRequestStatus( net::URLRequestStatus::FAILED, net::ERR_NOT_IMPLEMENTED)); diff --git a/atom/browser/render_process_preferences.cc b/atom/browser/render_process_preferences.cc new file mode 100644 index 000000000000..d109c8714f70 --- /dev/null +++ b/atom/browser/render_process_preferences.cc @@ -0,0 +1,63 @@ +// Copyright (c) 2016 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/browser/render_process_preferences.h" + +#include "atom/common/api/api_messages.h" +#include "content/public/browser/notification_service.h" +#include "content/public/browser/notification_types.h" +#include "content/public/browser/render_process_host.h" + +namespace atom { + +RenderProcessPreferences::RenderProcessPreferences(const Predicate& predicate) + : predicate_(predicate), + next_id_(0), + cache_needs_update_(true) { + registrar_.Add(this, + content::NOTIFICATION_RENDERER_PROCESS_CREATED, + content::NotificationService::AllBrowserContextsAndSources()); +} + +RenderProcessPreferences::~RenderProcessPreferences() { +} + +int RenderProcessPreferences::AddEntry(const base::DictionaryValue& entry) { + int id = ++next_id_; + entries_[id] = entry.CreateDeepCopy(); + cache_needs_update_ = true; + return id; +} + +void RenderProcessPreferences::RemoveEntry(int id) { + cache_needs_update_ = true; + entries_.erase(id); +} + +void RenderProcessPreferences::Observe( + int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) { + DCHECK_EQ(type, content::NOTIFICATION_RENDERER_PROCESS_CREATED); + content::RenderProcessHost* process = + content::Source(source).ptr(); + + if (!predicate_.Run(process)) + return; + + UpdateCache(); + process->Send(new AtomMsg_UpdatePreferences(cached_entries_)); +} + +void RenderProcessPreferences::UpdateCache() { + if (!cache_needs_update_) + return; + + cached_entries_.Clear(); + for (const auto& iter : entries_) + cached_entries_.Append(iter.second->CreateDeepCopy()); + cache_needs_update_ = false; +} + +} // namespace atom diff --git a/atom/browser/render_process_preferences.h b/atom/browser/render_process_preferences.h new file mode 100644 index 000000000000..77bf176f492c --- /dev/null +++ b/atom/browser/render_process_preferences.h @@ -0,0 +1,61 @@ +// Copyright (c) 2016 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_BROWSER_RENDER_PROCESS_PREFERENCES_H_ +#define ATOM_BROWSER_RENDER_PROCESS_PREFERENCES_H_ + +#include +#include + +#include "base/callback.h" +#include "base/values.h" +#include "content/public/browser/notification_observer.h" +#include "content/public/browser/notification_registrar.h" + +namespace content { +class RenderProcessHost; +} + +namespace atom { + +// Sets user preferences for render processes. +class RenderProcessPreferences : public content::NotificationObserver { + public: + using Predicate = base::Callback; + + // The |predicate| is used to determine whether to set preferences for a + // render process. + explicit RenderProcessPreferences(const Predicate& predicate); + virtual ~RenderProcessPreferences(); + + int AddEntry(const base::DictionaryValue& entry); + void RemoveEntry(int id); + + private: + // content::NotificationObserver: + void Observe(int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) override; + + void UpdateCache(); + + // Manages our notification registrations. + content::NotificationRegistrar registrar_; + + Predicate predicate_; + + int next_id_; + std::unordered_map> entries_; + + // We need to convert the |entries_| to ListValue for multiple times, this + // caches is only updated when we are sending messages. + bool cache_needs_update_; + base::ListValue cached_entries_; + + DISALLOW_COPY_AND_ASSIGN(RenderProcessPreferences); +}; + +} // namespace atom + +#endif // ATOM_BROWSER_RENDER_PROCESS_PREFERENCES_H_ diff --git a/atom/browser/web_contents_preferences.cc b/atom/browser/web_contents_preferences.cc index ef9fc4df0b8c..a32e23de68f5 100644 --- a/atom/browser/web_contents_preferences.cc +++ b/atom/browser/web_contents_preferences.cc @@ -150,6 +150,16 @@ void WebContentsPreferences::AppendExtraCommandLineSwitches( command_line->AppendSwitch(switches::kScrollBounce); #endif + // Custom command line switches. + const base::ListValue* args; + if (web_preferences.GetList("commandLineSwitches", &args)) { + for (size_t i = 0; i < args->GetSize(); ++i) { + std::string arg; + if (args->GetString(i, &arg) && !arg.empty()) + command_line->AppendSwitch(arg); + } + } + // Enable blink features. std::string blink_features; if (web_preferences.GetString(options::kBlinkFeatures, &blink_features)) diff --git a/atom/browser/web_contents_preferences.h b/atom/browser/web_contents_preferences.h index dd98a9658acf..daf1f6e84de5 100644 --- a/atom/browser/web_contents_preferences.h +++ b/atom/browser/web_contents_preferences.h @@ -29,6 +29,7 @@ class WebContentsPreferences : public content::WebContentsUserData { public: // Get WebContents according to process ID. + // FIXME(zcbenz): This method does not belong here. static content::WebContents* GetWebContentsFromProcessID(int process_id); // Append command paramters according to |web_contents|'s preferences. diff --git a/atom/common/api/api_messages.h b/atom/common/api/api_messages.h index eeb26614847b..ab27d5a2516e 100644 --- a/atom/common/api/api_messages.h +++ b/atom/common/api/api_messages.h @@ -30,10 +30,14 @@ IPC_SYNC_MESSAGE_ROUTED2_1(AtomViewHostMsg_Message_Sync, base::ListValue /* arguments */, base::string16 /* result (in JSON) */) -IPC_MESSAGE_ROUTED2(AtomViewMsg_Message, +IPC_MESSAGE_ROUTED3(AtomViewMsg_Message, + bool /* send_to_all */, base::string16 /* channel */, base::ListValue /* arguments */) // Sent by the renderer when the draggable regions are updated. IPC_MESSAGE_ROUTED1(AtomViewHostMsg_UpdateDraggableRegions, std::vector /* regions */) + +// Update renderer process preferences. +IPC_MESSAGE_CONTROL1(AtomMsg_UpdatePreferences, base::ListValue) diff --git a/atom/common/api/remote_callback_freer.cc b/atom/common/api/remote_callback_freer.cc index 7bc377efc5e7..d1a185d51f39 100644 --- a/atom/common/api/remote_callback_freer.cc +++ b/atom/common/api/remote_callback_freer.cc @@ -35,7 +35,7 @@ void RemoteCallbackFreer::RunDestructor() { base::ASCIIToUTF16("ELECTRON_RENDERER_RELEASE_CALLBACK"); base::ListValue args; args.AppendInteger(object_id_); - Send(new AtomViewMsg_Message(routing_id(), channel, args)); + Send(new AtomViewMsg_Message(routing_id(), false, channel, args)); Observe(nullptr); } diff --git a/atom/common/node_bindings.cc b/atom/common/node_bindings.cc index a55620557e56..ed2ea01675a1 100644 --- a/atom/common/node_bindings.cc +++ b/atom/common/node_bindings.cc @@ -42,6 +42,7 @@ REFERENCE_MODULE(atom_browser_power_monitor); REFERENCE_MODULE(atom_browser_power_save_blocker); REFERENCE_MODULE(atom_browser_protocol); REFERENCE_MODULE(atom_browser_global_shortcut); +REFERENCE_MODULE(atom_browser_render_process_preferences); REFERENCE_MODULE(atom_browser_session); REFERENCE_MODULE(atom_browser_system_preferences); REFERENCE_MODULE(atom_browser_tray); diff --git a/atom/renderer/atom_render_view_observer.cc b/atom/renderer/atom_render_view_observer.cc index bbaea351378b..7ee93efb39b7 100644 --- a/atom/renderer/atom_render_view_observer.cc +++ b/atom/renderer/atom_render_view_observer.cc @@ -59,6 +59,34 @@ std::vector> ListValueToVector( return result; } +void EmitIPCEvent(blink::WebFrame* frame, + const base::string16& channel, + const base::ListValue& args) { + if (!frame || frame->isWebRemoteFrame()) + return; + + v8::Isolate* isolate = blink::mainThreadIsolate(); + v8::HandleScope handle_scope(isolate); + + v8::Local context = frame->mainWorldScriptContext(); + v8::Context::Scope context_scope(context); + + // Only emit IPC event for context with node integration. + node::Environment* env = node::Environment::GetCurrent(context); + if (!env) + return; + + v8::Local ipc; + if (GetIPCObject(isolate, context, &ipc)) { + auto args_vector = ListValueToVector(isolate, args); + // Insert the Event object, event.sender is ipc. + mate::Dictionary event = mate::Dictionary::CreateEmpty(isolate); + event.Set("sender", ipc); + args_vector.insert(args_vector.begin(), event.GetHandle()); + mate::EmitEvent(isolate, ipc, channel, args_vector); + } +} + base::StringPiece NetResourceProvider(int key) { if (key == IDR_DIR_HEADER_HTML) { base::StringPiece html_data = @@ -123,7 +151,8 @@ bool AtomRenderViewObserver::OnMessageReceived(const IPC::Message& message) { return handled; } -void AtomRenderViewObserver::OnBrowserMessage(const base::string16& channel, +void AtomRenderViewObserver::OnBrowserMessage(bool send_to_all, + const base::string16& channel, const base::ListValue& args) { if (!document_created_) return; @@ -135,20 +164,13 @@ void AtomRenderViewObserver::OnBrowserMessage(const base::string16& channel, if (!frame || frame->isWebRemoteFrame()) return; - v8::Isolate* isolate = blink::mainThreadIsolate(); - v8::HandleScope handle_scope(isolate); + EmitIPCEvent(frame, channel, args); - v8::Local context = frame->mainWorldScriptContext(); - v8::Context::Scope context_scope(context); - - v8::Local ipc; - if (GetIPCObject(isolate, context, &ipc)) { - auto args_vector = ListValueToVector(isolate, args); - // Insert the Event object, event.sender is ipc. - mate::Dictionary event = mate::Dictionary::CreateEmpty(isolate); - event.Set("sender", ipc); - args_vector.insert(args_vector.begin(), event.GetHandle()); - mate::EmitEvent(isolate, ipc, channel, args_vector); + // Also send the message to all sub-frames. + if (send_to_all) { + for (blink::WebFrame* child = frame->firstChild(); child; + child = child->nextSibling()) + EmitIPCEvent(child, channel, args); } } diff --git a/atom/renderer/atom_render_view_observer.h b/atom/renderer/atom_render_view_observer.h index 4b9d59f3fa08..376138f0849a 100644 --- a/atom/renderer/atom_render_view_observer.h +++ b/atom/renderer/atom_render_view_observer.h @@ -30,7 +30,8 @@ class AtomRenderViewObserver : public content::RenderViewObserver { void DraggableRegionsChanged(blink::WebFrame* frame) override; bool OnMessageReceived(const IPC::Message& message) override; - void OnBrowserMessage(const base::string16& channel, + void OnBrowserMessage(bool send_to_all, + const base::string16& channel, const base::ListValue& args); // Weak reference to renderer client. diff --git a/atom/renderer/atom_renderer_client.cc b/atom/renderer/atom_renderer_client.cc index 5613f20d2ecc..283930b829a2 100644 --- a/atom/renderer/atom_renderer_client.cc +++ b/atom/renderer/atom_renderer_client.cc @@ -11,12 +11,14 @@ #include "atom/common/api/atom_bindings.h" #include "atom/common/api/event_emitter_caller.h" #include "atom/common/color_util.h" +#include "atom/common/native_mate_converters/value_converter.h" #include "atom/common/node_bindings.h" #include "atom/common/node_includes.h" #include "atom/common/options_switches.h" #include "atom/renderer/atom_render_view_observer.h" #include "atom/renderer/guest_view_container.h" #include "atom/renderer/node_array_buffer_bridge.h" +#include "atom/renderer/preferences_manager.h" #include "base/command_line.h" #include "base/strings/utf_string_conversions.h" #include "chrome/renderer/media/chrome_key_systems.h" @@ -29,8 +31,10 @@ #include "content/public/renderer/render_thread.h" #include "content/public/renderer/render_view.h" #include "ipc/ipc_message_macros.h" +#include "native_mate/dictionary.h" #include "net/base/net_errors.h" #include "third_party/WebKit/public/web/WebCustomElement.h" +#include "third_party/WebKit/public/web/WebDocument.h" #include "third_party/WebKit/public/web/WebFrameWidget.h" #include "third_party/WebKit/public/web/WebLocalFrame.h" #include "third_party/WebKit/public/web/WebPluginParams.h" @@ -59,6 +63,7 @@ class AtomRenderFrameObserver : public content::RenderFrameObserver { AtomRenderFrameObserver(content::RenderFrame* frame, AtomRendererClient* renderer_client) : content::RenderFrameObserver(frame), + render_frame_(frame), world_id_(-1), renderer_client_(renderer_client) {} @@ -69,22 +74,45 @@ class AtomRenderFrameObserver : public content::RenderFrameObserver { if (world_id_ != -1 && world_id_ != world_id) return; world_id_ = world_id; - renderer_client_->DidCreateScriptContext(context); + renderer_client_->DidCreateScriptContext(context, render_frame_); } void WillReleaseScriptContext(v8::Local context, int world_id) override { if (world_id_ != world_id) return; - renderer_client_->WillReleaseScriptContext(context); + renderer_client_->WillReleaseScriptContext(context, render_frame_); } private: + content::RenderFrame* render_frame_; int world_id_; AtomRendererClient* renderer_client_; DISALLOW_COPY_AND_ASSIGN(AtomRenderFrameObserver); }; +v8::Local GetRenderProcessPreferences( + const PreferencesManager* preferences_manager, v8::Isolate* isolate) { + if (preferences_manager->preferences()) + return mate::ConvertToV8(isolate, *preferences_manager->preferences()); + else + return v8::Null(isolate); +} + +void AddRenderBindings(v8::Isolate* isolate, + v8::Local process, + const PreferencesManager* preferences_manager) { + mate::Dictionary dict(isolate, process); + dict.SetMethod( + "getRenderProcessPreferences", + base::Bind(GetRenderProcessPreferences, preferences_manager)); +} + +bool IsDevToolsExtension(content::RenderFrame* render_frame) { + return static_cast(render_frame->GetWebFrame()->document().url()) + .SchemeIs("chrome-extension"); +} + } // namespace AtomRendererClient::AtomRendererClient() @@ -101,6 +129,8 @@ void AtomRendererClient::RenderThreadStarted() { OverrideNodeArrayBuffer(); + preferences_manager_.reset(new PreferencesManager); + #if defined(OS_WIN) // Set ApplicationUserModelID in renderer process. base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); @@ -128,15 +158,11 @@ void AtomRendererClient::RenderThreadStarted() { void AtomRendererClient::RenderFrameCreated( content::RenderFrame* render_frame) { new PepperHelper(render_frame); + new AtomRenderFrameObserver(render_frame, this); // Allow file scheme to handle service worker by default. + // FIXME(zcbenz): Can this be moved elsewhere? blink::WebSecurityPolicy::registerURLSchemeAsAllowingServiceWorkers("file"); - - // Only insert node integration for the main frame. - if (!render_frame->IsMainFrame()) - return; - - new AtomRenderFrameObserver(render_frame, this); } void AtomRendererClient::RenderViewCreated(content::RenderView* render_view) { @@ -164,6 +190,23 @@ void AtomRendererClient::RunScriptsAtDocumentStart( // Make sure every page will get a script context created. render_frame->GetWebFrame()->executeScript( blink::WebScriptSource("void 0")); + + // Inform the document start pharse. + node::Environment* env = node_bindings_->uv_env(); + if (env) { + v8::HandleScope handle_scope(env->isolate()); + mate::EmitEvent(env->isolate(), env->process_object(), "document-start"); + } +} + +void AtomRendererClient::RunScriptsAtDocumentEnd( + content::RenderFrame* render_frame) { + // Inform the document end pharse. + node::Environment* env = node_bindings_->uv_env(); + if (env) { + v8::HandleScope handle_scope(env->isolate()); + mate::EmitEvent(env->isolate(), env->process_object(), "document-end"); + } } blink::WebSpeechSynthesizer* AtomRendererClient::OverrideSpeechSynthesizer( @@ -186,7 +229,12 @@ bool AtomRendererClient::OverrideCreatePlugin( } void AtomRendererClient::DidCreateScriptContext( - v8::Handle context) { + v8::Handle context, content::RenderFrame* render_frame) { + // Only allow node integration for the main frame, unless it is a devtools + // extension page. + if (!render_frame->IsMainFrame() && !IsDevToolsExtension(render_frame)) + return; + // Whether the node binding has been initialized. bool first_time = node_bindings_->uv_env() == nullptr; @@ -201,6 +249,8 @@ void AtomRendererClient::DidCreateScriptContext( // Add atom-shell extended APIs. atom_bindings_->BindTo(env->isolate(), env->process_object()); + AddRenderBindings(env->isolate(), env->process_object(), + preferences_manager_.get()); // Load everything. node_bindings_->LoadEnvironment(env); @@ -215,9 +265,10 @@ void AtomRendererClient::DidCreateScriptContext( } void AtomRendererClient::WillReleaseScriptContext( - v8::Handle context) { + v8::Handle context, content::RenderFrame* render_frame) { node::Environment* env = node::Environment::GetCurrent(context); - mate::EmitEvent(env->isolate(), env->process_object(), "exit"); + if (env) + mate::EmitEvent(env->isolate(), env->process_object(), "exit"); } bool AtomRendererClient::ShouldFork(blink::WebLocalFrame* frame, diff --git a/atom/renderer/atom_renderer_client.h b/atom/renderer/atom_renderer_client.h index 16b975fd41f7..51872e5bcba9 100644 --- a/atom/renderer/atom_renderer_client.h +++ b/atom/renderer/atom_renderer_client.h @@ -13,6 +13,7 @@ namespace atom { class AtomBindings; +class PreferencesManager; class NodeBindings; class AtomRendererClient : public content::ContentRendererClient { @@ -20,8 +21,10 @@ class AtomRendererClient : public content::ContentRendererClient { AtomRendererClient(); virtual ~AtomRendererClient(); - void DidCreateScriptContext(v8::Handle context); - void WillReleaseScriptContext(v8::Handle context); + void DidCreateScriptContext( + v8::Handle context, content::RenderFrame* render_frame); + void WillReleaseScriptContext( + v8::Handle context, content::RenderFrame* render_frame); private: enum NodeIntegration { @@ -36,6 +39,7 @@ class AtomRendererClient : public content::ContentRendererClient { void RenderFrameCreated(content::RenderFrame*) override; void RenderViewCreated(content::RenderView*) override; void RunScriptsAtDocumentStart(content::RenderFrame* render_frame) override; + void RunScriptsAtDocumentEnd(content::RenderFrame* render_frame) override; blink::WebSpeechSynthesizer* OverrideSpeechSynthesizer( blink::WebSpeechSynthesizerClient* client) override; bool OverrideCreatePlugin(content::RenderFrame* render_frame, @@ -61,6 +65,7 @@ class AtomRendererClient : public content::ContentRendererClient { std::unique_ptr node_bindings_; std::unique_ptr atom_bindings_; + std::unique_ptr preferences_manager_; DISALLOW_COPY_AND_ASSIGN(AtomRendererClient); }; diff --git a/atom/renderer/preferences_manager.cc b/atom/renderer/preferences_manager.cc new file mode 100644 index 000000000000..a9ed710a9dbd --- /dev/null +++ b/atom/renderer/preferences_manager.cc @@ -0,0 +1,34 @@ +// Copyright (c) 2016 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/renderer/preferences_manager.h" + +#include "atom/common/api/api_messages.h" +#include "content/public/renderer/render_thread.h" + +namespace atom { + +PreferencesManager::PreferencesManager() { + content::RenderThread::Get()->AddObserver(this); +} + +PreferencesManager::~PreferencesManager() { +} + +bool PreferencesManager::OnControlMessageReceived( + const IPC::Message& message) { + bool handled = true; + IPC_BEGIN_MESSAGE_MAP(PreferencesManager, message) + IPC_MESSAGE_HANDLER(AtomMsg_UpdatePreferences, OnUpdatePreferences) + IPC_MESSAGE_UNHANDLED(handled = false) + IPC_END_MESSAGE_MAP() + return handled; +} + +void PreferencesManager::OnUpdatePreferences( + const base::ListValue& preferences) { + preferences_ = preferences.CreateDeepCopy(); +} + +} // namespace atom diff --git a/atom/renderer/preferences_manager.h b/atom/renderer/preferences_manager.h new file mode 100644 index 000000000000..451928085d12 --- /dev/null +++ b/atom/renderer/preferences_manager.h @@ -0,0 +1,35 @@ +// Copyright (c) 2016 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_RENDERER_PREFERENCES_MANAGER_H_ +#define ATOM_RENDERER_PREFERENCES_MANAGER_H_ + +#include + +#include "base/values.h" +#include "content/public/renderer/render_process_observer.h" + +namespace atom { + +class PreferencesManager : public content::RenderProcessObserver { + public: + PreferencesManager(); + ~PreferencesManager() override; + + const base::ListValue* preferences() const { return preferences_.get(); } + + private: + // content::RenderThreadObserver: + bool OnControlMessageReceived(const IPC::Message& message) override; + + void OnUpdatePreferences(const base::ListValue& preferences); + + std::unique_ptr preferences_; + + DISALLOW_COPY_AND_ASSIGN(PreferencesManager); +}; + +} // namespace atom + +#endif // ATOM_RENDERER_PREFERENCES_MANAGER_H_ diff --git a/docs/tutorial/devtools-extension.md b/docs/tutorial/devtools-extension.md index 1791c0459207..f004b1f31127 100644 --- a/docs/tutorial/devtools-extension.md +++ b/docs/tutorial/devtools-extension.md @@ -1,62 +1,52 @@ # DevTools Extension -To make debugging easier, Electron has basic support for the -[Chrome DevTools Extension][devtools-extension]. +Electron supports the [Chrome DevTools Extension][devtools-extension], which can +be used to extend the ability of devtools for debugging popular web frameworks. -For most DevTools extensions you can simply download the source code and use -the `BrowserWindow.addDevToolsExtension` API to load them. The loaded extensions -will be remembered so you don't need to call the API every time when creating -a window. +## How to load a DevTools Extension -** NOTE: React DevTools does not work, follow the issue here https://github.com/electron/electron/issues/915 ** +To load an extension in Electron, you need to download it in Chrome browser, +locate its filesystem path, and then load it by calling the +`BrowserWindow.addDevToolsExtension(extension)` API. -For example, to use the [React DevTools Extension](https://github.com/facebook/react-devtools) -, first you need to download its source code: +Using the [React Developer Tools][react-devtools] as example: -```bash -$ cd /some-directory -$ git clone --recursive https://github.com/facebook/react-devtools.git -``` +1. Install it in Chrome browser. +1. Navigate to `chrome://extensions`, and find its extension ID, which is a hash + string like `fmkadmapgofadopljbjfkapdkoienihi`. +1. Find out filesystem location used by Chrome for storing extensions: + * on Windows it is `%LOCALAPPDATA%\Google\Chrome\User Data\Default\Extensions`; + * on Linux it is `~/.config/google-chrome/Default/Extensions/`; + * on OS X it is `~/Library/Application Support/Google/Chrome/Default/Extensions`. +1. Pass the location of the extension to `BrowserWindow.addDevToolsExtension` + API, for the React Developer Tools, it is something like: + `~/Library/Application Support/Google/Chrome/Default/Extensions/fmkadmapgofadopljbjfkapdkoienihi/0.14.10_0` -Follow the instructions in [`react-devtools/shells/chrome/Readme.md`](https://github.com/facebook/react-devtools/blob/master/shells/chrome/Readme.md) to build the extension. +The name of the extension is returned by `BrowserWindow.addDevToolsExtension`, +and you can pass the name of the extension to the `BrowserWindow.removeDevToolsExtension` +API to unload it. -Then you can load the extension in Electron by opening DevTools in any window, -and running the following code in the DevTools console: +## Supported DevTools Extensions -```javascript -const BrowserWindow = require('electron').remote.BrowserWindow; -BrowserWindow.addDevToolsExtension('/some-directory/react-devtools/shells/chrome'); -``` +Electron only supports a limited set of `chrome.*` APIs, so some extensions +using unsupported `chrome.*` APIs for chrome extension features may not work. +Following Devtools Extensions are tested and guaranteed to work in Electron: -To unload the extension, you can call the `BrowserWindow.removeDevToolsExtension` -API with its name and it will not load the next time you open the DevTools: +* [Ember Inspector](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) +* [React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi) +* [Backbone Debugger](https://chrome.google.com/webstore/detail/backbone-debugger/bhljhndlimiafopmmhjlgfpnnchjjbhd) +* [jQuery Debugger](https://chrome.google.com/webstore/detail/jquery-debugger/dbhhnnnpaeobfddmlalhnehgclcmjimi) +* [AngularJS Batarang](https://chrome.google.com/webstore/detail/angularjs-batarang/ighdmehidhipcmcojjgiloacoafjmpfk) +* [Vue.js devtools](https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd) -```javascript -BrowserWindow.removeDevToolsExtension('React Developer Tools'); -``` +### What should I do if a DevTools Extension is not working? -## Format of DevTools Extension +Fist please make sure the extension is still being maintained, some extensions +can not even work for recent versions of Chrome browser, and we are not able to +do anything for them. -Ideally all DevTools extensions written for the Chrome browser can be loaded by -Electron, but they have to be in a plain directory. For those packaged with -`crx` extensions, there is no way for Electron to load them unless you find a -way to extract them into a directory. - -## Background Pages - -Currently Electron doesn't support the background pages feature in Chrome -extensions, so some DevTools extensions that rely on this feature may -not work in Electron. - -## `chrome.*` APIs - -Some Chrome extensions may use `chrome.*` APIs for features and while there has -been some effort to implement those APIs in Electron, not all have been -implemented. - -Given that not all `chrome.*` APIs are implemented if the DevTools extension is -using APIs other than `chrome.devtools.*`, the extension is very likely not to -work. You can report failing extensions in the issue tracker so that we can add -support for those APIs. +Then file a bug at Electron's issues list, and describe which part of the +extension is not working as expected. [devtools-extension]: https://developer.chrome.com/extensions/devtools +[react-devtools]: https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi diff --git a/filenames.gypi b/filenames.gypi index 14d01270f089..ae71164a8ab9 100644 --- a/filenames.gypi +++ b/filenames.gypi @@ -49,6 +49,7 @@ 'lib/common/init.js', 'lib/common/reset-search-paths.js', 'lib/renderer/chrome-api.js', + 'lib/renderer/content-scripts-injector.js', 'lib/renderer/init.js', 'lib/renderer/inspector.js', 'lib/renderer/override.js', @@ -109,6 +110,8 @@ 'atom/browser/api/atom_api_power_monitor.h', 'atom/browser/api/atom_api_power_save_blocker.cc', 'atom/browser/api/atom_api_power_save_blocker.h', + 'atom/browser/api/atom_api_render_process_preferences.cc', + 'atom/browser/api/atom_api_render_process_preferences.h', 'atom/browser/api/atom_api_protocol.cc', 'atom/browser/api/atom_api_protocol.h', 'atom/browser/api/atom_api_screen.cc', @@ -220,6 +223,8 @@ 'atom/browser/net/url_request_fetch_job.h', 'atom/browser/node_debugger.cc', 'atom/browser/node_debugger.h', + 'atom/browser/render_process_preferences.cc', + 'atom/browser/render_process_preferences.h', 'atom/browser/ui/accelerator_util.cc', 'atom/browser/ui/accelerator_util.h', 'atom/browser/ui/accelerator_util_mac.mm', @@ -396,6 +401,8 @@ 'atom/renderer/guest_view_container.h', 'atom/renderer/node_array_buffer_bridge.cc', 'atom/renderer/node_array_buffer_bridge.h', + 'atom/renderer/preferences_manager.cc', + 'atom/renderer/preferences_manager.h', 'atom/utility/atom_content_utility_client.cc', 'atom/utility/atom_content_utility_client.h', 'chromium_src/chrome/browser/browser_process.cc', diff --git a/lib/browser/api/web-contents.js b/lib/browser/api/web-contents.js index 6253f23b3063..0f79a185ffc9 100644 --- a/lib/browser/api/web-contents.js +++ b/lib/browser/api/web-contents.js @@ -71,12 +71,15 @@ let wrapWebContents = function (webContents) { webContents.setMaxListeners(0) // WebContents::send(channel, args..) - webContents.send = function (channel, ...args) { + // WebContents::sendToAll(channel, args..) + const sendWrapper = (allFrames, channel, ...args) => { if (channel == null) { throw new Error('Missing required channel argument') } - return this._send(channel, args) + return webContents._send(allFrames, channel, args) } + webContents.send = sendWrapper.bind(null, false) + webContents.sendToAll = sendWrapper.bind(null, true) // The navigation controller. controller = new NavigationController(webContents) @@ -218,9 +221,12 @@ binding._setWrapWebContents(wrapWebContents) debuggerBinding._setWrapDebugger(wrapDebugger) sessionBinding._setWrapSession(wrapSession) -module.exports.create = function (options) { - if (options == null) { - options = {} +module.exports = { + create (options = {}) { + return binding.create(options) + }, + + fromId (id) { + return binding.fromId(id) } - return binding.create(options) } diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index 6924f9a7d509..775a31d2d03e 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -1,56 +1,224 @@ -const {app, protocol, BrowserWindow} = require('electron') +const {app, ipcMain, protocol, webContents, BrowserWindow} = require('electron') +const renderProcessPreferences = process.atomBinding('render_process_preferences').forAllBrowserWindow() + const fs = require('fs') const path = require('path') const url = require('url') -// Mapping between hostname and file path. -var hostPathMap = {} -var hostPathMapNextKey = 0 - -var getHostForPath = function (path) { - var key - key = 'extension-' + (++hostPathMapNextKey) - hostPathMap[key] = path - return key +// TODO(zcbenz): Remove this when we have Object.values(). +const objectValues = function (object) { + return Object.keys(object).map(function (key) { return object[key] }) } -var getPathForHost = function (host) { - return hostPathMap[host] +// Mapping between extensionId(hostname) and manifest. +const manifestMap = {} // extensionId => manifest +const manifestNameMap = {} // name => manifest + +const generateExtensionIdFromName = function (name) { + return name.replace(/[\W_]+/g, '-').toLowerCase() } -// Cache extensionInfo. -var extensionInfoMap = {} - -var getExtensionInfoFromPath = function (srcDirectory) { - var manifest, page - manifest = JSON.parse(fs.readFileSync(path.join(srcDirectory, 'manifest.json'))) - if (extensionInfoMap[manifest.name] == null) { - // We can not use 'file://' directly because all resources in the extension - // will be treated as relative to the root in Chrome. - page = url.format({ - protocol: 'chrome-extension', - slashes: true, - hostname: getHostForPath(srcDirectory), - pathname: manifest.devtools_page - }) - extensionInfoMap[manifest.name] = { - startPage: page, - name: manifest.name, +// Create or get manifest object from |srcDirectory|. +const getManifestFromPath = function (srcDirectory) { + const manifest = JSON.parse(fs.readFileSync(path.join(srcDirectory, 'manifest.json'))) + if (!manifestNameMap[manifest.name]) { + const extensionId = generateExtensionIdFromName(manifest.name) + console.log(extensionId) + manifestMap[extensionId] = manifestNameMap[manifest.name] = manifest + Object.assign(manifest, { srcDirectory: srcDirectory, - exposeExperimentalAPIs: true - } - return extensionInfoMap[manifest.name] + extensionId: extensionId, + // We can not use 'file://' directly because all resources in the extension + // will be treated as relative to the root in Chrome. + startPage: url.format({ + protocol: 'chrome-extension', + slashes: true, + hostname: extensionId, + pathname: manifest.devtools_page + }) + }) + return manifest } } -// The loaded extensions cache and its persistent path. -var loadedExtensions = null -var loadedExtensionsPath = null +// Manage the background pages. +const backgroundPages = {} + +const startBackgroundPages = function (manifest) { + if (backgroundPages[manifest.extensionId] || !manifest.background) return + + const scripts = manifest.background.scripts.map((name) => { + return `` + }).join('') + const html = new Buffer(`${scripts}`) + + const contents = webContents.create({ + commandLineSwitches: ['--background-page'] + }) + backgroundPages[manifest.extensionId] = { html: html, webContents: contents } + contents.loadURL(url.format({ + protocol: 'chrome-extension', + slashes: true, + hostname: manifest.extensionId, + pathname: '_generated_background_page.html' + })) +} + +const removeBackgroundPages = function (manifest) { + if (!backgroundPages[manifest.extensionId]) return + + backgroundPages[manifest.extensionId].webContents.destroy() + delete backgroundPages[manifest.extensionId] +} + +// Dispatch tabs events. +const hookWindowForTabEvents = function (win) { + const tabId = win.webContents.id + for (const page of objectValues(backgroundPages)) { + page.webContents.sendToAll('CHROME_TABS_ONCREATED', tabId) + } + + win.once('closed', () => { + for (const page of objectValues(backgroundPages)) { + page.webContents.sendToAll('CHROME_TABS_ONREMOVED', tabId) + } + }) +} + +// Handle the chrome.* API messages. +let nextId = 0 + +ipcMain.on('CHROME_RUNTIME_CONNECT', function (event, extensionId, connectInfo) { + const page = backgroundPages[extensionId] + if (!page) { + console.error(`Connect to unkown extension ${extensionId}`) + return + } + + const portId = ++nextId + event.returnValue = {tabId: page.webContents.id, portId: portId} + + event.sender.once('render-view-deleted', () => { + if (page.webContents.isDestroyed()) return + page.webContents.sendToAll(`CHROME_PORT_DISCONNECT_${portId}`) + }) + page.webContents.sendToAll(`CHROME_RUNTIME_ONCONNECT_${extensionId}`, event.sender.id, portId, connectInfo) +}) + +ipcMain.on('CHROME_RUNTIME_SENDMESSAGE', function (event, extensionId, message) { + const page = backgroundPages[extensionId] + if (!page) { + console.error(`Connect to unkown extension ${extensionId}`) + return + } + + page.webContents.sendToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, event.sender.id, message) +}) + +ipcMain.on('CHROME_TABS_SEND_MESSAGE', function (event, tabId, extensionId, isBackgroundPage, message) { + const contents = webContents.fromId(tabId) + if (!contents) { + console.error(`Sending message to unkown tab ${tabId}`) + return + } + + const senderTabId = isBackgroundPage ? null : event.sender.id + + contents.sendToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, senderTabId, message) +}) + +ipcMain.on('CHROME_TABS_EXECUTESCRIPT', function (event, requestId, tabId, extensionId, details) { + const contents = webContents.fromId(tabId) + if (!contents) { + console.error(`Sending message to unkown tab ${tabId}`) + return + } + + let code, url + if (details.file) { + const manifest = manifestMap[extensionId] + code = String(fs.readFileSync(path.join(manifest.srcDirectory, details.file))) + url = `chrome-extension://${extensionId}${details.file}` + } else { + code = details.code + url = `chrome-extension://${extensionId}/${String(Math.random()).substr(2, 8)}.js` + } + + contents.send('CHROME_TABS_EXECUTESCRIPT', event.sender.id, requestId, extensionId, url, code) +}) + +// Transfer the content scripts to renderer. +const contentScripts = {} + +const injectContentScripts = function (manifest) { + if (contentScripts[manifest.name] || !manifest.content_scripts) return + + const readArrayOfFiles = function (relativePath) { + return { + url: `chrome-extension://${manifest.extensionId}/${relativePath}`, + code: String(fs.readFileSync(path.join(manifest.srcDirectory, relativePath))) + } + } + + const contentScriptToEntry = function (script) { + return { + matches: script.matches, + js: script.js.map(readArrayOfFiles), + runAt: script.run_at || 'document_idle' + } + } + + try { + const entry = { + extensionId: manifest.extensionId, + contentScripts: manifest.content_scripts.map(contentScriptToEntry) + } + contentScripts[manifest.name] = renderProcessPreferences.addEntry(entry) + } catch (e) { + console.error('Failed to read content scripts', e) + } +} + +const removeContentScripts = function (manifest) { + if (!contentScripts[manifest.name]) return + + renderProcessPreferences.removeEntry(contentScripts[manifest.name]) + delete contentScripts[manifest.name] +} + +// Transfer the |manifest| to a format that can be recognized by the +// |DevToolsAPI.addExtensions|. +const manifestToExtensionInfo = function (manifest) { + return { + startPage: manifest.startPage, + srcDirectory: manifest.srcDirectory, + name: manifest.name, + exposeExperimentalAPIs: true + } +} + +// Load the extensions for the window. +const loadExtension = function (manifest) { + startBackgroundPages(manifest) + injectContentScripts(manifest) +} + +const loadDevToolsExtensions = function (win, manifests) { + if (!win.devToolsWebContents) return + + manifests.forEach(loadExtension) + + const extensionInfoArray = manifests.map(manifestToExtensionInfo) + win.devToolsWebContents.executeJavaScript(`DevToolsAPI.addExtensions(${JSON.stringify(extensionInfoArray)})`) +} + +// The persistent path of "DevTools Extensions" preference file. +let loadedExtensionsPath = null app.on('will-quit', function () { try { - loadedExtensions = Object.keys(extensionInfoMap).map(function (key) { - return extensionInfoMap[key].srcDirectory + const loadedExtensions = objectValues(manifestMap).map(function (manifest) { + return manifest.srcDirectory }) if (loadedExtensions.length > 0) { try { @@ -69,74 +237,78 @@ app.on('will-quit', function () { // We can not use protocol or BrowserWindow until app is ready. app.once('ready', function () { - var chromeExtensionHandler, i, init, len, srcDirectory + // The chrome-extension: can map a extension URL request to real file path. + const chromeExtensionHandler = function (request, callback) { + const parsed = url.parse(request.url) + if (!parsed.hostname || !parsed.path) return callback() + + const manifest = manifestMap[parsed.hostname] + if (!manifest) return callback() + + if (parsed.path === '/_generated_background_page.html' && + backgroundPages[parsed.hostname]) { + return callback({ + mimeType: 'text/html', + data: backgroundPages[parsed.hostname].html + }) + } + + fs.readFile(path.join(manifest.srcDirectory, parsed.path), function (err, content) { + if (err) { + return callback(-6) // FILE_NOT_FOUND + } else { + return callback(content) + } + }) + } + protocol.registerBufferProtocol('chrome-extension', chromeExtensionHandler, function (error) { + if (error) { + console.error(`Unable to register chrome-extension protocol: ${error}`) + } + }) // Load persisted extensions. loadedExtensionsPath = path.join(app.getPath('userData'), 'DevTools Extensions') try { - loadedExtensions = JSON.parse(fs.readFileSync(loadedExtensionsPath)) - if (!Array.isArray(loadedExtensions)) { - loadedExtensions = [] - } - - // Preheat the extensionInfo cache. - for (i = 0, len = loadedExtensions.length; i < len; i++) { - srcDirectory = loadedExtensions[i] - getExtensionInfoFromPath(srcDirectory) + const loadedExtensions = JSON.parse(fs.readFileSync(loadedExtensionsPath)) + if (Array.isArray(loadedExtensions)) { + for (const srcDirectory of loadedExtensions) { + // Start background pages and set content scripts. + const manifest = getManifestFromPath(srcDirectory) + loadExtension(manifest) + } } } catch (error) { // Ignore error } - // The chrome-extension: can map a extension URL request to real file path. - chromeExtensionHandler = function (request, callback) { - var directory, parsed - parsed = url.parse(request.url) - if (!(parsed.hostname && (parsed.path != null))) { - return callback() - } - if (!/extension-\d+/.test(parsed.hostname)) { - return callback() - } - directory = getPathForHost(parsed.hostname) - if (directory == null) { - return callback() - } - return callback(path.join(directory, parsed.path)) - } - protocol.registerFileProtocol('chrome-extension', chromeExtensionHandler, function (error) { - if (error) { - return console.error('Unable to register chrome-extension protocol') - } - }) - BrowserWindow.prototype._loadDevToolsExtensions = function (extensionInfoArray) { - var ref - return (ref = this.devToolsWebContents) != null ? ref.executeJavaScript('DevToolsAPI.addExtensions(' + (JSON.stringify(extensionInfoArray)) + ');') : void 0 - } + // The public API to add/remove extensions. BrowserWindow.addDevToolsExtension = function (srcDirectory) { - var extensionInfo, j, len1, ref, window - extensionInfo = getExtensionInfoFromPath(srcDirectory) - if (extensionInfo) { - ref = BrowserWindow.getAllWindows() - for (j = 0, len1 = ref.length; j < len1; j++) { - window = ref[j] - window._loadDevToolsExtensions([extensionInfo]) + const manifest = getManifestFromPath(srcDirectory) + if (manifest) { + for (const win of BrowserWindow.getAllWindows()) { + loadDevToolsExtensions(win, [manifest]) } - return extensionInfo.name + return manifest.name } } BrowserWindow.removeDevToolsExtension = function (name) { - return delete extensionInfoMap[name] + const manifest = manifestNameMap[name] + if (!manifest) return + + removeBackgroundPages(manifest) + removeContentScripts(manifest) + delete manifestMap[manifest.extensionId] + delete manifestNameMap[name] } - // Load persisted extensions when devtools is opened. - init = BrowserWindow.prototype._init + // Load extensions automatically when devtools is opened. + const init = BrowserWindow.prototype._init BrowserWindow.prototype._init = function () { init.call(this) - return this.webContents.on('devtools-opened', () => { - return this._loadDevToolsExtensions(Object.keys(extensionInfoMap).map(function (key) { - return extensionInfoMap[key] - })) + hookWindowForTabEvents(this) + this.webContents.on('devtools-opened', () => { + loadDevToolsExtensions(this, objectValues(manifestMap)) }) } }) diff --git a/lib/browser/rpc-server.js b/lib/browser/rpc-server.js index fa9841aeecb0..0b954e518b95 100644 --- a/lib/browser/rpc-server.js +++ b/lib/browser/rpc-server.js @@ -2,7 +2,7 @@ const electron = require('electron') const v8Util = process.atomBinding('v8_util') -const {ipcMain, isPromise} = electron +const {ipcMain, isPromise, webContents} = electron const objectsRegistry = require('./objects-registry') @@ -353,3 +353,17 @@ ipcMain.on('ELECTRON_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', function (event, request event.returnValue = exceptionToMeta(error) } }) + +ipcMain.on('ELECTRON_BROWSER_SEND_TO', function (event, sendToAll, webContentsId, channel, ...args) { + let contents = webContents.fromId(webContentsId) + if (!contents) { + console.error(`Sending message to WebContents with unknown ID ${webContentsId}`) + return + } + + if (sendToAll) { + contents.sendToAll(channel, ...args) + } else { + contents.send(channel, ...args) + } +}) diff --git a/lib/renderer/api/ipc-renderer.js b/lib/renderer/api/ipc-renderer.js index a6b6b1851ea3..66c40d311da2 100644 --- a/lib/renderer/api/ipc-renderer.js +++ b/lib/renderer/api/ipc-renderer.js @@ -18,4 +18,20 @@ ipcRenderer.sendToHost = function (...args) { return binding.send('ipc-message-host', args) } +ipcRenderer.sendTo = function (webContentsId, channel, ...args) { + if (typeof webContentsId !== 'number') { + throw new TypeError('First argument has to be webContentsId') + } + + ipcRenderer.send('ELECTRON_BROWSER_SEND_TO', false, webContentsId, channel, ...args) +} + +ipcRenderer.sendToAll = function (webContentsId, channel, ...args) { + if (typeof webContentsId !== 'number') { + throw new TypeError('First argument has to be webContentsId') + } + + ipcRenderer.send('ELECTRON_BROWSER_SEND_TO', true, webContentsId, channel, ...args) +} + module.exports = ipcRenderer diff --git a/lib/renderer/chrome-api.js b/lib/renderer/chrome-api.js index 719066a6fae7..61fce14c5ba9 100644 --- a/lib/renderer/chrome-api.js +++ b/lib/renderer/chrome-api.js @@ -1,13 +1,200 @@ +const {ipcRenderer} = require('electron') const url = require('url') -const chrome = window.chrome = window.chrome || {} -chrome.extension = { - getURL: function (path) { - return url.format({ - protocol: window.location.protocol, - slashes: true, - hostname: window.location.hostname, - pathname: path - }) +let nextId = 0 + +class Event { + constructor () { + this.listeners = [] + } + + addListener (callback) { + this.listeners.push(callback) + } + + removeListener (callback) { + const index = this.listeners.indexOf(callback) + if (index !== -1) { + this.listeners.splice(index, 1) + } + } + + emit (...args) { + for (const listener of this.listeners) { + listener(...args) + } + } +} + +class Tab { + constructor (tabId) { + this.id = tabId + } +} + +class MessageSender { + constructor (tabId, extensionId) { + this.tab = tabId ? new Tab(tabId) : null + this.id = extensionId + this.url = `chrome-extension://${extensionId}` + } +} + +class Port { + constructor (tabId, portId, extensionId, name) { + this.tabId = tabId + this.portId = portId + this.disconnected = false + + this.name = name + this.onDisconnect = new Event() + this.onMessage = new Event() + this.sender = new MessageSender(tabId, extensionId) + + ipcRenderer.once(`CHROME_PORT_DISCONNECT_${portId}`, () => { + this._onDisconnect() + }) + ipcRenderer.on(`CHROME_PORT_POSTMESSAGE_${portId}`, (event, message) => { + const sendResponse = function () { console.error('sendResponse is not implemented') } + this.onMessage.emit(message, this.sender, sendResponse) + }) + } + + disconnect () { + if (this.disconnected) return + + ipcRenderer.sendToAll(this.tabId, `CHROME_PORT_DISCONNECT_${this.portId}`) + this._onDisconnect() + } + + postMessage (message) { + ipcRenderer.sendToAll(this.tabId, `CHROME_PORT_POSTMESSAGE_${this.portId}`, message) + } + + _onDisconnect () { + this.disconnected = true + ipcRenderer.removeAllListeners(`CHROME_PORT_POSTMESSAGE_${this.portId}`) + this.onDisconnect.emit() + } +} + +// Inject chrome API to the |context| +exports.injectTo = function (extensionId, isBackgroundPage, context) { + const chrome = context.chrome = context.chrome || {} + + ipcRenderer.on(`CHROME_RUNTIME_ONCONNECT_${extensionId}`, (event, tabId, portId, connectInfo) => { + chrome.runtime.onConnect.emit(new Port(tabId, portId, extensionId, connectInfo.name)) + }) + + ipcRenderer.on(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, (event, tabId, message) => { + chrome.runtime.onMessage.emit(message, new MessageSender(tabId, extensionId)) + }) + + ipcRenderer.on('CHROME_TABS_ONCREATED', (event, tabId) => { + chrome.tabs.onCreated.emit(new Tab(tabId)) + }) + + ipcRenderer.on('CHROME_TABS_ONREMOVED', (event, tabId) => { + chrome.tabs.onRemoved.emit(tabId) + }) + + chrome.runtime = { + getURL: function (path) { + return url.format({ + protocol: 'chrome-extension', + slashes: true, + hostname: extensionId, + pathname: path + }) + }, + + connect (...args) { + if (isBackgroundPage) { + console.error('chrome.runtime.connect is not supported in background page') + return + } + + // Parse the optional args. + let targetExtensionId = extensionId + let connectInfo = {name: ''} + if (args.length === 1) { + connectInfo = args[0] + } else if (args.length === 2) { + [targetExtensionId, connectInfo] = args + } + + const {tabId, portId} = ipcRenderer.sendSync('CHROME_RUNTIME_CONNECT', targetExtensionId, connectInfo) + return new Port(tabId, portId, extensionId, connectInfo.name) + }, + + sendMessage (...args) { + if (isBackgroundPage) { + console.error('chrome.runtime.sendMessage is not supported in background page') + return + } + + // Parse the optional args. + let targetExtensionId = extensionId + let message + if (args.length === 1) { + message = args[0] + } else if (args.length === 2) { + [targetExtensionId, message] = args + } else { + console.error('options and responseCallback are not supported') + } + + ipcRenderer.send('CHROME_RUNTIME_SENDMESSAGE', targetExtensionId, message) + }, + + onConnect: new Event(), + onMessage: new Event(), + onInstalled: new Event() + } + + chrome.tabs = { + executeScript (tabId, details, callback) { + const requestId = ++nextId + ipcRenderer.once(`CHROME_TABS_EXECUTESCRIPT_RESULT_${requestId}`, (event, result) => { + callback([event.result]) + }) + ipcRenderer.send('CHROME_TABS_EXECUTESCRIPT', requestId, tabId, extensionId, details) + }, + + sendMessage (tabId, message, options, responseCallback) { + if (responseCallback) { + console.error('responseCallback is not supported') + } + ipcRenderer.send('CHROME_TABS_SEND_MESSAGE', tabId, extensionId, isBackgroundPage, message) + }, + + onUpdated: new Event(), + onCreated: new Event(), + onRemoved: new Event() + } + + chrome.extension = { + getURL: chrome.runtime.getURL, + connect: chrome.runtime.connect, + onConnect: chrome.runtime.onConnect, + sendMessage: chrome.runtime.sendMessage, + onMessage: chrome.runtime.onMessage + } + + chrome.storage = { + sync: { + get () {}, + set () {} + } + } + + chrome.pageAction = { + show () {}, + hide () {}, + setTitle () {}, + getTitle () {}, + setIcon () {}, + setPopup () {}, + getPopup () {} } } diff --git a/lib/renderer/content-scripts-injector.js b/lib/renderer/content-scripts-injector.js new file mode 100644 index 000000000000..e4a801110f62 --- /dev/null +++ b/lib/renderer/content-scripts-injector.js @@ -0,0 +1,61 @@ +const {ipcRenderer} = require('electron') +const {runInThisContext} = require('vm') + +// Check whether pattern matches. +// https://developer.chrome.com/extensions/match_patterns +const matchesPattern = function (pattern) { + if (pattern === '') return true + + const regexp = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$') + return location.href.match(regexp) +} + +// Run the code with chrome API integrated. +const runContentScript = function (extensionId, url, code) { + const context = {} + require('./chrome-api').injectTo(extensionId, false, context) + const wrapper = `(function (chrome) {\n ${code}\n })` + const compiledWrapper = runInThisContext(wrapper, { + filename: url, + lineOffset: 1, + displayErrors: true + }) + return compiledWrapper.call(this, context.chrome) +} + +// Run injected scripts. +// https://developer.chrome.com/extensions/content_scripts +const injectContentScript = function (extensionId, script) { + for (const match of script.matches) { + if (!matchesPattern(match)) return + } + + for (const {url, code} of script.js) { + const fire = runContentScript.bind(window, extensionId, url, code) + if (script.runAt === 'document_start') { + process.once('document-start', fire) + } else if (script.runAt === 'document_end') { + process.once('document-end', fire) + } else if (script.runAt === 'document_idle') { + document.addEventListener('DOMContentLoaded', fire) + } + } +} + +// Handle the request of chrome.tabs.executeJavaScript. +ipcRenderer.on('CHROME_TABS_EXECUTESCRIPT', function (event, senderWebContentsId, requestId, extensionId, url, code) { + const result = runContentScript.call(window, extensionId, url, code) + ipcRenderer.sendToAll(senderWebContentsId, `CHROME_TABS_EXECUTESCRIPT_RESULT_${requestId}`, result) +}) + +// Read the renderer process preferences. +const preferences = process.getRenderProcessPreferences() +if (preferences) { + for (const pref of preferences) { + if (pref.contentScripts) { + for (const script of pref.contentScripts) { + injectContentScript(pref.extensionId, script) + } + } + } +} diff --git a/lib/renderer/init.js b/lib/renderer/init.js index ed0e0e800e62..f0875cae12c2 100644 --- a/lib/renderer/init.js +++ b/lib/renderer/init.js @@ -41,8 +41,9 @@ electron.ipcRenderer.on('ELECTRON_INTERNAL_RENDERER_ASYNC_WEB_FRAME_METHOD', (ev }) // Process command line arguments. -var nodeIntegration = 'false' -var preloadScript = null +let nodeIntegration = 'false' +let preloadScript = null +let isBackgroundPage = false for (let arg of process.argv) { if (arg.indexOf('--guest-instance-id=') === 0) { // This is a guest web view. @@ -54,6 +55,8 @@ for (let arg of process.argv) { nodeIntegration = arg.substr(arg.indexOf('=') + 1) } else if (arg.indexOf('--preload=') === 0) { preloadScript = arg.substr(arg.indexOf('=') + 1) + } else if (arg === '--background-page') { + isBackgroundPage = true } } @@ -63,12 +66,15 @@ if (window.location.protocol === 'chrome-devtools:') { nodeIntegration = 'true' } else if (window.location.protocol === 'chrome-extension:') { // Add implementations of chrome API. - require('./chrome-api') - nodeIntegration = 'true' + require('./chrome-api').injectTo(window.location.hostname, isBackgroundPage, window) + nodeIntegration = 'false' } else { // Override default web functions. require('./override') + // Inject content scripts. + require('./content-scripts-injector') + // Load webview tag implementation. if (nodeIntegration === 'true' && process.guestInstanceId == null) { require('./web-view/web-view') diff --git a/lib/renderer/inspector.js b/lib/renderer/inspector.js index aea87f7f2b81..ca60c84d4be9 100644 --- a/lib/renderer/inspector.js +++ b/lib/renderer/inspector.js @@ -1,7 +1,4 @@ window.onload = function () { - // Make sure |window.chrome| is defined for devtools extensions. - hijackSetInjectedScript(window.InspectorFrontendHost) - // Use menu API to show context menu. window.InspectorFrontendHost.showContextMenuAtPoint = createMenu @@ -9,18 +6,6 @@ window.onload = function () { window.WebInspector.createFileSelectorElement = createFileSelectorElement } -const hijackSetInjectedScript = function (InspectorFrontendHost) { - const {setInjectedScriptForOrigin} = InspectorFrontendHost - InspectorFrontendHost.setInjectedScriptForOrigin = function (origin, source) { - const wrapped = `(function (...args) { - window.chrome = {} - const original = ${source} - original(...args) - })` - setInjectedScriptForOrigin(origin, wrapped) - } -} - var convertToMenuTemplate = function (items) { var fn, i, item, len, template template = [] diff --git a/spec/api-ipc-spec.js b/spec/api-ipc-spec.js index 22c2edde08d0..66fa308dd621 100644 --- a/spec/api-ipc-spec.js +++ b/spec/api-ipc-spec.js @@ -3,11 +3,8 @@ const assert = require('assert') const path = require('path') -const ipcRenderer = require('electron').ipcRenderer -const remote = require('electron').remote - -const ipcMain = remote.require('electron').ipcMain -const BrowserWindow = remote.require('electron').BrowserWindow +const {ipcRenderer, remote} = require('electron') +const {ipcMain, webContents, BrowserWindow} = remote const comparePaths = function (path1, path2) { if (process.platform === 'win32') { @@ -244,6 +241,30 @@ describe('ipc module', function () { }) }) + describe('ipcRenderer.sendTo', function () { + let contents = null + beforeEach(function () { + contents = webContents.create({}) + }) + afterEach(function () { + ipcRenderer.removeAllListeners('pong') + contents.destroy() + contents = null + }) + + it('sends message to WebContents', function (done) { + const webContentsId = remote.getCurrentWebContents().id + ipcRenderer.once('pong', function (event, id) { + assert.equal(webContentsId, id) + done() + }) + contents.once('did-finish-load', function () { + ipcRenderer.sendTo(contents.id, 'ping', webContentsId) + }) + contents.loadURL('file://' + path.join(fixtures, 'pages', 'ping-pong.html')) + }) + }) + describe('remote listeners', function () { var w = null diff --git a/spec/fixtures/pages/ping-pong.html b/spec/fixtures/pages/ping-pong.html new file mode 100644 index 000000000000..d10e7898653b --- /dev/null +++ b/spec/fixtures/pages/ping-pong.html @@ -0,0 +1,11 @@ + + + + + +