ExtensionAPI.js revision 2bde8e466a4451c7319e3a072d118917957d6554
1/*
2 * Copyright (C) 2010 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
31WebInspector.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
40function EventSinkImpl(type, customDispatch)
41{
42    this._type = type;
43    this._listeners = [];
44    this._customDispatch = customDispatch;
45}
46
47EventSinkImpl.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
88function 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
98InspectorExtensionAPI.prototype = {
99    log: function(message)
100    {
101        extensionServer.sendRequest({ command: "log", message: message });
102    }
103}
104
105function 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
116Resources.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
137function ResourceImpl(id)
138{
139    this._id = id;
140}
141
142ResourceImpl.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
153function 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
167Panels.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
182function PanelImpl(id)
183{
184    this._id = id;
185}
186
187function PanelWithSidebarImpl(id)
188{
189    PanelImpl.call(this, id);
190}
191
192PanelWithSidebarImpl.prototype = {
193    createSidebarPane: function(title, url, callback)
194    {
195        var id = "extension-sidebar-" + extensionServer.nextObjectId();
196        var request = {
197            command: "createSidebarPane",
198            panel: this._id,
199            id: id,
200            title: title,
201            url: expandURL(url)
202        };
203        function callbackWrapper()
204        {
205            callback(new ExtensionSidebarPane(id));
206        }
207        extensionServer.sendRequest(request, callback && callbackWrapper);
208    },
209
210    createWatchExpressionSidebarPane: function(title, callback)
211    {
212        var id = "watch-sidebar-" + extensionServer.nextObjectId();
213        var request = {
214            command: "createWatchExpressionSidebarPane",
215            panel: this._id,
216            id: id,
217            title: title
218        };
219        function callbackWrapper()
220        {
221            callback(new WatchExpressionSidebarPane(id));
222        }
223        extensionServer.sendRequest(request, callback && callbackWrapper);
224    }
225}
226
227PanelWithSidebarImpl.prototype.__proto__ = PanelImpl.prototype;
228
229function ElementsPanel()
230{
231    var id = "elements";
232    PanelWithSidebar.call(this, id);
233    this.onSelectionChanged = new EventSink("panel-objectSelected-" + id);
234}
235
236function ExtensionPanel(id)
237{
238    Panel.call(this, id);
239    this.onSearch = new EventSink("panel-search-" + id);
240}
241
242function ExtensionSidebarPaneImpl(id)
243{
244    this._id = id;
245}
246
247ExtensionSidebarPaneImpl.prototype = {
248    setHeight: function(height)
249    {
250        extensionServer.sendRequest({ command: "setSidebarHeight", id: this._id, height: height });
251    }
252}
253
254function WatchExpressionSidebarPaneImpl(id)
255{
256    ExtensionSidebarPaneImpl.call(this, id);
257    this.onUpdated = new EventSink("watch-sidebar-updated-" + id);
258}
259
260WatchExpressionSidebarPaneImpl.prototype = {
261    setExpression: function(expression, rootTitle)
262    {
263        extensionServer.sendRequest({ command: "setWatchSidebarContent", id: this._id, expression: expression, rootTitle: rootTitle, evaluateOnPage: true });
264    },
265
266    setObject: function(jsonObject, rootTitle)
267    {
268        extensionServer.sendRequest({ command: "setWatchSidebarContent", id: this._id, expression: jsonObject, rootTitle: rootTitle });
269    }
270}
271
272WatchExpressionSidebarPaneImpl.prototype.__proto__ = ExtensionSidebarPaneImpl.prototype;
273
274function WatchExpressionSidebarPane(id)
275{
276    var impl = new WatchExpressionSidebarPaneImpl(id);
277    ExtensionSidebarPane.call(this, id, impl);
278}
279
280function Audits()
281{
282}
283
284Audits.prototype = {
285    addCategory: function(displayName, resultCount)
286    {
287        var id = "extension-audit-category-" + extensionServer.nextObjectId();
288        extensionServer.sendRequest({ command: "addAuditCategory", id: id, displayName: displayName, resultCount: resultCount });
289        return new AuditCategory(id);
290    }
291}
292
293function AuditCategoryImpl(id)
294{
295    function auditResultDispatch(request)
296    {
297        var auditResult = new AuditResult(request.arguments[0]);
298        try {
299            this._fire(auditResult);
300        } catch (e) {
301            console.error("Uncaught exception in extension audit event handler: " + e);
302            auditResult.done();
303        }
304    }
305    this._id = id;
306    this.onAuditStarted = new EventSink("audit-started-" + id, auditResultDispatch);
307}
308
309function AuditResultImpl(id)
310{
311    this._id = id;
312
313    var formatterTypes = [
314        "url",
315        "snippet",
316        "text"
317    ];
318    for (var i = 0; i < formatterTypes.length; ++i)
319        this[formatterTypes[i]] = bind(this._nodeFactory, null, formatterTypes[i]);
320}
321
322AuditResultImpl.prototype = {
323    addResult: function(displayName, description, severity, details)
324    {
325        // shorthand for specifying details directly in addResult().
326        if (details && !(details instanceof AuditResultNode))
327            details = details instanceof Array ? this.createNode.apply(this, details) : this.createNode(details);
328
329        var request = {
330            command: "addAuditResult",
331            resultId: this._id,
332            displayName: displayName,
333            description: description,
334            severity: severity,
335            details: details
336        };
337        extensionServer.sendRequest(request);
338    },
339
340    createResult: function()
341    {
342        var node = new AuditResultNode();
343        node.contents = Array.prototype.slice.call(arguments);
344        return node;
345    },
346
347    done: function()
348    {
349        extensionServer.sendRequest({ command: "stopAuditCategoryRun", resultId: this._id });
350    },
351
352    get Severity()
353    {
354        return apiPrivate.audits.Severity;
355    },
356
357    _nodeFactory: function(type)
358    {
359        return {
360            type: type,
361            arguments: Array.prototype.slice.call(arguments, 1)
362        };
363    }
364}
365
366function AuditResultNode(contents)
367{
368    this.contents = contents;
369    this.children = [];
370    this.expanded = false;
371}
372
373AuditResultNode.prototype = {
374    addChild: function()
375    {
376        var node = AuditResultImpl.prototype.createResult.apply(null, arguments);
377        this.children.push(node);
378        return node;
379    }
380};
381
382function InspectedWindow()
383{
384    this.onDOMContentLoaded = new EventSink("inspectedPageDOMContentLoaded");
385    this.onLoaded = new EventSink("inspectedPageLoaded");
386    this.onNavigated = new EventSink("inspectedURLChanged");
387}
388
389InspectedWindow.prototype = {
390    reload: function(userAgent)
391    {
392        return extensionServer.sendRequest({ command: "reload", userAgent: userAgent });
393    },
394
395    eval: function(expression, callback)
396    {
397        function callbackWrapper(result)
398        {
399            var value = result.value;
400            if (!result.isException)
401                value = value === "undefined" ? undefined : JSON.parse(value);
402            callback(value, result.isException);
403        }
404        return extensionServer.sendRequest({ command: "evaluateOnInspectedPage", expression: expression }, callback && callbackWrapper);
405    }
406}
407
408function ExtensionServerClient()
409{
410    this._callbacks = {};
411    this._handlers = {};
412    this._lastRequestId = 0;
413    this._lastObjectId = 0;
414
415    this.registerHandler("callback", bind(this._onCallback, this));
416
417    var channel = new MessageChannel();
418    this._port = channel.port1;
419    this._port.addEventListener("message", bind(this._onMessage, this), false);
420    this._port.start();
421
422    top.postMessage("registerExtension", [ channel.port2 ], "*");
423}
424
425ExtensionServerClient.prototype = {
426    sendRequest: function(message, callback)
427    {
428        if (typeof callback === "function")
429            message.requestId = this._registerCallback(callback);
430        return this._port.postMessage(message);
431    },
432
433    registerHandler: function(command, handler)
434    {
435        this._handlers[command] = handler;
436    },
437
438    nextObjectId: function()
439    {
440        return injectedScriptId + "_" + ++this._lastObjectId;
441    },
442
443    _registerCallback: function(callback)
444    {
445        var id = ++this._lastRequestId;
446        this._callbacks[id] = callback;
447        return id;
448    },
449
450    _onCallback: function(request)
451    {
452        if (request.requestId in this._callbacks) {
453            var callback = this._callbacks[request.requestId];
454            delete this._callbacks[request.requestId];
455            callback(request.result);
456        }
457    },
458
459    _onMessage: function(event)
460    {
461        var request = event.data;
462        var handler = this._handlers[request.command];
463        if (handler)
464            handler.call(this, request);
465    }
466}
467
468function expandURL(url)
469{
470    if (!url)
471        return url;
472    if (/^[^/]+:/.exec(url)) // See if url has schema.
473        return url;
474    var baseURL = location.protocol + "//" + location.hostname + location.port;
475    if (/^\//.exec(url))
476        return baseURL + url;
477    return baseURL + location.pathname.replace(/\/[^/]*$/,"/") + url;
478}
479
480function bind(func, thisObject)
481{
482    var args = Array.prototype.slice.call(arguments, 2);
483    return function() { return func.apply(thisObject, args.concat(Array.prototype.slice.call(arguments, 0))); };
484}
485
486function populateInterfaceClass(interface, implementation)
487{
488    for (var member in implementation) {
489        if (member.charAt(0) === "_")
490            continue;
491        var value = implementation[member];
492        interface[member] = typeof value === "function" ? bind(value, implementation)
493            : interface[member] = implementation[member];
494    }
495}
496
497function declareInterfaceClass(implConstructor)
498{
499    return function()
500    {
501        var impl = { __proto__: implConstructor.prototype };
502        implConstructor.apply(impl, arguments);
503        populateInterfaceClass(this, impl);
504    }
505}
506
507var AuditCategory = declareInterfaceClass(AuditCategoryImpl);
508var AuditResult = declareInterfaceClass(AuditResultImpl);
509var EventSink = declareInterfaceClass(EventSinkImpl);
510var ExtensionSidebarPane = declareInterfaceClass(ExtensionSidebarPaneImpl);
511var Panel = declareInterfaceClass(PanelImpl);
512var PanelWithSidebar = declareInterfaceClass(PanelWithSidebarImpl);
513var Resource = declareInterfaceClass(ResourceImpl);
514var WatchExpressionSidebarPane = declareInterfaceClass(WatchExpressionSidebarPaneImpl);
515
516var extensionServer = new ExtensionServerClient();
517
518webInspector = new InspectorExtensionAPI();
519experimental = window.experimental || {};
520experimental.webInspector = webInspector;
521
522}
523