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 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 this.onNavigated = new EventSink("inspectedURLChanged"); 115} 116 117Resources.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 138function ResourceImpl(id) 139{ 140 this._id = id; 141} 142 143ResourceImpl.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 154function 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 168Panels.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 183function 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 190function PanelWithSidebarImpl(id) 191{ 192 PanelImpl.call(this, id); 193} 194 195PanelWithSidebarImpl.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 213PanelWithSidebarImpl.prototype.__proto__ = PanelImpl.prototype; 214 215function ElementsPanel() 216{ 217 var id = "elements"; 218 PanelWithSidebar.call(this, id); 219 this.onSelectionChanged = new EventSink("panel-objectSelected-" + id); 220} 221 222function ExtensionPanel(id) 223{ 224 Panel.call(this, id); 225 this.onSearch = new EventSink("panel-search-" + id); 226} 227 228function ExtensionSidebarPaneImpl(id) 229{ 230 this._id = id; 231 this.onUpdated = new EventSink("sidebar-updated-" + id); 232} 233 234ExtensionSidebarPaneImpl.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 256function Audits() 257{ 258} 259 260Audits.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 269function 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 285function 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 298AuditResultImpl.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 342function AuditResultNode(contents) 343{ 344 this.contents = contents; 345 this.children = []; 346 this.expanded = false; 347} 348 349AuditResultNode.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 358function InspectedWindow() 359{ 360} 361 362InspectedWindow.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 381function 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 398ExtensionServerClient.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 441function 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 453function 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 459function 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 470function 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 480var AuditCategory = declareInterfaceClass(AuditCategoryImpl); 481var AuditResult = declareInterfaceClass(AuditResultImpl); 482var EventSink = declareInterfaceClass(EventSinkImpl); 483var ExtensionSidebarPane = declareInterfaceClass(ExtensionSidebarPaneImpl); 484var Panel = declareInterfaceClass(PanelImpl); 485var PanelWithSidebar = declareInterfaceClass(PanelWithSidebarImpl); 486var Resource = declareInterfaceClass(ResourceImpl); 487 488var extensionServer = new ExtensionServerClient(); 489 490webInspector = new InspectorExtensionAPI(); 491experimental = window.experimental || {}; 492experimental.webInspector = webInspector; 493 494} 495