1/* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4"use strict"; 5 6/** 7 * This module contains utilities and base classes for logic which is 8 * common between the parent and child process, and in particular 9 * between ExtensionParent.jsm and ExtensionChild.jsm. 10 */ 11 12const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; 13 14/* exported ExtensionCommon */ 15 16this.EXPORTED_SYMBOLS = ["ExtensionCommon"]; 17 18Cu.import("resource://gre/modules/Services.jsm"); 19Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 20 21XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel", 22 "resource://gre/modules/MessageChannel.jsm"); 23XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", 24 "resource://gre/modules/PrivateBrowsingUtils.jsm"); 25XPCOMUtils.defineLazyModuleGetter(this, "Schemas", 26 "resource://gre/modules/Schemas.jsm"); 27 28Cu.import("resource://gre/modules/ExtensionUtils.jsm"); 29 30var { 31 EventEmitter, 32 ExtensionError, 33 SpreadArgs, 34 getConsole, 35 getInnerWindowID, 36 getUniqueId, 37 runSafeSync, 38 runSafeSyncWithoutClone, 39 instanceOf, 40} = ExtensionUtils; 41 42XPCOMUtils.defineLazyGetter(this, "console", getConsole); 43 44class BaseContext { 45 constructor(envType, extension) { 46 this.envType = envType; 47 this.onClose = new Set(); 48 this.checkedLastError = false; 49 this._lastError = null; 50 this.contextId = getUniqueId(); 51 this.unloaded = false; 52 this.extension = extension; 53 this.jsonSandbox = null; 54 this.active = true; 55 this.incognito = null; 56 this.messageManager = null; 57 this.docShell = null; 58 this.contentWindow = null; 59 this.innerWindowID = 0; 60 } 61 62 setContentWindow(contentWindow) { 63 let {document} = contentWindow; 64 let docShell = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) 65 .getInterface(Ci.nsIDocShell); 66 67 this.innerWindowID = getInnerWindowID(contentWindow); 68 this.messageManager = docShell.QueryInterface(Ci.nsIInterfaceRequestor) 69 .getInterface(Ci.nsIContentFrameMessageManager); 70 71 if (this.incognito == null) { 72 this.incognito = PrivateBrowsingUtils.isContentWindowPrivate(contentWindow); 73 } 74 75 MessageChannel.setupMessageManagers([this.messageManager]); 76 77 let onPageShow = event => { 78 if (!event || event.target === document) { 79 this.docShell = docShell; 80 this.contentWindow = contentWindow; 81 this.active = true; 82 } 83 }; 84 let onPageHide = event => { 85 if (!event || event.target === document) { 86 // Put this off until the next tick. 87 Promise.resolve().then(() => { 88 this.docShell = null; 89 this.contentWindow = null; 90 this.active = false; 91 }); 92 } 93 }; 94 95 onPageShow(); 96 contentWindow.addEventListener("pagehide", onPageHide, true); 97 contentWindow.addEventListener("pageshow", onPageShow, true); 98 this.callOnClose({ 99 close: () => { 100 onPageHide(); 101 if (this.active) { 102 contentWindow.removeEventListener("pagehide", onPageHide, true); 103 contentWindow.removeEventListener("pageshow", onPageShow, true); 104 } 105 }, 106 }); 107 } 108 109 get cloneScope() { 110 throw new Error("Not implemented"); 111 } 112 113 get principal() { 114 throw new Error("Not implemented"); 115 } 116 117 runSafe(...args) { 118 if (this.unloaded) { 119 Cu.reportError("context.runSafe called after context unloaded"); 120 } else if (!this.active) { 121 Cu.reportError("context.runSafe called while context is inactive"); 122 } else { 123 return runSafeSync(this, ...args); 124 } 125 } 126 127 runSafeWithoutClone(...args) { 128 if (this.unloaded) { 129 Cu.reportError("context.runSafeWithoutClone called after context unloaded"); 130 } else if (!this.active) { 131 Cu.reportError("context.runSafeWithoutClone called while context is inactive"); 132 } else { 133 return runSafeSyncWithoutClone(...args); 134 } 135 } 136 137 checkLoadURL(url, options = {}) { 138 let ssm = Services.scriptSecurityManager; 139 140 let flags = ssm.STANDARD; 141 if (!options.allowScript) { 142 flags |= ssm.DISALLOW_SCRIPT; 143 } 144 if (!options.allowInheritsPrincipal) { 145 flags |= ssm.DISALLOW_INHERIT_PRINCIPAL; 146 } 147 if (options.dontReportErrors) { 148 flags |= ssm.DONT_REPORT_ERRORS; 149 } 150 151 try { 152 ssm.checkLoadURIStrWithPrincipal(this.principal, url, flags); 153 } catch (e) { 154 return false; 155 } 156 return true; 157 } 158 159 /** 160 * Safely call JSON.stringify() on an object that comes from an 161 * extension. 162 * 163 * @param {array<any>} args Arguments for JSON.stringify() 164 * @returns {string} The stringified representation of obj 165 */ 166 jsonStringify(...args) { 167 if (!this.jsonSandbox) { 168 this.jsonSandbox = Cu.Sandbox(this.principal, { 169 sameZoneAs: this.cloneScope, 170 wantXrays: false, 171 }); 172 } 173 174 return Cu.waiveXrays(this.jsonSandbox.JSON).stringify(...args); 175 } 176 177 callOnClose(obj) { 178 this.onClose.add(obj); 179 } 180 181 forgetOnClose(obj) { 182 this.onClose.delete(obj); 183 } 184 185 /** 186 * A wrapper around MessageChannel.sendMessage which adds the extension ID 187 * to the recipient object, and ensures replies are not processed after the 188 * context has been unloaded. 189 * 190 * @param {nsIMessageManager} target 191 * @param {string} messageName 192 * @param {object} data 193 * @param {object} [options] 194 * @param {object} [options.sender] 195 * @param {object} [options.recipient] 196 * 197 * @returns {Promise} 198 */ 199 sendMessage(target, messageName, data, options = {}) { 200 options.recipient = options.recipient || {}; 201 options.sender = options.sender || {}; 202 203 options.recipient.extensionId = this.extension.id; 204 options.sender.extensionId = this.extension.id; 205 options.sender.contextId = this.contextId; 206 207 return MessageChannel.sendMessage(target, messageName, data, options); 208 } 209 210 get lastError() { 211 this.checkedLastError = true; 212 return this._lastError; 213 } 214 215 set lastError(val) { 216 this.checkedLastError = false; 217 this._lastError = val; 218 } 219 220 /** 221 * Normalizes the given error object for use by the target scope. If 222 * the target is an error object which belongs to that scope, it is 223 * returned as-is. If it is an ordinary object with a `message` 224 * property, it is converted into an error belonging to the target 225 * scope. If it is an Error object which does *not* belong to the 226 * clone scope, it is reported, and converted to an unexpected 227 * exception error. 228 * 229 * @param {Error|object} error 230 * @returns {Error} 231 */ 232 normalizeError(error) { 233 if (error instanceof this.cloneScope.Error) { 234 return error; 235 } 236 let message; 237 if (instanceOf(error, "Object") || error instanceof ExtensionError) { 238 message = error.message; 239 } else if (typeof error == "object" && 240 this.principal.subsumes(Cu.getObjectPrincipal(error))) { 241 message = error.message; 242 } else { 243 Cu.reportError(error); 244 } 245 message = message || "An unexpected error occurred"; 246 return new this.cloneScope.Error(message); 247 } 248 249 /** 250 * Sets the value of `.lastError` to `error`, calls the given 251 * callback, and reports an error if the value has not been checked 252 * when the callback returns. 253 * 254 * @param {object} error An object with a `message` property. May 255 * optionally be an `Error` object belonging to the target scope. 256 * @param {function} callback The callback to call. 257 * @returns {*} The return value of callback. 258 */ 259 withLastError(error, callback) { 260 this.lastError = this.normalizeError(error); 261 try { 262 return callback(); 263 } finally { 264 if (!this.checkedLastError) { 265 Cu.reportError(`Unchecked lastError value: ${this.lastError}`); 266 } 267 this.lastError = null; 268 } 269 } 270 271 /** 272 * Wraps the given promise so it can be safely returned to extension 273 * code in this context. 274 * 275 * If `callback` is provided, however, it is used as a completion 276 * function for the promise, and no promise is returned. In this case, 277 * the callback is called when the promise resolves or rejects. In the 278 * latter case, `lastError` is set to the rejection value, and the 279 * callback function must check `browser.runtime.lastError` or 280 * `extension.runtime.lastError` in order to prevent it being reported 281 * to the console. 282 * 283 * @param {Promise} promise The promise with which to wrap the 284 * callback. May resolve to a `SpreadArgs` instance, in which case 285 * each element will be used as a separate argument. 286 * 287 * Unless the promise object belongs to the cloneScope global, its 288 * resolution value is cloned into cloneScope prior to calling the 289 * `callback` function or resolving the wrapped promise. 290 * 291 * @param {function} [callback] The callback function to wrap 292 * 293 * @returns {Promise|undefined} If callback is null, a promise object 294 * belonging to the target scope. Otherwise, undefined. 295 */ 296 wrapPromise(promise, callback = null) { 297 let runSafe = this.runSafe.bind(this); 298 if (promise instanceof this.cloneScope.Promise) { 299 runSafe = this.runSafeWithoutClone.bind(this); 300 } 301 302 if (callback) { 303 promise.then( 304 args => { 305 if (this.unloaded) { 306 dump(`Promise resolved after context unloaded\n`); 307 } else if (!this.active) { 308 dump(`Promise resolved while context is inactive\n`); 309 } else if (args instanceof SpreadArgs) { 310 runSafe(callback, ...args); 311 } else { 312 runSafe(callback, args); 313 } 314 }, 315 error => { 316 this.withLastError(error, () => { 317 if (this.unloaded) { 318 dump(`Promise rejected after context unloaded\n`); 319 } else if (!this.active) { 320 dump(`Promise rejected while context is inactive\n`); 321 } else { 322 this.runSafeWithoutClone(callback); 323 } 324 }); 325 }); 326 } else { 327 return new this.cloneScope.Promise((resolve, reject) => { 328 promise.then( 329 value => { 330 if (this.unloaded) { 331 dump(`Promise resolved after context unloaded\n`); 332 } else if (!this.active) { 333 dump(`Promise resolved while context is inactive\n`); 334 } else if (value instanceof SpreadArgs) { 335 runSafe(resolve, value.length == 1 ? value[0] : value); 336 } else { 337 runSafe(resolve, value); 338 } 339 }, 340 value => { 341 if (this.unloaded) { 342 dump(`Promise rejected after context unloaded: ${value && value.message}\n`); 343 } else if (!this.active) { 344 dump(`Promise rejected while context is inactive: ${value && value.message}\n`); 345 } else { 346 this.runSafeWithoutClone(reject, this.normalizeError(value)); 347 } 348 }); 349 }); 350 } 351 } 352 353 unload() { 354 this.unloaded = true; 355 356 MessageChannel.abortResponses({ 357 extensionId: this.extension.id, 358 contextId: this.contextId, 359 }); 360 361 for (let obj of this.onClose) { 362 obj.close(); 363 } 364 } 365 366 /** 367 * A simple proxy for unload(), for use with callOnClose(). 368 */ 369 close() { 370 this.unload(); 371 } 372} 373 374/** 375 * An object that runs the implementation of a schema API. Instantiations of 376 * this interfaces are used by Schemas.jsm. 377 * 378 * @interface 379 */ 380class SchemaAPIInterface { 381 /** 382 * Calls this as a function that returns its return value. 383 * 384 * @abstract 385 * @param {Array} args The parameters for the function. 386 * @returns {*} The return value of the invoked function. 387 */ 388 callFunction(args) { 389 throw new Error("Not implemented"); 390 } 391 392 /** 393 * Calls this as a function and ignores its return value. 394 * 395 * @abstract 396 * @param {Array} args The parameters for the function. 397 */ 398 callFunctionNoReturn(args) { 399 throw new Error("Not implemented"); 400 } 401 402 /** 403 * Calls this as a function that completes asynchronously. 404 * 405 * @abstract 406 * @param {Array} args The parameters for the function. 407 * @param {function(*)} [callback] The callback to be called when the function 408 * completes. 409 * @returns {Promise|undefined} Must be void if `callback` is set, and a 410 * promise otherwise. The promise is resolved when the function completes. 411 */ 412 callAsyncFunction(args, callback) { 413 throw new Error("Not implemented"); 414 } 415 416 /** 417 * Retrieves the value of this as a property. 418 * 419 * @abstract 420 * @returns {*} The value of the property. 421 */ 422 getProperty() { 423 throw new Error("Not implemented"); 424 } 425 426 /** 427 * Assigns the value to this as property. 428 * 429 * @abstract 430 * @param {string} value The new value of the property. 431 */ 432 setProperty(value) { 433 throw new Error("Not implemented"); 434 } 435 436 /** 437 * Registers a `listener` to this as an event. 438 * 439 * @abstract 440 * @param {function} listener The callback to be called when the event fires. 441 * @param {Array} args Extra parameters for EventManager.addListener. 442 * @see EventManager.addListener 443 */ 444 addListener(listener, args) { 445 throw new Error("Not implemented"); 446 } 447 448 /** 449 * Checks whether `listener` is listening to this as an event. 450 * 451 * @abstract 452 * @param {function} listener The event listener. 453 * @returns {boolean} Whether `listener` is registered with this as an event. 454 * @see EventManager.hasListener 455 */ 456 hasListener(listener) { 457 throw new Error("Not implemented"); 458 } 459 460 /** 461 * Unregisters `listener` from this as an event. 462 * 463 * @abstract 464 * @param {function} listener The event listener. 465 * @see EventManager.removeListener 466 */ 467 removeListener(listener) { 468 throw new Error("Not implemented"); 469 } 470} 471 472/** 473 * An object that runs a locally implemented API. 474 */ 475class LocalAPIImplementation extends SchemaAPIInterface { 476 /** 477 * Constructs an implementation of the `name` method or property of `pathObj`. 478 * 479 * @param {object} pathObj The object containing the member with name `name`. 480 * @param {string} name The name of the implemented member. 481 * @param {BaseContext} context The context in which the schema is injected. 482 */ 483 constructor(pathObj, name, context) { 484 super(); 485 this.pathObj = pathObj; 486 this.name = name; 487 this.context = context; 488 } 489 490 callFunction(args) { 491 return this.pathObj[this.name](...args); 492 } 493 494 callFunctionNoReturn(args) { 495 this.pathObj[this.name](...args); 496 } 497 498 callAsyncFunction(args, callback) { 499 let promise; 500 try { 501 promise = this.pathObj[this.name](...args) || Promise.resolve(); 502 } catch (e) { 503 promise = Promise.reject(e); 504 } 505 return this.context.wrapPromise(promise, callback); 506 } 507 508 getProperty() { 509 return this.pathObj[this.name]; 510 } 511 512 setProperty(value) { 513 this.pathObj[this.name] = value; 514 } 515 516 addListener(listener, args) { 517 try { 518 this.pathObj[this.name].addListener.call(null, listener, ...args); 519 } catch (e) { 520 throw this.context.normalizeError(e); 521 } 522 } 523 524 hasListener(listener) { 525 return this.pathObj[this.name].hasListener.call(null, listener); 526 } 527 528 removeListener(listener) { 529 this.pathObj[this.name].removeListener.call(null, listener); 530 } 531} 532 533/** 534 * This object loads the ext-*.js scripts that define the extension API. 535 * 536 * This class instance is shared with the scripts that it loads, so that the 537 * ext-*.js scripts and the instantiator can communicate with each other. 538 */ 539class SchemaAPIManager extends EventEmitter { 540 /** 541 * @param {string} processType 542 * "main" - The main, one and only chrome browser process. 543 * "addon" - An addon process. 544 * "content" - A content process. 545 */ 546 constructor(processType) { 547 super(); 548 this.processType = processType; 549 this.global = this._createExtGlobal(); 550 this._scriptScopes = []; 551 this._schemaApis = { 552 addon_parent: [], 553 addon_child: [], 554 content_parent: [], 555 content_child: [], 556 }; 557 } 558 559 /** 560 * Create a global object that is used as the shared global for all ext-*.js 561 * scripts that are loaded via `loadScript`. 562 * 563 * @returns {object} A sandbox that is used as the global by `loadScript`. 564 */ 565 _createExtGlobal() { 566 let global = Cu.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), { 567 wantXrays: false, 568 sandboxName: `Namespace of ext-*.js scripts for ${this.processType}`, 569 }); 570 571 Object.assign(global, {global, Cc, Ci, Cu, Cr, XPCOMUtils, extensions: this}); 572 573 XPCOMUtils.defineLazyGetter(global, "console", getConsole); 574 575 XPCOMUtils.defineLazyModuleGetter(global, "require", 576 "resource://devtools/shared/Loader.jsm"); 577 578 return global; 579 } 580 581 /** 582 * Load an ext-*.js script. The script runs in its own scope, if it wishes to 583 * share state with another script it can assign to the `global` variable. If 584 * it wishes to communicate with this API manager, use `extensions`. 585 * 586 * @param {string} scriptUrl The URL of the ext-*.js script. 587 */ 588 loadScript(scriptUrl) { 589 // Create the object in the context of the sandbox so that the script runs 590 // in the sandbox's context instead of here. 591 let scope = Cu.createObjectIn(this.global); 592 593 Services.scriptloader.loadSubScript(scriptUrl, scope, "UTF-8"); 594 595 // Save the scope to avoid it being garbage collected. 596 this._scriptScopes.push(scope); 597 } 598 599 /** 600 * Called by an ext-*.js script to register an API. 601 * 602 * @param {string} namespace The API namespace. 603 * Intended to match the namespace of the generated API, but not used at 604 * the moment - see bugzil.la/1295774. 605 * @param {string} envType Restricts the API to contexts that run in the 606 * given environment. Must be one of the following: 607 * - "addon_parent" - addon APIs that runs in the main process. 608 * - "addon_child" - addon APIs that runs in an addon process. 609 * - "content_parent" - content script APIs that runs in the main process. 610 * - "content_child" - content script APIs that runs in a content process. 611 * @param {function(BaseContext)} getAPI A function that returns an object 612 * that will be merged with |chrome| and |browser|. The next example adds 613 * the create, update and remove methods to the tabs API. 614 * 615 * registerSchemaAPI("tabs", "addon_parent", (context) => ({ 616 * tabs: { create, update }, 617 * })); 618 * registerSchemaAPI("tabs", "addon_parent", (context) => ({ 619 * tabs: { remove }, 620 * })); 621 */ 622 registerSchemaAPI(namespace, envType, getAPI) { 623 this._schemaApis[envType].push({namespace, getAPI}); 624 } 625 626 /** 627 * Exports all registered scripts to `obj`. 628 * 629 * @param {BaseContext} context The context for which the API bindings are 630 * generated. 631 * @param {object} obj The destination of the API. 632 */ 633 generateAPIs(context, obj) { 634 let apis = this._schemaApis[context.envType]; 635 if (!apis) { 636 Cu.reportError(`No APIs have been registered for ${context.envType}`); 637 return; 638 } 639 SchemaAPIManager.generateAPIs(context, apis, obj); 640 } 641 642 /** 643 * Mash together all the APIs from `apis` into `obj`. 644 * 645 * @param {BaseContext} context The context for which the API bindings are 646 * generated. 647 * @param {Array} apis A list of objects, see `registerSchemaAPI`. 648 * @param {object} obj The destination of the API. 649 */ 650 static generateAPIs(context, apis, obj) { 651 // Recursively copy properties from source to dest. 652 function copy(dest, source) { 653 for (let prop in source) { 654 let desc = Object.getOwnPropertyDescriptor(source, prop); 655 if (typeof(desc.value) == "object") { 656 if (!(prop in dest)) { 657 dest[prop] = {}; 658 } 659 copy(dest[prop], source[prop]); 660 } else { 661 Object.defineProperty(dest, prop, desc); 662 } 663 } 664 } 665 666 for (let api of apis) { 667 if (Schemas.checkPermissions(api.namespace, context.extension)) { 668 api = api.getAPI(context); 669 copy(obj, api); 670 } 671 } 672 } 673} 674 675const ExtensionCommon = { 676 BaseContext, 677 LocalAPIImplementation, 678 SchemaAPIInterface, 679 SchemaAPIManager, 680}; 681