bab5f18bd5b33eb68b5a7d5a5f450bd992807fd4
[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 }
115
116 Resources.prototype = {
117     getHAR: function(callback)
118     {
119         function callbackWrapper(result)
120         {
121             var entries = (result && result.entries) || [];
122             for (var i = 0; i < entries.length; ++i) {
123                 entries[i].__proto__ = new Resource(entries[i]._resourceId);
124                 delete entries[i]._resourceId;
125             }
126             callback(result);
127         }
128         return extensionServer.sendRequest({ command: "getHAR" }, callback && callbackWrapper);
129     },
130
131     addRequestHeaders: function(headers)
132     {
133         return extensionServer.sendRequest({ command: "addRequestHeaders", headers: headers, extensionId: location.hostname });
134     }
135 }
136
137 function ResourceImpl(id)
138 {
139     this._id = id;
140 }
141
142 ResourceImpl.prototype = {
143     getContent: function(callback)
144     {
145         function callbackWrapper(response)
146         {
147             callback(response.content, response.encoding);
148         }
149         extensionServer.sendRequest({ command: "getResourceContent", id: this._id }, callback && callbackWrapper);
150     }
151 };
152
153 function Panels()
154 {
155     var panels = {
156         elements: new ElementsPanel()
157     };
158
159     function panelGetter(name)
160     {
161         return panels[name];
162     }
163     for (var panel in panels)
164         this.__defineGetter__(panel, bind(panelGetter, null, panel));
165 }
166
167 Panels.prototype = {
168     create: function(title, iconURL, pageURL, callback)
169     {
170         var id = "extension-panel-" + extensionServer.nextObjectId();
171         var request = {
172             command: "createPanel",
173             id: id,
174             title: title,
175             icon: expandURL(iconURL),
176             url: expandURL(pageURL)
177         };
178         extensionServer.sendRequest(request, callback && bind(callback, this, new ExtensionPanel(id)));
179     }
180 }
181
182 function PanelImpl(id)
183 {
184     this._id = id;
185     this.onShown = new EventSink("panel-shown-" + id);
186     this.onHidden = new EventSink("panel-hidden-" + id);
187 }
188
189 function PanelWithSidebarImpl(id)
190 {
191     PanelImpl.call(this, id);
192 }
193
194 PanelWithSidebarImpl.prototype = {
195     createSidebarPane: function(title, callback)
196     {
197         var id = "extension-sidebar-" + extensionServer.nextObjectId();
198         var request = {
199             command: "createSidebarPane",
200             panel: this._id,
201             id: id,
202             title: title
203         };
204         function callbackWrapper()
205         {
206             callback(new ExtensionSidebarPane(id));
207         }
208         extensionServer.sendRequest(request, callback && callbackWrapper);
209     }
210 }
211
212 PanelWithSidebarImpl.prototype.__proto__ = PanelImpl.prototype;
213
214 function ElementsPanel()
215 {
216     var id = "elements";
217     PanelWithSidebar.call(this, id);
218     this.onSelectionChanged = new EventSink("panel-objectSelected-" + id);
219 }
220
221 function ExtensionPanel(id)
222 {
223     Panel.call(this, id);
224     this.onSearch = new EventSink("panel-search-" + id);
225 }
226
227 function ExtensionSidebarPaneImpl(id)
228 {
229     this._id = id;
230     this.onUpdated = new EventSink("sidebar-updated-" + id);
231 }
232
233 ExtensionSidebarPaneImpl.prototype = {
234     setHeight: function(height)
235     {
236         extensionServer.sendRequest({ command: "setSidebarHeight", id: this._id, height: height });
237     },
238
239     setExpression: function(expression, rootTitle)
240     {
241         extensionServer.sendRequest({ command: "setSidebarContent", id: this._id, expression: expression, rootTitle: rootTitle, evaluateOnPage: true });
242     },
243
244     setObject: function(jsonObject, rootTitle)
245     {
246         extensionServer.sendRequest({ command: "setSidebarContent", id: this._id, expression: jsonObject, rootTitle: rootTitle });
247     },
248
249     setPage: function(url)
250     {
251         extensionServer.sendRequest({ command: "setSidebarPage", id: this._id, url: expandURL(url) });
252     }
253 }
254
255 function Audits()
256 {
257 }
258
259 Audits.prototype = {
260     addCategory: function(displayName, resultCount)
261     {
262         var id = "extension-audit-category-" + extensionServer.nextObjectId();
263         extensionServer.sendRequest({ command: "addAuditCategory", id: id, displayName: displayName, resultCount: resultCount });
264         return new AuditCategory(id);
265     }
266 }
267
268 function AuditCategoryImpl(id)
269 {
270     function auditResultDispatch(request)
271     {
272         var auditResult = new AuditResult(request.arguments[0]);
273         try {
274             this._fire(auditResult);
275         } catch (e) {
276             console.error("Uncaught exception in extension audit event handler: " + e);
277             auditResult.done();
278         }
279     }
280     this._id = id;
281     this.onAuditStarted = new EventSink("audit-started-" + id, auditResultDispatch);
282 }
283
284 function AuditResultImpl(id)
285 {
286     this._id = id;
287
288     var formatterTypes = [
289         "url",
290         "snippet",
291         "text"
292     ];
293     for (var i = 0; i < formatterTypes.length; ++i)
294         this[formatterTypes[i]] = bind(this._nodeFactory, null, formatterTypes[i]);
295 }
296
297 AuditResultImpl.prototype = {
298     addResult: function(displayName, description, severity, details)
299     {
300         // shorthand for specifying details directly in addResult().
301         if (details && !(details instanceof AuditResultNode))
302             details = details instanceof Array ? this.createNode.apply(this, details) : this.createNode(details);
303
304         var request = {
305             command: "addAuditResult",
306             resultId: this._id,
307             displayName: displayName,
308             description: description,
309             severity: severity,
310             details: details
311         };
312         extensionServer.sendRequest(request);
313     },
314
315     createResult: function()
316     {
317         var node = new AuditResultNode();
318         node.contents = Array.prototype.slice.call(arguments);
319         return node;
320     },
321
322     done: function()
323     {
324         extensionServer.sendRequest({ command: "stopAuditCategoryRun", resultId: this._id });
325     },
326
327     get Severity()
328     {
329         return apiPrivate.audits.Severity;
330     },
331
332     _nodeFactory: function(type)
333     {
334         return {
335             type: type,
336             arguments: Array.prototype.slice.call(arguments, 1)
337         };
338     }
339 }
340
341 function AuditResultNode(contents)
342 {
343     this.contents = contents;
344     this.children = [];
345     this.expanded = false;
346 }
347
348 AuditResultNode.prototype = {
349     addChild: function()
350     {
351         var node = AuditResultImpl.prototype.createResult.apply(null, arguments);
352         this.children.push(node);
353         return node;
354     }
355 };
356
357 function InspectedWindow()
358 {
359     this.onDOMContentLoaded = new EventSink("inspectedPageDOMContentLoaded");
360     this.onLoaded = new EventSink("inspectedPageLoaded");
361     this.onNavigated = new EventSink("inspectedURLChanged");
362 }
363
364 InspectedWindow.prototype = {
365     reload: function(userAgent)
366     {
367         return extensionServer.sendRequest({ command: "reload", userAgent: userAgent });
368     },
369
370     eval: function(expression, callback)
371     {
372         function callbackWrapper(result)
373         {
374             var value = result.value;
375             if (!result.isException)
376                 value = value === "undefined" ? undefined : JSON.parse(value);
377             callback(value, result.isException);
378         }
379         return extensionServer.sendRequest({ command: "evaluateOnInspectedPage", expression: expression }, callback && callbackWrapper);
380     }
381 }
382
383 function ExtensionServerClient()
384 {
385     this._callbacks = {};
386     this._handlers = {};
387     this._lastRequestId = 0;
388     this._lastObjectId = 0;
389
390     this.registerHandler("callback", bind(this._onCallback, this));
391
392     var channel = new MessageChannel();
393     this._port = channel.port1;
394     this._port.addEventListener("message", bind(this._onMessage, this), false);
395     this._port.start();
396
397     top.postMessage("registerExtension", [ channel.port2 ], "*");
398 }
399
400 ExtensionServerClient.prototype = {
401     sendRequest: function(message, callback)
402     {
403         if (typeof callback === "function")
404             message.requestId = this._registerCallback(callback);
405         return this._port.postMessage(message);
406     },
407
408     registerHandler: function(command, handler)
409     {
410         this._handlers[command] = handler;
411     },
412
413     nextObjectId: function()
414     {
415         return injectedScriptId + "_" + ++this._lastObjectId;
416     },
417
418     _registerCallback: function(callback)
419     {
420         var id = ++this._lastRequestId;
421         this._callbacks[id] = callback;
422         return id;
423     },
424
425     _onCallback: function(request)
426     {
427         if (request.requestId in this._callbacks) {
428             var callback = this._callbacks[request.requestId];
429             delete this._callbacks[request.requestId];
430             callback(request.result);
431         }
432     },
433
434     _onMessage: function(event)
435     {
436         var request = event.data;
437         var handler = this._handlers[request.command];
438         if (handler)
439             handler.call(this, request);
440     }
441 }
442
443 function expandURL(url)
444 {
445     if (!url)
446         return url;
447     if (/^[^/]+:/.exec(url)) // See if url has schema.
448         return url;
449     var baseURL = location.protocol + "//" + location.hostname + location.port;
450     if (/^\//.exec(url))
451         return baseURL + url;
452     return baseURL + location.pathname.replace(/\/[^/]*$/,"/") + url;
453 }
454
455 function bind(func, thisObject)
456 {
457     var args = Array.prototype.slice.call(arguments, 2);
458     return function() { return func.apply(thisObject, args.concat(Array.prototype.slice.call(arguments, 0))); };
459 }
460
461 function populateInterfaceClass(interface, implementation)
462 {
463     for (var member in implementation) {
464         if (member.charAt(0) === "_")
465             continue;
466         var value = implementation[member];
467         interface[member] = typeof value === "function" ? bind(value, implementation)
468             : interface[member] = implementation[member];
469     }
470 }
471
472 function declareInterfaceClass(implConstructor)
473 {
474     return function()
475     {
476         var impl = { __proto__: implConstructor.prototype };
477         implConstructor.apply(impl, arguments);
478         populateInterfaceClass(this, impl);
479     }
480 }
481
482 var AuditCategory = declareInterfaceClass(AuditCategoryImpl);
483 var AuditResult = declareInterfaceClass(AuditResultImpl);
484 var EventSink = declareInterfaceClass(EventSinkImpl);
485 var ExtensionSidebarPane = declareInterfaceClass(ExtensionSidebarPaneImpl);
486 var Panel = declareInterfaceClass(PanelImpl);
487 var PanelWithSidebar = declareInterfaceClass(PanelWithSidebarImpl);
488 var Resource = declareInterfaceClass(ResourceImpl);
489
490 var extensionServer = new ExtensionServerClient();
491
492 webInspector = new InspectorExtensionAPI();
493 experimental = window.experimental || {};
494 experimental.webInspector = webInspector;
495
496 }