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
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    this.onNavigated = new EventSink("inspectedURLChanged");
115}
116
117Resources.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
138function ResourceImpl(id)
139{
140    this._id = id;
141}
142
143ResourceImpl.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
154function 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
168Panels.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
183function 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
190function PanelWithSidebarImpl(id)
191{
192    PanelImpl.call(this, id);
193}
194
195PanelWithSidebarImpl.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
213PanelWithSidebarImpl.prototype.__proto__ = PanelImpl.prototype;
214
215function ElementsPanel()
216{
217    var id = "elements";
218    PanelWithSidebar.call(this, id);
219    this.onSelectionChanged = new EventSink("panel-objectSelected-" + id);
220}
221
222function ExtensionPanel(id)
223{
224    Panel.call(this, id);
225    this.onSearch = new EventSink("panel-search-" + id);
226}
227
228function ExtensionSidebarPaneImpl(id)
229{
230    this._id = id;
231    this.onUpdated = new EventSink("sidebar-updated-" + id);
232}
233
234ExtensionSidebarPaneImpl.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
256function Audits()
257{
258}
259
260Audits.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
269function 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
285function 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
298AuditResultImpl.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
342function AuditResultNode(contents)
343{
344    this.contents = contents;
345    this.children = [];
346    this.expanded = false;
347}
348
349AuditResultNode.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
358function InspectedWindow()
359{
360}
361
362InspectedWindow.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
381function 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
398ExtensionServerClient.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
441function 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
453function 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
459function 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
470function 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
480var AuditCategory = declareInterfaceClass(AuditCategoryImpl);
481var AuditResult = declareInterfaceClass(AuditResultImpl);
482var EventSink = declareInterfaceClass(EventSinkImpl);
483var ExtensionSidebarPane = declareInterfaceClass(ExtensionSidebarPaneImpl);
484var Panel = declareInterfaceClass(PanelImpl);
485var PanelWithSidebar = declareInterfaceClass(PanelWithSidebarImpl);
486var Resource = declareInterfaceClass(ResourceImpl);
487
488var extensionServer = new ExtensionServerClient();
489
490webInspector = new InspectorExtensionAPI();
491experimental = window.experimental || {};
492experimental.webInspector = webInspector;
493
494}
495