2011-02-07 Pavel Feldman <pfeldman@chromium.org>
[WebKit-https.git] / Source / WebCore / inspector / front-end / ExtensionServer.js
1 /*
2  * Copyright (C) 2011 Google Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
8  *     * Redistributions of source code must retain the above copyright
9  * notice, this list of conditions and the following disclaimer.
10  *     * Redistributions in binary form must reproduce the above
11  * copyright notice, this list of conditions and the following disclaimer
12  * in the documentation and/or other materials provided with the
13  * distribution.
14  *     * Neither the name of Google Inc. nor the names of its
15  * contributors may be used to endorse or promote products derived from
16  * this software without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30
31 WebInspector.ExtensionServer = function()
32 {
33     this._clientObjects = {};
34     this._handlers = {};
35     this._subscribers = {};
36     this._extraHeaders = {};
37     this._status = new WebInspector.ExtensionStatus();
38
39     this._registerHandler("addRequestHeaders", this._onAddRequestHeaders.bind(this));
40     this._registerHandler("addAuditCategory", this._onAddAuditCategory.bind(this));
41     this._registerHandler("addAuditResult", this._onAddAuditResult.bind(this));
42     this._registerHandler("createPanel", this._onCreatePanel.bind(this));
43     this._registerHandler("createSidebarPane", this._onCreateSidebar.bind(this));
44     this._registerHandler("createWatchExpressionSidebarPane", this._onCreateWatchExpressionSidebarPane.bind(this));
45     this._registerHandler("evaluateOnInspectedPage", this._onEvaluateOnInspectedPage.bind(this));
46     this._registerHandler("getHAR", this._onGetHAR.bind(this));
47     this._registerHandler("getResourceContent", this._onGetResourceContent.bind(this));
48     this._registerHandler("log", this._onLog.bind(this));
49     this._registerHandler("reload", this._onReload.bind(this));
50     this._registerHandler("setSidebarHeight", this._onSetSidebarHeight.bind(this));
51     this._registerHandler("setWatchSidebarContent", this._onSetWatchSidebarContent.bind(this));
52     this._registerHandler("stopAuditCategoryRun", this._onStopAuditCategoryRun.bind(this));
53     this._registerHandler("subscribe", this._onSubscribe.bind(this));
54     this._registerHandler("unsubscribe", this._onUnsubscribe.bind(this));
55
56     window.addEventListener("message", this._onWindowMessage.bind(this), false);
57 }
58
59 WebInspector.ExtensionServer.prototype = {
60     notifyPanelShown: function(panelName)
61     {
62         this._postNotification("panel-shown-" + panelName);
63     },
64
65     notifyObjectSelected: function(panelId, objectId)
66     {
67         this._postNotification("panel-objectSelected-" + panelId, objectId);
68     },
69
70     notifySearchAction: function(panelId, action, searchString)
71     {
72         this._postNotification("panel-search-" + panelId, action, searchString);
73     },
74
75     notifyPageLoaded: function(milliseconds)
76     {
77         this._postNotification("inspectedPageLoaded", milliseconds);
78     },
79
80     notifyPageDOMContentLoaded: function(milliseconds)
81     {
82         this._postNotification("inspectedPageDOMContentLoaded", milliseconds);
83     },
84
85     notifyInspectedURLChanged: function()
86     {
87         this._postNotification("inspectedURLChanged");
88     },
89
90     notifyInspectorReset: function()
91     {
92         this._postNotification("reset");
93     },
94
95     notifyExtensionWatchSidebarUpdated: function(id)
96     {
97         this._postNotification("watch-sidebar-updated-" + id);
98     },
99
100     startAuditRun: function(category, auditRun)
101     {
102         this._clientObjects[auditRun.id] = auditRun;
103         this._postNotification("audit-started-" + category.id, auditRun.id);
104     },
105
106     stopAuditRun: function(auditRun)
107     {
108         delete this._clientObjects[auditRun.id];
109     },
110
111     _notifyResourceFinished: function(event)
112     {
113         var resource = event.data;
114         this._postNotification("resource-finished", resource.identifier, (new WebInspector.HAREntry(resource)).build());
115     },
116
117     _postNotification: function(type, details)
118     {
119         var subscribers = this._subscribers[type];
120         if (!subscribers)
121             return;
122         var message = {
123             command: "notify-" + type,
124             arguments: Array.prototype.slice.call(arguments, 1)
125         };
126         for (var i = 0; i < subscribers.length; ++i)
127             subscribers[i].postMessage(message);
128     },
129
130     _onSubscribe: function(message, port)
131     {
132         var subscribers = this._subscribers[message.type];
133         if (subscribers)
134             subscribers.push(port);
135         else
136             this._subscribers[message.type] = [ port ];
137     },
138
139     _onUnsubscribe: function(message, port)
140     {
141         var subscribers = this._subscribers[message.type];
142         if (!subscribers)
143             return;
144         subscribers.remove(port);
145         if (!subscribers.length)
146             delete this._subscribers[message.type];
147     },
148
149     _onAddRequestHeaders: function(message)
150     {
151         var id = message.extensionId;
152         if (typeof id !== "string")
153             return this._status.E_BADARGTYPE("extensionId", typeof id, "string");
154         var extensionHeaders = this._extraHeaders[id];
155         if (!extensionHeaders) {
156             extensionHeaders = {};
157             this._extraHeaders[id] = extensionHeaders;
158         }
159         for (name in message.headers)
160             extensionHeaders[name] = message.headers[name];
161         var allHeaders = {};
162         for (extension in this._extraHeaders) {
163             var headers = this._extraHeaders[extension];
164             for (name in headers) {
165                 if (typeof headers[name] === "string")
166                     allHeaders[name] = headers[name];
167             }
168         }
169         InspectorBackend.setExtraHeaders(allHeaders);
170     },
171
172     _onCreatePanel: function(message, port)
173     {
174         var id = message.id;
175         // The ids are generated on the client API side and must be unique, so the check below
176         // shouldn't be hit unless someone is bypassing the API.
177         if (id in this._clientObjects || id in WebInspector.panels)
178             return this._status.E_EXISTS(id);
179         var panel = new WebInspector.ExtensionPanel(id, message.title, message.icon);
180         this._clientObjects[id] = panel;
181
182         var toolbarElement = document.getElementById("toolbar");
183         var lastToolbarItem = WebInspector.panelOrder[WebInspector.panelOrder.length - 1].toolbarItem;
184         WebInspector.addPanelToolbarIcon(toolbarElement, panel, lastToolbarItem);
185         WebInspector.panels[id] = panel;
186         var iframe = this._createClientIframe(panel.element, message.url);
187         iframe.style.height = "100%";
188         return this._status.OK();
189     },
190
191     _onCreateSidebar: function(message)
192     {
193         var sidebar = this._createSidebar(message, WebInspector.SidebarPane);
194         if (sidebar.isError)
195             return sidebar;
196         this._createClientIframe(sidebar.bodyElement, message.url);
197         return this._status.OK();
198     },
199
200     _onCreateWatchExpressionSidebarPane: function(message)
201     {
202         var sidebar = this._createSidebar(message, WebInspector.ExtensionWatchSidebarPane);
203         return sidebar.isError ? sidebar : this._status.OK();
204     },
205
206     _createSidebar: function(message, constructor)
207     {
208         var panel = WebInspector.panels[message.panel];
209         if (!panel)
210             return this._status.E_NOTFOUND(message.panel);
211         if (!panel.sidebarElement || !panel.sidebarPanes)
212             return this._status.E_NOTSUPPORTED();
213         var id = message.id;
214         var sidebar = new constructor(message.title, message.id);
215         this._clientObjects[id] = sidebar;
216         panel.sidebarPanes[id] = sidebar;
217         panel.sidebarElement.appendChild(sidebar.element);
218
219         return sidebar;
220     },
221
222     _createClientIframe: function(parent, url, requestId, port)
223     {
224         var iframe = document.createElement("iframe");
225         iframe.src = url;
226         iframe.style.width = "100%";
227         parent.appendChild(iframe);
228         return iframe;
229     },
230
231     _onSetSidebarHeight: function(message)
232     {
233         var sidebar = this._clientObjects[message.id];
234         if (!sidebar)
235             return this._status.E_NOTFOUND(message.id);
236         sidebar.bodyElement.firstChild.style.height = message.height;
237     },
238
239     _onSetWatchSidebarContent: function(message)
240     {
241         var sidebar = this._clientObjects[message.id];
242         if (!sidebar)
243             return this._status.E_NOTFOUND(message.id);
244         if (message.evaluateOnPage)
245             sidebar.setExpression(message.expression, message.rootTitle);
246         else
247             sidebar.setObject(message.expression, message.rootTitle);
248     },
249
250     _onLog: function(message)
251     {
252         WebInspector.log(message.message);
253     },
254
255     _onReload: function(message)
256     {
257         if (typeof message.userAgent === "string")
258             InspectorBackend.setUserAgentOverride(message.userAgent);
259
260         InspectorBackend.reloadPage(false);
261         return this._status.OK();
262     },
263
264     _onEvaluateOnInspectedPage: function(message, port)
265     {
266         function callback(resultPayload)
267         {
268             var resultObject = WebInspector.RemoteObject.fromPayload(resultPayload);
269             var result = {};
270             if (resultObject.isError())
271                 result.isException = true;
272             result.value = resultObject.description;
273             this._dispatchCallback(message.requestId, port, result);
274         }
275         var evalExpression = "JSON.stringify(eval('" +
276             "with (window.console._commandLineAPI) with (window) {' + unescape('" + escape(message.expression) +
277             "') + '}'));";
278         InspectorBackend.evaluate(evalExpression, "none", false, callback.bind(this));
279     },
280
281     _onRevealAndSelect: function(message)
282     {
283         if (message.panelId === "resources" && type === "resource")
284             return this._onRevealAndSelectResource(message);
285         else
286             return this._status.E_NOTSUPPORTED(message.panelId, message.type);
287     },
288
289     _onRevealAndSelectResource: function(message)
290     {
291         var id = message.id;
292         var resource = null;
293
294         resource = WebInspector.networkResourceById(id) || WebInspector.resourceForURL(id);
295         if (!resource)
296             return this._status.E_NOTFOUND(typeof id + ": " + id);
297
298         WebInspector.panels.resources.showResource(resource, message.line);
299         WebInspector.showPanel("resources");
300     },
301
302     _dispatchCallback: function(requestId, port, result)
303     {
304         port.postMessage({ command: "callback", requestId: requestId, result: result });
305     },
306
307     _onGetHAR: function(request)
308     {
309         var harLog = new WebInspector.HARLog();
310         harLog.includeResourceIds = true;
311         return harLog.build();
312     },
313
314     _onGetResourceContent: function(message, port)
315     {
316         function onContentAvailable(content, encoded)
317         {
318             var response = {
319                 encoding: encoded ? "base64" : "",
320                 content: content
321             };
322             this._dispatchCallback(message.requestId, port, response);
323         }
324         var resource = WebInspector.networkResourceById(message.id);
325         if (!resource)
326             return this._status.E_NOTFOUND(message.id);
327         resource.requestContent(onContentAvailable.bind(this));
328     },
329
330     _onAddAuditCategory: function(request)
331     {
332         var category = new WebInspector.ExtensionAuditCategory(request.id, request.displayName, request.resultCount);
333         if (WebInspector.panels.audits.getCategory(category.id))
334             return this._status.E_EXISTS(category.id);
335         this._clientObjects[request.id] = category;
336         WebInspector.panels.audits.addCategory(category);
337     },
338
339     _onAddAuditResult: function(request)
340     {
341         var auditResult = this._clientObjects[request.resultId];
342         if (!auditResult)
343             return this._status.E_NOTFOUND(request.resultId);
344         try {
345             auditResult.addResult(request.displayName, request.description, request.severity, request.details);
346         } catch (e) {
347             return e;
348         }
349         return this._status.OK();
350     },
351
352     _onStopAuditCategoryRun: function(request)
353     {
354         var auditRun = this._clientObjects[request.resultId];
355         if (!auditRun)
356             return this._status.E_NOTFOUND(request.resultId);
357         auditRun.cancel();
358     },
359
360     initExtensions: function()
361     {
362         // The networkManager is normally created after the ExtensionServer is constructed, but before initExtensions() is called.
363         WebInspector.networkManager.addEventListener(WebInspector.NetworkManager.EventTypes.ResourceFinished, this._notifyResourceFinished, this);
364
365         InspectorExtensionRegistry.getExtensionsAsync();
366     },
367
368     _addExtensions: function(extensions)
369     {
370         // See ExtensionAPI.js and ExtensionCommon.js for details.
371         InspectorFrontendHost.setExtensionAPI(this._buildExtensionAPIInjectedScript());
372         for (var i = 0; i < extensions.length; ++i) {
373             var extension = extensions[i];
374             try {
375                 if (!extension.startPage)
376                     return;
377                 var iframe = document.createElement("iframe");
378                 iframe.src = extension.startPage;
379                 iframe.style.display = "none";
380                 document.body.appendChild(iframe);
381             } catch (e) {
382                 console.error("Failed to initialize extension " + extension.startPage + ":" + e);
383             }
384         }
385     },
386
387     _buildExtensionAPIInjectedScript: function()
388     {
389         var resourceTypes = {};
390         var resourceTypeProperties = Object.getOwnPropertyNames(WebInspector.Resource.Type);
391         for (var i = 0; i < resourceTypeProperties.length; ++i) {
392              var propName = resourceTypeProperties[i];
393              var propValue = WebInspector.Resource.Type[propName];
394              if (typeof propValue === "number")
395                  resourceTypes[propName] = WebInspector.Resource.Type.toString(propValue);
396         }
397         var platformAPI = WebInspector.buildPlatformExtensionAPI ? WebInspector.buildPlatformExtensionAPI() : "";
398         return "(function(){ " +
399             "var apiPrivate = {};" +
400             "(" + WebInspector.commonExtensionSymbols.toString() + ")(apiPrivate);" +
401             "(" + WebInspector.injectedExtensionAPI.toString() + ").apply(this, arguments);" +
402             platformAPI +
403             "})";
404     },
405
406     _onWindowMessage: function(event)
407     {
408         if (event.data !== "registerExtension")
409             return;
410         var port = event.ports[0];
411         port.addEventListener("message", this._onmessage.bind(this), false);
412         port.start();
413     },
414
415     _onmessage: function(event)
416     {
417         var request = event.data;
418         var result;
419
420         if (request.command in this._handlers)
421             result = this._handlers[request.command](request, event.target);
422         else
423             result = this._status.E_NOTSUPPORTED(request.command);
424
425         if (result && request.requestId)
426             this._dispatchCallback(request.requestId, event.target, result);
427     },
428
429     _registerHandler: function(command, callback)
430     {
431         this._handlers[command] = callback;
432     }
433 }
434
435 WebInspector.ExtensionServer._statuses =
436 {
437     OK: "",
438     E_EXISTS: "Object already exists: %s",
439     E_BADARG: "Invalid argument %s: %s",
440     E_BADARGTYPE: "Invalid type for argument %s: got %s, expected %s",
441     E_NOTFOUND: "Object not found: %s",
442     E_NOTSUPPORTED: "Object does not support requested operation: %s",
443 }
444
445 WebInspector.ExtensionStatus = function()
446 {
447     function makeStatus(code)
448     {
449         var description = WebInspector.ExtensionServer._statuses[code] || code;
450         var details = Array.prototype.slice.call(arguments, 1);
451         var status = { code: code, description: description, details: details };
452         if (code !== "OK") {
453             status.isError = true;
454             console.log("Extension server error: " + String.vsprintf(description, details));
455         }
456         return status;
457     }
458     for (status in WebInspector.ExtensionServer._statuses)
459         this[status] = makeStatus.bind(null, status);
460 }
461
462 WebInspector.addExtensions = function(extensions)
463 {
464     WebInspector.extensionServer._addExtensions(extensions);
465 }
466
467 WebInspector.extensionServer = new WebInspector.ExtensionServer();