2011-04-06 Andrey Kosyakov <caseq@chromium.org>
[WebKit-https.git] / Source / WebCore / inspector / front-end / ExtensionAPI.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.injectedExtensionAPI = function(InjectedScriptHost, inspectedWindow, injectedScriptId)
32 {
33
34 // Here and below, all constructors are private to API implementation.
35 // For a public type Foo, if internal fields are present, these are on
36 // a private FooImpl type, an instance of FooImpl is used in a closure
37 // by Foo consutrctor to re-bind publicly exported members to an instance
38 // of Foo.
39
40 function EventSinkImpl(type, customDispatch)
41 {
42     this._type = type;
43     this._listeners = [];
44     this._customDispatch = customDispatch;
45 }
46
47 EventSinkImpl.prototype = {
48     addListener: function(callback)
49     {
50         if (typeof callback != "function")
51             throw new "addListener: callback is not a function";
52         if (this._listeners.length === 0)
53             extensionServer.sendRequest({ command: "subscribe", type: this._type });
54         this._listeners.push(callback);
55         extensionServer.registerHandler("notify-" + this._type, bind(this._dispatch, this));
56     },
57
58     removeListener: function(callback)
59     {
60         var listeners = this._listeners;
61
62         for (var i = 0; i < listeners.length; ++i) {
63             if (listeners[i] === callback) {
64                 listeners.splice(i, 1);
65                 break;
66             }
67         }
68         if (this._listeners.length === 0)
69             extensionServer.sendRequest({ command: "unsubscribe", type: this._type });
70     },
71
72     _fire: function()
73     {
74         var listeners = this._listeners.slice();
75         for (var i = 0; i < listeners.length; ++i)
76             listeners[i].apply(null, arguments);
77     },
78
79     _dispatch: function(request)
80     {
81          if (this._customDispatch)
82              this._customDispatch.call(this, request);
83          else
84              this._fire.apply(this, request.arguments);
85     }
86 }
87
88 function InspectorExtensionAPI()
89 {
90     this.audits = new Audits();
91     this.inspectedWindow = new InspectedWindow();
92     this.panels = new Panels();
93     this.resources = new Resources();
94
95     this.onReset = new EventSink("reset");
96 }
97
98 InspectorExtensionAPI.prototype = {
99     log: function(message)
100     {
101         extensionServer.sendRequest({ command: "log", message: message });
102     }
103 }
104
105 function Resources()
106 {
107     function resourceDispatch(request)
108     {
109         var resource = request.arguments[1];
110         resource.__proto__ = new Resource(request.arguments[0]);
111         this._fire(resource);
112     }
113     this.onFinished = new EventSink("resource-finished", resourceDispatch);
114     this.onNavigated = new EventSink("inspectedURLChanged");
115 }
116
117 Resources.prototype = {
118     getHAR: function(callback)
119     {
120         function callbackWrapper(result)
121         {
122             var entries = (result && result.entries) || [];
123             for (var i = 0; i < entries.length; ++i) {
124                 entries[i].__proto__ = new Resource(entries[i]._resourceId);
125                 delete entries[i]._resourceId;
126             }
127             callback(result);
128         }
129         return extensionServer.sendRequest({ command: "getHAR" }, callback && callbackWrapper);
130     },
131
132     addRequestHeaders: function(headers)
133     {
134         return extensionServer.sendRequest({ command: "addRequestHeaders", headers: headers, extensionId: location.hostname });
135     }
136 }
137
138 function ResourceImpl(id)
139 {
140     this._id = id;
141 }
142
143 ResourceImpl.prototype = {
144     getContent: function(callback)
145     {
146         function callbackWrapper(response)
147         {
148             callback(response.content, response.encoding);
149         }
150         extensionServer.sendRequest({ command: "getResourceContent", id: this._id }, callback && callbackWrapper);
151     }
152 };
153
154 function Panels()
155 {
156     var panels = {
157         elements: new ElementsPanel()
158     };
159
160     function panelGetter(name)
161     {
162         return panels[name];
163     }
164     for (var panel in panels)
165         this.__defineGetter__(panel, bind(panelGetter, null, panel));
166 }
167
168 Panels.prototype = {
169     create: function(title, iconURL, pageURL, callback)
170     {
171         var id = "extension-panel-" + extensionServer.nextObjectId();
172         var request = {
173             command: "createPanel",
174             id: id,
175             title: title,
176             icon: expandURL(iconURL),
177             url: expandURL(pageURL)
178         };
179         extensionServer.sendRequest(request, callback && bind(callback, this, new ExtensionPanel(id)));
180     }
181 }
182
183 function PanelImpl(id)
184 {
185     this._id = id;
186     this.onShown = new EventSink("panel-shown-" + id);
187     this.onHidden = new EventSink("panel-hidden-" + id);
188 }
189
190 function PanelWithSidebarImpl(id)
191 {
192     PanelImpl.call(this, id);
193 }
194
195 PanelWithSidebarImpl.prototype = {
196     createSidebarPane: function(title, callback)
197     {
198         var id = "extension-sidebar-" + extensionServer.nextObjectId();
199         var request = {
200             command: "createSidebarPane",
201             panel: this._id,
202             id: id,
203             title: title
204         };
205         function callbackWrapper()
206         {
207             callback(new ExtensionSidebarPane(id));
208         }
209         extensionServer.sendRequest(request, callback && callbackWrapper);
210     }
211 }
212
213 PanelWithSidebarImpl.prototype.__proto__ = PanelImpl.prototype;
214
215 function ElementsPanel()
216 {
217     var id = "elements";
218     PanelWithSidebar.call(this, id);
219     this.onSelectionChanged = new EventSink("panel-objectSelected-" + id);
220 }
221
222 function ExtensionPanel(id)
223 {
224     Panel.call(this, id);
225     this.onSearch = new EventSink("panel-search-" + id);
226 }
227
228 function ExtensionSidebarPaneImpl(id)
229 {
230     this._id = id;
231     this.onUpdated = new EventSink("sidebar-updated-" + id);
232 }
233
234 ExtensionSidebarPaneImpl.prototype = {
235     setHeight: function(height)
236     {
237         extensionServer.sendRequest({ command: "setSidebarHeight", id: this._id, height: height });
238     },
239
240     setExpression: function(expression, rootTitle)
241     {
242         extensionServer.sendRequest({ command: "setSidebarContent", id: this._id, expression: expression, rootTitle: rootTitle, evaluateOnPage: true });
243     },
244
245     setObject: function(jsonObject, rootTitle)
246     {
247         extensionServer.sendRequest({ command: "setSidebarContent", id: this._id, expression: jsonObject, rootTitle: rootTitle });
248     },
249
250     setPage: function(url)
251     {
252         extensionServer.sendRequest({ command: "setSidebarPage", id: this._id, url: expandURL(url) });
253     }
254 }
255
256 function Audits()
257 {
258 }
259
260 Audits.prototype = {
261     addCategory: function(displayName, resultCount)
262     {
263         var id = "extension-audit-category-" + extensionServer.nextObjectId();
264         extensionServer.sendRequest({ command: "addAuditCategory", id: id, displayName: displayName, resultCount: resultCount });
265         return new AuditCategory(id);
266     }
267 }
268
269 function AuditCategoryImpl(id)
270 {
271     function auditResultDispatch(request)
272     {
273         var auditResult = new AuditResult(request.arguments[0]);
274         try {
275             this._fire(auditResult);
276         } catch (e) {
277             console.error("Uncaught exception in extension audit event handler: " + e);
278             auditResult.done();
279         }
280     }
281     this._id = id;
282     this.onAuditStarted = new EventSink("audit-started-" + id, auditResultDispatch);
283 }
284
285 function AuditResultImpl(id)
286 {
287     this._id = id;
288
289     var formatterTypes = [
290         "url",
291         "snippet",
292         "text"
293     ];
294     for (var i = 0; i < formatterTypes.length; ++i)
295         this[formatterTypes[i]] = bind(this._nodeFactory, null, formatterTypes[i]);
296 }
297
298 AuditResultImpl.prototype = {
299     addResult: function(displayName, description, severity, details)
300     {
301         // shorthand for specifying details directly in addResult().
302         if (details && !(details instanceof AuditResultNode))
303             details = details instanceof Array ? this.createNode.apply(this, details) : this.createNode(details);
304
305         var request = {
306             command: "addAuditResult",
307             resultId: this._id,
308             displayName: displayName,
309             description: description,
310             severity: severity,
311             details: details
312         };
313         extensionServer.sendRequest(request);
314     },
315
316     createResult: function()
317     {
318         var node = new AuditResultNode();
319         node.contents = Array.prototype.slice.call(arguments);
320         return node;
321     },
322
323     done: function()
324     {
325         extensionServer.sendRequest({ command: "stopAuditCategoryRun", resultId: this._id });
326     },
327
328     get Severity()
329     {
330         return apiPrivate.audits.Severity;
331     },
332
333     _nodeFactory: function(type)
334     {
335         return {
336             type: type,
337             arguments: Array.prototype.slice.call(arguments, 1)
338         };
339     }
340 }
341
342 function AuditResultNode(contents)
343 {
344     this.contents = contents;
345     this.children = [];
346     this.expanded = false;
347 }
348
349 AuditResultNode.prototype = {
350     addChild: function()
351     {
352         var node = AuditResultImpl.prototype.createResult.apply(null, arguments);
353         this.children.push(node);
354         return node;
355     }
356 };
357
358 function InspectedWindow()
359 {
360 }
361
362 InspectedWindow.prototype = {
363     reload: function(userAgent)
364     {
365         return extensionServer.sendRequest({ command: "reload", userAgent: userAgent });
366     },
367
368     eval: function(expression, callback)
369     {
370         function callbackWrapper(result)
371         {
372             var value = result.value;
373             if (!result.isException)
374                 value = value === "undefined" ? undefined : JSON.parse(value);
375             callback(value, result.isException);
376         }
377         return extensionServer.sendRequest({ command: "evaluateOnInspectedPage", expression: expression }, callback && callbackWrapper);
378     }
379 }
380
381 function ExtensionServerClient()
382 {
383     this._callbacks = {};
384     this._handlers = {};
385     this._lastRequestId = 0;
386     this._lastObjectId = 0;
387
388     this.registerHandler("callback", bind(this._onCallback, this));
389
390     var channel = new MessageChannel();
391     this._port = channel.port1;
392     this._port.addEventListener("message", bind(this._onMessage, this), false);
393     this._port.start();
394
395     top.postMessage("registerExtension", [ channel.port2 ], "*");
396 }
397
398 ExtensionServerClient.prototype = {
399     sendRequest: function(message, callback)
400     {
401         if (typeof callback === "function")
402             message.requestId = this._registerCallback(callback);
403         return this._port.postMessage(message);
404     },
405
406     registerHandler: function(command, handler)
407     {
408         this._handlers[command] = handler;
409     },
410
411     nextObjectId: function()
412     {
413         return injectedScriptId + "_" + ++this._lastObjectId;
414     },
415
416     _registerCallback: function(callback)
417     {
418         var id = ++this._lastRequestId;
419         this._callbacks[id] = callback;
420         return id;
421     },
422
423     _onCallback: function(request)
424     {
425         if (request.requestId in this._callbacks) {
426             var callback = this._callbacks[request.requestId];
427             delete this._callbacks[request.requestId];
428             callback(request.result);
429         }
430     },
431
432     _onMessage: function(event)
433     {
434         var request = event.data;
435         var handler = this._handlers[request.command];
436         if (handler)
437             handler.call(this, request);
438     }
439 }
440
441 function expandURL(url)
442 {
443     if (!url)
444         return url;
445     if (/^[^/]+:/.exec(url)) // See if url has schema.
446         return url;
447     var baseURL = location.protocol + "//" + location.hostname + location.port;
448     if (/^\//.exec(url))
449         return baseURL + url;
450     return baseURL + location.pathname.replace(/\/[^/]*$/,"/") + url;
451 }
452
453 function bind(func, thisObject)
454 {
455     var args = Array.prototype.slice.call(arguments, 2);
456     return function() { return func.apply(thisObject, args.concat(Array.prototype.slice.call(arguments, 0))); };
457 }
458
459 function populateInterfaceClass(interface, implementation)
460 {
461     for (var member in implementation) {
462         if (member.charAt(0) === "_")
463             continue;
464         var value = implementation[member];
465         interface[member] = typeof value === "function" ? bind(value, implementation)
466             : interface[member] = implementation[member];
467     }
468 }
469
470 function declareInterfaceClass(implConstructor)
471 {
472     return function()
473     {
474         var impl = { __proto__: implConstructor.prototype };
475         implConstructor.apply(impl, arguments);
476         populateInterfaceClass(this, impl);
477     }
478 }
479
480 var AuditCategory = declareInterfaceClass(AuditCategoryImpl);
481 var AuditResult = declareInterfaceClass(AuditResultImpl);
482 var EventSink = declareInterfaceClass(EventSinkImpl);
483 var ExtensionSidebarPane = declareInterfaceClass(ExtensionSidebarPaneImpl);
484 var Panel = declareInterfaceClass(PanelImpl);
485 var PanelWithSidebar = declareInterfaceClass(PanelWithSidebarImpl);
486 var Resource = declareInterfaceClass(ResourceImpl);
487
488 var extensionServer = new ExtensionServerClient();
489
490 webInspector = new InspectorExtensionAPI();
491 experimental = window.experimental || {};
492 experimental.webInspector = webInspector;
493
494 }