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.ExtensionServer = function()
     32 {
     33     this._clientObjects = {};
     34     this._handlers = {};
     35     this._subscribers = {};
     36     this._extraHeaders = {};
     37     this._resources = {};
     38     this._lastResourceId = 0;
     39     this._status = new WebInspector.ExtensionStatus();
     40 
     41     this._registerHandler("addRequestHeaders", this._onAddRequestHeaders.bind(this));
     42     this._registerHandler("addAuditCategory", this._onAddAuditCategory.bind(this));
     43     this._registerHandler("addAuditResult", this._onAddAuditResult.bind(this));
     44     this._registerHandler("createPanel", this._onCreatePanel.bind(this));
     45     this._registerHandler("createSidebarPane", this._onCreateSidebarPane.bind(this));
     46     this._registerHandler("evaluateOnInspectedPage", this._onEvaluateOnInspectedPage.bind(this));
     47     this._registerHandler("getHAR", this._onGetHAR.bind(this));
     48     this._registerHandler("getResourceContent", this._onGetResourceContent.bind(this));
     49     this._registerHandler("log", this._onLog.bind(this));
     50     this._registerHandler("reload", this._onReload.bind(this));
     51     this._registerHandler("setSidebarHeight", this._onSetSidebarHeight.bind(this));
     52     this._registerHandler("setSidebarContent", this._onSetSidebarContent.bind(this));
     53     this._registerHandler("setSidebarPage", this._onSetSidebarPage.bind(this));
     54     this._registerHandler("stopAuditCategoryRun", this._onStopAuditCategoryRun.bind(this));
     55     this._registerHandler("subscribe", this._onSubscribe.bind(this));
     56     this._registerHandler("unsubscribe", this._onUnsubscribe.bind(this));
     57 
     58     window.addEventListener("message", this._onWindowMessage.bind(this), false);
     59 }
     60 
     61 WebInspector.ExtensionServer.prototype = {
     62     notifyObjectSelected: function(panelId, objectId)
     63     {
     64         this._postNotification("panel-objectSelected-" + panelId, objectId);
     65     },
     66 
     67     notifySearchAction: function(panelId, action, searchString)
     68     {
     69         this._postNotification("panel-search-" + panelId, action, searchString);
     70     },
     71 
     72     notifyPanelShown: function(panelId)
     73     {
     74         this._postNotification("panel-shown-" + panelId);
     75     },
     76 
     77     notifyPanelHidden: function(panelId)
     78     {
     79         this._postNotification("panel-hidden-" + panelId);
     80     },
     81 
     82     notifyInspectedURLChanged: function(url)
     83     {
     84         this._postNotification("inspectedURLChanged", url);
     85     },
     86 
     87     notifyInspectorReset: function()
     88     {
     89         this._postNotification("reset");
     90     },
     91 
     92     notifyExtensionSidebarUpdated: function(id)
     93     {
     94         this._postNotification("sidebar-updated-" + id);
     95     },
     96 
     97     startAuditRun: function(category, auditRun)
     98     {
     99         this._clientObjects[auditRun.id] = auditRun;
    100         this._postNotification("audit-started-" + category.id, auditRun.id);
    101     },
    102 
    103     stopAuditRun: function(auditRun)
    104     {
    105         delete this._clientObjects[auditRun.id];
    106     },
    107 
    108     resetResources: function()
    109     {
    110         this._resources = {};
    111     },
    112 
    113     _notifyResourceFinished: function(event)
    114     {
    115         var resource = event.data;
    116         if (this._hasSubscribers("resource-finished"))
    117             this._postNotification("resource-finished", this._resourceId(resource), (new WebInspector.HAREntry(resource)).build());
    118     },
    119 
    120     _hasSubscribers: function(type)
    121     {
    122         return !!this._subscribers[type];
    123     },
    124 
    125     _postNotification: function(type, details)
    126     {
    127         var subscribers = this._subscribers[type];
    128         if (!subscribers)
    129             return;
    130         var message = {
    131             command: "notify-" + type,
    132             arguments: Array.prototype.slice.call(arguments, 1)
    133         };
    134         for (var i = 0; i < subscribers.length; ++i)
    135             subscribers[i].postMessage(message);
    136     },
    137 
    138     _onSubscribe: function(message, port)
    139     {
    140         var subscribers = this._subscribers[message.type];
    141         if (subscribers)
    142             subscribers.push(port);
    143         else
    144             this._subscribers[message.type] = [ port ];
    145     },
    146 
    147     _onUnsubscribe: function(message, port)
    148     {
    149         var subscribers = this._subscribers[message.type];
    150         if (!subscribers)
    151             return;
    152         subscribers.remove(port);
    153         if (!subscribers.length)
    154             delete this._subscribers[message.type];
    155     },
    156 
    157     _onAddRequestHeaders: function(message)
    158     {
    159         var id = message.extensionId;
    160         if (typeof id !== "string")
    161             return this._status.E_BADARGTYPE("extensionId", typeof id, "string");
    162         var extensionHeaders = this._extraHeaders[id];
    163         if (!extensionHeaders) {
    164             extensionHeaders = {};
    165             this._extraHeaders[id] = extensionHeaders;
    166         }
    167         for (name in message.headers)
    168             extensionHeaders[name] = message.headers[name];
    169         var allHeaders = {};
    170         for (extension in this._extraHeaders) {
    171             var headers = this._extraHeaders[extension];
    172             for (name in headers) {
    173                 if (typeof headers[name] === "string")
    174                     allHeaders[name] = headers[name];
    175             }
    176         }
    177         NetworkAgent.setExtraHeaders(allHeaders);
    178     },
    179 
    180     _onCreatePanel: function(message, port)
    181     {
    182         var id = message.id;
    183         // The ids are generated on the client API side and must be unique, so the check below
    184         // shouldn't be hit unless someone is bypassing the API.
    185         if (id in this._clientObjects || id in WebInspector.panels)
    186             return this._status.E_EXISTS(id);
    187 
    188         var panel = new WebInspector.ExtensionPanel(id, message.title, message.icon);
    189         this._clientObjects[id] = panel;
    190         WebInspector.panels[id] = panel;
    191         WebInspector.addPanel(panel);
    192 
    193         var iframe = this.createClientIframe(panel.element, message.url);
    194         iframe.style.height = "100%";
    195         return this._status.OK();
    196     },
    197 
    198     _onCreateSidebarPane: function(message, constructor)
    199     {
    200         var panel = WebInspector.panels[message.panel];
    201         if (!panel)
    202             return this._status.E_NOTFOUND(message.panel);
    203         if (!panel.sidebarElement || !panel.sidebarPanes)
    204             return this._status.E_NOTSUPPORTED();
    205         var id = message.id;
    206         var sidebar = new WebInspector.ExtensionSidebarPane(message.title, message.id);
    207         this._clientObjects[id] = sidebar;
    208         panel.sidebarPanes[id] = sidebar;
    209         panel.sidebarElement.appendChild(sidebar.element);
    210 
    211         return this._status.OK();
    212     },
    213 
    214     createClientIframe: function(parent, url)
    215     {
    216         var iframe = document.createElement("iframe");
    217         iframe.src = url;
    218         iframe.style.width = "100%";
    219         parent.appendChild(iframe);
    220         return iframe;
    221     },
    222 
    223     _onSetSidebarHeight: function(message)
    224     {
    225         var sidebar = this._clientObjects[message.id];
    226         if (!sidebar)
    227             return this._status.E_NOTFOUND(message.id);
    228         sidebar.bodyElement.firstChild.style.height = message.height;
    229     },
    230 
    231     _onSetSidebarContent: function(message)
    232     {
    233         var sidebar = this._clientObjects[message.id];
    234         if (!sidebar)
    235             return this._status.E_NOTFOUND(message.id);
    236         if (message.evaluateOnPage)
    237             sidebar.setExpression(message.expression, message.rootTitle);
    238         else
    239             sidebar.setObject(message.expression, message.rootTitle);
    240     },
    241 
    242     _onSetSidebarPage: function(message)
    243     {
    244         var sidebar = this._clientObjects[message.id];
    245         if (!sidebar)
    246             return this._status.E_NOTFOUND(message.id);
    247         sidebar.setPage(message.url);
    248     },
    249 
    250     _onLog: function(message)
    251     {
    252         WebInspector.log(message.message);
    253     },
    254 
    255     _onReload: function(message)
    256     {
    257         if (typeof message.userAgent === "string")
    258             PageAgent.setUserAgentOverride(message.userAgent);
    259 
    260         PageAgent.reloadPage(false);
    261         return this._status.OK();
    262     },
    263 
    264     _onEvaluateOnInspectedPage: function(message, port)
    265     {
    266         function callback(error, resultPayload)
    267         {
    268             if (error)
    269                 return;
    270             var resultObject = WebInspector.RemoteObject.fromPayload(resultPayload);
    271             var result = {};
    272             if (resultObject.isError())
    273                 result.isException = true;
    274             result.value = resultObject.description;
    275             this._dispatchCallback(message.requestId, port, result);
    276         }
    277         var evalExpression = "JSON.stringify(eval(unescape('" + escape(message.expression) + "')));";
    278         RuntimeAgent.evaluate(evalExpression, "", true, callback.bind(this));
    279     },
    280 
    281     _onRevealAndSelect: function(message)
    282     {
    283         if (message.panelId === "resources" && type === "resource")
    284             return this._onRevealAndSelectResource(message);
    285         else
    286             return this._status.E_NOTSUPPORTED(message.panelId, message.type);
    287     },
    288 
    289     _onRevealAndSelectResource: function(message)
    290     {
    291         var id = message.id;
    292         var resource = null;
    293 
    294         resource = this._resourceById(id) || WebInspector.resourceForURL(id);
    295         if (!resource)
    296             return this._status.E_NOTFOUND(typeof id + ": " + id);
    297 
    298         WebInspector.panels.resources.showResource(resource, message.line);
    299         WebInspector.showPanel("resources");
    300     },
    301 
    302     _dispatchCallback: function(requestId, port, result)
    303     {
    304         port.postMessage({ command: "callback", requestId: requestId, result: result });
    305     },
    306 
    307     _onGetHAR: function(request)
    308     {
    309         var harLog = (new WebInspector.HARLog()).build();
    310         for (var i = 0; i < harLog.entries.length; ++i)
    311             harLog.entries[i]._resourceId = this._resourceId(WebInspector.networkResources[i]);
    312         return harLog;
    313     },
    314 
    315     _onGetResourceContent: function(message, port)
    316     {
    317         function onContentAvailable(content, encoded)
    318         {
    319             var response = {
    320                 encoding: encoded ? "base64" : "",
    321                 content: content
    322             };
    323             this._dispatchCallback(message.requestId, port, response);
    324         }
    325         var resource = this._resourceById(message.id);
    326         if (!resource)
    327             return this._status.E_NOTFOUND(message.id);
    328         resource.requestContent(onContentAvailable.bind(this));
    329     },
    330 
    331     _resourceId: function(resource)
    332     {
    333         if (!resource._extensionResourceId) {
    334             resource._extensionResourceId = ++this._lastResourceId;
    335             this._resources[resource._extensionResourceId] = resource;
    336         }
    337         return resource._extensionResourceId;
    338     },
    339 
    340     _resourceById: function(id)
    341     {
    342         return this._resources[id];
    343     },
    344 
    345     _onAddAuditCategory: function(request)
    346     {
    347         var category = new WebInspector.ExtensionAuditCategory(request.id, request.displayName, request.resultCount);
    348         if (WebInspector.panels.audits.getCategory(category.id))
    349             return this._status.E_EXISTS(category.id);
    350         this._clientObjects[request.id] = category;
    351         WebInspector.panels.audits.addCategory(category);
    352     },
    353 
    354     _onAddAuditResult: function(request)
    355     {
    356         var auditResult = this._clientObjects[request.resultId];
    357         if (!auditResult)
    358             return this._status.E_NOTFOUND(request.resultId);
    359         try {
    360             auditResult.addResult(request.displayName, request.description, request.severity, request.details);
    361         } catch (e) {
    362             return e;
    363         }
    364         return this._status.OK();
    365     },
    366 
    367     _onStopAuditCategoryRun: function(request)
    368     {
    369         var auditRun = this._clientObjects[request.resultId];
    370         if (!auditRun)
    371             return this._status.E_NOTFOUND(request.resultId);
    372         auditRun.cancel();
    373     },
    374 
    375     initExtensions: function()
    376     {
    377         // The networkManager is normally created after the ExtensionServer is constructed, but before initExtensions() is called.
    378         WebInspector.networkManager.addEventListener(WebInspector.NetworkManager.EventTypes.ResourceFinished, this._notifyResourceFinished, this);
    379 
    380         InspectorExtensionRegistry.getExtensionsAsync();
    381     },
    382 
    383     _addExtensions: function(extensions)
    384     {
    385         // See ExtensionAPI.js and ExtensionCommon.js for details.
    386         InspectorFrontendHost.setExtensionAPI(this._buildExtensionAPIInjectedScript());
    387         for (var i = 0; i < extensions.length; ++i) {
    388             var extension = extensions[i];
    389             try {
    390                 if (!extension.startPage)
    391                     return;
    392                 var iframe = document.createElement("iframe");
    393                 iframe.src = extension.startPage;
    394                 iframe.style.display = "none";
    395                 document.body.appendChild(iframe);
    396             } catch (e) {
    397                 console.error("Failed to initialize extension " + extension.startPage + ":" + e);
    398             }
    399         }
    400     },
    401 
    402     _buildExtensionAPIInjectedScript: function()
    403     {
    404         var resourceTypes = {};
    405         var resourceTypeProperties = Object.getOwnPropertyNames(WebInspector.Resource.Type);
    406         for (var i = 0; i < resourceTypeProperties.length; ++i) {
    407              var propName = resourceTypeProperties[i];
    408              var propValue = WebInspector.Resource.Type[propName];
    409              if (typeof propValue === "number")
    410                  resourceTypes[propName] = WebInspector.Resource.Type.toString(propValue);
    411         }
    412         var platformAPI = WebInspector.buildPlatformExtensionAPI ? WebInspector.buildPlatformExtensionAPI() : "";
    413         return "(function(){ " +
    414             "var apiPrivate = {};" +
    415             "(" + WebInspector.commonExtensionSymbols.toString() + ")(apiPrivate);" +
    416             "(" + WebInspector.injectedExtensionAPI.toString() + ").apply(this, arguments);" +
    417             platformAPI +
    418             "})";
    419     },
    420 
    421     _onWindowMessage: function(event)
    422     {
    423         if (event.data !== "registerExtension")
    424             return;
    425         var port = event.ports[0];
    426         port.addEventListener("message", this._onmessage.bind(this), false);
    427         port.start();
    428     },
    429 
    430     _onmessage: function(event)
    431     {
    432         var request = event.data;
    433         var result;
    434 
    435         if (request.command in this._handlers)
    436             result = this._handlers[request.command](request, event.target);
    437         else
    438             result = this._status.E_NOTSUPPORTED(request.command);
    439 
    440         if (result && request.requestId)
    441             this._dispatchCallback(request.requestId, event.target, result);
    442     },
    443 
    444     _registerHandler: function(command, callback)
    445     {
    446         this._handlers[command] = callback;
    447     }
    448 }
    449 
    450 WebInspector.ExtensionServer._statuses =
    451 {
    452     OK: "",
    453     E_EXISTS: "Object already exists: %s",
    454     E_BADARG: "Invalid argument %s: %s",
    455     E_BADARGTYPE: "Invalid type for argument %s: got %s, expected %s",
    456     E_NOTFOUND: "Object not found: %s",
    457     E_NOTSUPPORTED: "Object does not support requested operation: %s",
    458 }
    459 
    460 WebInspector.ExtensionStatus = function()
    461 {
    462     function makeStatus(code)
    463     {
    464         var description = WebInspector.ExtensionServer._statuses[code] || code;
    465         var details = Array.prototype.slice.call(arguments, 1);
    466         var status = { code: code, description: description, details: details };
    467         if (code !== "OK") {
    468             status.isError = true;
    469             console.log("Extension server error: " + String.vsprintf(description, details));
    470         }
    471         return status;
    472     }
    473     for (status in WebInspector.ExtensionServer._statuses)
    474         this[status] = makeStatus.bind(null, status);
    475 }
    476 
    477 WebInspector.addExtensions = function(extensions)
    478 {
    479     WebInspector.extensionServer._addExtensions(extensions);
    480 }
    481 
    482 WebInspector.extensionServer = new WebInspector.ExtensionServer();
    483