1/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2/* vim: set sts=2 sw=2 et tw=80: */ 3/* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6"use strict"; 7 8/** 9 * This module contains utilities and base classes for logic which is 10 * common between the parent and child process, and in particular 11 * between ExtensionParent.jsm and ExtensionChild.jsm. 12 */ 13 14/* exported ExtensionCommon */ 15 16var EXPORTED_SYMBOLS = ["ExtensionCommon"]; 17 18const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 19const { XPCOMUtils } = ChromeUtils.import( 20 "resource://gre/modules/XPCOMUtils.jsm" 21); 22 23XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]); 24 25XPCOMUtils.defineLazyModuleGetters(this, { 26 AppConstants: "resource://gre/modules/AppConstants.jsm", 27 ConsoleAPI: "resource://gre/modules/Console.jsm", 28 MessageChannel: "resource://gre/modules/MessageChannel.jsm", 29 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm", 30 Schemas: "resource://gre/modules/Schemas.jsm", 31 SchemaRoot: "resource://gre/modules/Schemas.jsm", 32}); 33 34XPCOMUtils.defineLazyServiceGetter( 35 this, 36 "styleSheetService", 37 "@mozilla.org/content/style-sheet-service;1", 38 "nsIStyleSheetService" 39); 40 41const { ExtensionUtils } = ChromeUtils.import( 42 "resource://gre/modules/ExtensionUtils.jsm" 43); 44 45var { 46 DefaultMap, 47 DefaultWeakMap, 48 ExtensionError, 49 filterStack, 50 getInnerWindowID, 51 getUniqueId, 52 getWinUtils, 53} = ExtensionUtils; 54 55function getConsole() { 56 return new ConsoleAPI({ 57 maxLogLevelPref: "extensions.webextensions.log.level", 58 prefix: "WebExtensions", 59 }); 60} 61 62XPCOMUtils.defineLazyGetter(this, "console", getConsole); 63 64XPCOMUtils.defineLazyPreferenceGetter( 65 this, 66 "DELAYED_BG_STARTUP", 67 "extensions.webextensions.background-delayed-startup" 68); 69 70var ExtensionCommon; 71 72// Run a function and report exceptions. 73function runSafeSyncWithoutClone(f, ...args) { 74 try { 75 return f(...args); 76 } catch (e) { 77 dump( 78 `Extension error: ${e} ${e.fileName} ${ 79 e.lineNumber 80 }\n[[Exception stack\n${filterStack(e)}Current stack\n${filterStack( 81 Error() 82 )}]]\n` 83 ); 84 Cu.reportError(e); 85 } 86} 87 88// Return true if the given value is an instance of the given 89// native type. 90function instanceOf(value, type) { 91 return ( 92 value && 93 typeof value === "object" && 94 ChromeUtils.getClassName(value) === type 95 ); 96} 97 98/** 99 * Convert any of several different representations of a date/time to a Date object. 100 * Accepts several formats: 101 * a Date object, an ISO8601 string, or a number of milliseconds since the epoch as 102 * either a number or a string. 103 * 104 * @param {Date|string|number} date 105 * The date to convert. 106 * @returns {Date} 107 * A Date object 108 */ 109function normalizeTime(date) { 110 // Of all the formats we accept the "number of milliseconds since the epoch as a string" 111 // is an outlier, everything else can just be passed directly to the Date constructor. 112 return new Date( 113 typeof date == "string" && /^\d+$/.test(date) ? parseInt(date, 10) : date 114 ); 115} 116 117function withHandlingUserInput(window, callable) { 118 let handle = getWinUtils(window).setHandlingUserInput(true); 119 try { 120 return callable(); 121 } finally { 122 handle.destruct(); 123 } 124} 125 126/** 127 * Defines a lazy getter for the given property on the given object. The 128 * first time the property is accessed, the return value of the getter 129 * is defined on the current `this` object with the given property name. 130 * Importantly, this means that a lazy getter defined on an object 131 * prototype will be invoked separately for each object instance that 132 * it's accessed on. 133 * 134 * @param {object} object 135 * The prototype object on which to define the getter. 136 * @param {string|Symbol} prop 137 * The property name for which to define the getter. 138 * @param {function} getter 139 * The function to call in order to generate the final property 140 * value. 141 */ 142function defineLazyGetter(object, prop, getter) { 143 let redefine = (obj, value) => { 144 Object.defineProperty(obj, prop, { 145 enumerable: true, 146 configurable: true, 147 writable: true, 148 value, 149 }); 150 return value; 151 }; 152 153 Object.defineProperty(object, prop, { 154 enumerable: true, 155 configurable: true, 156 157 get() { 158 return redefine(this, getter.call(this)); 159 }, 160 161 set(value) { 162 redefine(this, value); 163 }, 164 }); 165} 166 167function checkLoadURL(url, principal, options) { 168 let ssm = Services.scriptSecurityManager; 169 170 let flags = ssm.STANDARD; 171 if (!options.allowScript) { 172 flags |= ssm.DISALLOW_SCRIPT; 173 } 174 if (!options.allowInheritsPrincipal) { 175 flags |= ssm.DISALLOW_INHERIT_PRINCIPAL; 176 } 177 if (options.dontReportErrors) { 178 flags |= ssm.DONT_REPORT_ERRORS; 179 } 180 181 try { 182 ssm.checkLoadURIWithPrincipal(principal, Services.io.newURI(url), flags); 183 } catch (e) { 184 return false; 185 } 186 return true; 187} 188 189function makeWidgetId(id) { 190 id = id.toLowerCase(); 191 // FIXME: This allows for collisions. 192 return id.replace(/[^a-z0-9_-]/g, "_"); 193} 194 195/** 196 * A sentinel class to indicate that an array of values should be 197 * treated as an array when used as a promise resolution value, but as a 198 * spread expression (...args) when passed to a callback. 199 */ 200class SpreadArgs extends Array { 201 constructor(args) { 202 super(); 203 this.push(...args); 204 } 205} 206 207/** 208 * Like SpreadArgs, but also indicates that the array values already 209 * belong to the target compartment, and should not be cloned before 210 * being passed. 211 * 212 * The `unwrappedValues` property contains an Array object which belongs 213 * to the target compartment, and contains the same unwrapped values 214 * passed the NoCloneSpreadArgs constructor. 215 */ 216class NoCloneSpreadArgs { 217 constructor(args) { 218 this.unwrappedValues = args; 219 } 220 221 [Symbol.iterator]() { 222 return this.unwrappedValues[Symbol.iterator](); 223 } 224} 225 226const LISTENERS = Symbol("listeners"); 227const ONCE_MAP = Symbol("onceMap"); 228 229class EventEmitter { 230 constructor() { 231 this[LISTENERS] = new Map(); 232 this[ONCE_MAP] = new WeakMap(); 233 } 234 235 /** 236 * Checks whether there is some listener for the given event. 237 * 238 * @param {string} event 239 * The name of the event to listen for. 240 * @returns {boolean} 241 */ 242 has(event) { 243 return this[LISTENERS].has(event); 244 } 245 246 /** 247 * Adds the given function as a listener for the given event. 248 * 249 * The listener function may optionally return a Promise which 250 * resolves when it has completed all operations which event 251 * dispatchers may need to block on. 252 * 253 * @param {string} event 254 * The name of the event to listen for. 255 * @param {function(string, ...any)} listener 256 * The listener to call when events are emitted. 257 */ 258 on(event, listener) { 259 let listeners = this[LISTENERS].get(event); 260 if (!listeners) { 261 listeners = new Set(); 262 this[LISTENERS].set(event, listeners); 263 } 264 265 listeners.add(listener); 266 } 267 268 /** 269 * Removes the given function as a listener for the given event. 270 * 271 * @param {string} event 272 * The name of the event to stop listening for. 273 * @param {function(string, ...any)} listener 274 * The listener function to remove. 275 */ 276 off(event, listener) { 277 let set = this[LISTENERS].get(event); 278 if (set) { 279 set.delete(listener); 280 set.delete(this[ONCE_MAP].get(listener)); 281 if (!set.size) { 282 this[LISTENERS].delete(event); 283 } 284 } 285 } 286 287 /** 288 * Adds the given function as a listener for the given event once. 289 * 290 * @param {string} event 291 * The name of the event to listen for. 292 * @param {function(string, ...any)} listener 293 * The listener to call when events are emitted. 294 */ 295 once(event, listener) { 296 let wrapper = (...args) => { 297 this.off(event, wrapper); 298 this[ONCE_MAP].delete(listener); 299 300 return listener(...args); 301 }; 302 this[ONCE_MAP].set(listener, wrapper); 303 304 this.on(event, wrapper); 305 } 306 307 /** 308 * Triggers all listeners for the given event. If any listeners return 309 * a value, returns a promise which resolves when all returned 310 * promises have resolved. Otherwise, returns undefined. 311 * 312 * @param {string} event 313 * The name of the event to emit. 314 * @param {any} args 315 * Arbitrary arguments to pass to the listener functions, after 316 * the event name. 317 * @returns {Promise?} 318 */ 319 emit(event, ...args) { 320 let listeners = this[LISTENERS].get(event); 321 322 if (listeners) { 323 let promises = []; 324 325 for (let listener of listeners) { 326 try { 327 let result = listener(event, ...args); 328 if (result !== undefined) { 329 promises.push(result); 330 } 331 } catch (e) { 332 Cu.reportError(e); 333 } 334 } 335 336 if (promises.length) { 337 return Promise.all(promises); 338 } 339 } 340 } 341} 342 343/** 344 * Base class for WebExtension APIs. Each API creates a new class 345 * that inherits from this class, the derived class is instantiated 346 * once for each extension that uses the API. 347 */ 348class ExtensionAPI extends EventEmitter { 349 constructor(extension) { 350 super(); 351 352 this.extension = extension; 353 354 extension.once("shutdown", (what, isAppShutdown) => { 355 if (this.onShutdown) { 356 this.onShutdown(isAppShutdown); 357 } 358 this.extension = null; 359 }); 360 } 361 362 destroy() {} 363 364 onManifestEntry(entry) {} 365 366 getAPI(context) { 367 throw new Error("Not Implemented"); 368 } 369} 370 371/** 372 * A wrapper around a window that returns the window iff the inner window 373 * matches the inner window at the construction of this wrapper. 374 * 375 * This wrapper should not be used after the inner window is destroyed. 376 **/ 377class InnerWindowReference { 378 constructor(contentWindow, innerWindowID) { 379 this.contentWindow = contentWindow; 380 this.innerWindowID = innerWindowID; 381 this.needWindowIDCheck = false; 382 383 contentWindow.addEventListener( 384 "pagehide", 385 this, 386 { mozSystemGroup: true }, 387 false 388 ); 389 contentWindow.addEventListener( 390 "pageshow", 391 this, 392 { mozSystemGroup: true }, 393 false 394 ); 395 } 396 397 get() { 398 // If the pagehide event has fired, the inner window ID needs to be checked, 399 // in case the window ref is dereferenced in a pageshow listener (before our 400 // pageshow listener was dispatched) or during the unload event. 401 if ( 402 !this.needWindowIDCheck || 403 getInnerWindowID(this.contentWindow) === this.innerWindowID 404 ) { 405 return this.contentWindow; 406 } 407 return null; 408 } 409 410 invalidate() { 411 // If invalidate() is called while the inner window is in the bfcache, then 412 // we are unable to remove the event listener, and handleEvent will be 413 // called once more if the page is revived from the bfcache. 414 if (this.contentWindow && !Cu.isDeadWrapper(this.contentWindow)) { 415 this.contentWindow.removeEventListener("pagehide", this, { 416 mozSystemGroup: true, 417 }); 418 this.contentWindow.removeEventListener("pageshow", this, { 419 mozSystemGroup: true, 420 }); 421 } 422 this.contentWindow = null; 423 this.needWindowIDCheck = false; 424 } 425 426 handleEvent(event) { 427 if (this.contentWindow) { 428 this.needWindowIDCheck = event.type === "pagehide"; 429 } else { 430 // Remove listener when restoring from the bfcache - see invalidate(). 431 event.currentTarget.removeEventListener("pagehide", this, { 432 mozSystemGroup: true, 433 }); 434 event.currentTarget.removeEventListener("pageshow", this, { 435 mozSystemGroup: true, 436 }); 437 } 438 } 439} 440 441/** 442 * This class contains the information we have about an individual 443 * extension. It is never instantiated directly, instead subclasses 444 * for each type of process extend this class and add members that are 445 * relevant for that process. 446 * @abstract 447 */ 448class BaseContext { 449 constructor(envType, extension) { 450 this.envType = envType; 451 this.onClose = new Set(); 452 this.checkedLastError = false; 453 this._lastError = null; 454 this.contextId = getUniqueId(); 455 this.unloaded = false; 456 this.extension = extension; 457 this.jsonSandbox = null; 458 this.active = true; 459 this.incognito = null; 460 this.messageManager = null; 461 this.contentWindow = null; 462 this.innerWindowID = 0; 463 464 // These two properties are assigned in ContentScriptContextChild subclass 465 // to keep a copy of the content script sandbox Error and Promise globals 466 // (which are used by the WebExtensions internals) before any extension 467 // content script code had any chance to redefine them. 468 this.cloneScopeError = null; 469 this.cloneScopePromise = null; 470 } 471 472 get Error() { 473 // Return the copy stored in the context instance (when the context is an instance of 474 // ContentScriptContextChild or the global from extension page window otherwise). 475 return this.cloneScopeError || this.cloneScope.Error; 476 } 477 478 get Promise() { 479 // Return the copy stored in the context instance (when the context is an instance of 480 // ContentScriptContextChild or the global from extension page window otherwise). 481 return this.cloneScopePromise || this.cloneScope.Promise; 482 } 483 484 get privateBrowsingAllowed() { 485 return this.extension.privateBrowsingAllowed; 486 } 487 488 canAccessWindow(window) { 489 return this.extension.canAccessWindow(window); 490 } 491 492 /** 493 * Opens a conduit linked to this context, populating related address fields. 494 * Only available in child contexts with an associated contentWindow. 495 * @param {object} subject 496 * @param {ConduitAddress} address 497 * @returns {PointConduit} 498 */ 499 openConduit(subject, address) { 500 let wgc = this.contentWindow.windowGlobalChild; 501 let conduit = wgc.getActor("Conduits").openConduit(subject, { 502 id: subject.id || getUniqueId(), 503 extensionId: this.extension.id, 504 envType: this.envType, 505 ...address, 506 }); 507 this.callOnClose(conduit); 508 conduit.setCloseCallback(() => { 509 this.forgetOnClose(conduit); 510 }); 511 return conduit; 512 } 513 514 setContentWindow(contentWindow) { 515 if (!this.canAccessWindow(contentWindow)) { 516 throw new Error( 517 "BaseContext attempted to load when extension is not allowed due to incognito settings." 518 ); 519 } 520 521 this.innerWindowID = getInnerWindowID(contentWindow); 522 this.messageManager = contentWindow.docShell.messageManager; 523 524 if (this.incognito == null) { 525 this.incognito = PrivateBrowsingUtils.isContentWindowPrivate( 526 contentWindow 527 ); 528 } 529 530 MessageChannel.setupMessageManagers([this.messageManager]); 531 532 let windowRef = new InnerWindowReference(contentWindow, this.innerWindowID); 533 Object.defineProperty(this, "active", { 534 configurable: true, 535 enumerable: true, 536 get: () => windowRef.get() !== null, 537 }); 538 Object.defineProperty(this, "contentWindow", { 539 configurable: true, 540 enumerable: true, 541 get: () => windowRef.get(), 542 }); 543 this.callOnClose({ 544 close: () => { 545 // Allow other "close" handlers to use these properties, until the next tick. 546 Promise.resolve().then(() => { 547 windowRef.invalidate(); 548 windowRef = null; 549 Object.defineProperty(this, "contentWindow", { value: null }); 550 Object.defineProperty(this, "active", { value: false }); 551 }); 552 }, 553 }); 554 } 555 556 // All child contexts must implement logActivity. This is handled if the child 557 // context subclasses ExtensionBaseContextChild. ProxyContextParent overrides 558 // this with a noop for parent contexts. 559 logActivity(type, name, data) { 560 throw new Error(`Not implemented for ${this.envType}`); 561 } 562 563 get cloneScope() { 564 throw new Error("Not implemented"); 565 } 566 567 get principal() { 568 throw new Error("Not implemented"); 569 } 570 571 runSafe(callback, ...args) { 572 return this.applySafe(callback, args); 573 } 574 575 runSafeWithoutClone(callback, ...args) { 576 return this.applySafeWithoutClone(callback, args); 577 } 578 579 applySafe(callback, args, caller) { 580 if (this.unloaded) { 581 Cu.reportError("context.runSafe called after context unloaded", caller); 582 } else if (!this.active) { 583 Cu.reportError( 584 "context.runSafe called while context is inactive", 585 caller 586 ); 587 } else { 588 try { 589 let { cloneScope } = this; 590 args = args.map(arg => Cu.cloneInto(arg, cloneScope)); 591 } catch (e) { 592 Cu.reportError(e); 593 dump( 594 `runSafe failure: cloning into ${ 595 this.cloneScope 596 }: ${e}\n\n${filterStack(Error())}` 597 ); 598 } 599 600 return this.applySafeWithoutClone(callback, args, caller); 601 } 602 } 603 604 applySafeWithoutClone(callback, args, caller) { 605 if (this.unloaded) { 606 Cu.reportError( 607 "context.runSafeWithoutClone called after context unloaded", 608 caller 609 ); 610 } else if (!this.active) { 611 Cu.reportError( 612 "context.runSafeWithoutClone called while context is inactive", 613 caller 614 ); 615 } else { 616 try { 617 return Reflect.apply(callback, null, args); 618 } catch (e) { 619 dump( 620 `Extension error: ${e} ${e.fileName} ${ 621 e.lineNumber 622 }\n[[Exception stack\n${filterStack(e)}Current stack\n${filterStack( 623 Error() 624 )}]]\n` 625 ); 626 Cu.reportError(e); 627 } 628 } 629 } 630 631 checkLoadURL(url, options = {}) { 632 // As an optimization, f the URL starts with the extension's base URL, 633 // don't do any further checks. It's always allowed to load it. 634 if (url.startsWith(this.extension.baseURL)) { 635 return true; 636 } 637 638 return checkLoadURL(url, this.principal, options); 639 } 640 641 /** 642 * Safely call JSON.stringify() on an object that comes from an 643 * extension. 644 * 645 * @param {array<any>} args Arguments for JSON.stringify() 646 * @returns {string} The stringified representation of obj 647 */ 648 jsonStringify(...args) { 649 if (!this.jsonSandbox) { 650 this.jsonSandbox = Cu.Sandbox(this.principal, { 651 sameZoneAs: this.cloneScope, 652 wantXrays: false, 653 }); 654 } 655 656 return Cu.waiveXrays(this.jsonSandbox.JSON).stringify(...args); 657 } 658 659 callOnClose(obj) { 660 this.onClose.add(obj); 661 } 662 663 forgetOnClose(obj) { 664 this.onClose.delete(obj); 665 } 666 667 /** 668 * A wrapper around MessageChannel.sendMessage which adds the extension ID 669 * to the recipient object, and ensures replies are not processed after the 670 * context has been unloaded. 671 * 672 * @param {nsIMessageManager} target 673 * @param {string} messageName 674 * @param {object} data 675 * @param {object} [options] 676 * @param {object} [options.sender] 677 * @param {object} [options.recipient] 678 * 679 * @returns {Promise} 680 */ 681 sendMessage(target, messageName, data, options = {}) { 682 options.recipient = Object.assign( 683 { extensionId: this.extension.id }, 684 options.recipient 685 ); 686 options.sender = options.sender || {}; 687 688 options.sender.extensionId = this.extension.id; 689 options.sender.contextId = this.contextId; 690 691 return MessageChannel.sendMessage(target, messageName, data, options); 692 } 693 694 get lastError() { 695 this.checkedLastError = true; 696 return this._lastError; 697 } 698 699 set lastError(val) { 700 this.checkedLastError = false; 701 this._lastError = val; 702 } 703 704 /** 705 * Normalizes the given error object for use by the target scope. If 706 * the target is an error object which belongs to that scope, it is 707 * returned as-is. If it is an ordinary object with a `message` 708 * property, it is converted into an error belonging to the target 709 * scope. If it is an Error object which does *not* belong to the 710 * clone scope, it is reported, and converted to an unexpected 711 * exception error. 712 * 713 * @param {Error|object} error 714 * @param {SavedFrame?} [caller] 715 * @returns {Error} 716 */ 717 normalizeError(error, caller) { 718 if (error instanceof this.Error) { 719 return error; 720 } 721 let message, fileName; 722 if (error && typeof error === "object") { 723 const isPlain = ChromeUtils.getClassName(error) === "Object"; 724 if (isPlain && error.mozWebExtLocation) { 725 caller = error.mozWebExtLocation; 726 } 727 if (isPlain && caller && (error.mozWebExtLocation || !error.fileName)) { 728 caller = Cu.cloneInto(caller, this.cloneScope); 729 return ChromeUtils.createError(error.message, caller); 730 } 731 732 if ( 733 isPlain || 734 error instanceof ExtensionError || 735 this.principal.subsumes(Cu.getObjectPrincipal(error)) 736 ) { 737 message = error.message; 738 fileName = error.fileName; 739 } 740 } 741 742 if (!message) { 743 Cu.reportError(error); 744 message = "An unexpected error occurred"; 745 } 746 return new this.Error(message, fileName); 747 } 748 749 /** 750 * Sets the value of `.lastError` to `error`, calls the given 751 * callback, and reports an error if the value has not been checked 752 * when the callback returns. 753 * 754 * @param {object} error An object with a `message` property. May 755 * optionally be an `Error` object belonging to the target scope. 756 * @param {SavedFrame?} caller 757 * The optional caller frame which triggered this callback, to be used 758 * in error reporting. 759 * @param {function} callback The callback to call. 760 * @returns {*} The return value of callback. 761 */ 762 withLastError(error, caller, callback) { 763 this.lastError = this.normalizeError(error); 764 try { 765 return callback(); 766 } finally { 767 if (!this.checkedLastError) { 768 Cu.reportError(`Unchecked lastError value: ${this.lastError}`, caller); 769 } 770 this.lastError = null; 771 } 772 } 773 774 /** 775 * Captures the most recent stack frame which belongs to the extension. 776 * 777 * @returns {SavedFrame?} 778 */ 779 getCaller() { 780 return ChromeUtils.getCallerLocation(this.principal); 781 } 782 783 /** 784 * Wraps the given promise so it can be safely returned to extension 785 * code in this context. 786 * 787 * If `callback` is provided, however, it is used as a completion 788 * function for the promise, and no promise is returned. In this case, 789 * the callback is called when the promise resolves or rejects. In the 790 * latter case, `lastError` is set to the rejection value, and the 791 * callback function must check `browser.runtime.lastError` or 792 * `extension.runtime.lastError` in order to prevent it being reported 793 * to the console. 794 * 795 * @param {Promise} promise The promise with which to wrap the 796 * callback. May resolve to a `SpreadArgs` instance, in which case 797 * each element will be used as a separate argument. 798 * 799 * Unless the promise object belongs to the cloneScope global, its 800 * resolution value is cloned into cloneScope prior to calling the 801 * `callback` function or resolving the wrapped promise. 802 * 803 * @param {function} [callback] The callback function to wrap 804 * 805 * @returns {Promise|undefined} If callback is null, a promise object 806 * belonging to the target scope. Otherwise, undefined. 807 */ 808 wrapPromise(promise, callback = null) { 809 let caller = this.getCaller(); 810 let applySafe = this.applySafe.bind(this); 811 if (Cu.getGlobalForObject(promise) === this.cloneScope) { 812 applySafe = this.applySafeWithoutClone.bind(this); 813 } 814 815 if (callback) { 816 promise.then( 817 args => { 818 if (this.unloaded) { 819 Cu.reportError(`Promise resolved after context unloaded\n`, caller); 820 } else if (!this.active) { 821 Cu.reportError( 822 `Promise resolved while context is inactive\n`, 823 caller 824 ); 825 } else if (args instanceof NoCloneSpreadArgs) { 826 this.applySafeWithoutClone(callback, args.unwrappedValues, caller); 827 } else if (args instanceof SpreadArgs) { 828 applySafe(callback, args, caller); 829 } else { 830 applySafe(callback, [args], caller); 831 } 832 }, 833 error => { 834 this.withLastError(error, caller, () => { 835 if (this.unloaded) { 836 Cu.reportError( 837 `Promise rejected after context unloaded\n`, 838 caller 839 ); 840 } else if (!this.active) { 841 Cu.reportError( 842 `Promise rejected while context is inactive\n`, 843 caller 844 ); 845 } else { 846 this.applySafeWithoutClone(callback, [], caller); 847 } 848 }); 849 } 850 ); 851 } else { 852 return new this.Promise((resolve, reject) => { 853 promise.then( 854 value => { 855 if (this.unloaded) { 856 Cu.reportError( 857 `Promise resolved after context unloaded\n`, 858 caller 859 ); 860 } else if (!this.active) { 861 Cu.reportError( 862 `Promise resolved while context is inactive\n`, 863 caller 864 ); 865 } else if (value instanceof NoCloneSpreadArgs) { 866 let values = value.unwrappedValues; 867 this.applySafeWithoutClone( 868 resolve, 869 values.length == 1 ? [values[0]] : [values], 870 caller 871 ); 872 } else if (value instanceof SpreadArgs) { 873 applySafe(resolve, value.length == 1 ? value : [value], caller); 874 } else { 875 applySafe(resolve, [value], caller); 876 } 877 }, 878 value => { 879 if (this.unloaded) { 880 Cu.reportError( 881 `Promise rejected after context unloaded: ${value && 882 value.message}\n`, 883 caller 884 ); 885 } else if (!this.active) { 886 Cu.reportError( 887 `Promise rejected while context is inactive: ${value && 888 value.message}\n`, 889 caller 890 ); 891 } else { 892 this.applySafeWithoutClone( 893 reject, 894 [this.normalizeError(value, caller)], 895 caller 896 ); 897 } 898 } 899 ); 900 }); 901 } 902 } 903 904 unload() { 905 this.unloaded = true; 906 907 MessageChannel.abortResponses({ 908 extensionId: this.extension.id, 909 contextId: this.contextId, 910 }); 911 912 for (let obj of this.onClose) { 913 obj.close(); 914 } 915 this.onClose.clear(); 916 } 917 918 /** 919 * A simple proxy for unload(), for use with callOnClose(). 920 */ 921 close() { 922 this.unload(); 923 } 924} 925 926/** 927 * An object that runs the implementation of a schema API. Instantiations of 928 * this interfaces are used by Schemas.jsm. 929 * 930 * @interface 931 */ 932class SchemaAPIInterface { 933 /** 934 * Calls this as a function that returns its return value. 935 * 936 * @abstract 937 * @param {Array} args The parameters for the function. 938 * @returns {*} The return value of the invoked function. 939 */ 940 callFunction(args) { 941 throw new Error("Not implemented"); 942 } 943 944 /** 945 * Calls this as a function and ignores its return value. 946 * 947 * @abstract 948 * @param {Array} args The parameters for the function. 949 */ 950 callFunctionNoReturn(args) { 951 throw new Error("Not implemented"); 952 } 953 954 /** 955 * Calls this as a function that completes asynchronously. 956 * 957 * @abstract 958 * @param {Array} args The parameters for the function. 959 * @param {function(*)} [callback] The callback to be called when the function 960 * completes. 961 * @param {boolean} [requireUserInput=false] If true, the function should 962 * fail if the browser is not currently handling user input. 963 * @returns {Promise|undefined} Must be void if `callback` is set, and a 964 * promise otherwise. The promise is resolved when the function completes. 965 */ 966 callAsyncFunction(args, callback, requireUserInput = false) { 967 throw new Error("Not implemented"); 968 } 969 970 /** 971 * Retrieves the value of this as a property. 972 * 973 * @abstract 974 * @returns {*} The value of the property. 975 */ 976 getProperty() { 977 throw new Error("Not implemented"); 978 } 979 980 /** 981 * Assigns the value to this as property. 982 * 983 * @abstract 984 * @param {string} value The new value of the property. 985 */ 986 setProperty(value) { 987 throw new Error("Not implemented"); 988 } 989 990 /** 991 * Registers a `listener` to this as an event. 992 * 993 * @abstract 994 * @param {function} listener The callback to be called when the event fires. 995 * @param {Array} args Extra parameters for EventManager.addListener. 996 * @see EventManager.addListener 997 */ 998 addListener(listener, args) { 999 throw new Error("Not implemented"); 1000 } 1001 1002 /** 1003 * Checks whether `listener` is listening to this as an event. 1004 * 1005 * @abstract 1006 * @param {function} listener The event listener. 1007 * @returns {boolean} Whether `listener` is registered with this as an event. 1008 * @see EventManager.hasListener 1009 */ 1010 hasListener(listener) { 1011 throw new Error("Not implemented"); 1012 } 1013 1014 /** 1015 * Unregisters `listener` from this as an event. 1016 * 1017 * @abstract 1018 * @param {function} listener The event listener. 1019 * @see EventManager.removeListener 1020 */ 1021 removeListener(listener) { 1022 throw new Error("Not implemented"); 1023 } 1024 1025 /** 1026 * Revokes the implementation object, and prevents any further method 1027 * calls from having external effects. 1028 * 1029 * @abstract 1030 */ 1031 revoke() { 1032 throw new Error("Not implemented"); 1033 } 1034} 1035 1036/** 1037 * An object that runs a locally implemented API. 1038 */ 1039class LocalAPIImplementation extends SchemaAPIInterface { 1040 /** 1041 * Constructs an implementation of the `name` method or property of `pathObj`. 1042 * 1043 * @param {object} pathObj The object containing the member with name `name`. 1044 * @param {string} name The name of the implemented member. 1045 * @param {BaseContext} context The context in which the schema is injected. 1046 */ 1047 constructor(pathObj, name, context) { 1048 super(); 1049 this.pathObj = pathObj; 1050 this.name = name; 1051 this.context = context; 1052 } 1053 1054 revoke() { 1055 if (this.pathObj[this.name][Schemas.REVOKE]) { 1056 this.pathObj[this.name][Schemas.REVOKE](); 1057 } 1058 1059 this.pathObj = null; 1060 this.name = null; 1061 this.context = null; 1062 } 1063 1064 callFunction(args) { 1065 try { 1066 return this.pathObj[this.name](...args); 1067 } catch (e) { 1068 throw this.context.normalizeError(e); 1069 } 1070 } 1071 1072 callFunctionNoReturn(args) { 1073 try { 1074 this.pathObj[this.name](...args); 1075 } catch (e) { 1076 throw this.context.normalizeError(e); 1077 } 1078 } 1079 1080 callAsyncFunction(args, callback, requireUserInput) { 1081 let promise; 1082 try { 1083 if (requireUserInput) { 1084 if (!getWinUtils(this.context.contentWindow).isHandlingUserInput) { 1085 throw new ExtensionError( 1086 `${this.name} may only be called from a user input handler` 1087 ); 1088 } 1089 } 1090 promise = this.pathObj[this.name](...args) || Promise.resolve(); 1091 } catch (e) { 1092 promise = Promise.reject(e); 1093 } 1094 return this.context.wrapPromise(promise, callback); 1095 } 1096 1097 getProperty() { 1098 return this.pathObj[this.name]; 1099 } 1100 1101 setProperty(value) { 1102 this.pathObj[this.name] = value; 1103 } 1104 1105 addListener(listener, args) { 1106 try { 1107 this.pathObj[this.name].addListener.call(null, listener, ...args); 1108 } catch (e) { 1109 throw this.context.normalizeError(e); 1110 } 1111 } 1112 1113 hasListener(listener) { 1114 return this.pathObj[this.name].hasListener.call(null, listener); 1115 } 1116 1117 removeListener(listener) { 1118 this.pathObj[this.name].removeListener.call(null, listener); 1119 } 1120} 1121 1122// Recursively copy properties from source to dest. 1123function deepCopy(dest, source) { 1124 for (let prop in source) { 1125 let desc = Object.getOwnPropertyDescriptor(source, prop); 1126 if (typeof desc.value == "object") { 1127 if (!(prop in dest)) { 1128 dest[prop] = {}; 1129 } 1130 deepCopy(dest[prop], source[prop]); 1131 } else { 1132 Object.defineProperty(dest, prop, desc); 1133 } 1134 } 1135} 1136 1137function getChild(map, key) { 1138 let child = map.children.get(key); 1139 if (!child) { 1140 child = { 1141 modules: new Set(), 1142 children: new Map(), 1143 }; 1144 1145 map.children.set(key, child); 1146 } 1147 return child; 1148} 1149 1150function getPath(map, path) { 1151 for (let key of path) { 1152 map = getChild(map, key); 1153 } 1154 return map; 1155} 1156 1157function mergePaths(dest, source) { 1158 for (let name of source.modules) { 1159 dest.modules.add(name); 1160 } 1161 1162 for (let [name, child] of source.children.entries()) { 1163 mergePaths(getChild(dest, name), child); 1164 } 1165} 1166 1167/** 1168 * Manages loading and accessing a set of APIs for a specific extension 1169 * context. 1170 * 1171 * @param {BaseContext} context 1172 * The context to manage APIs for. 1173 * @param {SchemaAPIManager} apiManager 1174 * The API manager holding the APIs to manage. 1175 * @param {object} root 1176 * The root object into which APIs will be injected. 1177 */ 1178class CanOfAPIs { 1179 constructor(context, apiManager, root) { 1180 this.context = context; 1181 this.scopeName = context.envType; 1182 this.apiManager = apiManager; 1183 this.root = root; 1184 1185 this.apiPaths = new Map(); 1186 1187 this.apis = new Map(); 1188 } 1189 1190 /** 1191 * Synchronously loads and initializes an ExtensionAPI instance. 1192 * 1193 * @param {string} name 1194 * The name of the API to load. 1195 */ 1196 loadAPI(name) { 1197 if (this.apis.has(name)) { 1198 return; 1199 } 1200 1201 let { extension } = this.context; 1202 1203 let api = this.apiManager.getAPI(name, extension, this.scopeName); 1204 if (!api) { 1205 return; 1206 } 1207 1208 this.apis.set(name, api); 1209 1210 deepCopy(this.root, api.getAPI(this.context)); 1211 } 1212 1213 /** 1214 * Asynchronously loads and initializes an ExtensionAPI instance. 1215 * 1216 * @param {string} name 1217 * The name of the API to load. 1218 */ 1219 async asyncLoadAPI(name) { 1220 if (this.apis.has(name)) { 1221 return; 1222 } 1223 1224 let { extension } = this.context; 1225 if (!Schemas.checkPermissions(name, extension)) { 1226 return; 1227 } 1228 1229 let api = await this.apiManager.asyncGetAPI( 1230 name, 1231 extension, 1232 this.scopeName 1233 ); 1234 // Check again, because async; 1235 if (this.apis.has(name)) { 1236 return; 1237 } 1238 1239 this.apis.set(name, api); 1240 1241 deepCopy(this.root, api.getAPI(this.context)); 1242 } 1243 1244 /** 1245 * Finds the API at the given path from the root object, and 1246 * synchronously loads the API that implements it if it has not 1247 * already been loaded. 1248 * 1249 * @param {string} path 1250 * The "."-separated path to find. 1251 * @returns {*} 1252 */ 1253 findAPIPath(path) { 1254 if (this.apiPaths.has(path)) { 1255 return this.apiPaths.get(path); 1256 } 1257 1258 let obj = this.root; 1259 let modules = this.apiManager.modulePaths; 1260 1261 let parts = path.split("."); 1262 for (let [i, key] of parts.entries()) { 1263 if (!obj) { 1264 return; 1265 } 1266 modules = getChild(modules, key); 1267 1268 for (let name of modules.modules) { 1269 if (!this.apis.has(name)) { 1270 this.loadAPI(name); 1271 } 1272 } 1273 1274 if (!(key in obj) && i < parts.length - 1) { 1275 obj[key] = {}; 1276 } 1277 obj = obj[key]; 1278 } 1279 1280 this.apiPaths.set(path, obj); 1281 return obj; 1282 } 1283 1284 /** 1285 * Finds the API at the given path from the root object, and 1286 * asynchronously loads the API that implements it if it has not 1287 * already been loaded. 1288 * 1289 * @param {string} path 1290 * The "."-separated path to find. 1291 * @returns {Promise<*>} 1292 */ 1293 async asyncFindAPIPath(path) { 1294 if (this.apiPaths.has(path)) { 1295 return this.apiPaths.get(path); 1296 } 1297 1298 let obj = this.root; 1299 let modules = this.apiManager.modulePaths; 1300 1301 let parts = path.split("."); 1302 for (let [i, key] of parts.entries()) { 1303 if (!obj) { 1304 return; 1305 } 1306 modules = getChild(modules, key); 1307 1308 for (let name of modules.modules) { 1309 if (!this.apis.has(name)) { 1310 await this.asyncLoadAPI(name); 1311 } 1312 } 1313 1314 if (!(key in obj) && i < parts.length - 1) { 1315 obj[key] = {}; 1316 } 1317 1318 if (typeof obj[key] === "function") { 1319 obj = obj[key].bind(obj); 1320 } else { 1321 obj = obj[key]; 1322 } 1323 } 1324 1325 this.apiPaths.set(path, obj); 1326 return obj; 1327 } 1328} 1329 1330/** 1331 * @class APIModule 1332 * @abstract 1333 * 1334 * @property {string} url 1335 * The URL of the script which contains the module's 1336 * implementation. This script must define a global property 1337 * matching the modules name, which must be a class constructor 1338 * which inherits from {@link ExtensionAPI}. 1339 * 1340 * @property {string} schema 1341 * The URL of the JSON schema which describes the module's API. 1342 * 1343 * @property {Array<string>} scopes 1344 * The list of scope names into which the API may be loaded. 1345 * 1346 * @property {Array<string>} manifest 1347 * The list of top-level manifest properties which will trigger 1348 * the module to be loaded, and its `onManifestEntry` method to be 1349 * called. 1350 * 1351 * @property {Array<string>} events 1352 * The list events which will trigger the module to be loaded, and 1353 * its appropriate event handler method to be called. Currently 1354 * only accepts "startup". 1355 * 1356 * @property {Array<string>} permissions 1357 * An optional list of permissions, any of which must be present 1358 * in order for the module to load. 1359 * 1360 * @property {Array<Array<string>>} paths 1361 * A list of paths from the root API object which, when accessed, 1362 * will cause the API module to be instantiated and injected. 1363 */ 1364 1365/** 1366 * This object loads the ext-*.js scripts that define the extension API. 1367 * 1368 * This class instance is shared with the scripts that it loads, so that the 1369 * ext-*.js scripts and the instantiator can communicate with each other. 1370 */ 1371class SchemaAPIManager extends EventEmitter { 1372 /** 1373 * @param {string} processType 1374 * "main" - The main, one and only chrome browser process. 1375 * "addon" - An addon process. 1376 * "content" - A content process. 1377 * "devtools" - A devtools process. 1378 * @param {SchemaRoot} schema 1379 */ 1380 constructor(processType, schema) { 1381 super(); 1382 this.processType = processType; 1383 this.global = null; 1384 if (schema) { 1385 this.schema = schema; 1386 } 1387 1388 this.modules = new Map(); 1389 this.modulePaths = { children: new Map(), modules: new Set() }; 1390 this.manifestKeys = new Map(); 1391 this.eventModules = new DefaultMap(() => new Set()); 1392 this.settingsModules = new Set(); 1393 1394 this._modulesJSONLoaded = false; 1395 1396 this.schemaURLs = new Map(); 1397 1398 this.apis = new DefaultWeakMap(() => new Map()); 1399 1400 this._scriptScopes = []; 1401 } 1402 1403 onStartup(extension) { 1404 let promises = []; 1405 for (let apiName of this.eventModules.get("startup")) { 1406 promises.push( 1407 extension.apiManager.asyncGetAPI(apiName, extension).then(api => { 1408 if (api) { 1409 api.onStartup(); 1410 } 1411 }) 1412 ); 1413 } 1414 1415 return Promise.all(promises); 1416 } 1417 1418 async loadModuleJSON(urls) { 1419 let promises = urls.map(url => fetch(url).then(resp => resp.json())); 1420 1421 return this.initModuleJSON(await Promise.all(promises)); 1422 } 1423 1424 initModuleJSON(blobs) { 1425 for (let json of blobs) { 1426 this.registerModules(json); 1427 } 1428 1429 this._modulesJSONLoaded = true; 1430 1431 return new StructuredCloneHolder({ 1432 modules: this.modules, 1433 modulePaths: this.modulePaths, 1434 manifestKeys: this.manifestKeys, 1435 eventModules: this.eventModules, 1436 settingsModules: this.settingsModules, 1437 schemaURLs: this.schemaURLs, 1438 }); 1439 } 1440 1441 initModuleData(moduleData) { 1442 if (!this._modulesJSONLoaded) { 1443 let data = moduleData.deserialize({}, true); 1444 1445 this.modules = data.modules; 1446 this.modulePaths = data.modulePaths; 1447 this.manifestKeys = data.manifestKeys; 1448 this.eventModules = new DefaultMap(() => new Set(), data.eventModules); 1449 this.settingsModules = new Set(data.settingsModules); 1450 this.schemaURLs = data.schemaURLs; 1451 } 1452 1453 this._modulesJSONLoaded = true; 1454 } 1455 1456 /** 1457 * Registers a set of ExtensionAPI modules to be lazily loaded and 1458 * managed by this manager. 1459 * 1460 * @param {object} obj 1461 * An object containing property for eacy API module to be 1462 * registered. Each value should be an object implementing the 1463 * APIModule interface. 1464 */ 1465 registerModules(obj) { 1466 for (let [name, details] of Object.entries(obj)) { 1467 details.namespaceName = name; 1468 1469 if (this.modules.has(name)) { 1470 throw new Error(`Module '${name}' already registered`); 1471 } 1472 this.modules.set(name, details); 1473 1474 if (details.schema) { 1475 let content = 1476 details.scopes && 1477 (details.scopes.includes("content_parent") || 1478 details.scopes.includes("content_child")); 1479 this.schemaURLs.set(details.schema, { content }); 1480 } 1481 1482 for (let event of details.events || []) { 1483 this.eventModules.get(event).add(name); 1484 } 1485 1486 if (details.settings) { 1487 this.settingsModules.add(name); 1488 } 1489 1490 for (let key of details.manifest || []) { 1491 if (this.manifestKeys.has(key)) { 1492 throw new Error( 1493 `Manifest key '${key}' already registered by '${this.manifestKeys.get( 1494 key 1495 )}'` 1496 ); 1497 } 1498 1499 this.manifestKeys.set(key, name); 1500 } 1501 1502 for (let path of details.paths || []) { 1503 getPath(this.modulePaths, path).modules.add(name); 1504 } 1505 } 1506 } 1507 1508 /** 1509 * Emits an `onManifestEntry` event for the top-level manifest entry 1510 * on all relevant {@link ExtensionAPI} instances for the given 1511 * extension. 1512 * 1513 * The API modules will be synchronously loaded if they have not been 1514 * loaded already. 1515 * 1516 * @param {Extension} extension 1517 * The extension for which to emit the events. 1518 * @param {string} entry 1519 * The name of the top-level manifest entry. 1520 * 1521 * @returns {*} 1522 */ 1523 emitManifestEntry(extension, entry) { 1524 let apiName = this.manifestKeys.get(entry); 1525 if (apiName) { 1526 let api = extension.apiManager.getAPI(apiName, extension); 1527 return api.onManifestEntry(entry); 1528 } 1529 } 1530 /** 1531 * Emits an `onManifestEntry` event for the top-level manifest entry 1532 * on all relevant {@link ExtensionAPI} instances for the given 1533 * extension. 1534 * 1535 * The API modules will be asynchronously loaded if they have not been 1536 * loaded already. 1537 * 1538 * @param {Extension} extension 1539 * The extension for which to emit the events. 1540 * @param {string} entry 1541 * The name of the top-level manifest entry. 1542 * 1543 * @returns {Promise<*>} 1544 */ 1545 async asyncEmitManifestEntry(extension, entry) { 1546 let apiName = this.manifestKeys.get(entry); 1547 if (apiName) { 1548 let api = await extension.apiManager.asyncGetAPI(apiName, extension); 1549 return api.onManifestEntry(entry); 1550 } 1551 } 1552 1553 /** 1554 * Returns the {@link ExtensionAPI} instance for the given API module, 1555 * for the given extension, in the given scope, synchronously loading 1556 * and instantiating it if necessary. 1557 * 1558 * @param {string} name 1559 * The name of the API module to load. 1560 * @param {Extension} extension 1561 * The extension for which to load the API. 1562 * @param {string} [scope = null] 1563 * The scope type for which to retrieve the API, or null if not 1564 * being retrieved for a particular scope. 1565 * 1566 * @returns {ExtensionAPI?} 1567 */ 1568 getAPI(name, extension, scope = null) { 1569 if (!this._checkGetAPI(name, extension, scope)) { 1570 return; 1571 } 1572 1573 let apis = this.apis.get(extension); 1574 if (apis.has(name)) { 1575 return apis.get(name); 1576 } 1577 1578 let module = this.loadModule(name); 1579 1580 let api = new module(extension); 1581 apis.set(name, api); 1582 return api; 1583 } 1584 /** 1585 * Returns the {@link ExtensionAPI} instance for the given API module, 1586 * for the given extension, in the given scope, asynchronously loading 1587 * and instantiating it if necessary. 1588 * 1589 * @param {string} name 1590 * The name of the API module to load. 1591 * @param {Extension} extension 1592 * The extension for which to load the API. 1593 * @param {string} [scope = null] 1594 * The scope type for which to retrieve the API, or null if not 1595 * being retrieved for a particular scope. 1596 * 1597 * @returns {Promise<ExtensionAPI>?} 1598 */ 1599 async asyncGetAPI(name, extension, scope = null) { 1600 if (!this._checkGetAPI(name, extension, scope)) { 1601 return; 1602 } 1603 1604 let apis = this.apis.get(extension); 1605 if (apis.has(name)) { 1606 return apis.get(name); 1607 } 1608 1609 let module = await this.asyncLoadModule(name); 1610 1611 // Check again, because async. 1612 if (apis.has(name)) { 1613 return apis.get(name); 1614 } 1615 1616 let api = new module(extension); 1617 apis.set(name, api); 1618 return api; 1619 } 1620 1621 /** 1622 * Synchronously loads an API module, if not already loaded, and 1623 * returns its ExtensionAPI constructor. 1624 * 1625 * @param {string} name 1626 * The name of the module to load. 1627 * 1628 * @returns {class} 1629 */ 1630 loadModule(name) { 1631 let module = this.modules.get(name); 1632 if (module.loaded) { 1633 return this.global[name]; 1634 } 1635 1636 this._checkLoadModule(module, name); 1637 1638 this.initGlobal(); 1639 1640 Services.scriptloader.loadSubScript(module.url, this.global); 1641 1642 module.loaded = true; 1643 1644 return this.global[name]; 1645 } 1646 /** 1647 * aSynchronously loads an API module, if not already loaded, and 1648 * returns its ExtensionAPI constructor. 1649 * 1650 * @param {string} name 1651 * The name of the module to load. 1652 * 1653 * @returns {Promise<class>} 1654 */ 1655 asyncLoadModule(name) { 1656 let module = this.modules.get(name); 1657 if (module.loaded) { 1658 return Promise.resolve(this.global[name]); 1659 } 1660 if (module.asyncLoaded) { 1661 return module.asyncLoaded; 1662 } 1663 1664 this._checkLoadModule(module, name); 1665 1666 module.asyncLoaded = ChromeUtils.compileScript(module.url).then(script => { 1667 this.initGlobal(); 1668 script.executeInGlobal(this.global); 1669 1670 module.loaded = true; 1671 1672 return this.global[name]; 1673 }); 1674 1675 return module.asyncLoaded; 1676 } 1677 1678 asyncLoadSettingsModules() { 1679 return Promise.all( 1680 Array.from(this.settingsModules).map(apiName => 1681 this.asyncLoadModule(apiName) 1682 ) 1683 ); 1684 } 1685 1686 getModule(name) { 1687 return this.modules.get(name); 1688 } 1689 1690 /** 1691 * Checks whether the given API module may be loaded for the given 1692 * extension, in the given scope. 1693 * 1694 * @param {string} name 1695 * The name of the API module to check. 1696 * @param {Extension} extension 1697 * The extension for which to check the API. 1698 * @param {string} [scope = null] 1699 * The scope type for which to check the API, or null if not 1700 * being checked for a particular scope. 1701 * 1702 * @returns {boolean} 1703 * Whether the module may be loaded. 1704 */ 1705 _checkGetAPI(name, extension, scope = null) { 1706 let module = this.getModule(name); 1707 1708 if ( 1709 module.permissions && 1710 !module.permissions.some(perm => extension.hasPermission(perm)) 1711 ) { 1712 return false; 1713 } 1714 1715 if (!scope) { 1716 return true; 1717 } 1718 1719 if (!module.scopes.includes(scope)) { 1720 return false; 1721 } 1722 1723 if (!Schemas.checkPermissions(module.namespaceName, extension)) { 1724 return false; 1725 } 1726 1727 return true; 1728 } 1729 1730 _checkLoadModule(module, name) { 1731 if (!module) { 1732 throw new Error(`Module '${name}' does not exist`); 1733 } 1734 if (module.asyncLoaded) { 1735 throw new Error(`Module '${name}' currently being lazily loaded`); 1736 } 1737 if (this.global && this.global[name]) { 1738 throw new Error( 1739 `Module '${name}' conflicts with existing global property` 1740 ); 1741 } 1742 } 1743 1744 /** 1745 * Create a global object that is used as the shared global for all ext-*.js 1746 * scripts that are loaded via `loadScript`. 1747 * 1748 * @returns {object} A sandbox that is used as the global by `loadScript`. 1749 */ 1750 _createExtGlobal() { 1751 let global = Cu.Sandbox( 1752 Services.scriptSecurityManager.getSystemPrincipal(), 1753 { 1754 wantXrays: false, 1755 wantGlobalProperties: ["ChromeUtils"], 1756 sandboxName: `Namespace of ext-*.js scripts for ${this.processType} (from: resource://gre/modules/ExtensionCommon.jsm)`, 1757 } 1758 ); 1759 1760 Object.assign(global, { 1761 Cc, 1762 ChromeWorker, 1763 Ci, 1764 Cr, 1765 Cu, 1766 ExtensionAPI, 1767 ExtensionCommon, 1768 MatchGlob, 1769 MatchPattern, 1770 MatchPatternSet, 1771 StructuredCloneHolder, 1772 XPCOMUtils, 1773 extensions: this, 1774 global, 1775 }); 1776 1777 ChromeUtils.import("resource://gre/modules/AppConstants.jsm", global); 1778 1779 XPCOMUtils.defineLazyGetter(global, "console", getConsole); 1780 1781 XPCOMUtils.defineLazyModuleGetters(global, { 1782 ExtensionUtils: "resource://gre/modules/ExtensionUtils.jsm", 1783 XPCOMUtils: "resource://gre/modules/XPCOMUtils.jsm", 1784 }); 1785 1786 return global; 1787 } 1788 1789 initGlobal() { 1790 if (!this.global) { 1791 this.global = this._createExtGlobal(); 1792 } 1793 } 1794 1795 /** 1796 * Load an ext-*.js script. The script runs in its own scope, if it wishes to 1797 * share state with another script it can assign to the `global` variable. If 1798 * it wishes to communicate with this API manager, use `extensions`. 1799 * 1800 * @param {string} scriptUrl The URL of the ext-*.js script. 1801 */ 1802 loadScript(scriptUrl) { 1803 // Create the object in the context of the sandbox so that the script runs 1804 // in the sandbox's context instead of here. 1805 let scope = Cu.createObjectIn(this.global); 1806 1807 Services.scriptloader.loadSubScript(scriptUrl, scope); 1808 1809 // Save the scope to avoid it being garbage collected. 1810 this._scriptScopes.push(scope); 1811 } 1812} 1813 1814class LazyAPIManager extends SchemaAPIManager { 1815 constructor(processType, moduleData, schemaURLs) { 1816 super(processType); 1817 1818 this.initialized = false; 1819 1820 this.initModuleData(moduleData); 1821 1822 this.schemaURLs = schemaURLs; 1823 } 1824} 1825 1826defineLazyGetter(LazyAPIManager.prototype, "schema", function() { 1827 let root = new SchemaRoot(Schemas.rootSchema, this.schemaURLs); 1828 root.parseSchemas(); 1829 return root; 1830}); 1831 1832class MultiAPIManager extends SchemaAPIManager { 1833 constructor(processType, children) { 1834 super(processType); 1835 1836 this.initialized = false; 1837 1838 this.children = children; 1839 } 1840 1841 async lazyInit() { 1842 if (!this.initialized) { 1843 this.initialized = true; 1844 1845 for (let child of this.children) { 1846 if (child.lazyInit) { 1847 let res = child.lazyInit(); 1848 if (res && typeof res.then === "function") { 1849 await res; 1850 } 1851 } 1852 1853 mergePaths(this.modulePaths, child.modulePaths); 1854 } 1855 } 1856 } 1857 1858 onStartup(extension) { 1859 return Promise.all(this.children.map(child => child.onStartup(extension))); 1860 } 1861 1862 getModule(name) { 1863 for (let child of this.children) { 1864 if (child.modules.has(name)) { 1865 return child.modules.get(name); 1866 } 1867 } 1868 } 1869 1870 loadModule(name) { 1871 for (let child of this.children) { 1872 if (child.modules.has(name)) { 1873 return child.loadModule(name); 1874 } 1875 } 1876 } 1877 1878 asyncLoadModule(name) { 1879 for (let child of this.children) { 1880 if (child.modules.has(name)) { 1881 return child.asyncLoadModule(name); 1882 } 1883 } 1884 } 1885} 1886 1887defineLazyGetter(MultiAPIManager.prototype, "schema", function() { 1888 let bases = this.children.map(child => child.schema); 1889 1890 // All API manager schema roots should derive from the global schema root, 1891 // so it doesn't need its own entry. 1892 if (bases[bases.length - 1] === Schemas) { 1893 bases.pop(); 1894 } 1895 1896 if (bases.length === 1) { 1897 bases = bases[0]; 1898 } 1899 return new SchemaRoot(bases, new Map()); 1900}); 1901 1902function LocaleData(data) { 1903 this.defaultLocale = data.defaultLocale; 1904 this.selectedLocale = data.selectedLocale; 1905 this.locales = data.locales || new Map(); 1906 this.warnedMissingKeys = new Set(); 1907 1908 // Map(locale-name -> Map(message-key -> localized-string)) 1909 // 1910 // Contains a key for each loaded locale, each of which is a 1911 // Map of message keys to their localized strings. 1912 this.messages = data.messages || new Map(); 1913 1914 if (data.builtinMessages) { 1915 this.messages.set(this.BUILTIN, data.builtinMessages); 1916 } 1917} 1918 1919LocaleData.prototype = { 1920 // Representation of the object to send to content processes. This 1921 // should include anything the content process might need. 1922 serialize() { 1923 return { 1924 defaultLocale: this.defaultLocale, 1925 selectedLocale: this.selectedLocale, 1926 messages: this.messages, 1927 locales: this.locales, 1928 }; 1929 }, 1930 1931 BUILTIN: "@@BUILTIN_MESSAGES", 1932 1933 has(locale) { 1934 return this.messages.has(locale); 1935 }, 1936 1937 // https://developer.chrome.com/extensions/i18n 1938 localizeMessage(message, substitutions = [], options = {}) { 1939 let defaultOptions = { 1940 defaultValue: "", 1941 cloneScope: null, 1942 }; 1943 1944 let locales = this.availableLocales; 1945 if (options.locale) { 1946 locales = new Set( 1947 [this.BUILTIN, options.locale, this.defaultLocale].filter(locale => 1948 this.messages.has(locale) 1949 ) 1950 ); 1951 } 1952 1953 options = Object.assign(defaultOptions, options); 1954 1955 // Message names are case-insensitive, so normalize them to lower-case. 1956 message = message.toLowerCase(); 1957 for (let locale of locales) { 1958 let messages = this.messages.get(locale); 1959 if (messages.has(message)) { 1960 let str = messages.get(message); 1961 1962 if (!str.includes("$")) { 1963 return str; 1964 } 1965 1966 if (!Array.isArray(substitutions)) { 1967 substitutions = [substitutions]; 1968 } 1969 1970 let replacer = (matched, index, dollarSigns) => { 1971 if (index) { 1972 // This is not quite Chrome-compatible. Chrome consumes any number 1973 // of digits following the $, but only accepts 9 substitutions. We 1974 // accept any number of substitutions. 1975 index = parseInt(index, 10) - 1; 1976 return index in substitutions ? substitutions[index] : ""; 1977 } 1978 // For any series of contiguous `$`s, the first is dropped, and 1979 // the rest remain in the output string. 1980 return dollarSigns; 1981 }; 1982 return str.replace(/\$(?:([1-9]\d*)|(\$+))/g, replacer); 1983 } 1984 } 1985 1986 // Check for certain pre-defined messages. 1987 if (message == "@@ui_locale") { 1988 return this.uiLocale; 1989 } else if (message.startsWith("@@bidi_")) { 1990 let rtl = Services.locale.isAppLocaleRTL; 1991 1992 if (message == "@@bidi_dir") { 1993 return rtl ? "rtl" : "ltr"; 1994 } else if (message == "@@bidi_reversed_dir") { 1995 return rtl ? "ltr" : "rtl"; 1996 } else if (message == "@@bidi_start_edge") { 1997 return rtl ? "right" : "left"; 1998 } else if (message == "@@bidi_end_edge") { 1999 return rtl ? "left" : "right"; 2000 } 2001 } 2002 2003 if (!this.warnedMissingKeys.has(message)) { 2004 let error = `Unknown localization message ${message}`; 2005 if (options.cloneScope) { 2006 error = new options.cloneScope.Error(error); 2007 } 2008 Cu.reportError(error); 2009 this.warnedMissingKeys.add(message); 2010 } 2011 return options.defaultValue; 2012 }, 2013 2014 // Localize a string, replacing all |__MSG_(.*)__| tokens with the 2015 // matching string from the current locale, as determined by 2016 // |this.selectedLocale|. 2017 // 2018 // This may not be called before calling either |initLocale| or 2019 // |initAllLocales|. 2020 localize(str, locale = this.selectedLocale) { 2021 if (!str) { 2022 return str; 2023 } 2024 2025 return str.replace(/__MSG_([A-Za-z0-9@_]+?)__/g, (matched, message) => { 2026 return this.localizeMessage(message, [], { 2027 locale, 2028 defaultValue: matched, 2029 }); 2030 }); 2031 }, 2032 2033 // Validates the contents of a locale JSON file, normalizes the 2034 // messages into a Map of message key -> localized string pairs. 2035 addLocale(locale, messages, extension) { 2036 let result = new Map(); 2037 2038 let isPlainObject = obj => 2039 obj && 2040 typeof obj === "object" && 2041 ChromeUtils.getClassName(obj) === "Object"; 2042 2043 // Chrome does not document the semantics of its localization 2044 // system very well. It handles replacements by pre-processing 2045 // messages, replacing |$[a-zA-Z0-9@_]+$| tokens with the value of their 2046 // replacements. Later, it processes the resulting string for 2047 // |$[0-9]| replacements. 2048 // 2049 // Again, it does not document this, but it accepts any number 2050 // of sequential |$|s, and replaces them with that number minus 2051 // 1. It also accepts |$| followed by any number of sequential 2052 // digits, but refuses to process a localized string which 2053 // provides more than 9 substitutions. 2054 if (!isPlainObject(messages)) { 2055 extension.packagingError(`Invalid locale data for ${locale}`); 2056 return result; 2057 } 2058 2059 for (let key of Object.keys(messages)) { 2060 let msg = messages[key]; 2061 2062 if (!isPlainObject(msg) || typeof msg.message != "string") { 2063 extension.packagingError( 2064 `Invalid locale message data for ${locale}, message ${JSON.stringify( 2065 key 2066 )}` 2067 ); 2068 continue; 2069 } 2070 2071 // Substitutions are case-insensitive, so normalize all of their names 2072 // to lower-case. 2073 let placeholders = new Map(); 2074 if ("placeholders" in msg && isPlainObject(msg.placeholders)) { 2075 for (let key of Object.keys(msg.placeholders)) { 2076 placeholders.set(key.toLowerCase(), msg.placeholders[key]); 2077 } 2078 } 2079 2080 let replacer = (match, name) => { 2081 let replacement = placeholders.get(name.toLowerCase()); 2082 if (isPlainObject(replacement) && "content" in replacement) { 2083 return replacement.content; 2084 } 2085 return ""; 2086 }; 2087 2088 let value = msg.message.replace(/\$([A-Za-z0-9@_]+)\$/g, replacer); 2089 2090 // Message names are also case-insensitive, so normalize them to lower-case. 2091 result.set(key.toLowerCase(), value); 2092 } 2093 2094 this.messages.set(locale, result); 2095 return result; 2096 }, 2097 2098 get acceptLanguages() { 2099 let result = Services.prefs.getComplexValue( 2100 "intl.accept_languages", 2101 Ci.nsIPrefLocalizedString 2102 ).data; 2103 return result.split(/\s*,\s*/g); 2104 }, 2105 2106 get uiLocale() { 2107 return Services.locale.appLocaleAsBCP47; 2108 }, 2109}; 2110 2111defineLazyGetter(LocaleData.prototype, "availableLocales", function() { 2112 return new Set( 2113 [this.BUILTIN, this.selectedLocale, this.defaultLocale].filter(locale => 2114 this.messages.has(locale) 2115 ) 2116 ); 2117}); 2118 2119/** 2120 * This is a generic class for managing event listeners. 2121 * 2122 * @example 2123 * new EventManager({ 2124 * context, 2125 * name: "api.subAPI", 2126 * register: fire => { 2127 * let listener = (...) => { 2128 * // Fire any listeners registered with addListener. 2129 * fire.async(arg1, arg2); 2130 * }; 2131 * // Register the listener. 2132 * SomehowRegisterListener(listener); 2133 * return () => { 2134 * // Return a way to unregister the listener. 2135 * SomehowUnregisterListener(listener); 2136 * }; 2137 * } 2138 * }).api() 2139 * 2140 * The result is an object with addListener, removeListener, and 2141 * hasListener methods. `context` is an add-on scope (either an 2142 * ExtensionContext in the chrome process or ExtensionContext in a 2143 * content process). 2144 */ 2145class EventManager { 2146 /* 2147 * @param {object} params 2148 * Parameters that control this EventManager. 2149 * @param {BaseContext} params.context 2150 * An object representing the extension instance using this event. 2151 * @param {string} params.name 2152 * A name used only for debugging. 2153 * @param {functon} params.register 2154 * A function called whenever a new listener is added. 2155 * @param {boolean} [params.inputHandling=false] 2156 * If true, the "handling user input" flag is set while handlers 2157 * for this event are executing. 2158 * @param {object} [params.persistent] 2159 * Details for persistent event listeners 2160 * @param {string} params.persistent.module 2161 * The name of the module in which this event is defined. 2162 * @param {string} params.persistent.event 2163 * The name of this event. 2164 */ 2165 constructor(params) { 2166 let { 2167 context, 2168 name, 2169 register, 2170 inputHandling = false, 2171 persistent = null, 2172 } = params; 2173 this.context = context; 2174 this.name = name; 2175 this.register = register; 2176 this.inputHandling = inputHandling; 2177 this.persistent = persistent; 2178 2179 // Don't bother with persistent event handling if delayed background 2180 // startup is not enabled. 2181 if (!DELAYED_BG_STARTUP) { 2182 this.persistent = null; 2183 } 2184 2185 this.unregister = new Map(); 2186 this.remove = new Map(); 2187 2188 if (this.persistent) { 2189 if (AppConstants.DEBUG) { 2190 if (this.context.envType !== "addon_parent") { 2191 throw new Error( 2192 "Persistent event managers can only be created for addon_parent" 2193 ); 2194 } 2195 if (!this.persistent.module || !this.persistent.event) { 2196 throw new Error( 2197 "Persistent event manager must specify module and event" 2198 ); 2199 } 2200 } 2201 if (this.context.viewType !== "background") { 2202 this.persistent = null; 2203 } 2204 } 2205 } 2206 2207 /* 2208 * Information about listeners to persistent events is associated with 2209 * the extension to which they belong. Any extension thas has such 2210 * listeners has a property called `persistentListeners` that is a 2211 * 3-level Map. The first 2 keys are the module name (e.g., webRequest) 2212 * and the name of the event within the module (e.g., onBeforeRequest). 2213 * The third level of the map is used to track multiple listeners for 2214 * the same event, these listeners are distinguished by the extra arguments 2215 * passed to addListener(). For quick lookups, the key to the third Map 2216 * is the result of calling uneval() on the array of extra arguments. 2217 * 2218 * The value stored in the Map is a plain object with a property called 2219 * `params` that is the original (ie, not uneval()ed) extra arguments to 2220 * addListener(). For a primed listener (i.e., the stub listener created 2221 * during browser startup before the extension background page is started, 2222 * the object also has a `primed` property that holds the things needed 2223 * to handle events during startup and eventually connect the listener 2224 * with a callback registered from the extension. 2225 * 2226 * @param {Extension} extension 2227 * @returns {boolean} True if the extension had any persistent listeners. 2228 */ 2229 static _initPersistentListeners(extension) { 2230 if (extension.persistentListeners) { 2231 return false; 2232 } 2233 2234 let listeners = new DefaultMap(() => new DefaultMap(() => new Map())); 2235 extension.persistentListeners = listeners; 2236 2237 let { persistentListeners } = extension.startupData; 2238 if (!persistentListeners) { 2239 return false; 2240 } 2241 2242 let found = false; 2243 for (let [module, entry] of Object.entries(persistentListeners)) { 2244 for (let [event, paramlists] of Object.entries(entry)) { 2245 for (let paramlist of paramlists) { 2246 let key = uneval(paramlist); 2247 listeners 2248 .get(module) 2249 .get(event) 2250 .set(key, { params: paramlist }); 2251 found = true; 2252 } 2253 } 2254 } 2255 return found; 2256 } 2257 2258 // Extract just the information needed at startup for all persistent 2259 // listeners, and arrange for it to be saved. This should be called 2260 // whenever the set of persistent listeners for an extension changes. 2261 static _writePersistentListeners(extension) { 2262 let startupListeners = {}; 2263 for (let [module, moduleEntry] of extension.persistentListeners) { 2264 startupListeners[module] = {}; 2265 for (let [event, eventEntry] of moduleEntry) { 2266 startupListeners[module][event] = Array.from( 2267 eventEntry.values(), 2268 listener => listener.params 2269 ); 2270 } 2271 } 2272 2273 extension.startupData.persistentListeners = startupListeners; 2274 extension.saveStartupData(); 2275 } 2276 2277 // Set up "primed" event listeners for any saved event listeners 2278 // in an extension's startup data. 2279 // This function is only called during browser startup, it stores details 2280 // about all primed listeners in the extension's persistentListeners Map. 2281 static primeListeners(extension) { 2282 if (!EventManager._initPersistentListeners(extension)) { 2283 return; 2284 } 2285 2286 for (let [module, moduleEntry] of extension.persistentListeners) { 2287 let api = extension.apiManager.getAPI(module, extension, "addon_parent"); 2288 if (!api.primeListener) { 2289 // The runtime module no longer implements primed listeners, drop them. 2290 extension.persistentListeners.delete(module); 2291 EventManager._writePersistentListeners(extension); 2292 continue; 2293 } 2294 for (let [event, eventEntry] of moduleEntry) { 2295 for (let listener of eventEntry.values()) { 2296 let primed = { pendingEvents: [] }; 2297 listener.primed = primed; 2298 2299 let fireEvent = (...args) => 2300 new Promise((resolve, reject) => { 2301 if (!listener.primed) { 2302 reject(new Error("primed listener not re-registered")); 2303 return; 2304 } 2305 primed.pendingEvents.push({ args, resolve, reject }); 2306 extension.emit("background-page-event"); 2307 }); 2308 2309 let fire = { 2310 wakeup: () => extension.wakeupBackground(), 2311 sync: fireEvent, 2312 async: fireEvent, 2313 }; 2314 2315 let { unregister, convert } = api.primeListener( 2316 extension, 2317 event, 2318 fire, 2319 listener.params 2320 ); 2321 Object.assign(primed, { unregister, convert }); 2322 } 2323 } 2324 } 2325 } 2326 2327 // Remove any primed listeners that were not re-registered. 2328 // This function is called after the background page has started. 2329 // The removed listeners are removed from the set of saved listeners, unless 2330 // `clearPersistent` is false. If false, the listeners are cleared from 2331 // memory, but not removed from the extension's startup data. 2332 static clearPrimedListeners(extension, clearPersistent = true) { 2333 for (let [module, moduleEntry] of extension.persistentListeners) { 2334 for (let [event, listeners] of moduleEntry) { 2335 for (let [key, listener] of listeners) { 2336 let { primed } = listener; 2337 if (!primed) { 2338 continue; 2339 } 2340 listener.primed = null; 2341 2342 for (let evt of primed.pendingEvents) { 2343 evt.reject(new Error("listener not re-registered")); 2344 } 2345 2346 if (clearPersistent) { 2347 EventManager.clearPersistentListener(extension, module, event, key); 2348 } 2349 primed.unregister(); 2350 } 2351 } 2352 } 2353 } 2354 2355 // Record the fact that there is a listener for the given event in 2356 // the given extension. `args` is an Array containing any extra 2357 // arguments that were passed to addListener(). 2358 static savePersistentListener(extension, module, event, args = []) { 2359 EventManager._initPersistentListeners(extension); 2360 let key = uneval(args); 2361 extension.persistentListeners 2362 .get(module) 2363 .get(event) 2364 .set(key, { params: args }); 2365 EventManager._writePersistentListeners(extension); 2366 } 2367 2368 // Remove the record for the given event listener from the extension's 2369 // startup data. `key` must be a string, the result of calling uneval() 2370 // on the array of extra arguments originally passed to addListener(). 2371 static clearPersistentListener(extension, module, event, key = uneval([])) { 2372 let listeners = extension.persistentListeners.get(module).get(event); 2373 listeners.delete(key); 2374 2375 if (listeners.size == 0) { 2376 let moduleEntry = extension.persistentListeners.get(module); 2377 moduleEntry.delete(event); 2378 if (moduleEntry.size == 0) { 2379 extension.persistentListeners.delete(module); 2380 } 2381 } 2382 2383 EventManager._writePersistentListeners(extension); 2384 } 2385 2386 addListener(callback, ...args) { 2387 if (this.unregister.has(callback)) { 2388 return; 2389 } 2390 this.context.logActivity("api_call", `${this.name}.addListener`, { args }); 2391 2392 let shouldFire = () => { 2393 if (this.context.unloaded) { 2394 dump(`${this.name} event fired after context unloaded.\n`); 2395 } else if (!this.context.active) { 2396 dump(`${this.name} event fired while context is inactive.\n`); 2397 } else if (this.unregister.has(callback)) { 2398 return true; 2399 } 2400 return false; 2401 }; 2402 2403 let fire = { 2404 sync: (...args) => { 2405 if (shouldFire()) { 2406 let result = this.context.applySafe(callback, args); 2407 this.context.logActivity("api_event", this.name, { args, result }); 2408 return result; 2409 } 2410 }, 2411 async: (...args) => { 2412 return Promise.resolve().then(() => { 2413 if (shouldFire()) { 2414 let result = this.context.applySafe(callback, args); 2415 this.context.logActivity("api_event", this.name, { args, result }); 2416 return result; 2417 } 2418 }); 2419 }, 2420 raw: (...args) => { 2421 if (!shouldFire()) { 2422 throw new Error("Called raw() on unloaded/inactive context"); 2423 } 2424 let result = Reflect.apply(callback, null, args); 2425 this.context.logActivity("api_event", this.name, { args, result }); 2426 return result; 2427 }, 2428 asyncWithoutClone: (...args) => { 2429 return Promise.resolve().then(() => { 2430 if (shouldFire()) { 2431 let result = this.context.applySafeWithoutClone(callback, args); 2432 this.context.logActivity("api_event", this.name, { args, result }); 2433 return result; 2434 } 2435 }); 2436 }, 2437 }; 2438 2439 let { extension } = this.context; 2440 2441 let unregister = null; 2442 let recordStartupData = false; 2443 2444 // If this is a persistent event, check for a listener that was already 2445 // created during startup. If there is one, use it and don't create a 2446 // new one. 2447 if (this.persistent) { 2448 recordStartupData = true; 2449 let { module, event } = this.persistent; 2450 2451 let key = uneval(args); 2452 EventManager._initPersistentListeners(extension); 2453 let listener = extension.persistentListeners 2454 .get(module) 2455 .get(event) 2456 .get(key); 2457 2458 if (listener) { 2459 // If extensions.webextensions.background-delayed-startup is disabled, 2460 // we can have stored info here but no primed listener. This check 2461 // can be removed if/when we make delayed background startup the only 2462 // supported setting. 2463 let { primed } = listener; 2464 if (primed) { 2465 listener.primed = null; 2466 2467 primed.convert(fire, this.context); 2468 unregister = primed.unregister; 2469 2470 for (let evt of primed.pendingEvents) { 2471 evt.resolve(fire.async(...evt.args)); 2472 } 2473 } 2474 2475 recordStartupData = false; 2476 this.remove.set(callback, () => { 2477 EventManager.clearPersistentListener( 2478 extension, 2479 module, 2480 event, 2481 uneval(args) 2482 ); 2483 }); 2484 } 2485 } 2486 2487 if (!unregister) { 2488 unregister = this.register(fire, ...args); 2489 } 2490 2491 this.unregister.set(callback, unregister); 2492 this.context.callOnClose(this); 2493 2494 // If this is a new listener for a persistent event, record 2495 // the details for subsequent startups. 2496 if (recordStartupData) { 2497 let { module, event } = this.persistent; 2498 EventManager.savePersistentListener(extension, module, event, args); 2499 this.remove.set(callback, () => { 2500 EventManager.clearPersistentListener( 2501 extension, 2502 module, 2503 event, 2504 uneval(args) 2505 ); 2506 }); 2507 } 2508 } 2509 2510 removeListener(callback, clearPersistentListener = true) { 2511 if (!this.unregister.has(callback)) { 2512 return; 2513 } 2514 this.context.logActivity("api_call", `${this.name}.removeListener`, { 2515 args: [], 2516 }); 2517 2518 let unregister = this.unregister.get(callback); 2519 this.unregister.delete(callback); 2520 try { 2521 unregister(); 2522 } catch (e) { 2523 Cu.reportError(e); 2524 } 2525 2526 if (clearPersistentListener && this.remove.has(callback)) { 2527 let cleanup = this.remove.get(callback); 2528 this.remove.delete(callback); 2529 cleanup(); 2530 } 2531 2532 if (this.unregister.size == 0) { 2533 this.context.forgetOnClose(this); 2534 } 2535 } 2536 2537 hasListener(callback) { 2538 return this.unregister.has(callback); 2539 } 2540 2541 revoke() { 2542 for (let callback of this.unregister.keys()) { 2543 this.removeListener(callback, false); 2544 } 2545 } 2546 2547 close() { 2548 this.revoke(); 2549 } 2550 2551 api() { 2552 return { 2553 addListener: (...args) => this.addListener(...args), 2554 removeListener: (...args) => this.removeListener(...args), 2555 hasListener: (...args) => this.hasListener(...args), 2556 setUserInput: this.inputHandling, 2557 [Schemas.REVOKE]: () => this.revoke(), 2558 }; 2559 } 2560} 2561 2562// Simple API for event listeners where events never fire. 2563function ignoreEvent(context, name) { 2564 return { 2565 addListener: function(callback) { 2566 let id = context.extension.id; 2567 let frame = Components.stack.caller; 2568 let msg = `In add-on ${id}, attempting to use listener "${name}", which is unimplemented.`; 2569 let scriptError = Cc["@mozilla.org/scripterror;1"].createInstance( 2570 Ci.nsIScriptError 2571 ); 2572 scriptError.init( 2573 msg, 2574 frame.filename, 2575 null, 2576 frame.lineNumber, 2577 frame.columnNumber, 2578 Ci.nsIScriptError.warningFlag, 2579 "content javascript" 2580 ); 2581 Services.console.logMessage(scriptError); 2582 }, 2583 removeListener: function(callback) {}, 2584 hasListener: function(callback) {}, 2585 }; 2586} 2587 2588const stylesheetMap = new DefaultMap(url => { 2589 let uri = Services.io.newURI(url); 2590 return styleSheetService.preloadSheet(uri, styleSheetService.AGENT_SHEET); 2591}); 2592 2593ExtensionCommon = { 2594 BaseContext, 2595 CanOfAPIs, 2596 EventManager, 2597 ExtensionAPI, 2598 EventEmitter, 2599 LocalAPIImplementation, 2600 LocaleData, 2601 NoCloneSpreadArgs, 2602 SchemaAPIInterface, 2603 SchemaAPIManager, 2604 SpreadArgs, 2605 checkLoadURL, 2606 defineLazyGetter, 2607 getConsole, 2608 ignoreEvent, 2609 instanceOf, 2610 makeWidgetId, 2611 normalizeTime, 2612 runSafeSyncWithoutClone, 2613 stylesheetMap, 2614 withHandlingUserInput, 2615 2616 MultiAPIManager, 2617 LazyAPIManager, 2618}; 2619