d520fd987cc9bebff90f1ab02e3e0a0a9e2b190a
[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._resources = {};
38     this._lastResourceId = 0;
39     this._status = new WebInspector.ExtensionStatus();
40
41     this._registerHandler("addRequestHeaders", this._onAddRequestHeaders.bind(this));
42     this._registerHandler("addAuditCategory", this._onAddAuditCategory.bind(this));
43     this._registerHandler("addAuditResult", this._onAddAuditResult.bind(this));
44     this._registerHandler("createPanel", this._onCreatePanel.bind(this));
45     this._registerHandler("createSidebarPane", this._onCreateSidebarPane.bind(this));
46     this._registerHandler("evaluateOnInspectedPage", this._onEvaluateOnInspectedPage.bind(this));
47     this._registerHandler("getHAR", this._onGetHAR.bind(this));
48     this._registerHandler("getResourceContent", this._onGetResourceContent.bind(this));
49     this._registerHandler("log", this._onLog.bind(this));
50     this._registerHandler("reload", this._onReload.bind(this));
51     this._registerHandler("setSidebarHeight", this._onSetSidebarHeight.bind(this));
52     this._registerHandler("setSidebarContent", this._onSetSidebarContent.bind(this));
53     this._registerHandler("setSidebarPage", this._onSetSidebarPage.bind(this));
54     this._registerHandler("stopAuditCategoryRun", this._onStopAuditCategoryRun.bind(this));
55     this._registerHandler("subscribe", this._onSubscribe.bind(this));
56     this._registerHandler("unsubscribe", this._onUnsubscribe.bind(this));
57
58     window.addEventListener("message", this._onWindowMessage.bind(this), false);
59 }
60
61 WebInspector.ExtensionServer.prototype = {
62     notifyObjectSelected: function(panelId, objectId)
63     {
64         this._postNotification("panel-objectSelected-" + panelId, objectId);
65     },
66
67     notifySearchAction: function(panelId, action, searchString)
68     {
69         this._postNotification("panel-search-" + panelId, action, searchString);
70     },
71
72     notifyPanelShown: function(panelId)
73     {
74         this._postNotification("panel-shown-" + panelId);
75     },
76
77     notifyPanelHidden: function(panelId)
78     {
79         this._postNotification("panel-hidden-" + panelId);
80     },
81
82     notifyPageLoaded: function(milliseconds)
83     {
84         this._postNotification("inspectedPageLoaded", milliseconds);
85     },
86
87     notifyPageDOMContentLoaded: function(milliseconds)
88     {
89         this._postNotification("inspectedPageDOMContentLoaded", milliseconds);
90     },
91
92     notifyInspectedURLChanged: function()
93     {
94         this._postNotification("inspectedURLChanged");
95     },
96
97     notifyInspectorReset: function()
98     {
99         this._postNotification("reset");
100     },
101
102     notifyExtensionSidebarUpdated: function(id)
103     {
104         this._postNotification("sidebar-updated-" + id);
105     },
106
107     startAuditRun: function(category, auditRun)
108     {
109         this._clientObjects[auditRun.id] = auditRun;
110         this._postNotification("audit-started-" + category.id, auditRun.id);
111     },
112
113     stopAuditRun: function(auditRun)
114     {
115         delete this._clientObjects[auditRun.id];
116     },
117
118     resetResources: function()
119     {
120         this._resources = {};
121     },
122
123     _notifyResourceFinished: function(event)
124     {
125         var resource = event.data;
126         if (this._hasSubscribers("resource-finished"))
127             this._postNotification("resource-finished", this._resourceId(resource), (new WebInspector.HAREntry(resource)).build());
128     },
129
130     _hasSubscribers: function(type)
131     {
132         return !!this._subscribers[type];
133     },
134
135     _postNotification: function(type, details)
136     {
137         var subscribers = this._subscribers[type];
138         if (!subscribers)
139             return;
140         var message = {
141             command: "notify-" + type,
142             arguments: Array.prototype.slice.call(arguments, 1)
143         };
144         for (var i = 0; i < subscribers.length; ++i)
145             subscribers[i].postMessage(message);
146     },
147
148     _onSubscribe: function(message, port)
149     {
150         var subscribers = this._subscribers[message.type];
151         if (subscribers)
152             subscribers.push(port);
153         else
154             this._subscribers[message.type] = [ port ];
155     },
156
157     _onUnsubscribe: function(message, port)
158     {
159         var subscribers = this._subscribers[message.type];
160         if (!subscribers)
161             return;
162         subscribers.remove(port);
163         if (!subscribers.length)
164             delete this._subscribers[message.type];
165     },
166
167     _onAddRequestHeaders: function(message)
168     {
169         var id = message.extensionId;
170         if (typeof id !== "string")
171             return this._status.E_BADARGTYPE("extensionId", typeof id, "string");
172         var extensionHeaders = this._extraHeaders[id];
173         if (!extensionHeaders) {
174             extensionHeaders = {};
175             this._extraHeaders[id] = extensionHeaders;
176         }
177         for (name in message.headers)
178             extensionHeaders[name] = message.headers[name];
179         var allHeaders = {};
180         for (extension in this._extraHeaders) {
181             var headers = this._extraHeaders[extension];
182             for (name in headers) {
183                 if (typeof headers[name] === "string")
184                     allHeaders[name] = headers[name];
185             }
186         }
187         NetworkAgent.setExtraHeaders(allHeaders);
188     },
189
190     _onCreatePanel: function(message, port)
191     {
192         var id = message.id;
193         // The ids are generated on the client API side and must be unique, so the check below
194         // shouldn't be hit unless someone is bypassing the API.
195         if (id in this._clientObjects || id in WebInspector.panels)
196             return this._status.E_EXISTS(id);
197
198         var panel = new WebInspector.ExtensionPanel(id, message.title, message.icon);
199         this._clientObjects[id] = panel;
200         WebInspector.panels[id] = panel;
201         WebInspector.addPanel(panel);
202
203         var iframe = this.createClientIframe(panel.element, message.url);
204         iframe.style.height = "100%";
205         return this._status.OK();
206     },
207
208     _onCreateSidebarPane: function(message, constructor)
209     {
210         var panel = WebInspector.panels[message.panel];
211         if (!panel)
212             return this._status.E_NOTFOUND(message.panel);
213         if (!panel.sidebarElement || !panel.sidebarPanes)
214             return this._status.E_NOTSUPPORTED();
215         var id = message.id;
216         var sidebar = new WebInspector.ExtensionSidebarPane(message.title, message.id);
217         this._clientObjects[id] = sidebar;
218         panel.sidebarPanes[id] = sidebar;
219         panel.sidebarElement.appendChild(sidebar.element);
220
221         return this._status.OK();
222     },
223
224     createClientIframe: function(parent, url)
225     {
226         var iframe = document.createElement("iframe");
227         iframe.src = url;
228         iframe.style.width = "100%";
229         parent.appendChild(iframe);
230         return iframe;
231     },
232
233     _onSetSidebarHeight: function(message)
234     {
235         var sidebar = this._clientObjects[message.id];
236         if (!sidebar)
237             return this._status.E_NOTFOUND(message.id);
238         sidebar.bodyElement.firstChild.style.height = message.height;
239     },
240
241     _onSetSidebarContent: function(message)
242     {
243         var sidebar = this._clientObjects[message.id];
244         if (!sidebar)
245             return this._status.E_NOTFOUND(message.id);
246         if (message.evaluateOnPage)
247             sidebar.setExpression(message.expression, message.rootTitle);
248         else
249             sidebar.setObject(message.expression, message.rootTitle);
250     },
251
252     _onSetSidebarPage: function(message)
253     {
254         var sidebar = this._clientObjects[message.id];
255         if (!sidebar)
256             return this._status.E_NOTFOUND(message.id);
257         sidebar.setPage(message.url);
258     },
259
260     _onLog: function(message)
261     {
262         WebInspector.log(message.message);
263     },
264
265     _onReload: function(message)
266     {
267         if (typeof message.userAgent === "string")
268             PageAgent.setUserAgentOverride(message.userAgent);
269
270         PageAgent.reloadPage(false);
271         return this._status.OK();
272     },
273
274     _onEvaluateOnInspectedPage: function(message, port)
275     {
276         function callback(error, resultPayload)
277         {
278             if (error)
279                 return;
280             var resultObject = WebInspector.RemoteObject.fromPayload(resultPayload);
281             var result = {};
282             if (resultObject.isError())
283                 result.isException = true;
284             result.value = resultObject.description;
285             this._dispatchCallback(message.requestId, port, result);
286         }
287         var evalExpression = "JSON.stringify(eval(unescape('" + escape(message.expression) + "')));";
288         RuntimeAgent.evaluate(evalExpression, "", true, callback.bind(this));
289     },
290
291     _onRevealAndSelect: function(message)
292     {
293         if (message.panelId === "resources" && type === "resource")
294             return this._onRevealAndSelectResource(message);
295         else
296             return this._status.E_NOTSUPPORTED(message.panelId, message.type);
297     },
298
299     _onRevealAndSelectResource: function(message)
300     {
301         var id = message.id;
302         var resource = null;
303
304         resource = this._resourceById(id) || WebInspector.resourceForURL(id);
305         if (!resource)
306             return this._status.E_NOTFOUND(typeof id + ": " + id);
307
308         WebInspector.panels.resources.showResource(resource, message.line);
309         WebInspector.showPanel("resources");
310     },
311
312     _dispatchCallback: function(requestId, port, result)
313     {
314         port.postMessage({ command: "callback", requestId: requestId, result: result });
315     },
316
317     _onGetHAR: function(request)
318     {
319         var harLog = (new WebInspector.HARLog()).build();
320         for (var i = 0; i < harLog.entries.length; ++i)
321             harLog.entries[i]._resourceId = this._resourceId(WebInspector.networkResources[i]);
322         return harLog;
323     },
324
325     _onGetResourceContent: function(message, port)
326     {
327         function onContentAvailable(content, encoded)
328         {
329             var response = {
330                 encoding: encoded ? "base64" : "",
331                 content: content
332             };
333             this._dispatchCallback(message.requestId, port, response);
334         }
335         var resource = this._resourceById(message.id);
336         if (!resource)
337             return this._status.E_NOTFOUND(message.id);
338         resource.requestContent(onContentAvailable.bind(this));
339     },
340
341     _resourceId: function(resource)
342     {
343         if (!resource._extensionResourceId) {
344             resource._extensionResourceId = ++this._lastResourceId;
345             this._resources[resource._extensionResourceId] = resource;
346         }
347         return resource._extensionResourceId;
348     },
349
350     _resourceById: function(id)
351     {
352         return this._resources[id];
353     },
354
355     _onAddAuditCategory: function(request)
356     {
357         var category = new WebInspector.ExtensionAuditCategory(request.id, request.displayName, request.resultCount);
358         if (WebInspector.panels.audits.getCategory(category.id))
359             return this._status.E_EXISTS(category.id);
360         this._clientObjects[request.id] = category;
361         WebInspector.panels.audits.addCategory(category);
362     },
363
364     _onAddAuditResult: function(request)
365     {
366         var auditResult = this._clientObjects[request.resultId];
367         if (!auditResult)
368             return this._status.E_NOTFOUND(request.resultId);
369         try {
370             auditResult.addResult(request.displayName, request.description, request.severity, request.details);
371         } catch (e) {
372             return e;
373         }
374         return this._status.OK();
375     },
376
377     _onStopAuditCategoryRun: function(request)
378     {
379         var auditRun = this._clientObjects[request.resultId];
380         if (!auditRun)
381             return this._status.E_NOTFOUND(request.resultId);
382         auditRun.cancel();
383     },
384
385     initExtensions: function()
386     {
387         // The networkManager is normally created after the ExtensionServer is constructed, but before initExtensions() is called.
388         WebInspector.networkManager.addEventListener(WebInspector.NetworkManager.EventTypes.ResourceFinished, this._notifyResourceFinished, this);
389
390         InspectorExtensionRegistry.getExtensionsAsync();
391     },
392
393     _addExtensions: function(extensions)
394     {
395         // See ExtensionAPI.js and ExtensionCommon.js for details.
396         InspectorFrontendHost.setExtensionAPI(this._buildExtensionAPIInjectedScript());
397         for (var i = 0; i < extensions.length; ++i) {
398             var extension = extensions[i];
399             try {
400                 if (!extension.startPage)
401                     return;
402                 var iframe = document.createElement("iframe");
403                 iframe.src = extension.startPage;
404                 iframe.style.display = "none";
405                 document.body.appendChild(iframe);
406             } catch (e) {
407                 console.error("Failed to initialize extension " + extension.startPage + ":" + e);
408             }
409         }
410     },
411
412     _buildExtensionAPIInjectedScript: function()
413     {
414         var resourceTypes = {};
415         var resourceTypeProperties = Object.getOwnPropertyNames(WebInspector.Resource.Type);
416         for (var i = 0; i < resourceTypeProperties.length; ++i) {
417              var propName = resourceTypeProperties[i];
418              var propValue = WebInspector.Resource.Type[propName];
419              if (typeof propValue === "number")
420                  resourceTypes[propName] = WebInspector.Resource.Type.toString(propValue);
421         }
422         var platformAPI = WebInspector.buildPlatformExtensionAPI ? WebInspector.buildPlatformExtensionAPI() : "";
423         return "(function(){ " +
424             "var apiPrivate = {};" +
425             "(" + WebInspector.commonExtensionSymbols.toString() + ")(apiPrivate);" +
426             "(" + WebInspector.injectedExtensionAPI.toString() + ").apply(this, arguments);" +
427             platformAPI +
428             "})";
429     },
430
431     _onWindowMessage: function(event)
432     {
433         if (event.data !== "registerExtension")
434             return;
435         var port = event.ports[0];
436         port.addEventListener("message", this._onmessage.bind(this), false);
437         port.start();
438     },
439
440     _onmessage: function(event)
441     {
442         var request = event.data;
443         var result;
444
445         if (request.command in this._handlers)
446             result = this._handlers[request.command](request, event.target);
447         else
448             result = this._status.E_NOTSUPPORTED(request.command);
449
450         if (result && request.requestId)
451             this._dispatchCallback(request.requestId, event.target, result);
452     },
453
454     _registerHandler: function(command, callback)
455     {
456         this._handlers[command] = callback;
457     }
458 }
459
460 WebInspector.ExtensionServer._statuses =
461 {
462     OK: "",
463     E_EXISTS: "Object already exists: %s",
464     E_BADARG: "Invalid argument %s: %s",
465     E_BADARGTYPE: "Invalid type for argument %s: got %s, expected %s",
466     E_NOTFOUND: "Object not found: %s",
467     E_NOTSUPPORTED: "Object does not support requested operation: %s",
468 }
469
470 WebInspector.ExtensionStatus = function()
471 {
472     function makeStatus(code)
473     {
474         var description = WebInspector.ExtensionServer._statuses[code] || code;
475         var details = Array.prototype.slice.call(arguments, 1);
476         var status = { code: code, description: description, details: details };
477         if (code !== "OK") {
478             status.isError = true;
479             console.log("Extension server error: " + String.vsprintf(description, details));
480         }
481         return status;
482     }
483     for (status in WebInspector.ExtensionServer._statuses)
484         this[status] = makeStatus.bind(null, status);
485 }
486
487 WebInspector.addExtensions = function(extensions)
488 {
489     WebInspector.extensionServer._addExtensions(extensions);
490 }
491
492 WebInspector.extensionServer = new WebInspector.ExtensionServer();