Home | History | Annotate | Download | only in front-end
      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 }
    495