diff options
author | mpcomplete@google.com <mpcomplete@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-04-02 23:56:11 +0000 |
---|---|---|
committer | mpcomplete@google.com <mpcomplete@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-04-02 23:56:11 +0000 |
commit | 75e5a879a500897b2409c49d4ef205e2e954c9fd (patch) | |
tree | 166ca53e8cc29520402efd7cc6ee2804edf744eb | |
parent | 720ac73783ea732c130ff6d11dffa41919a25809 (diff) | |
download | chromium_src-75e5a879a500897b2409c49d4ef205e2e954c9fd.zip chromium_src-75e5a879a500897b2409c49d4ef205e2e954c9fd.tar.gz chromium_src-75e5a879a500897b2409c49d4ef205e2e954c9fd.tar.bz2 |
Add code to support 2-way communication between extensions and renderers. The code is almost fully symmetrical, except that right now a channel can only be opened to an extension (by ID). It should be trivial to open a channel to a tab, once we have a solid tab API.
Review URL: http://codereview.chromium.org/56037
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@13057 0039d316-1c4b-4281-b951-d872f2087c98
19 files changed, 518 insertions, 194 deletions
diff --git a/chrome/browser/extensions/extension_message_service.cc b/chrome/browser/extensions/extension_message_service.cc index 95143ce..87469f9 100755 --- a/chrome/browser/extensions/extension_message_service.cc +++ b/chrome/browser/extensions/extension_message_service.cc @@ -13,121 +13,117 @@ #include "chrome/browser/renderer_host/resource_message_filter.h" #include "chrome/common/render_messages.h" -// This class acts as the port to an extension process. It is basically just -// gymnastics to get access to the IPC::Channel (not the ChannelProxy) belonging -// to an ExtensionView. -// Created on the UI thread, but accessed fully on the IO thread. -class ExtensionMessageService::ExtensionFilter : - public IPC::ChannelProxy::MessageFilter { - public: - ExtensionFilter(const std::string& extension_id, int routing_id) : - extension_id_(extension_id), routing_id_(routing_id), channel_(NULL) { - } - ~ExtensionFilter() { - ExtensionMessageService::GetInstance()->OnExtensionUnregistered(this); - } - - virtual void OnFilterAdded(IPC::Channel* channel) { - channel_ = channel; - ExtensionMessageService::GetInstance()->OnExtensionRegistered(this); - } - virtual void OnChannelClosing() { - channel_ = NULL; - } - - bool Send(IPC::Message* message) { - if (!channel_) { - delete message; - return false; - } - return channel_->Send(message); - } +// Since we have 2 ports for every channel, we just index channels by half the +// port ID. +#define GET_CHANNEL_ID(port_id) (port_id / 2) - IPC::Channel* channel() { return channel_; } - const std::string& extension_id() { return extension_id_; } - int routing_id() { return routing_id_; } +// Port1 is always even, port2 is always odd. +#define IS_PORT1_ID(port_id) ((port_id & 1) == 0) - private: - std::string extension_id_; - int routing_id_; - IPC::Channel* channel_; -}; +// Change even to odd and vice versa, to get the other side of a given channel. +#define GET_OPPOSITE_PORT_ID(source_port_id) (source_port_id ^ 1) ExtensionMessageService* ExtensionMessageService::GetInstance() { return Singleton<ExtensionMessageService>::get(); } ExtensionMessageService::ExtensionMessageService() - : next_channel_id_(0) { + : next_port_id_(0) { } -void ExtensionMessageService::RegisterExtensionView(ExtensionView* view) { - view->render_view_host()->process()->channel()->AddFilter( - new ExtensionFilter(view->extension()->id(), - view->render_view_host()->routing_id())); -} - -void ExtensionMessageService::OnExtensionRegistered(ExtensionFilter* filter) { - extensions_[filter->extension_id()] = filter; -} - -void ExtensionMessageService::OnExtensionUnregistered(ExtensionFilter* filter) { - // TODO(mpcomplete): support multiple filters per extension_id - //DCHECK(extensions_[filter->extension_id()] == filter); - extensions_.erase(filter->extension_id()); - - // Close any channels that share this filter. - for (MessageChannelMap::iterator it = channels_.begin(); - it != channels_.end(); ) { - MessageChannelMap::iterator current = it++; - if (current->second.extension_port == filter) - channels_.erase(current); - } +void ExtensionMessageService::RegisterExtension( + const std::string& extension_id, int render_process_id) { + AutoLock lock(renderers_lock_); + // TODO(mpcomplete): We need to ensure an extension always ends up in a single + // process. I think this means having an ExtensionProcessManager which holds + // a BrowsingContext for each extension. + //DCHECK(process_ids_.find(extension_id) == process_ids_.end()); + process_ids_[extension_id] = render_process_id; } int ExtensionMessageService::OpenChannelToExtension( - const std::string& extension_id, ResourceMessageFilter* renderer_port) { + const std::string& extension_id, ResourceMessageFilter* source) { DCHECK(MessageLoop::current() == ChromeThread::GetMessageLoop(ChromeThread::IO)); - ExtensionMap::iterator extension_port = extensions_.find(extension_id); - if (extension_port == extensions_.end()) - return -1; + // Lookup the targeted extension process. + ResourceMessageFilter* dest = NULL; + { + AutoLock lock(renderers_lock_); + ProcessIDMap::iterator process_id = process_ids_.find(extension_id); + if (process_id == process_ids_.end()) + return -1; + + RendererMap::iterator renderer = renderers_.find(process_id->second); + if (renderer == renderers_.end()) + return -1; + dest = renderer->second; + } + + // Create a channel ID for both sides of the channel. + int port1_id = next_port_id_++; + int port2_id = next_port_id_++; + DCHECK(IS_PORT1_ID(port1_id)); + DCHECK(GET_OPPOSITE_PORT_ID(port1_id) == port2_id); + DCHECK(GET_OPPOSITE_PORT_ID(port2_id) == port1_id); + DCHECK(GET_CHANNEL_ID(port1_id) == GET_CHANNEL_ID(port2_id)); - int channel_id = next_channel_id_++; MessageChannel channel; - channel.renderer_port = renderer_port; - channel.extension_port = extension_port->second; - channels_[channel_id] = channel; + channel.port1 = source; + channel.port2 = dest; + channels_[GET_CHANNEL_ID(port1_id)] = channel; - return channel_id; + // Send each process the id for the opposite port. + dest->Send(new ViewMsg_ExtensionHandleConnect(port1_id)); + return port2_id; } -void ExtensionMessageService::PostMessageToExtension( - int channel_id, const std::string& message) { +void ExtensionMessageService::PostMessageFromRenderer( + int port_id, const std::string& message, ResourceMessageFilter* source) { DCHECK(MessageLoop::current() == ChromeThread::GetMessageLoop(ChromeThread::IO)); - MessageChannelMap::iterator iter = channels_.find(channel_id); + // Look up the channel by port1's ID. + MessageChannelMap::iterator iter = + channels_.find(GET_CHANNEL_ID(port_id)); if (iter == channels_.end()) return; - MessageChannel& channel = iter->second; - channel.extension_port->Send(new ViewMsg_HandleExtensionMessage( - channel.extension_port->routing_id(), message, channel_id)); + + // Figure out which port the ID corresponds to. + ResourceMessageFilter* dest = NULL; + if (IS_PORT1_ID(port_id)) { + dest = channel.port1; + DCHECK(source == channel.port2); + } else { + dest = channel.port2; + DCHECK(source == channel.port1); + } + + int source_port_id = GET_OPPOSITE_PORT_ID(port_id); + dest->Send(new ViewMsg_ExtensionHandleMessage(message, source_port_id)); +} + +void ExtensionMessageService::RendererReady(ResourceMessageFilter* renderer) { + AutoLock lock(renderers_lock_); + DCHECK(renderers_.find(renderer->GetProcessId()) == renderers_.end()); + renderers_[renderer->GetProcessId()] = renderer; } void ExtensionMessageService::RendererShutdown( - ResourceMessageFilter* renderer_port) { - DCHECK(MessageLoop::current() == - ChromeThread::GetMessageLoop(ChromeThread::IO)); + ResourceMessageFilter* renderer) { + { + AutoLock lock(renderers_lock_); + DCHECK(renderers_.find(renderer->GetProcessId()) != renderers_.end()); + renderers_.erase(renderer->GetProcessId()); + } // Close any channels that share this filter. // TODO(mpcomplete): should we notify the other side of the port? for (MessageChannelMap::iterator it = channels_.begin(); it != channels_.end(); ) { MessageChannelMap::iterator current = it++; - if (current->second.renderer_port == renderer_port) + if (current->second.port1 == renderer || current->second.port2 == renderer) channels_.erase(current); } } diff --git a/chrome/browser/extensions/extension_message_service.h b/chrome/browser/extensions/extension_message_service.h index 1b4040d..0020a30 100755 --- a/chrome/browser/extensions/extension_message_service.h +++ b/chrome/browser/extensions/extension_message_service.h @@ -8,13 +8,14 @@ #include <map> #include <string> +#include "base/lock.h" + class ExtensionView; class ResourceMessageFilter; -// This class manages message passing to and from extension processes. It -// maintains a list of available extensions, as well as a set of open channels. -// It should only be accessed on the IO thread, with the exception of -// RegisterExtensionView(). +// This class manages message passing between renderer processes. It maintains +// a list of available extensions and which renderers each lives in, as well as +// a set of open channels. // // Terminology: // channel: connection between two ports (one of which belongs to an extension) @@ -27,39 +28,53 @@ class ExtensionMessageService { ExtensionMessageService(); - // Registers an extension so that it can be referenced by its ID. This method - // should only be used by the UI thread. - void RegisterExtensionView(ExtensionView* view); + // --- UI thread only: + + // Register an extension and its corresponding renderer process. + void RegisterExtension(const std::string& extension_id, + int render_process_id); + + // --- IO thread only: // Given an extension's ID, opens a channel between the given renderer "port" // and that extension. Returns a channel ID to be used for posting messages // between the processes, or -1 if the extension doesn't exist. int OpenChannelToExtension(const std::string& extension_id, - ResourceMessageFilter* renderer_port); + ResourceMessageFilter* source); + + // Sends a message from a renderer to the given port. + void PostMessageFromRenderer(int port_id, const std::string& message, + ResourceMessageFilter* source); - // Sends a message to the extension via the given channel. - void PostMessageToExtension(int channel_id, const std::string& message); + // --- UI or IO thread: + + // Called to let us know that a renderer has been started. + void RendererReady(ResourceMessageFilter* port); // Called to let us know that a renderer is going away. - void RendererShutdown(ResourceMessageFilter* renderer_port); + void RendererShutdown(ResourceMessageFilter* port); + private: - class ExtensionFilter; - friend class ExtensionFilter; + // A map of extension ID to the render_process_id that the extension lives in. + typedef std::map<std::string, int> ProcessIDMap; + ProcessIDMap process_ids_; + + // A map of render_process_id to its corresponding message filter, which we + // use for sending messages. + typedef std::map<int, ResourceMessageFilter*> RendererMap; + RendererMap renderers_; - // Called when our ExtensionFilter is ready/going away. - void OnExtensionRegistered(ExtensionFilter* extension); - void OnExtensionUnregistered(ExtensionFilter* extension); + // Protects the two maps above, since each can be accessed on the IO thread + // or UI thread. Be careful not to hold this lock when calling external + // code (especially sending messages) to avoid deadlock. + Lock renderers_lock_; - // A map of extension ID to the extension port to communicate through. - // TODO(mpcomplete): Handle the case where there's multiple ExtensionViews - // in a given extension. - typedef std::map<std::string, ExtensionFilter*> ExtensionMap; - ExtensionMap extensions_; + // --- IO thread only: - // The connection between the renderer and extension. + // The connection between two renderers. struct MessageChannel { - ExtensionFilter* extension_port; - ResourceMessageFilter* renderer_port; + ResourceMessageFilter* port1; + ResourceMessageFilter* port2; }; // A map of channel ID to its channel object. @@ -67,7 +82,7 @@ class ExtensionMessageService { MessageChannelMap channels_; // For generating unique channel IDs. - int next_channel_id_; + int next_port_id_; }; #endif // CHROME_BROWSER_EXTENSIONS_EXTENSION_MESSAGE_SERVICE_H_ diff --git a/chrome/browser/extensions/extension_view.cc b/chrome/browser/extensions/extension_view.cc index 8213227..a9b3ed0 100755 --- a/chrome/browser/extensions/extension_view.cc +++ b/chrome/browser/extensions/extension_view.cc @@ -7,6 +7,7 @@ #include "chrome/browser/extensions/extension.h" #include "chrome/browser/extensions/extension_message_service.h" #include "chrome/browser/renderer_host/render_view_host.h" +#include "chrome/browser/renderer_host/render_process_host.h" #include "chrome/common/resource_bundle.h" #include "grit/browser_resources.h" @@ -26,8 +27,9 @@ void ExtensionView::CreatingRenderer() { render_view_host()->AllowExtensionBindings(); } -void ExtensionView::RenderViewCreated(RenderViewHost* render_view_host) { - ExtensionMessageService::GetInstance()->RegisterExtensionView(this); +void ExtensionView::RenderViewCreated(RenderViewHost* rvh) { + ExtensionMessageService::GetInstance()->RegisterExtension( + extension_->id(), render_view_host()->process()->pid()); } WebPreferences ExtensionView::GetWebkitPrefs() { diff --git a/chrome/browser/renderer_host/resource_message_filter.cc b/chrome/browser/renderer_host/resource_message_filter.cc index b7c59af..007c5fd 100644 --- a/chrome/browser/renderer_host/resource_message_filter.cc +++ b/chrome/browser/renderer_host/resource_message_filter.cc @@ -155,6 +155,8 @@ ResourceMessageFilter::~ResourceMessageFilter() { void ResourceMessageFilter::Init(int render_process_id) { render_process_id_ = render_process_id; render_widget_helper_->Init(render_process_id, resource_dispatcher_host_); + + ExtensionMessageService::GetInstance()->RendererReady(this); } // Called on the IPC thread: @@ -809,13 +811,13 @@ void ResourceMessageFilter::OnFreeTransportDIB( #endif void ResourceMessageFilter::OnOpenChannelToExtension( - const std::string& extension_id, int* channel_id) { - *channel_id = ExtensionMessageService::GetInstance()-> + const std::string& extension_id, int* port_id) { + *port_id = ExtensionMessageService::GetInstance()-> OpenChannelToExtension(extension_id, this); } void ResourceMessageFilter::OnExtensionPostMessage( - int channel_id, const std::string& message) { - ExtensionMessageService::GetInstance()->PostMessageToExtension( - channel_id, message); + int port_id, const std::string& message) { + ExtensionMessageService::GetInstance()-> + PostMessageFromRenderer(port_id, message, this); } diff --git a/chrome/browser/renderer_host/resource_message_filter.h b/chrome/browser/renderer_host/resource_message_filter.h index 64a10ba..db7c60b 100644 --- a/chrome/browser/renderer_host/resource_message_filter.h +++ b/chrome/browser/renderer_host/resource_message_filter.h @@ -214,9 +214,8 @@ class ResourceMessageFilter : public IPC::ChannelProxy::MessageFilter, TransportDIB::Handle* result); void OnFreeTransportDIB(TransportDIB::Id dib_id); - void OnOpenChannelToExtension(const std::string& extension_id, - int* channel_id); - void OnExtensionPostMessage(int channel_id, const std::string& message); + void OnOpenChannelToExtension(const std::string& extension_id, int* port_id); + void OnExtensionPostMessage(int port_id, const std::string& message); // We have our own clipboard service because we want to access the clipboard // on the IO thread instead of forwarding (possibly synchronous) messages to diff --git a/chrome/common/ipc_message_utils.h b/chrome/common/ipc_message_utils.h index ac3cbef..6e82d40 100644 --- a/chrome/common/ipc_message_utils.h +++ b/chrome/common/ipc_message_utils.h @@ -1247,9 +1247,12 @@ void GenerateLogData(const std::wstring& channel, const Message& message, LogData* data); // Used for synchronous messages. -template <class SendParam, class ReplyParam> +template <class SendParamType, class ReplyParamType> class MessageWithReply : public SyncMessage { public: + typedef SendParamType SendParam; + typedef ReplyParamType ReplyParam; + MessageWithReply(int32 routing_id, uint16 type, const SendParam& send, const ReplyParam& reply) : SyncMessage(routing_id, type, PRIORITY_NORMAL, diff --git a/chrome/common/render_messages_internal.h b/chrome/common/render_messages_internal.h index cc296a6..ece0cd1 100644 --- a/chrome/common/render_messages_internal.h +++ b/chrome/common/render_messages_internal.h @@ -528,11 +528,16 @@ IPC_BEGIN_MESSAGES(View) int /* callback id */, std::string /* response */) - // Relay a message sent from a renderer to an extension process. channel_id - // is a handle that can be used for sending a response. - IPC_MESSAGE_ROUTED2(ViewMsg_HandleExtensionMessage, - std::string /* message */, - int /* channel_id */) + // Tell the extension process about a new channel that has been opened from a + // renderer. source_port_id identifies the port that the extension can + // respond to. + IPC_MESSAGE_CONTROL1(ViewMsg_ExtensionHandleConnect, + int /* source_port_id */) + + // Send a javascript message to a renderer from the given port. + IPC_MESSAGE_CONTROL2(ViewMsg_ExtensionHandleMessage, + std::string /* message */, + int /* source_port_id */) // Tell the renderer process all known extension function names. IPC_MESSAGE_CONTROL1(ViewMsg_Extension_SetFunctionNames, @@ -1280,16 +1285,16 @@ IPC_BEGIN_MESSAGES(ViewHost) IPC_MESSAGE_ROUTED1(ViewHostMsg_UpdateFeedList, ViewHostMsg_UpdateFeedList_Params) - // Get a handle to a currently-running extension process for the extension - // with the given ID. If no such extension is found, -1 is returned. The - // handle can be used for sending messages to the extension. + // Get a port handle to a currently-running extension process for the + // extension with the given ID. If no such extension is found, -1 is + // returned. The handle can be used for sending messages to the extension. IPC_SYNC_MESSAGE_CONTROL1_1(ViewHostMsg_OpenChannelToExtension, std::string /* extension_id */, - int /* channel_id */) + int /* port_id */) // Send a message to an extension process. The handle is the value returned // by ViewHostMsg_OpenChannelToExtension. IPC_MESSAGE_CONTROL2(ViewHostMsg_ExtensionPostMessage, - int /* channel_id */, + int /* port_id */, std::string /* message */) IPC_END_MESSAGES(ViewHost) diff --git a/chrome/renderer/extensions/renderer_extension_bindings.cc b/chrome/renderer/extensions/renderer_extension_bindings.cc index 9301579..8254bc6 100755 --- a/chrome/renderer/extensions/renderer_extension_bindings.cc +++ b/chrome/renderer/extensions/renderer_extension_bindings.cc @@ -4,50 +4,54 @@ #include "chrome/renderer/extensions/renderer_extension_bindings.h" +#include "base/basictypes.h" +#include "base/singleton.h" #include "chrome/common/render_messages.h" +#include "chrome/common/resource_bundle.h" #include "chrome/renderer/render_thread.h" -#include "third_party/WebKit/WebKit/chromium/public/WebScriptSource.h" +#include "grit/renderer_resources.h" #include "webkit/glue/webframe.h" -using WebKit::WebScriptSource; -using WebKit::WebString; +// Message passing API example (in a content script): +// var extension = +// new chromium.Extension('00123456789abcdef0123456789abcdef0123456'); +// var channel = extension.openChannel(); +// channel.postMessage('Can you hear me now?'); +// channel.onMessage = function(msg, port) { +// alert('response=' + msg); +// port.postMessage('I got your reponse'); +// } namespace { -const char* kExtensionName = "v8/RendererExtensionBindings"; +// Keep a list of contexts that have registered themselves with us. This lets +// us know where to dispatch events when we receive them. +typedef std::list< v8::Persistent<v8::Context> > ContextList; +struct SingletonData { + ContextList contexts; + std::string js_source; + + SingletonData() : + js_source(ResourceBundle::GetSharedInstance().GetRawDataResource( + IDR_RENDERER_EXTENSION_BINDINGS_JS).as_string()) { + } +}; +ContextList& GetRegisteredContexts() { + return Singleton<SingletonData>::get()->contexts; +} +const char* GetSource() { + return Singleton<SingletonData>::get()->js_source.c_str(); +} -const char* kExtensionScript = - "var chromium = chromium || {};" - "(function () {" - " native function OpenChannelToExtension(id);" - " native function PostMessage(channel_id, msg);" - " chromium.Extension = function(id) {" - " this.channel_id_ = OpenChannelToExtension(id);" - " if (this.channel_id_ == -1)" - " throw new Error('No such extension \"' + id + '\"');" - " chromium.Extension.extensions_[this.channel_id_] = this;" - " };" - " chromium.Extension.extensions_ = {};" - " chromium.Extension.dispatchOnMessage = function(msg, channel_id) {" - // TODO(mpcomplete): port param for onMessage - " var e = chromium.Extension.extensions_[channel_id];" - " if (e && e.onMessage) e.onMessage(msg);" - " if (chromium.Extension.onMessage) chromium.Extension.onMessage(msg);" - " };" - " chromium.Extension.prototype.postMessage = function(msg) {" - " return PostMessage(this.channel_id_, msg);" - " };" - "})();"; +// We use the generic interface so that unit tests can inject a mock. +RenderThreadBase* render_thread_ = NULL; -// Message passing API example (in a content script): -// var extension = -// new chromium.Extension('00123456789abcdef0123456789abcdef0123456'); -// extension.postMessage('Can you hear me now?'); -// extension.onMessage = function(msg) { alert('response=' + msg); } +const char* kExtensionName = "v8/RendererExtensionBindings"; +const char* kScriptAPI = "chromium.extensions.scriptAPI"; class ExtensionImpl : public v8::Extension { public: - ExtensionImpl() : v8::Extension(kExtensionName, kExtensionScript) {} + ExtensionImpl() : v8::Extension(kExtensionName, GetSource()) {} ~ExtensionImpl() {} virtual v8::Handle<v8::FunctionTemplate> GetNativeFunction( @@ -56,49 +60,131 @@ class ExtensionImpl : public v8::Extension { return v8::FunctionTemplate::New(OpenChannelToExtension); } else if (name->Equals(v8::String::New("PostMessage"))) { return v8::FunctionTemplate::New(PostMessage); + } else if (name->Equals(v8::String::New("RegisterScriptAPI"))) { + return v8::FunctionTemplate::New(RegisterScriptAPI); } return v8::Handle<v8::FunctionTemplate>(); } + + // Creates a new messaging channel to the given extension. static v8::Handle<v8::Value> OpenChannelToExtension( const v8::Arguments& args) { if (args.Length() >= 1 && args[0]->IsString()) { std::string id = *v8::String::Utf8Value(args[0]->ToString()); - int channel_id; - RenderThread::current()->Send( - new ViewHostMsg_OpenChannelToExtension(id, &channel_id)); - return v8::Integer::New(channel_id); - // TODO(mpcomplete): should we associate channel_id with the frame it - // came from, so we can run the onmessage handler in that context for - // responses? + int port_id = -1; + render_thread_->Send( + new ViewHostMsg_OpenChannelToExtension(id, &port_id)); + return v8::Integer::New(port_id); } return v8::Undefined(); } + + // Sends a message along the given channel. static v8::Handle<v8::Value> PostMessage(const v8::Arguments& args) { if (args.Length() >= 2 && args[0]->IsInt32() && args[1]->IsString()) { - int channel_id = args[1]->Int32Value(); + int port_id = args[0]->Int32Value(); std::string message = *v8::String::Utf8Value(args[1]->ToString()); - RenderThread::current()->Send( - new ViewHostMsg_ExtensionPostMessage(channel_id, message)); + render_thread_->Send( + new ViewHostMsg_ExtensionPostMessage(port_id, message)); + } + return v8::Undefined(); + } + + // This method is internal to the extension, and fulfills a dual purpose: + // 1. Keep track of which v8::Contexts have registered event listeners. + // 2. Registers a private variable on the context that we use for dispatching + // events as they come in, by calling designated methods. + static v8::Handle<v8::Value> RegisterScriptAPI(const v8::Arguments& args) { + if (args.Length() >= 1 && args[0]->IsObject()) { + v8::Persistent<v8::Context> context = + v8::Persistent<v8::Context>::New(v8::Context::GetCurrent()); + GetRegisteredContexts().push_back(context); + context.MakeWeak(NULL, WeakContextCallback); + context->Global()->SetHiddenValue(v8::String::New(kScriptAPI), args[0]); + DCHECK(args[0]->ToObject()->Get(v8::String::New("dispatchOnConnect"))-> + IsFunction()); + DCHECK(args[0]->ToObject()->Get(v8::String::New("dispatchOnMessage"))-> + IsFunction()); + return v8::Undefined(); } return v8::Undefined(); } + + // Calls the given chromiumPrivate method in each registered context. + static void CallMethod(const std::string& method_name, int argc, + v8::Handle<v8::Value>* argv) { + for (ContextList::iterator it = GetRegisteredContexts().begin(); + it != GetRegisteredContexts().end(); ++it) { + DCHECK(!it->IsEmpty()); + v8::Context::Scope context_scope(*it); + v8::Local<v8::Object> global = (*it)->Global(); + + // Check if the window object is gone, which means this context's frame + // has been unloaded. + v8::Local<v8::Value> window = global->Get(v8::String::New("window")); + if (!window->IsObject()) + continue; + + // Retrieve our hidden variable and call the method on it. + v8::Local<v8::Value> script_api = global->GetHiddenValue( + v8::String::New(kScriptAPI)); + if (!script_api->IsObject()) { + NOTREACHED(); + continue; + } + + v8::Handle<v8::Value> function_obj = script_api->ToObject()->Get( + v8::String::New(method_name.c_str())); + if (!function_obj->IsFunction()) { + NOTREACHED(); + continue; + } + + v8::Handle<v8::Function> function = + v8::Handle<v8::Function>::Cast(function_obj); + if (!function.IsEmpty()) + function->Call(v8::Object::New(), argc, argv); + } + } + + // Called when a registered context is garbage collected. + static void WeakContextCallback(v8::Persistent<v8::Value> obj, void*) { + ContextList::iterator it = std::find(GetRegisteredContexts().begin(), + GetRegisteredContexts().end(), obj); + if (it == GetRegisteredContexts().end()) { + NOTREACHED(); + return; + } + + it->Dispose(); + it->Clear(); + GetRegisteredContexts().erase(it); + } }; } // namespace namespace extensions_v8 { -v8::Extension* RendererExtensionBindings::Get() { +v8::Extension* RendererExtensionBindings::Get(RenderThreadBase* render_thread) { + render_thread_ = render_thread; return new ExtensionImpl(); } -void RendererExtensionBindings::HandleExtensionMessage( - WebFrame* webframe, const std::string& message, int channel_id) { - // TODO(mpcomplete): escape message - std::string script = StringPrintf( - "void(chromium.Extension.dispatchOnMessage(\"%s\", %d))", - message.c_str(), channel_id); - webframe->ExecuteScript(WebScriptSource(WebString::fromUTF8(script))); +void RendererExtensionBindings::HandleConnect(int port_id) { + v8::HandleScope handle_scope; + v8::Handle<v8::Value> argv[1]; + argv[0] = v8::Integer::New(port_id); + ExtensionImpl::CallMethod("dispatchOnConnect", arraysize(argv), argv); +} + +void RendererExtensionBindings::HandleMessage(const std::string& message, + int port_id) { + v8::HandleScope handle_scope; + v8::Handle<v8::Value> argv[2]; + argv[0] = v8::String::New(message.c_str()); + argv[1] = v8::Integer::New(port_id); + ExtensionImpl::CallMethod("dispatchOnMessage", arraysize(argv), argv); } } // namespace extensions_v8 diff --git a/chrome/renderer/extensions/renderer_extension_bindings.h b/chrome/renderer/extensions/renderer_extension_bindings.h index 284f9bb..7c951ec 100755 --- a/chrome/renderer/extensions/renderer_extension_bindings.h +++ b/chrome/renderer/extensions/renderer_extension_bindings.h @@ -9,16 +9,23 @@ #include <string> +class RenderThreadBase; class WebFrame; namespace extensions_v8 { -// This class adds extension-related javascript bindings to a renderer. +// This class adds extension-related javascript bindings to a renderer. It is +// used by both web renderers and extension processes. class RendererExtensionBindings { public: - static v8::Extension* Get(); - static void HandleExtensionMessage( - WebFrame* webframe, const std::string& message, int channel_id); + static v8::Extension* Get(RenderThreadBase* render_thread); + + // Notify any listeners that a message channel has been opened to this + // process. + static void HandleConnect(int port_id); + + // Dispatch the given message sent on this channel. + static void HandleMessage(const std::string& message, int port_id); }; } // namespace extensions_v8 diff --git a/chrome/renderer/mock_render_thread.cc b/chrome/renderer/mock_render_thread.cc index 9e71a53..19fdd48 100644 --- a/chrome/renderer/mock_render_thread.cc +++ b/chrome/renderer/mock_render_thread.cc @@ -71,6 +71,8 @@ void MockRenderThread::OnMessageReceived(const IPC::Message& msg) { bool msg_is_ok = true; IPC_BEGIN_MESSAGE_MAP_EX(MockRenderThread, msg, msg_is_ok) IPC_MESSAGE_HANDLER(ViewHostMsg_CreateWidget, OnMsgCreateWidget); + IPC_MESSAGE_HANDLER(ViewHostMsg_OpenChannelToExtension, + OnMsgOpenChannelToExtension); IPC_MESSAGE_UNHANDLED(handled = false) IPC_END_MESSAGE_MAP_EX() } @@ -82,3 +84,8 @@ void MockRenderThread::OnMsgCreateWidget(int opener_id, opener_id_ = opener_id; *route_id = routing_id_; } + +void MockRenderThread::OnMsgOpenChannelToExtension( + const std::string& extension_id, int* channel_id) { + *channel_id = 0; +} diff --git a/chrome/renderer/mock_render_thread.h b/chrome/renderer/mock_render_thread.h index fb47a15..a89822a6 100644 --- a/chrome/renderer/mock_render_thread.h +++ b/chrome/renderer/mock_render_thread.h @@ -69,6 +69,10 @@ class MockRenderThread : public RenderThreadBase { bool activatable, int* route_id); + // The callee expects to be returned a valid channel_id. + void OnMsgOpenChannelToExtension(const std::string& extension_id, + int* channel_id); + IPC::TestSink sink_; // Routing id what will be assigned to the Widget. diff --git a/chrome/renderer/render_thread.cc b/chrome/renderer/render_thread.cc index 2e4ad09..e161438 100644 --- a/chrome/renderer/render_thread.cc +++ b/chrome/renderer/render_thread.cc @@ -166,6 +166,10 @@ void RenderThread::OnControlMessageReceived(const IPC::Message& msg) { OnGetCacheResourceStats) IPC_MESSAGE_HANDLER(ViewMsg_UserScripts_NewScripts, OnUpdateUserScripts) + IPC_MESSAGE_HANDLER(ViewMsg_ExtensionHandleConnect, + OnExtensionHandleConnect) + IPC_MESSAGE_HANDLER(ViewMsg_ExtensionHandleMessage, + OnExtensionHandleMessage) IPC_MESSAGE_HANDLER(ViewMsg_Extension_SetFunctionNames, OnSetExtensionFunctionNames) IPC_END_MESSAGE_MAP() @@ -265,7 +269,8 @@ void RenderThread::EnsureWebKitInitialized() { WebKit::registerExtension(extensions_v8::GearsExtension::Get()); WebKit::registerExtension(extensions_v8::IntervalExtension::Get()); - WebKit::registerExtension(extensions_v8::RendererExtensionBindings::Get()); + WebKit::registerExtension( + extensions_v8::RendererExtensionBindings::Get(this)); WebKit::registerExtension(extensions_v8::ExtensionProcessBindings::Get(), WebKit::WebString::fromUTF8(chrome::kExtensionScheme)); @@ -280,3 +285,12 @@ void RenderThread::EnsureWebKitInitialized() { WebKit::enableWebWorkers(); } } + +void RenderThread::OnExtensionHandleConnect(int port_id) { + extensions_v8::RendererExtensionBindings::HandleConnect(port_id); +} + +void RenderThread::OnExtensionHandleMessage(const std::string& message, + int port_id) { + extensions_v8::RendererExtensionBindings::HandleMessage(message, port_id); +} diff --git a/chrome/renderer/render_thread.h b/chrome/renderer/render_thread.h index 0c1290c..5340b0e 100644 --- a/chrome/renderer/render_thread.h +++ b/chrome/renderer/render_thread.h @@ -122,6 +122,9 @@ class RenderThread : public RenderThreadBase, // Send all histograms to browser. void OnGetRendererHistograms(); + void OnExtensionHandleConnect(int channel_id); + void OnExtensionHandleMessage(const std::string& message, int channel_id); + // Gather usage statistics from the in-memory cache and inform our host. // These functions should be call periodically so that the host can make // decisions about how to allocation resources using current information. diff --git a/chrome/renderer/render_view.cc b/chrome/renderer/render_view.cc index 6ed31e2..2feecfc 100644 --- a/chrome/renderer/render_view.cc +++ b/chrome/renderer/render_view.cc @@ -32,7 +32,6 @@ #include "chrome/renderer/devtools_agent.h" #include "chrome/renderer/devtools_client.h" #include "chrome/renderer/extensions/extension_process_bindings.h" -#include "chrome/renderer/extensions/renderer_extension_bindings.h" #include "chrome/renderer/localized_error.h" #include "chrome/renderer/media/audio_renderer_impl.h" #include "chrome/renderer/render_process.h" @@ -428,8 +427,6 @@ void RenderView::OnMessageReceived(const IPC::Message& message) { OnAudioStreamStateChanged) IPC_MESSAGE_HANDLER(ViewMsg_NotifyAudioStreamVolume, OnAudioStreamVolume) IPC_MESSAGE_HANDLER(ViewMsg_MoveOrResizeStarted, OnMoveOrResizeStarted) - IPC_MESSAGE_HANDLER(ViewMsg_HandleExtensionMessage, - OnHandleExtensionMessage) IPC_MESSAGE_HANDLER(ViewMsg_ExtensionResponse, OnExtensionResponse) IPC_MESSAGE_HANDLER(ViewMsg_RequestSelectionText, OnRequestSelectionText) @@ -2967,13 +2964,6 @@ void RenderView::OnResize(const gfx::Size& new_size, RenderWidget::OnResize(new_size, resizer_rect); } -void RenderView::OnHandleExtensionMessage(const std::string& message, - int channel_id) { - if (webview() && webview()->GetMainFrame()) - extensions_v8::RendererExtensionBindings::HandleExtensionMessage( - webview()->GetMainFrame(), message, channel_id); -} - void RenderView::SendExtensionRequest(const std::string& name, const std::string& args, int callback_id, diff --git a/chrome/renderer/render_view.h b/chrome/renderer/render_view.h index 25692f2..a9c8915 100644 --- a/chrome/renderer/render_view.h +++ b/chrome/renderer/render_view.h @@ -594,8 +594,6 @@ class RenderView : public RenderWidget, // Notification of volume property of an audio output stream. void OnAudioStreamVolume(int stream_id, double left, double right); - void OnHandleExtensionMessage(const std::string& message, int channel_id); - // Sends the selection text to the browser. void OnRequestSelectionText(); diff --git a/chrome/renderer/render_view_unittest.cc b/chrome/renderer/render_view_unittest.cc index 88c4fa7..c27bcbf 100644 --- a/chrome/renderer/render_view_unittest.cc +++ b/chrome/renderer/render_view_unittest.cc @@ -4,6 +4,7 @@ #include "base/scoped_ptr.h" #include "chrome/common/render_messages.h" +#include "chrome/renderer/extensions/renderer_extension_bindings.h" #include "chrome/renderer/mock_render_process.h" #include "chrome/renderer/mock_render_thread.h" #include "chrome/renderer/render_view.h" @@ -65,6 +66,8 @@ class RenderViewTest : public testing::Test { // testing::Test virtual void SetUp() { WebKit::initialize(&webkitclient_); + WebKit::registerExtension( + extensions_v8::RendererExtensionBindings::Get(&render_thread_)); mock_process_.reset(new MockProcess()); @@ -367,3 +370,90 @@ TEST_F(RenderViewTest, OnSetTextDirection) { EXPECT_EQ(output, kTextDirection[i].expected_result); } } + +// Tests that the bindings for opening a channel to an extension and sending +// and receiving messages through that channel all works. +TEST_F(RenderViewTest, ExtensionMessagesOpenChannel) { + render_thread_.sink().ClearMessages(); + LoadHTML("<body></body>"); + ExecuteJavaScript( + "var e = new chromium.Extension('foobar');" + "var port = e.openChannel();" + "port.onMessage = doOnMessage;" + "port.postMessage('content ready');" + "function doOnMessage(msg, port) {" + " alert('content got: ' + msg);" + "}"); + + // Verify that we opened a channel and sent a message through it. + const IPC::Message* open_channel_msg = + render_thread_.sink().GetUniqueMessageMatching( + ViewHostMsg_OpenChannelToExtension::ID); + EXPECT_TRUE(open_channel_msg); + + const IPC::Message* post_msg = + render_thread_.sink().GetUniqueMessageMatching( + ViewHostMsg_ExtensionPostMessage::ID); + EXPECT_TRUE(post_msg); + ViewHostMsg_ExtensionPostMessage::Param post_params; + ViewHostMsg_ExtensionPostMessage::Read(post_msg, &post_params); + EXPECT_EQ("content ready", post_params.b); + + // Now simulate getting a message back from the other side. + render_thread_.sink().ClearMessages(); + const int kPortId = 0; + extensions_v8::RendererExtensionBindings::HandleMessage("42", kPortId); + + // Verify that we got it. + const IPC::Message* alert_msg = + render_thread_.sink().GetUniqueMessageMatching( + ViewHostMsg_RunJavaScriptMessage::ID); + EXPECT_TRUE(alert_msg); + void* iter = IPC::SyncMessage::GetDataIterator(alert_msg); + ViewHostMsg_RunJavaScriptMessage::SendParam alert_param; + IPC::ReadParam(alert_msg, &iter, &alert_param); + EXPECT_EQ(L"content got: 42", alert_param.a); +} + +// Tests that the bindings for handling a new channel connection and sending +// and receiving messages through that channel all works. +TEST_F(RenderViewTest, ExtensionMessagesOnConnect) { + LoadHTML("<body></body>"); + ExecuteJavaScript( + "chromium.addConnectListener(function (port) {" + " port.onMessage = doOnMessage;" + " port.postMessage('onconnect');" + " });" + "function doOnMessage(msg, port) {" + " alert('got: ' + msg);" + "}"); + + render_thread_.sink().ClearMessages(); + + // Simulate a new connection being opened. + const int kPortId = 0; + extensions_v8::RendererExtensionBindings::HandleConnect(kPortId); + + // Verify that we handled the new connection by posting a message. + const IPC::Message* post_msg = + render_thread_.sink().GetUniqueMessageMatching( + ViewHostMsg_ExtensionPostMessage::ID); + EXPECT_TRUE(post_msg); + ViewHostMsg_ExtensionPostMessage::Param post_params; + ViewHostMsg_ExtensionPostMessage::Read(post_msg, &post_params); + EXPECT_EQ("onconnect", post_params.b); + + // Now simulate getting a message back from the channel opener. + render_thread_.sink().ClearMessages(); + extensions_v8::RendererExtensionBindings::HandleMessage("42", kPortId); + + // Verify that we got it. + const IPC::Message* alert_msg = + render_thread_.sink().GetUniqueMessageMatching( + ViewHostMsg_RunJavaScriptMessage::ID); + EXPECT_TRUE(alert_msg); + void* iter = IPC::SyncMessage::GetDataIterator(alert_msg); + ViewHostMsg_RunJavaScriptMessage::SendParam alert_param; + IPC::ReadParam(alert_msg, &iter, &alert_param); + EXPECT_EQ(L"got: 42", alert_param.a); +} diff --git a/chrome/renderer/renderer_resources.grd b/chrome/renderer/renderer_resources.grd index 48bd7b4..e805630 100755 --- a/chrome/renderer/renderer_resources.grd +++ b/chrome/renderer/renderer_resources.grd @@ -14,6 +14,7 @@ <include name="IDR_ERROR_NO_DETAILS_HTML" file="resources\error_no_details.html" type="BINDATA" /> <include name="IDR_GREASEMONKEY_API_JS" file="resources\greasemonkey_api.js" type="BINDATA" /> <include name="IDR_EXTENSION_PROCESS_BINDINGS_JS" file="resources\extension_process_bindings.js" type="BINDATA" /> + <include name="IDR_RENDERER_EXTENSION_BINDINGS_JS" file="resources\renderer_extension_bindings.js" type="BINDATA" /> </includes> </release> </grit>
\ No newline at end of file diff --git a/chrome/renderer/resources/renderer_extension_bindings.js b/chrome/renderer/resources/renderer_extension_bindings.js new file mode 100644 index 0000000..401f82c --- /dev/null +++ b/chrome/renderer/resources/renderer_extension_bindings.js @@ -0,0 +1,93 @@ +var chromium = chromium || {}; +(function () { + native function OpenChannelToExtension(id); + native function PostMessage(portId, msg); + native function RegisterScriptAPI(private); + // chromium: Public API. + + // Represents info we know about a chrome extension. + chromium.Extension = function(id) { + this.id_ = id; + }; + + // Opens a channel to the extension for message passing. + chromium.Extension.prototype.openChannel = function() { + portId = OpenChannelToExtension(this.id_); + if (portId == -1) + throw new Error('No such extension \"' + this.id_ + '\"'); + return new Port(portId); + }; + + // Adds a listener that fires when a renderer opens a channel to talk + // to us. + chromium.addConnectListener = function(callback) { + chromium.addEventListener('channel-connect', + function (e) { callback(e.data.port); }); + }; + + // Adds a generic event listener. + chromium.addEventListener = function(type, callback) { + var listeners = getPrivateData().eventListeners; + if (!listeners[type]) + listeners[type] = []; + listeners[type].push(callback); + }; + + // Dispatches the given event to anyone listening for that event. + chromium.dispatchEvent = function(type, data) { + var event = {type: type, data: data}; + var listeners = getPrivateData().eventListeners; + for (var i in listeners[type]) { + listeners[type][i](event); + } + }; + + // Private API. + + // Always access privateData through this function, to ensure that we + // have registered our native API. We do this lazily to avoid registering + // on pages that don't use these bindings. + function getPrivateData() { + if (!scriptAPI.registered_) { + RegisterScriptAPI(scriptAPI); + scriptAPI.registered_ = true; + } + return privateData; + } + var privateData = { + eventListeners: {}, + ports: {} + }; + + // Represents a port through which we can send messages to another process. + var Port = function(portId) { + // TODO(mpcomplete): we probably want to hide this portId_ so + // it can't be guessed at. One idea is to expose v8's SetHiddenValue + // to our extension. + this.portId_ = portId; + getPrivateData().ports[portId] = this; + }; + + // Sends a message to the other side of the channel. + Port.prototype.postMessage = function(msg) { + PostMessage(this.portId_, msg); + }; + + // Script API: javascript APIs exposed to C++ only. + // This object allows our native code to call back to us through a + // private interface that isn't exposed to web content. + var scriptAPI = {}; + + // Called by native code when a channel has been opened to this process. + scriptAPI.dispatchOnConnect = function(portId) { + chromium.dispatchEvent('channel-connect', {port: new Port(portId)}); + }; + + // Called by native code when a message has been sent over the given + // channel. + scriptAPI.dispatchOnMessage = function(msg, portId) { + var port = getPrivateData().ports[portId]; + if (port && port.onMessage) + port.onMessage(msg, port); + }; +})(); diff --git a/chrome/test/data/extensions/good/extension1/1/toolstrip1.html b/chrome/test/data/extensions/good/extension1/1/toolstrip1.html index b4fe99b..66ba2b3 100644 --- a/chrome/test/data/extensions/good/extension1/1/toolstrip1.html +++ b/chrome/test/data/extensions/good/extension1/1/toolstrip1.html @@ -15,8 +15,17 @@ body { <div class="content"> <script> alert('Sir, I exist'); - chromium.Extension.onMessage = function(msg) { - alert('Incoming: ' + msg); + + chromium.addConnectListener(function (port) { + port.onMessage = doOnMessage; + port.postMessage('extension onconnect'); + }); + + function doOnMessage(msg, port) { + if (!port.didRespond) { + port.postMessage('extension msg ack: ' + msg); + port.didRespond = true; + } } </script> <button onclick="alert('clicked')">HTML button</button> |