Home | History | Annotate | Download | only in front_end
      1 /*
      2  * Copyright (C) 2012 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 function defineCommonExtensionSymbols(apiPrivate)
     32 {
     33     if (!apiPrivate.audits)
     34         apiPrivate.audits = {};
     35     apiPrivate.audits.Severity = {
     36         Info: "info",
     37         Warning: "warning",
     38         Severe: "severe"
     39     };
     40 
     41     if (!apiPrivate.console)
     42         apiPrivate.console = {};
     43     apiPrivate.console.Severity = {
     44         Debug: "debug",
     45         Log: "log",
     46         Warning: "warning",
     47         Error: "error"
     48     };
     49 
     50     if (!apiPrivate.panels)
     51         apiPrivate.panels = {};
     52     apiPrivate.panels.SearchAction = {
     53         CancelSearch: "cancelSearch",
     54         PerformSearch: "performSearch",
     55         NextSearchResult: "nextSearchResult",
     56         PreviousSearchResult: "previousSearchResult"
     57     };
     58 
     59     apiPrivate.Events = {
     60         AuditStarted: "audit-started-",
     61         ButtonClicked: "button-clicked-",
     62         ConsoleMessageAdded: "console-message-added",
     63         ElementsPanelObjectSelected: "panel-objectSelected-elements",
     64         NetworkRequestFinished: "network-request-finished",
     65         OpenResource: "open-resource",
     66         PanelSearch: "panel-search-",
     67         ResourceAdded: "resource-added",
     68         ResourceContentCommitted: "resource-content-committed",
     69         TimelineEventRecorded: "timeline-event-recorded",
     70         ViewShown: "view-shown-",
     71         ViewHidden: "view-hidden-"
     72     };
     73 
     74     apiPrivate.Commands = {
     75         AddAuditCategory: "addAuditCategory",
     76         AddAuditResult: "addAuditResult",
     77         AddConsoleMessage: "addConsoleMessage",
     78         AddRequestHeaders: "addRequestHeaders",
     79         CreatePanel: "createPanel",
     80         CreateSidebarPane: "createSidebarPane",
     81         CreateStatusBarButton: "createStatusBarButton",
     82         EvaluateOnInspectedPage: "evaluateOnInspectedPage",
     83         ForwardKeyboardEvent: "_forwardKeyboardEvent",
     84         GetConsoleMessages: "getConsoleMessages",
     85         GetHAR: "getHAR",
     86         GetPageResources: "getPageResources",
     87         GetRequestContent: "getRequestContent",
     88         GetResourceContent: "getResourceContent",
     89         Reload: "Reload",
     90         Subscribe: "subscribe",
     91         SetOpenResourceHandler: "setOpenResourceHandler",
     92         SetResourceContent: "setResourceContent",
     93         SetSidebarContent: "setSidebarContent",
     94         SetSidebarHeight: "setSidebarHeight",
     95         SetSidebarPage: "setSidebarPage",
     96         ShowPanel: "showPanel",
     97         StopAuditCategoryRun: "stopAuditCategoryRun",
     98         OpenResource: "openResource",
     99         Reload: "Reload",
    100         Unsubscribe: "unsubscribe",
    101         UpdateAuditProgress: "updateAuditProgress",
    102         UpdateButton: "updateButton",
    103         InspectedURLChanged: "inspectedURLChanged"
    104     };
    105 }
    106 
    107 function injectedExtensionAPI(injectedScriptId)
    108 {
    109 
    110 var apiPrivate = {};
    111 
    112 defineCommonExtensionSymbols(apiPrivate);
    113 
    114 var commands = apiPrivate.Commands;
    115 var events = apiPrivate.Events;
    116 var userAction = false;
    117 
    118 // Here and below, all constructors are private to API implementation.
    119 // For a public type Foo, if internal fields are present, these are on
    120 // a private FooImpl type, an instance of FooImpl is used in a closure
    121 // by Foo consutrctor to re-bind publicly exported members to an instance
    122 // of Foo.
    123 
    124 /**
    125  * @constructor
    126  */
    127 function EventSinkImpl(type, customDispatch)
    128 {
    129     this._type = type;
    130     this._listeners = [];
    131     this._customDispatch = customDispatch;
    132 }
    133 
    134 EventSinkImpl.prototype = {
    135     addListener: function(callback)
    136     {
    137         if (typeof callback !== "function")
    138             throw "addListener: callback is not a function";
    139         if (this._listeners.length === 0)
    140             extensionServer.sendRequest({ command: commands.Subscribe, type: this._type });
    141         this._listeners.push(callback);
    142         extensionServer.registerHandler("notify-" + this._type, this._dispatch.bind(this));
    143     },
    144 
    145     removeListener: function(callback)
    146     {
    147         var listeners = this._listeners;
    148 
    149         for (var i = 0; i < listeners.length; ++i) {
    150             if (listeners[i] === callback) {
    151                 listeners.splice(i, 1);
    152                 break;
    153             }
    154         }
    155         if (this._listeners.length === 0)
    156             extensionServer.sendRequest({ command: commands.Unsubscribe, type: this._type });
    157     },
    158 
    159     _fire: function()
    160     {
    161         var listeners = this._listeners.slice();
    162         for (var i = 0; i < listeners.length; ++i)
    163             listeners[i].apply(null, arguments);
    164     },
    165 
    166     _dispatch: function(request)
    167     {
    168          if (this._customDispatch)
    169              this._customDispatch.call(this, request);
    170          else
    171              this._fire.apply(this, request.arguments);
    172     }
    173 }
    174 
    175 /**
    176  * @constructor
    177  */
    178 function InspectorExtensionAPI()
    179 {
    180     this.audits = new Audits();
    181     this.inspectedWindow = new InspectedWindow();
    182     this.panels = new Panels();
    183     this.network = new Network();
    184     defineDeprecatedProperty(this, "webInspector", "resources", "network");
    185     this.timeline = new Timeline();
    186     this.console = new ConsoleAPI();
    187 }
    188 
    189 /**
    190  * @constructor
    191  */
    192 function ConsoleAPI()
    193 {
    194     this.onMessageAdded = new EventSink(events.ConsoleMessageAdded);
    195 }
    196 
    197 ConsoleAPI.prototype = {
    198     getMessages: function(callback)
    199     {
    200         extensionServer.sendRequest({ command: commands.GetConsoleMessages }, callback);
    201     },
    202 
    203     addMessage: function(severity, text, url, line)
    204     {
    205         extensionServer.sendRequest({ command: commands.AddConsoleMessage, severity: severity, text: text, url: url, line: line });
    206     },
    207 
    208     get Severity()
    209     {
    210         return apiPrivate.console.Severity;
    211     }
    212 }
    213 
    214 /**
    215  * @constructor
    216  */
    217 function Network()
    218 {
    219     function dispatchRequestEvent(message)
    220     {
    221         var request = message.arguments[1];
    222         request.__proto__ = new Request(message.arguments[0]);
    223         this._fire(request);
    224     }
    225     this.onRequestFinished = new EventSink(events.NetworkRequestFinished, dispatchRequestEvent);
    226     defineDeprecatedProperty(this, "network", "onFinished", "onRequestFinished");
    227     this.onNavigated = new EventSink(events.InspectedURLChanged);
    228 }
    229 
    230 Network.prototype = {
    231     getHAR: function(callback)
    232     {
    233         function callbackWrapper(result)
    234         {
    235             var entries = (result && result.entries) || [];
    236             for (var i = 0; i < entries.length; ++i) {
    237                 entries[i].__proto__ = new Request(entries[i]._requestId);
    238                 delete entries[i]._requestId;
    239             }
    240             callback(result);
    241         }
    242         return extensionServer.sendRequest({ command: commands.GetHAR }, callback && callbackWrapper);
    243     },
    244 
    245     addRequestHeaders: function(headers)
    246     {
    247         return extensionServer.sendRequest({ command: commands.AddRequestHeaders, headers: headers, extensionId: window.location.hostname });
    248     }
    249 }
    250 
    251 /**
    252  * @constructor
    253  */
    254 function RequestImpl(id)
    255 {
    256     this._id = id;
    257 }
    258 
    259 RequestImpl.prototype = {
    260     getContent: function(callback)
    261     {
    262         function callbackWrapper(response)
    263         {
    264             callback(response.content, response.encoding);
    265         }
    266         extensionServer.sendRequest({ command: commands.GetRequestContent, id: this._id }, callback && callbackWrapper);
    267     }
    268 }
    269 
    270 /**
    271  * @constructor
    272  */
    273 function Panels()
    274 {
    275     var panels = {
    276         elements: new ElementsPanel()
    277     };
    278 
    279     function panelGetter(name)
    280     {
    281         return panels[name];
    282     }
    283     for (var panel in panels)
    284         this.__defineGetter__(panel, panelGetter.bind(null, panel));
    285 }
    286 
    287 Panels.prototype = {
    288     create: function(title, icon, page, callback)
    289     {
    290         var id = "extension-panel-" + extensionServer.nextObjectId();
    291         var request = {
    292             command: commands.CreatePanel,
    293             id: id,
    294             title: title,
    295             icon: icon,
    296             page: page
    297         };
    298         extensionServer.sendRequest(request, callback && callback.bind(this, new ExtensionPanel(id)));
    299     },
    300 
    301     setOpenResourceHandler: function(callback)
    302     {
    303         var hadHandler = extensionServer.hasHandler(events.OpenResource);
    304 
    305         if (!callback)
    306             extensionServer.unregisterHandler(events.OpenResource);
    307         else {
    308             function callbackWrapper(message)
    309             {
    310                 // Allow the panel to show itself when handling the event.
    311                 userAction = true;
    312                 try {
    313                     callback.call(null, new Resource(message.resource), message.lineNumber);
    314                 } finally {
    315                     userAction = false;
    316                 }
    317             }
    318             extensionServer.registerHandler(events.OpenResource, callbackWrapper);
    319         }
    320         // Only send command if we either removed an existing handler or added handler and had none before.
    321         if (hadHandler === !callback)
    322             extensionServer.sendRequest({ command: commands.SetOpenResourceHandler, "handlerPresent": !!callback });
    323     },
    324 
    325     openResource: function(url, lineNumber, callback)
    326     {
    327         extensionServer.sendRequest({ command: commands.OpenResource, "url": url, "lineNumber": lineNumber }, callback);
    328     },
    329 
    330     get SearchAction()
    331     {
    332         return apiPrivate.panels.SearchAction;
    333     }
    334 }
    335 
    336 /**
    337  * @constructor
    338  */
    339 function ExtensionViewImpl(id)
    340 {
    341     this._id = id;
    342 
    343     function dispatchShowEvent(message)
    344     {
    345         var frameIndex = message.arguments[0];
    346         this._fire(window.parent.frames[frameIndex]);
    347     }
    348     this.onShown = new EventSink(events.ViewShown + id, dispatchShowEvent);
    349     this.onHidden = new EventSink(events.ViewHidden + id);
    350 }
    351 
    352 /**
    353  * @constructor
    354  */
    355 function PanelWithSidebarImpl(id)
    356 {
    357     this._id = id;
    358 }
    359 
    360 PanelWithSidebarImpl.prototype = {
    361     createSidebarPane: function(title, callback)
    362     {
    363         var id = "extension-sidebar-" + extensionServer.nextObjectId();
    364         var request = {
    365             command: commands.CreateSidebarPane,
    366             panel: this._id,
    367             id: id,
    368             title: title
    369         };
    370         function callbackWrapper()
    371         {
    372             callback(new ExtensionSidebarPane(id));
    373         }
    374         extensionServer.sendRequest(request, callback && callbackWrapper);
    375     },
    376 
    377     __proto__: ExtensionViewImpl.prototype
    378 }
    379 
    380 /**
    381  * @constructor
    382  * @extends {PanelWithSidebar}
    383  */
    384 function ElementsPanel()
    385 {
    386     var id = "elements";
    387     PanelWithSidebar.call(this, id);
    388     this.onSelectionChanged = new EventSink(events.ElementsPanelObjectSelected);
    389 }
    390 
    391 /**
    392  * @constructor
    393  * @extends {ExtensionViewImpl}
    394  */
    395 function ExtensionPanelImpl(id)
    396 {
    397     ExtensionViewImpl.call(this, id);
    398     this.onSearch = new EventSink(events.PanelSearch + id);
    399 }
    400 
    401 ExtensionPanelImpl.prototype = {
    402     createStatusBarButton: function(iconPath, tooltipText, disabled)
    403     {
    404         var id = "button-" + extensionServer.nextObjectId();
    405         var request = {
    406             command: commands.CreateStatusBarButton,
    407             panel: this._id,
    408             id: id,
    409             icon: iconPath,
    410             tooltip: tooltipText,
    411             disabled: !!disabled
    412         };
    413         extensionServer.sendRequest(request);
    414         return new Button(id);
    415     },
    416 
    417     show: function()
    418     {
    419         if (!userAction)
    420             return;
    421 
    422         var request = {
    423             command: commands.ShowPanel,
    424             id: this._id
    425         };
    426         extensionServer.sendRequest(request);
    427     },
    428 
    429     __proto__: ExtensionViewImpl.prototype
    430 }
    431 
    432 /**
    433  * @constructor
    434  * @extends {ExtensionViewImpl}
    435  */
    436 function ExtensionSidebarPaneImpl(id)
    437 {
    438     ExtensionViewImpl.call(this, id);
    439 }
    440 
    441 ExtensionSidebarPaneImpl.prototype = {
    442     setHeight: function(height)
    443     {
    444         extensionServer.sendRequest({ command: commands.SetSidebarHeight, id: this._id, height: height });
    445     },
    446 
    447     setExpression: function(expression, rootTitle, evaluateOptions)
    448     {
    449         var request = {
    450             command: commands.SetSidebarContent,
    451             id: this._id,
    452             expression: expression,
    453             rootTitle: rootTitle,
    454             evaluateOnPage: true,
    455         };
    456         if (typeof evaluateOptions === "object")
    457             request.evaluateOptions = evaluateOptions;
    458         extensionServer.sendRequest(request, extractCallbackArgument(arguments));
    459     },
    460 
    461     setObject: function(jsonObject, rootTitle, callback)
    462     {
    463         extensionServer.sendRequest({ command: commands.SetSidebarContent, id: this._id, expression: jsonObject, rootTitle: rootTitle }, callback);
    464     },
    465 
    466     setPage: function(page)
    467     {
    468         extensionServer.sendRequest({ command: commands.SetSidebarPage, id: this._id, page: page });
    469     }
    470 }
    471 
    472 /**
    473  * @constructor
    474  */
    475 function ButtonImpl(id)
    476 {
    477     this._id = id;
    478     this.onClicked = new EventSink(events.ButtonClicked + id);
    479 }
    480 
    481 ButtonImpl.prototype = {
    482     update: function(iconPath, tooltipText, disabled)
    483     {
    484         var request = {
    485             command: commands.UpdateButton,
    486             id: this._id,
    487             icon: iconPath,
    488             tooltip: tooltipText,
    489             disabled: !!disabled
    490         };
    491         extensionServer.sendRequest(request);
    492     }
    493 };
    494 
    495 /**
    496  * @constructor
    497  */
    498 function Audits()
    499 {
    500 }
    501 
    502 Audits.prototype = {
    503     addCategory: function(displayName, resultCount)
    504     {
    505         var id = "extension-audit-category-" + extensionServer.nextObjectId();
    506         if (typeof resultCount !== "undefined")
    507             console.warn("Passing resultCount to audits.addCategory() is deprecated. Use AuditResult.updateProgress() instead.");
    508         extensionServer.sendRequest({ command: commands.AddAuditCategory, id: id, displayName: displayName, resultCount: resultCount });
    509         return new AuditCategory(id);
    510     }
    511 }
    512 
    513 /**
    514  * @constructor
    515  */
    516 function AuditCategoryImpl(id)
    517 {
    518     function dispatchAuditEvent(request)
    519     {
    520         var auditResult = new AuditResult(request.arguments[0]);
    521         try {
    522             this._fire(auditResult);
    523         } catch (e) {
    524             console.error("Uncaught exception in extension audit event handler: " + e);
    525             auditResult.done();
    526         }
    527     }
    528     this._id = id;
    529     this.onAuditStarted = new EventSink(events.AuditStarted + id, dispatchAuditEvent);
    530 }
    531 
    532 /**
    533  * @constructor
    534  */
    535 function AuditResultImpl(id)
    536 {
    537     this._id = id;
    538 
    539     this.createURL = this._nodeFactory.bind(null, "url");
    540     this.createSnippet = this._nodeFactory.bind(null, "snippet");
    541     this.createText = this._nodeFactory.bind(null, "text");
    542     this.createObject = this._nodeFactory.bind(null, "object");
    543     this.createNode = this._nodeFactory.bind(null, "node");
    544 }
    545 
    546 AuditResultImpl.prototype = {
    547     addResult: function(displayName, description, severity, details)
    548     {
    549         // shorthand for specifying details directly in addResult().
    550         if (details && !(details instanceof AuditResultNode))
    551             details = new AuditResultNode(details instanceof Array ? details : [details]);
    552 
    553         var request = {
    554             command: commands.AddAuditResult,
    555             resultId: this._id,
    556             displayName: displayName,
    557             description: description,
    558             severity: severity,
    559             details: details
    560         };
    561         extensionServer.sendRequest(request);
    562     },
    563 
    564     createResult: function()
    565     {
    566         return new AuditResultNode(Array.prototype.slice.call(arguments));
    567     },
    568 
    569     updateProgress: function(worked, totalWork)
    570     {
    571         extensionServer.sendRequest({ command: commands.UpdateAuditProgress, resultId: this._id, progress: worked / totalWork });
    572     },
    573 
    574     done: function()
    575     {
    576         extensionServer.sendRequest({ command: commands.StopAuditCategoryRun, resultId: this._id });
    577     },
    578 
    579     get Severity()
    580     {
    581         return apiPrivate.audits.Severity;
    582     },
    583 
    584     createResourceLink: function(url, lineNumber)
    585     {
    586         return {
    587             type: "resourceLink",
    588             arguments: [url, lineNumber && lineNumber - 1]
    589         };
    590     },
    591 
    592     _nodeFactory: function(type)
    593     {
    594         return {
    595             type: type,
    596             arguments: Array.prototype.slice.call(arguments, 1)
    597         };
    598     }
    599 }
    600 
    601 /**
    602  * @constructor
    603  */
    604 function AuditResultNode(contents)
    605 {
    606     this.contents = contents;
    607     this.children = [];
    608     this.expanded = false;
    609 }
    610 
    611 AuditResultNode.prototype = {
    612     addChild: function()
    613     {
    614         var node = new AuditResultNode(Array.prototype.slice.call(arguments));
    615         this.children.push(node);
    616         return node;
    617     }
    618 };
    619 
    620 /**
    621  * @constructor
    622  */
    623 function InspectedWindow()
    624 {
    625     function dispatchResourceEvent(message)
    626     {
    627         this._fire(new Resource(message.arguments[0]));
    628     }
    629     function dispatchResourceContentEvent(message)
    630     {
    631         this._fire(new Resource(message.arguments[0]), message.arguments[1]);
    632     }
    633     this.onResourceAdded = new EventSink(events.ResourceAdded, dispatchResourceEvent);
    634     this.onResourceContentCommitted = new EventSink(events.ResourceContentCommitted, dispatchResourceContentEvent);
    635 }
    636 
    637 InspectedWindow.prototype = {
    638     reload: function(optionsOrUserAgent)
    639     {
    640         var options = null;
    641         if (typeof optionsOrUserAgent === "object")
    642             options = optionsOrUserAgent;
    643         else if (typeof optionsOrUserAgent === "string") {
    644             options = { userAgent: optionsOrUserAgent };
    645             console.warn("Passing userAgent as string parameter to inspectedWindow.reload() is deprecated. " +
    646                          "Use inspectedWindow.reload({ userAgent: value}) instead.");
    647         }
    648         return extensionServer.sendRequest({ command: commands.Reload, options: options });
    649     },
    650 
    651     eval: function(expression, evaluateOptions)
    652     {
    653         var callback = extractCallbackArgument(arguments);
    654         function callbackWrapper(result)
    655         {
    656             if (result.isError || result.isException)
    657                 callback(undefined, result);
    658             else
    659                 callback(result.value);
    660         }
    661         var request = {
    662             command: commands.EvaluateOnInspectedPage,
    663             expression: expression
    664         };
    665         if (typeof evaluateOptions === "object")
    666             request.evaluateOptions = evaluateOptions;
    667         return extensionServer.sendRequest(request, callback && callbackWrapper);
    668     },
    669 
    670     getResources: function(callback)
    671     {
    672         function wrapResource(resourceData)
    673         {
    674             return new Resource(resourceData);
    675         }
    676         function callbackWrapper(resources)
    677         {
    678             callback(resources.map(wrapResource));
    679         }
    680         return extensionServer.sendRequest({ command: commands.GetPageResources }, callback && callbackWrapper);
    681     }
    682 }
    683 
    684 /**
    685  * @constructor
    686  */
    687 function ResourceImpl(resourceData)
    688 {
    689     this._url = resourceData.url
    690     this._type = resourceData.type;
    691 }
    692 
    693 ResourceImpl.prototype = {
    694     get url()
    695     {
    696         return this._url;
    697     },
    698 
    699     get type()
    700     {
    701         return this._type;
    702     },
    703 
    704     getContent: function(callback)
    705     {
    706         function callbackWrapper(response)
    707         {
    708             callback(response.content, response.encoding);
    709         }
    710 
    711         return extensionServer.sendRequest({ command: commands.GetResourceContent, url: this._url }, callback && callbackWrapper);
    712     },
    713 
    714     setContent: function(content, commit, callback)
    715     {
    716         return extensionServer.sendRequest({ command: commands.SetResourceContent, url: this._url, content: content, commit: commit }, callback);
    717     }
    718 }
    719 
    720 /**
    721  * @constructor
    722  */
    723 function TimelineImpl()
    724 {
    725     this.onEventRecorded = new EventSink(events.TimelineEventRecorded);
    726 }
    727 
    728 function forwardKeyboardEvent(event)
    729 {
    730     const Esc = "U+001B";
    731     // We only care about global hotkeys, not about random text
    732     if (!event.ctrlKey && !event.altKey && !event.metaKey && !/^F\d+$/.test(event.keyIdentifier) && event.keyIdentifier !== Esc)
    733         return;
    734     var request = {
    735         command: commands.ForwardKeyboardEvent,
    736         eventType: event.type,
    737         ctrlKey: event.ctrlKey,
    738         altKey: event.altKey,
    739         metaKey: event.metaKey,
    740         keyIdentifier: event.keyIdentifier,
    741         location: event.location
    742     };
    743     extensionServer.sendRequest(request);
    744 }
    745 
    746 document.addEventListener("keydown", forwardKeyboardEvent, false);
    747 document.addEventListener("keypress", forwardKeyboardEvent, false);
    748 
    749 /**
    750  * @constructor
    751  */
    752 function ExtensionServerClient()
    753 {
    754     this._callbacks = {};
    755     this._handlers = {};
    756     this._lastRequestId = 0;
    757     this._lastObjectId = 0;
    758 
    759     this.registerHandler("callback", this._onCallback.bind(this));
    760 
    761     var channel = new MessageChannel();
    762     this._port = channel.port1;
    763     this._port.addEventListener("message", this._onMessage.bind(this), false);
    764     this._port.start();
    765 
    766     window.parent.postMessage("registerExtension", [ channel.port2 ], "*");
    767 }
    768 
    769 ExtensionServerClient.prototype = {
    770     /**
    771      * @param {function()=} callback
    772      */
    773     sendRequest: function(message, callback)
    774     {
    775         if (typeof callback === "function")
    776             message.requestId = this._registerCallback(callback);
    777         return this._port.postMessage(message);
    778     },
    779 
    780     hasHandler: function(command)
    781     {
    782         return !!this._handlers[command];
    783     },
    784 
    785     registerHandler: function(command, handler)
    786     {
    787         this._handlers[command] = handler;
    788     },
    789 
    790     unregisterHandler: function(command)
    791     {
    792         delete this._handlers[command];
    793     },
    794 
    795     nextObjectId: function()
    796     {
    797         return injectedScriptId + "_" + ++this._lastObjectId;
    798     },
    799 
    800     _registerCallback: function(callback)
    801     {
    802         var id = ++this._lastRequestId;
    803         this._callbacks[id] = callback;
    804         return id;
    805     },
    806 
    807     _onCallback: function(request)
    808     {
    809         if (request.requestId in this._callbacks) {
    810             var callback = this._callbacks[request.requestId];
    811             delete this._callbacks[request.requestId];
    812             callback(request.result);
    813         }
    814     },
    815 
    816     _onMessage: function(event)
    817     {
    818         var request = event.data;
    819         var handler = this._handlers[request.command];
    820         if (handler)
    821             handler.call(this, request);
    822     }
    823 }
    824 
    825 function populateInterfaceClass(interface, implementation)
    826 {
    827     for (var member in implementation) {
    828         if (member.charAt(0) === "_")
    829             continue;
    830         var descriptor = null;
    831         // Traverse prototype chain until we find the owner.
    832         for (var owner = implementation; owner && !descriptor; owner = owner.__proto__)
    833             descriptor = Object.getOwnPropertyDescriptor(owner, member);
    834         if (!descriptor)
    835             continue;
    836         if (typeof descriptor.value === "function")
    837             interface[member] = descriptor.value.bind(implementation);
    838         else if (typeof descriptor.get === "function")
    839             interface.__defineGetter__(member, descriptor.get.bind(implementation));
    840         else
    841             Object.defineProperty(interface, member, descriptor);
    842     }
    843 }
    844 
    845 function declareInterfaceClass(implConstructor)
    846 {
    847     return function()
    848     {
    849         var impl = { __proto__: implConstructor.prototype };
    850         implConstructor.apply(impl, arguments);
    851         populateInterfaceClass(this, impl);
    852     }
    853 }
    854 
    855 function defineDeprecatedProperty(object, className, oldName, newName)
    856 {
    857     var warningGiven = false;
    858     function getter()
    859     {
    860         if (!warningGiven) {
    861             console.warn(className + "." + oldName + " is deprecated. Use " + className + "." + newName + " instead");
    862             warningGiven = true;
    863         }
    864         return object[newName];
    865     }
    866     object.__defineGetter__(oldName, getter);
    867 }
    868 
    869 function extractCallbackArgument(args)
    870 {
    871     var lastArgument = args[args.length - 1];
    872     return typeof lastArgument === "function" ? lastArgument : undefined;
    873 }
    874 
    875 var AuditCategory = declareInterfaceClass(AuditCategoryImpl);
    876 var AuditResult = declareInterfaceClass(AuditResultImpl);
    877 var Button = declareInterfaceClass(ButtonImpl);
    878 var EventSink = declareInterfaceClass(EventSinkImpl);
    879 var ExtensionPanel = declareInterfaceClass(ExtensionPanelImpl);
    880 var ExtensionSidebarPane = declareInterfaceClass(ExtensionSidebarPaneImpl);
    881 var PanelWithSidebar = declareInterfaceClass(PanelWithSidebarImpl);
    882 var Request = declareInterfaceClass(RequestImpl);
    883 var Resource = declareInterfaceClass(ResourceImpl);
    884 var Timeline = declareInterfaceClass(TimelineImpl);
    885 
    886 // extensionServer is a closure variable defined by the glue below -- make sure we fail if it's not there.
    887 if (!extensionServer)
    888     extensionServer = new ExtensionServerClient();
    889 
    890 return new InspectorExtensionAPI();
    891 }
    892 
    893 /**
    894  * @param {ExtensionDescriptor} extensionInfo
    895  * @return {string}
    896  */
    897 function buildExtensionAPIInjectedScript(extensionInfo)
    898 {
    899     return "(function(injectedScriptId){ " +
    900         "var extensionServer;" +
    901         defineCommonExtensionSymbols.toString() + ";" +
    902         injectedExtensionAPI.toString() + ";" +
    903         buildPlatformExtensionAPI(extensionInfo) + ";" +
    904         "platformExtensionAPI(injectedExtensionAPI(injectedScriptId));" +
    905         "return {};" +
    906         "})";
    907 }
    908