ExtensionAPI.js revision 2bde8e466a4451c7319e3a072d118917957d6554
1/* 2 * Copyright (C) 2010 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} 115 116Resources.prototype = { 117 getHAR: function(callback) 118 { 119 function callbackWrapper(result) 120 { 121 var entries = (result && result.entries) || []; 122 for (var i = 0; i < entries.length; ++i) { 123 entries[i].__proto__ = new Resource(entries[i]._resourceId); 124 delete entries[i]._resourceId; 125 } 126 callback(result); 127 } 128 return extensionServer.sendRequest({ command: "getHAR" }, callback && callbackWrapper); 129 }, 130 131 addRequestHeaders: function(headers) 132 { 133 return extensionServer.sendRequest({ command: "addRequestHeaders", headers: headers, extensionId: location.hostname }); 134 } 135} 136 137function ResourceImpl(id) 138{ 139 this._id = id; 140} 141 142ResourceImpl.prototype = { 143 getContent: function(callback) 144 { 145 function callbackWrapper(response) 146 { 147 callback(response.content, response.encoding); 148 } 149 extensionServer.sendRequest({ command: "getResourceContent", id: this._id }, callback && callbackWrapper); 150 } 151}; 152 153function Panels() 154{ 155 var panels = { 156 elements: new ElementsPanel() 157 }; 158 159 function panelGetter(name) 160 { 161 return panels[name]; 162 } 163 for (var panel in panels) 164 this.__defineGetter__(panel, bind(panelGetter, null, panel)); 165} 166 167Panels.prototype = { 168 create: function(title, iconURL, pageURL, callback) 169 { 170 var id = "extension-panel-" + extensionServer.nextObjectId(); 171 var request = { 172 command: "createPanel", 173 id: id, 174 title: title, 175 icon: expandURL(iconURL), 176 url: expandURL(pageURL) 177 }; 178 extensionServer.sendRequest(request, callback && bind(callback, this, new ExtensionPanel(id))); 179 } 180} 181 182function PanelImpl(id) 183{ 184 this._id = id; 185} 186 187function PanelWithSidebarImpl(id) 188{ 189 PanelImpl.call(this, id); 190} 191 192PanelWithSidebarImpl.prototype = { 193 createSidebarPane: function(title, url, callback) 194 { 195 var id = "extension-sidebar-" + extensionServer.nextObjectId(); 196 var request = { 197 command: "createSidebarPane", 198 panel: this._id, 199 id: id, 200 title: title, 201 url: expandURL(url) 202 }; 203 function callbackWrapper() 204 { 205 callback(new ExtensionSidebarPane(id)); 206 } 207 extensionServer.sendRequest(request, callback && callbackWrapper); 208 }, 209 210 createWatchExpressionSidebarPane: function(title, callback) 211 { 212 var id = "watch-sidebar-" + extensionServer.nextObjectId(); 213 var request = { 214 command: "createWatchExpressionSidebarPane", 215 panel: this._id, 216 id: id, 217 title: title 218 }; 219 function callbackWrapper() 220 { 221 callback(new WatchExpressionSidebarPane(id)); 222 } 223 extensionServer.sendRequest(request, callback && callbackWrapper); 224 } 225} 226 227PanelWithSidebarImpl.prototype.__proto__ = PanelImpl.prototype; 228 229function ElementsPanel() 230{ 231 var id = "elements"; 232 PanelWithSidebar.call(this, id); 233 this.onSelectionChanged = new EventSink("panel-objectSelected-" + id); 234} 235 236function ExtensionPanel(id) 237{ 238 Panel.call(this, id); 239 this.onSearch = new EventSink("panel-search-" + id); 240} 241 242function ExtensionSidebarPaneImpl(id) 243{ 244 this._id = id; 245} 246 247ExtensionSidebarPaneImpl.prototype = { 248 setHeight: function(height) 249 { 250 extensionServer.sendRequest({ command: "setSidebarHeight", id: this._id, height: height }); 251 } 252} 253 254function WatchExpressionSidebarPaneImpl(id) 255{ 256 ExtensionSidebarPaneImpl.call(this, id); 257 this.onUpdated = new EventSink("watch-sidebar-updated-" + id); 258} 259 260WatchExpressionSidebarPaneImpl.prototype = { 261 setExpression: function(expression, rootTitle) 262 { 263 extensionServer.sendRequest({ command: "setWatchSidebarContent", id: this._id, expression: expression, rootTitle: rootTitle, evaluateOnPage: true }); 264 }, 265 266 setObject: function(jsonObject, rootTitle) 267 { 268 extensionServer.sendRequest({ command: "setWatchSidebarContent", id: this._id, expression: jsonObject, rootTitle: rootTitle }); 269 } 270} 271 272WatchExpressionSidebarPaneImpl.prototype.__proto__ = ExtensionSidebarPaneImpl.prototype; 273 274function WatchExpressionSidebarPane(id) 275{ 276 var impl = new WatchExpressionSidebarPaneImpl(id); 277 ExtensionSidebarPane.call(this, id, impl); 278} 279 280function Audits() 281{ 282} 283 284Audits.prototype = { 285 addCategory: function(displayName, resultCount) 286 { 287 var id = "extension-audit-category-" + extensionServer.nextObjectId(); 288 extensionServer.sendRequest({ command: "addAuditCategory", id: id, displayName: displayName, resultCount: resultCount }); 289 return new AuditCategory(id); 290 } 291} 292 293function AuditCategoryImpl(id) 294{ 295 function auditResultDispatch(request) 296 { 297 var auditResult = new AuditResult(request.arguments[0]); 298 try { 299 this._fire(auditResult); 300 } catch (e) { 301 console.error("Uncaught exception in extension audit event handler: " + e); 302 auditResult.done(); 303 } 304 } 305 this._id = id; 306 this.onAuditStarted = new EventSink("audit-started-" + id, auditResultDispatch); 307} 308 309function AuditResultImpl(id) 310{ 311 this._id = id; 312 313 var formatterTypes = [ 314 "url", 315 "snippet", 316 "text" 317 ]; 318 for (var i = 0; i < formatterTypes.length; ++i) 319 this[formatterTypes[i]] = bind(this._nodeFactory, null, formatterTypes[i]); 320} 321 322AuditResultImpl.prototype = { 323 addResult: function(displayName, description, severity, details) 324 { 325 // shorthand for specifying details directly in addResult(). 326 if (details && !(details instanceof AuditResultNode)) 327 details = details instanceof Array ? this.createNode.apply(this, details) : this.createNode(details); 328 329 var request = { 330 command: "addAuditResult", 331 resultId: this._id, 332 displayName: displayName, 333 description: description, 334 severity: severity, 335 details: details 336 }; 337 extensionServer.sendRequest(request); 338 }, 339 340 createResult: function() 341 { 342 var node = new AuditResultNode(); 343 node.contents = Array.prototype.slice.call(arguments); 344 return node; 345 }, 346 347 done: function() 348 { 349 extensionServer.sendRequest({ command: "stopAuditCategoryRun", resultId: this._id }); 350 }, 351 352 get Severity() 353 { 354 return apiPrivate.audits.Severity; 355 }, 356 357 _nodeFactory: function(type) 358 { 359 return { 360 type: type, 361 arguments: Array.prototype.slice.call(arguments, 1) 362 }; 363 } 364} 365 366function AuditResultNode(contents) 367{ 368 this.contents = contents; 369 this.children = []; 370 this.expanded = false; 371} 372 373AuditResultNode.prototype = { 374 addChild: function() 375 { 376 var node = AuditResultImpl.prototype.createResult.apply(null, arguments); 377 this.children.push(node); 378 return node; 379 } 380}; 381 382function InspectedWindow() 383{ 384 this.onDOMContentLoaded = new EventSink("inspectedPageDOMContentLoaded"); 385 this.onLoaded = new EventSink("inspectedPageLoaded"); 386 this.onNavigated = new EventSink("inspectedURLChanged"); 387} 388 389InspectedWindow.prototype = { 390 reload: function(userAgent) 391 { 392 return extensionServer.sendRequest({ command: "reload", userAgent: userAgent }); 393 }, 394 395 eval: function(expression, callback) 396 { 397 function callbackWrapper(result) 398 { 399 var value = result.value; 400 if (!result.isException) 401 value = value === "undefined" ? undefined : JSON.parse(value); 402 callback(value, result.isException); 403 } 404 return extensionServer.sendRequest({ command: "evaluateOnInspectedPage", expression: expression }, callback && callbackWrapper); 405 } 406} 407 408function ExtensionServerClient() 409{ 410 this._callbacks = {}; 411 this._handlers = {}; 412 this._lastRequestId = 0; 413 this._lastObjectId = 0; 414 415 this.registerHandler("callback", bind(this._onCallback, this)); 416 417 var channel = new MessageChannel(); 418 this._port = channel.port1; 419 this._port.addEventListener("message", bind(this._onMessage, this), false); 420 this._port.start(); 421 422 top.postMessage("registerExtension", [ channel.port2 ], "*"); 423} 424 425ExtensionServerClient.prototype = { 426 sendRequest: function(message, callback) 427 { 428 if (typeof callback === "function") 429 message.requestId = this._registerCallback(callback); 430 return this._port.postMessage(message); 431 }, 432 433 registerHandler: function(command, handler) 434 { 435 this._handlers[command] = handler; 436 }, 437 438 nextObjectId: function() 439 { 440 return injectedScriptId + "_" + ++this._lastObjectId; 441 }, 442 443 _registerCallback: function(callback) 444 { 445 var id = ++this._lastRequestId; 446 this._callbacks[id] = callback; 447 return id; 448 }, 449 450 _onCallback: function(request) 451 { 452 if (request.requestId in this._callbacks) { 453 var callback = this._callbacks[request.requestId]; 454 delete this._callbacks[request.requestId]; 455 callback(request.result); 456 } 457 }, 458 459 _onMessage: function(event) 460 { 461 var request = event.data; 462 var handler = this._handlers[request.command]; 463 if (handler) 464 handler.call(this, request); 465 } 466} 467 468function expandURL(url) 469{ 470 if (!url) 471 return url; 472 if (/^[^/]+:/.exec(url)) // See if url has schema. 473 return url; 474 var baseURL = location.protocol + "//" + location.hostname + location.port; 475 if (/^\//.exec(url)) 476 return baseURL + url; 477 return baseURL + location.pathname.replace(/\/[^/]*$/,"/") + url; 478} 479 480function bind(func, thisObject) 481{ 482 var args = Array.prototype.slice.call(arguments, 2); 483 return function() { return func.apply(thisObject, args.concat(Array.prototype.slice.call(arguments, 0))); }; 484} 485 486function populateInterfaceClass(interface, implementation) 487{ 488 for (var member in implementation) { 489 if (member.charAt(0) === "_") 490 continue; 491 var value = implementation[member]; 492 interface[member] = typeof value === "function" ? bind(value, implementation) 493 : interface[member] = implementation[member]; 494 } 495} 496 497function declareInterfaceClass(implConstructor) 498{ 499 return function() 500 { 501 var impl = { __proto__: implConstructor.prototype }; 502 implConstructor.apply(impl, arguments); 503 populateInterfaceClass(this, impl); 504 } 505} 506 507var AuditCategory = declareInterfaceClass(AuditCategoryImpl); 508var AuditResult = declareInterfaceClass(AuditResultImpl); 509var EventSink = declareInterfaceClass(EventSinkImpl); 510var ExtensionSidebarPane = declareInterfaceClass(ExtensionSidebarPaneImpl); 511var Panel = declareInterfaceClass(PanelImpl); 512var PanelWithSidebar = declareInterfaceClass(PanelWithSidebarImpl); 513var Resource = declareInterfaceClass(ResourceImpl); 514var WatchExpressionSidebarPane = declareInterfaceClass(WatchExpressionSidebarPaneImpl); 515 516var extensionServer = new ExtensionServerClient(); 517 518webInspector = new InspectorExtensionAPI(); 519experimental = window.experimental || {}; 520experimental.webInspector = webInspector; 521 522} 523