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