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