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 8var EXPORTED_SYMBOLS = ["ExtensionUtils"]; 9 10ChromeUtils.import("resource://gre/modules/Services.jsm"); 11ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); 12 13ChromeUtils.defineModuleGetter(this, "ConsoleAPI", 14 "resource://gre/modules/Console.jsm"); 15 16function getConsole() { 17 return new ConsoleAPI({ 18 maxLogLevelPref: "extensions.webextensions.log.level", 19 prefix: "WebExtensions", 20 }); 21} 22 23XPCOMUtils.defineLazyGetter(this, "console", getConsole); 24 25// xpcshell doesn't handle idle callbacks well. 26XPCOMUtils.defineLazyGetter(this, "idleTimeout", 27 () => Services.appinfo.name === "XPCShell" ? 500 : undefined); 28 29// It would be nicer to go through `Services.appinfo`, but some tests need to be 30// able to replace that field with a custom implementation before it is first 31// called. 32// eslint-disable-next-line mozilla/use-services 33const appinfo = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime); 34 35let nextId = 0; 36const uniqueProcessID = appinfo.uniqueProcessID; 37// Store the process ID in a 16 bit field left shifted to end of a 38// double's mantissa. 39// Note: We can't use bitwise ops here, since they truncate to a 32 bit 40// integer and we need all 53 mantissa bits. 41const processIDMask = (uniqueProcessID & 0xffff) * (2 ** 37); 42 43function getUniqueId() { 44 // Note: We can't use bitwise ops here, since they truncate to a 32 bit 45 // integer and we need all 53 mantissa bits. 46 return processIDMask + nextId++; 47} 48 49 50/** 51 * An Error subclass for which complete error messages are always passed 52 * to extensions, rather than being interpreted as an unknown error. 53 */ 54class ExtensionError extends Error {} 55 56function filterStack(error) { 57 return String(error.stack).replace(/(^.*(Task\.jsm|Promise-backend\.js).*\n)+/gm, "<Promise Chain>\n"); 58} 59 60// Run a function and report exceptions. 61function runSafeSyncWithoutClone(f, ...args) { 62 try { 63 return f(...args); 64 } catch (e) { 65 dump(`Extension error: ${e} ${e.fileName} ${e.lineNumber}\n[[Exception stack\n${filterStack(e)}Current stack\n${filterStack(Error())}]]\n`); 66 Cu.reportError(e); 67 } 68} 69 70// Return true if the given value is an instance of the given 71// native type. 72function instanceOf(value, type) { 73 return (value && typeof value === "object" && 74 ChromeUtils.getClassName(value) === type); 75} 76 77/** 78 * Similar to a WeakMap, but creates a new key with the given 79 * constructor if one is not present. 80 */ 81class DefaultWeakMap extends WeakMap { 82 constructor(defaultConstructor = undefined, init = undefined) { 83 super(init); 84 if (defaultConstructor) { 85 this.defaultConstructor = defaultConstructor; 86 } 87 } 88 89 get(key) { 90 let value = super.get(key); 91 if (value === undefined && !this.has(key)) { 92 value = this.defaultConstructor(key); 93 this.set(key, value); 94 } 95 return value; 96 } 97} 98 99class DefaultMap extends Map { 100 constructor(defaultConstructor = undefined, init = undefined) { 101 super(init); 102 if (defaultConstructor) { 103 this.defaultConstructor = defaultConstructor; 104 } 105 } 106 107 get(key) { 108 let value = super.get(key); 109 if (value === undefined && !this.has(key)) { 110 value = this.defaultConstructor(key); 111 this.set(key, value); 112 } 113 return value; 114 } 115} 116 117const _winUtils = new DefaultWeakMap(win => { 118 return win.QueryInterface(Ci.nsIInterfaceRequestor) 119 .getInterface(Ci.nsIDOMWindowUtils); 120}); 121const getWinUtils = win => _winUtils.get(win); 122 123function getInnerWindowID(window) { 124 return getWinUtils(window).currentInnerWindowID; 125} 126 127function withHandlingUserInput(window, callable) { 128 let handle = getWinUtils(window).setHandlingUserInput(true); 129 try { 130 return callable(); 131 } finally { 132 handle.destruct(); 133 } 134} 135 136const LISTENERS = Symbol("listeners"); 137const ONCE_MAP = Symbol("onceMap"); 138 139class EventEmitter { 140 constructor() { 141 this[LISTENERS] = new Map(); 142 this[ONCE_MAP] = new WeakMap(); 143 } 144 145 /** 146 * Adds the given function as a listener for the given event. 147 * 148 * The listener function may optionally return a Promise which 149 * resolves when it has completed all operations which event 150 * dispatchers may need to block on. 151 * 152 * @param {string} event 153 * The name of the event to listen for. 154 * @param {function(string, ...any)} listener 155 * The listener to call when events are emitted. 156 */ 157 on(event, listener) { 158 let listeners = this[LISTENERS].get(event); 159 if (!listeners) { 160 listeners = new Set(); 161 this[LISTENERS].set(event, listeners); 162 } 163 164 listeners.add(listener); 165 } 166 167 /** 168 * Removes the given function as a listener for the given event. 169 * 170 * @param {string} event 171 * The name of the event to stop listening for. 172 * @param {function(string, ...any)} listener 173 * The listener function to remove. 174 */ 175 off(event, listener) { 176 let set = this[LISTENERS].get(event); 177 if (set) { 178 set.delete(listener); 179 set.delete(this[ONCE_MAP].get(listener)); 180 if (!set.size) { 181 this[LISTENERS].delete(event); 182 } 183 } 184 } 185 186 /** 187 * Adds the given function as a listener for the given event once. 188 * 189 * @param {string} event 190 * The name of the event to listen for. 191 * @param {function(string, ...any)} listener 192 * The listener to call when events are emitted. 193 */ 194 once(event, listener) { 195 let wrapper = (...args) => { 196 this.off(event, wrapper); 197 this[ONCE_MAP].delete(listener); 198 199 return listener(...args); 200 }; 201 this[ONCE_MAP].set(listener, wrapper); 202 203 this.on(event, wrapper); 204 } 205 206 207 /** 208 * Triggers all listeners for the given event. If any listeners return 209 * a value, returns a promise which resolves when all returned 210 * promises have resolved. Otherwise, returns undefined. 211 * 212 * @param {string} event 213 * The name of the event to emit. 214 * @param {any} args 215 * Arbitrary arguments to pass to the listener functions, after 216 * the event name. 217 * @returns {Promise?} 218 */ 219 emit(event, ...args) { 220 let listeners = this[LISTENERS].get(event); 221 222 if (listeners) { 223 let promises = []; 224 225 for (let listener of listeners) { 226 try { 227 let result = listener(event, ...args); 228 if (result !== undefined) { 229 promises.push(result); 230 } 231 } catch (e) { 232 Cu.reportError(e); 233 } 234 } 235 236 if (promises.length) { 237 return Promise.all(promises); 238 } 239 } 240 } 241} 242 243/** 244 * A set with a limited number of slots, which flushes older entries as 245 * newer ones are added. 246 * 247 * @param {integer} limit 248 * The maximum size to trim the set to after it grows too large. 249 * @param {integer} [slop = limit * .25] 250 * The number of extra entries to allow in the set after it 251 * reaches the size limit, before it is truncated to the limit. 252 * @param {iterable} [iterable] 253 * An iterable of initial entries to add to the set. 254 */ 255class LimitedSet extends Set { 256 constructor(limit, slop = Math.round(limit * .25), iterable = undefined) { 257 super(iterable); 258 this.limit = limit; 259 this.slop = slop; 260 } 261 262 truncate(limit) { 263 for (let item of this) { 264 // Live set iterators can ge relatively expensive, since they need 265 // to be updated after every modification to the set. Since 266 // breaking out of the loop early will keep the iterator alive 267 // until the next full GC, we're currently better off finishing 268 // the entire loop even after we're done truncating. 269 if (this.size > limit) { 270 this.delete(item); 271 } 272 } 273 } 274 275 add(item) { 276 if (this.size >= this.limit + this.slop && !this.has(item)) { 277 this.truncate(this.limit - 1); 278 } 279 super.add(item); 280 } 281} 282 283/** 284 * Returns a Promise which resolves when the given document's DOM has 285 * fully loaded. 286 * 287 * @param {Document} doc The document to await the load of. 288 * @returns {Promise<Document>} 289 */ 290function promiseDocumentReady(doc) { 291 if (doc.readyState == "interactive" || doc.readyState == "complete") { 292 return Promise.resolve(doc); 293 } 294 295 return new Promise(resolve => { 296 doc.addEventListener("DOMContentLoaded", function onReady(event) { 297 if (event.target === event.currentTarget) { 298 doc.removeEventListener("DOMContentLoaded", onReady, true); 299 resolve(doc); 300 } 301 }, true); 302 }); 303} 304 305/** 306 * Returns a Promise which resolves when the given window's document's DOM has 307 * fully loaded, the <head> stylesheets have fully loaded, and we have hit an 308 * idle time. 309 * 310 * @param {Window} window The window whose document we will await 311 the readiness of. 312 * @returns {Promise<IdleDeadline>} 313 */ 314function promiseDocumentIdle(window) { 315 return window.document.documentReadyForIdle.then(() => { 316 return new Promise(resolve => 317 window.requestIdleCallback(resolve, {timeout: idleTimeout})); 318 }); 319} 320 321/** 322 * Returns a Promise which resolves when the given document is fully 323 * loaded. 324 * 325 * @param {Document} doc The document to await the load of. 326 * @returns {Promise<Document>} 327 */ 328function promiseDocumentLoaded(doc) { 329 if (doc.readyState == "complete") { 330 return Promise.resolve(doc); 331 } 332 333 return new Promise(resolve => { 334 doc.defaultView.addEventListener("load", () => resolve(doc), {once: true}); 335 }); 336} 337 338/** 339 * Returns a Promise which resolves when the given event is dispatched to the 340 * given element. 341 * 342 * @param {Element} element 343 * The element on which to listen. 344 * @param {string} eventName 345 * The event to listen for. 346 * @param {boolean} [useCapture = true] 347 * If true, listen for the even in the capturing rather than 348 * bubbling phase. 349 * @param {Event} [test] 350 * An optional test function which, when called with the 351 * observer's subject and data, should return true if this is the 352 * expected event, false otherwise. 353 * @returns {Promise<Event>} 354 */ 355function promiseEvent(element, eventName, useCapture = true, test = event => true) { 356 return new Promise(resolve => { 357 function listener(event) { 358 if (test(event)) { 359 element.removeEventListener(eventName, listener, useCapture); 360 resolve(event); 361 } 362 } 363 element.addEventListener(eventName, listener, useCapture); 364 }); 365} 366 367/** 368 * Returns a Promise which resolves the given observer topic has been 369 * observed. 370 * 371 * @param {string} topic 372 * The topic to observe. 373 * @param {function(nsISupports, string)} [test] 374 * An optional test function which, when called with the 375 * observer's subject and data, should return true if this is the 376 * expected notification, false otherwise. 377 * @returns {Promise<object>} 378 */ 379function promiseObserved(topic, test = () => true) { 380 return new Promise(resolve => { 381 let observer = (subject, topic, data) => { 382 if (test(subject, data)) { 383 Services.obs.removeObserver(observer, topic); 384 resolve({subject, data}); 385 } 386 }; 387 Services.obs.addObserver(observer, topic); 388 }); 389} 390 391function getMessageManager(target) { 392 if (target.frameLoader) { 393 return target.frameLoader.messageManager; 394 } 395 return target.QueryInterface(Ci.nsIMessageSender); 396} 397 398function flushJarCache(jarPath) { 399 Services.obs.notifyObservers(null, "flush-cache-entry", jarPath); 400} 401 402/** 403 * Convert any of several different representations of a date/time to a Date object. 404 * Accepts several formats: 405 * a Date object, an ISO8601 string, or a number of milliseconds since the epoch as 406 * either a number or a string. 407 * 408 * @param {Date|string|number} date 409 * The date to convert. 410 * @returns {Date} 411 * A Date object 412 */ 413function normalizeTime(date) { 414 // Of all the formats we accept the "number of milliseconds since the epoch as a string" 415 // is an outlier, everything else can just be passed directly to the Date constructor. 416 return new Date((typeof date == "string" && /^\d+$/.test(date)) 417 ? parseInt(date, 10) : date); 418} 419 420/** 421 * Defines a lazy getter for the given property on the given object. The 422 * first time the property is accessed, the return value of the getter 423 * is defined on the current `this` object with the given property name. 424 * Importantly, this means that a lazy getter defined on an object 425 * prototype will be invoked separately for each object instance that 426 * it's accessed on. 427 * 428 * @param {object} object 429 * The prototype object on which to define the getter. 430 * @param {string|Symbol} prop 431 * The property name for which to define the getter. 432 * @param {function} getter 433 * The function to call in order to generate the final property 434 * value. 435 */ 436function defineLazyGetter(object, prop, getter) { 437 let redefine = (obj, value) => { 438 Object.defineProperty(obj, prop, { 439 enumerable: true, 440 configurable: true, 441 writable: true, 442 value, 443 }); 444 return value; 445 }; 446 447 Object.defineProperty(object, prop, { 448 enumerable: true, 449 configurable: true, 450 451 get() { 452 return redefine(this, getter.call(this)); 453 }, 454 455 set(value) { 456 redefine(this, value); 457 }, 458 }); 459} 460 461/** 462 * Acts as a proxy for a message manager or message manager owner, and 463 * tracks docShell swaps so that messages are always sent to the same 464 * receiver, even if it is moved to a different <browser>. 465 * 466 * @param {nsIMessageSender|Element} target 467 * The target message manager on which to send messages, or the 468 * <browser> element which owns it. 469 */ 470class MessageManagerProxy { 471 constructor(target) { 472 this.listeners = new DefaultMap(() => new Map()); 473 474 if (target instanceof Ci.nsIMessageSender) { 475 this.messageManager = target; 476 } else { 477 this.addListeners(target); 478 } 479 } 480 481 /** 482 * Disposes of the proxy object, removes event listeners, and drops 483 * all references to the underlying message manager. 484 * 485 * Must be called before the last reference to the proxy is dropped, 486 * unless the underlying message manager or <browser> is also being 487 * destroyed. 488 */ 489 dispose() { 490 if (this.eventTarget) { 491 this.removeListeners(this.eventTarget); 492 this.eventTarget = null; 493 } 494 this.messageManager = null; 495 } 496 497 /** 498 * Returns true if the given target is the same as, or owns, the given 499 * message manager. 500 * 501 * @param {nsIMessageSender|MessageManagerProxy|Element} target 502 * The message manager, MessageManagerProxy, or <browser> 503 * element agaisnt which to match. 504 * @param {nsIMessageSender} messageManager 505 * The message manager against which to match `target`. 506 * 507 * @returns {boolean} 508 * True if `messageManager` is the same object as `target`, or 509 * `target` is a MessageManagerProxy or <browser> element that 510 * is tied to it. 511 */ 512 static matches(target, messageManager) { 513 return target === messageManager || target.messageManager === messageManager; 514 } 515 516 /** 517 * @property {nsIMessageSender|null} messageManager 518 * The message manager that is currently being proxied. This 519 * may change during the life of the proxy object, so should 520 * not be stored elsewhere. 521 */ 522 523 /** 524 * Sends a message on the proxied message manager. 525 * 526 * @param {array} args 527 * Arguments to be passed verbatim to the underlying 528 * sendAsyncMessage method. 529 * @returns {undefined} 530 */ 531 sendAsyncMessage(...args) { 532 if (this.messageManager) { 533 return this.messageManager.sendAsyncMessage(...args); 534 } 535 536 Cu.reportError(`Cannot send message: Other side disconnected: ${uneval(args)}`); 537 } 538 539 get isDisconnected() { 540 return !this.messageManager; 541 } 542 543 /** 544 * Adds a message listener to the current message manager, and 545 * transfers it to the new message manager after a docShell swap. 546 * 547 * @param {string} message 548 * The name of the message to listen for. 549 * @param {nsIMessageListener} listener 550 * The listener to add. 551 * @param {boolean} [listenWhenClosed = false] 552 * If true, the listener will receive messages which were sent 553 * after the remote side of the listener began closing. 554 */ 555 addMessageListener(message, listener, listenWhenClosed = false) { 556 this.messageManager.addMessageListener(message, listener, listenWhenClosed); 557 this.listeners.get(message).set(listener, listenWhenClosed); 558 } 559 560 /** 561 * Adds a message listener from the current message manager. 562 * 563 * @param {string} message 564 * The name of the message to stop listening for. 565 * @param {nsIMessageListener} listener 566 * The listener to remove. 567 */ 568 removeMessageListener(message, listener) { 569 this.messageManager.removeMessageListener(message, listener); 570 571 let listeners = this.listeners.get(message); 572 listeners.delete(listener); 573 if (!listeners.size) { 574 this.listeners.delete(message); 575 } 576 } 577 578 /** 579 * @private 580 * Iterates over all of the currently registered message listeners. 581 */ 582 * iterListeners() { 583 for (let [message, listeners] of this.listeners) { 584 for (let [listener, listenWhenClosed] of listeners) { 585 yield {message, listener, listenWhenClosed}; 586 } 587 } 588 } 589 590 /** 591 * @private 592 * Adds docShell swap listeners to the message manager owner. 593 * 594 * @param {Element} target 595 * The target element. 596 */ 597 addListeners(target) { 598 target.addEventListener("SwapDocShells", this); 599 600 this.eventTarget = target; 601 this.messageManager = target.messageManager; 602 603 for (let {message, listener, listenWhenClosed} of this.iterListeners()) { 604 this.messageManager.addMessageListener(message, listener, listenWhenClosed); 605 } 606 } 607 608 /** 609 * @private 610 * Removes docShell swap listeners to the message manager owner. 611 * 612 * @param {Element} target 613 * The target element. 614 */ 615 removeListeners(target) { 616 target.removeEventListener("SwapDocShells", this); 617 618 for (let {message, listener} of this.iterListeners()) { 619 this.messageManager.removeMessageListener(message, listener); 620 } 621 } 622 623 handleEvent(event) { 624 if (event.type == "SwapDocShells") { 625 this.removeListeners(this.eventTarget); 626 this.addListeners(event.detail); 627 } 628 } 629} 630 631function checkLoadURL(url, principal, options) { 632 let ssm = Services.scriptSecurityManager; 633 634 let flags = ssm.STANDARD; 635 if (!options.allowScript) { 636 flags |= ssm.DISALLOW_SCRIPT; 637 } 638 if (!options.allowInheritsPrincipal) { 639 flags |= ssm.DISALLOW_INHERIT_PRINCIPAL; 640 } 641 if (options.dontReportErrors) { 642 flags |= ssm.DONT_REPORT_ERRORS; 643 } 644 645 try { 646 ssm.checkLoadURIWithPrincipal(principal, 647 Services.io.newURI(url), 648 flags); 649 } catch (e) { 650 return false; 651 } 652 return true; 653} 654 655var ExtensionUtils = { 656 checkLoadURL, 657 defineLazyGetter, 658 flushJarCache, 659 getConsole, 660 getInnerWindowID, 661 getMessageManager, 662 getUniqueId, 663 filterStack, 664 getWinUtils, 665 instanceOf, 666 normalizeTime, 667 promiseDocumentIdle, 668 promiseDocumentLoaded, 669 promiseDocumentReady, 670 promiseEvent, 671 promiseObserved, 672 runSafeSyncWithoutClone, 673 withHandlingUserInput, 674 DefaultMap, 675 DefaultWeakMap, 676 EventEmitter, 677 ExtensionError, 678 LimitedSet, 679 MessageManagerProxy, 680}; 681