SpecialPowersParent.sys.mjs (46928B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 ExtensionData: "resource://gre/modules/Extension.sys.mjs", 11 ExtensionTestCommon: "resource://testing-common/ExtensionTestCommon.sys.mjs", 12 HiddenFrame: "resource://gre/modules/HiddenFrame.sys.mjs", 13 PerTestCoverageUtils: 14 "resource://testing-common/PerTestCoverageUtils.sys.mjs", 15 ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.sys.mjs", 16 SpecialPowersSandbox: 17 "resource://testing-common/SpecialPowersSandbox.sys.mjs", 18 }); 19 20 class SpecialPowersError extends Error { 21 get name() { 22 return "SpecialPowersError"; 23 } 24 } 25 26 const PREF_TYPES = { 27 [Ci.nsIPrefBranch.PREF_INVALID]: "INVALID", 28 [Ci.nsIPrefBranch.PREF_INT]: "INT", 29 [Ci.nsIPrefBranch.PREF_BOOL]: "BOOL", 30 [Ci.nsIPrefBranch.PREF_STRING]: "STRING", 31 number: "INT", 32 boolean: "BOOL", 33 string: "STRING", 34 }; 35 36 // We share a single preference environment stack between all 37 // SpecialPowers instances, across all processes. 38 let prefUndoStack = []; 39 let inPrefEnvOp = false; 40 41 let permissionUndoStack = []; 42 43 function doPrefEnvOp(fn) { 44 if (inPrefEnvOp) { 45 throw new Error( 46 "Reentrant preference environment operations not supported" 47 ); 48 } 49 inPrefEnvOp = true; 50 try { 51 return fn(); 52 } finally { 53 inPrefEnvOp = false; 54 } 55 } 56 57 async function createWindowlessBrowser({ isPrivate = false } = {}) { 58 const { promiseDocumentLoaded, promiseEvent, promiseObserved } = 59 ChromeUtils.importESModule( 60 "resource://gre/modules/ExtensionUtils.sys.mjs" 61 ).ExtensionUtils; 62 63 let windowlessBrowser = Services.appShell.createWindowlessBrowser(true); 64 65 if (isPrivate) { 66 let loadContext = windowlessBrowser.docShell.QueryInterface( 67 Ci.nsILoadContext 68 ); 69 loadContext.usePrivateBrowsing = true; 70 } 71 72 let chromeShell = windowlessBrowser.docShell.QueryInterface( 73 Ci.nsIWebNavigation 74 ); 75 76 const system = Services.scriptSecurityManager.getSystemPrincipal(); 77 chromeShell.createAboutBlankDocumentViewer(system, system); 78 windowlessBrowser.browsingContext.useGlobalHistory = false; 79 chromeShell.loadURI( 80 Services.io.newURI("chrome://extensions/content/dummy.xhtml"), 81 { 82 triggeringPrincipal: system, 83 } 84 ); 85 86 await promiseObserved( 87 "chrome-document-global-created", 88 win => win.document == chromeShell.document 89 ); 90 91 let chromeDoc = await promiseDocumentLoaded(chromeShell.document); 92 93 let browser = chromeDoc.createXULElement("browser"); 94 browser.setAttribute("type", "content"); 95 browser.setAttribute("disableglobalhistory", "true"); 96 browser.setAttribute("remote", "true"); 97 98 let promise = promiseEvent(browser, "XULFrameLoaderCreated"); 99 chromeDoc.documentElement.appendChild(browser); 100 101 await promise; 102 103 return { windowlessBrowser, browser }; 104 } 105 106 // Supplies the unique IDs for tasks created by SpecialPowers.spawn(), 107 // used to bounce assertion messages back down to the correct child. 108 let nextTaskID = 1; 109 110 // The default actor to send assertions to if a task originated in a 111 // window without a test harness. 112 let defaultAssertHandler; 113 114 export class SpecialPowersParent extends JSWindowActorParent { 115 constructor() { 116 super(); 117 118 this._messageManager = Services.mm; 119 this._serviceWorkerListener = null; 120 121 this._observer = this.observe.bind(this); 122 123 this.didDestroy = this.uninit.bind(this); 124 125 this._registerObservers = { 126 _self: this, 127 _topics: [], 128 _add(topic) { 129 if (!this._topics.includes(topic)) { 130 this._topics.push(topic); 131 Services.obs.addObserver(this, topic); 132 } 133 }, 134 observe(aSubject, aTopic, aData) { 135 var msg = { aData }; 136 switch (aTopic) { 137 case "csp-on-violate-policy": { 138 // the subject is either an nsIURI or an nsISupportsCString 139 let subject = null; 140 if (aSubject instanceof Ci.nsIURI) { 141 subject = aSubject.asciiSpec; 142 } else if (aSubject instanceof Ci.nsISupportsCString) { 143 subject = aSubject.data; 144 } else { 145 throw new Error("Subject must be nsIURI or nsISupportsCString"); 146 } 147 msg = { 148 subject, 149 data: aData, 150 }; 151 this._self.sendAsyncMessage("specialpowers-" + aTopic, msg); 152 return; 153 } 154 case "xfo-on-violate-policy": { 155 let uriSpec = null; 156 if (aSubject instanceof Ci.nsIURI) { 157 uriSpec = aSubject.asciiSpec; 158 } else { 159 throw new Error("Subject must be nsIURI"); 160 } 161 msg = { 162 subject: uriSpec, 163 data: aData, 164 }; 165 this._self.sendAsyncMessage("specialpowers-" + aTopic, msg); 166 return; 167 } 168 default: 169 this._self.sendAsyncMessage("specialpowers-" + aTopic, msg); 170 } 171 }, 172 }; 173 174 this._basePrefs = null; 175 this.init(); 176 177 this._crashDumpDir = null; 178 this._processCrashObserversRegistered = false; 179 this._chromeScriptListeners = []; 180 this._extensions = new Map(); 181 this._taskActors = new Map(); 182 } 183 184 static registerActor() { 185 ChromeUtils.registerWindowActor("SpecialPowers", { 186 allFrames: true, 187 includeChrome: true, 188 child: { 189 esModuleURI: "resource://testing-common/SpecialPowersChild.sys.mjs", 190 observers: [ 191 "chrome-document-global-created", 192 "content-document-global-created", 193 ], 194 }, 195 parent: { 196 esModuleURI: "resource://testing-common/SpecialPowersParent.sys.mjs", 197 }, 198 }); 199 ChromeUtils.registerProcessActor("SpecialPowersProcessActor", { 200 child: { 201 esModuleURI: 202 "resource://testing-common/SpecialPowersProcessActor.sys.mjs", 203 }, 204 parent: { 205 esModuleURI: 206 "resource://testing-common/SpecialPowersProcessActor.sys.mjs", 207 }, 208 }); 209 } 210 211 static unregisterActor() { 212 ChromeUtils.unregisterWindowActor("SpecialPowers"); 213 ChromeUtils.unregisterProcessActor("SpecialPowersProcessActor"); 214 } 215 216 init() { 217 Services.obs.addObserver(this._observer, "http-on-modify-request"); 218 219 // We would like to check that tests don't leave service workers around 220 // after they finish, but that information lives in the parent process. 221 // Ideally, we'd be able to tell the child processes whenever service 222 // workers are registered or unregistered so they would know at all times, 223 // but service worker lifetimes are complicated enough to make that 224 // difficult. For the time being, let the child process know when a test 225 // registers a service worker so it can ask, synchronously, at the end if 226 // the service worker had unregister called on it. 227 let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( 228 Ci.nsIServiceWorkerManager 229 ); 230 let self = this; 231 this._serviceWorkerListener = { 232 onRegister() { 233 self.onRegister(); 234 }, 235 236 onUnregister() { 237 // no-op 238 }, 239 }; 240 swm.addListener(this._serviceWorkerListener); 241 242 this.getBaselinePrefs(); 243 } 244 245 uninit() { 246 if (defaultAssertHandler === this) { 247 defaultAssertHandler = null; 248 } 249 250 var obs = Services.obs; 251 obs.removeObserver(this._observer, "http-on-modify-request"); 252 this._registerObservers._topics.splice(0).forEach(element => { 253 obs.removeObserver(this._registerObservers, element); 254 }); 255 this._removeProcessCrashObservers(); 256 257 let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( 258 Ci.nsIServiceWorkerManager 259 ); 260 swm.removeListener(this._serviceWorkerListener); 261 } 262 263 observe(aSubject, aTopic) { 264 function addDumpIDToMessage(propertyName) { 265 try { 266 var id = aSubject.getPropertyAsAString(propertyName); 267 } catch (ex) { 268 id = null; 269 } 270 if (id) { 271 message.dumpIDs.push({ id, extension: "dmp" }); 272 message.dumpIDs.push({ id, extension: "extra" }); 273 } 274 } 275 276 switch (aTopic) { 277 case "http-on-modify-request": 278 if (aSubject instanceof Ci.nsIChannel) { 279 let uri = aSubject.URI.spec; 280 this.sendAsyncMessage("specialpowers-http-notify-request", { uri }); 281 } 282 break; 283 284 case "ipc:content-shutdown": 285 aSubject = aSubject.QueryInterface(Ci.nsIPropertyBag2); 286 if (!aSubject.hasKey("abnormal")) { 287 return; // This is a normal shutdown, ignore it 288 } 289 290 var message = { type: "crash-observed", dumpIDs: [] }; 291 addDumpIDToMessage("dumpID"); 292 this.sendAsyncMessage("SPProcessCrashService", message); 293 break; 294 } 295 } 296 297 _getCrashDumpDir() { 298 if (!this._crashDumpDir) { 299 this._crashDumpDir = Services.dirsvc.get("ProfD", Ci.nsIFile); 300 this._crashDumpDir.append("minidumps"); 301 } 302 return this._crashDumpDir; 303 } 304 305 _getPendingCrashDumpDir() { 306 if (!this._pendingCrashDumpDir) { 307 this._pendingCrashDumpDir = Services.dirsvc.get("UAppData", Ci.nsIFile); 308 this._pendingCrashDumpDir.append("Crash Reports"); 309 this._pendingCrashDumpDir.append("pending"); 310 } 311 return this._pendingCrashDumpDir; 312 } 313 314 _deleteCrashDumpFiles(aFilenames) { 315 var crashDumpDir = this._getCrashDumpDir(); 316 if (!crashDumpDir.exists()) { 317 return false; 318 } 319 320 var success = !!aFilenames.length; 321 aFilenames.forEach(function (crashFilename) { 322 var file = crashDumpDir.clone(); 323 file.append(crashFilename); 324 if (file.exists()) { 325 file.remove(false); 326 } else { 327 success = false; 328 } 329 }); 330 return success; 331 } 332 333 _findCrashDumpFiles(aToIgnore) { 334 var crashDumpDir = this._getCrashDumpDir(); 335 var entries = crashDumpDir.exists() && crashDumpDir.directoryEntries; 336 if (!entries) { 337 return []; 338 } 339 340 var crashDumpFiles = []; 341 while (entries.hasMoreElements()) { 342 var file = entries.nextFile; 343 var path = String(file.path); 344 if (path.match(/\.(dmp|extra)$/) && !aToIgnore[path]) { 345 crashDumpFiles.push(path); 346 } 347 } 348 return crashDumpFiles.concat(); 349 } 350 351 _deletePendingCrashDumpFiles() { 352 var crashDumpDir = this._getPendingCrashDumpDir(); 353 var removed = false; 354 if (crashDumpDir.exists()) { 355 let entries = crashDumpDir.directoryEntries; 356 while (entries.hasMoreElements()) { 357 let file = entries.nextFile; 358 if (file.isFile()) { 359 file.remove(false); 360 removed = true; 361 } 362 } 363 } 364 return removed; 365 } 366 367 _addProcessCrashObservers() { 368 if (this._processCrashObserversRegistered) { 369 return; 370 } 371 372 Services.obs.addObserver(this._observer, "ipc:content-shutdown"); 373 this._processCrashObserversRegistered = true; 374 } 375 376 _removeProcessCrashObservers() { 377 if (!this._processCrashObserversRegistered) { 378 return; 379 } 380 381 Services.obs.removeObserver(this._observer, "ipc:content-shutdown"); 382 this._processCrashObserversRegistered = false; 383 } 384 385 onRegister() { 386 this.sendAsyncMessage("SPServiceWorkerRegistered", { registered: true }); 387 } 388 389 _getURI(url) { 390 return Services.io.newURI(url); 391 } 392 _notifyCategoryAndObservers(subject, topic, data) { 393 const serviceMarker = "service,"; 394 395 // First create observers from the category manager. 396 397 let observers = []; 398 399 for (let { value: contractID } of Services.catMan.enumerateCategory( 400 topic 401 )) { 402 let factoryFunction; 403 if (contractID.substring(0, serviceMarker.length) == serviceMarker) { 404 contractID = contractID.substring(serviceMarker.length); 405 factoryFunction = "getService"; 406 } else { 407 factoryFunction = "createInstance"; 408 } 409 410 try { 411 let handler = Cc[contractID][factoryFunction](); 412 if (handler) { 413 let observer = handler.QueryInterface(Ci.nsIObserver); 414 observers.push(observer); 415 } 416 } catch (e) {} 417 } 418 419 // Next enumerate the registered observers. 420 for (let observer of Services.obs.enumerateObservers(topic)) { 421 if (observer instanceof Ci.nsIObserver && !observers.includes(observer)) { 422 observers.push(observer); 423 } 424 } 425 426 observers.forEach(function (observer) { 427 try { 428 observer.observe(subject, topic, data); 429 } catch (e) {} 430 }); 431 } 432 433 /* 434 Iterate through one atomic set of pref actions and perform sets/clears as appropriate. 435 All actions performed must modify the relevant pref. 436 437 Returns whether we need to wait for a refresh driver tick for the pref to 438 have effect. This is only needed for ui. and font. prefs, which affect the 439 look and feel code and have some change-coalescing going on. 440 */ 441 _applyPrefs(actions) { 442 let requiresRefresh = false; 443 for (let pref of actions) { 444 // This logic should match PrefRequiresRefresh in reftest.sys.mjs 445 requiresRefresh = 446 requiresRefresh || 447 pref.name == "layout.css.prefers-color-scheme.content-override" || 448 pref.name.startsWith("ui.") || 449 pref.name.startsWith("browser.display.") || 450 pref.name.startsWith("font."); 451 if (pref.action == "set") { 452 this._setPref(pref.name, pref.type, pref.value, pref.iid); 453 } else if (pref.action == "clear") { 454 Services.prefs.clearUserPref(pref.name); 455 } 456 } 457 return requiresRefresh; 458 } 459 460 /** 461 * Take in a list of pref changes to make, pushes their current values 462 * onto the restore stack, and makes the changes. When the test 463 * finishes, these changes are reverted. 464 * 465 * |inPrefs| must be an object with up to two properties: "set" and "clear". 466 * pushPrefEnv will set prefs as indicated in |inPrefs.set| and will unset 467 * the prefs indicated in |inPrefs.clear|. 468 * 469 * For example, you might pass |inPrefs| as: 470 * 471 * inPrefs = {'set': [['foo.bar', 2], ['magic.pref', 'baz']], 472 * 'clear': [['clear.this'], ['also.this']] }; 473 * 474 * Notice that |set| and |clear| are both an array of arrays. In |set|, each 475 * of the inner arrays must have the form [pref_name, value] or [pref_name, 476 * value, iid]. (The latter form is used for prefs with "complex" values.) 477 * 478 * In |clear|, each inner array should have the form [pref_name]. 479 * 480 * If you set the same pref more than once (or both set and clear a pref), 481 * the behavior of this method is undefined. 482 */ 483 pushPrefEnv(inPrefs) { 484 return doPrefEnvOp(() => { 485 let pendingActions = []; 486 let cleanupActions = []; 487 488 for (let [action, prefs] of Object.entries(inPrefs)) { 489 for (let pref of prefs) { 490 let name = pref[0]; 491 let value = null; 492 let iid = null; 493 let type = PREF_TYPES[Services.prefs.getPrefType(name)]; 494 let originalValue = null; 495 496 if (pref.length == 3) { 497 value = pref[1]; 498 iid = pref[2]; 499 } else if (pref.length == 2) { 500 value = pref[1]; 501 } 502 503 /* If pref is not found or invalid it doesn't exist. */ 504 if (type !== "INVALID") { 505 if ( 506 (Services.prefs.prefHasUserValue(name) && action == "clear") || 507 action == "set" 508 ) { 509 originalValue = this._getPref(name, type); 510 } 511 } else if (action == "set") { 512 /* name doesn't exist, so 'clear' is pointless */ 513 if (iid) { 514 type = "COMPLEX"; 515 } 516 } 517 518 if (type === "INVALID") { 519 type = PREF_TYPES[typeof value]; 520 } 521 if (type === "INVALID") { 522 throw new Error("Unexpected preference type for " + name); 523 } 524 525 pendingActions.push({ action, type, name, value, iid }); 526 527 /* Push original preference value or clear into cleanup array */ 528 var cleanupTodo = { type, name, value: originalValue, iid }; 529 if (originalValue == null) { 530 cleanupTodo.action = "clear"; 531 } else { 532 cleanupTodo.action = "set"; 533 } 534 cleanupActions.push(cleanupTodo); 535 } 536 } 537 538 prefUndoStack.push(cleanupActions); 539 let requiresRefresh = this._applyPrefs(pendingActions); 540 return { requiresRefresh }; 541 }); 542 } 543 544 popPrefEnv() { 545 return doPrefEnvOp(() => { 546 let env = prefUndoStack.pop(); 547 if (env) { 548 let requiresRefresh = this._applyPrefs(env); 549 return { popped: true, requiresRefresh }; 550 } 551 return { popped: false, requiresRefresh: false }; 552 }); 553 } 554 555 flushPrefEnv() { 556 let requiresRefresh = false; 557 while (prefUndoStack.length) { 558 // bitwise |= (and not logical ||=) so that we always call popPrefEnv and 559 // don't lazily evaluate. 560 requiresRefresh |= this.popPrefEnv().requiresRefresh; 561 } 562 // Make requiresRefresh a boolean from number. 563 requiresRefresh = !!requiresRefresh; 564 return { requiresRefresh }; 565 } 566 567 _setPref(name, type, value, iid) { 568 switch (type) { 569 case "BOOL": 570 return Services.prefs.setBoolPref(name, value); 571 case "INT": 572 return Services.prefs.setIntPref(name, value); 573 case "CHAR": 574 return Services.prefs.setCharPref(name, value); 575 case "COMPLEX": 576 return Services.prefs.setComplexValue(name, iid, value); 577 case "STRING": 578 return Services.prefs.setStringPref(name, value); 579 } 580 switch (typeof value) { 581 case "boolean": 582 return Services.prefs.setBoolPref(name, value); 583 case "number": 584 return Services.prefs.setIntPref(name, value); 585 case "string": 586 return Services.prefs.setStringPref(name, value); 587 } 588 throw new Error( 589 `Unexpected preference type: ${type} for ${name} with value ${value} and type ${typeof value}` 590 ); 591 } 592 593 _getPref(name, type, defaultValue, iid) { 594 switch (type) { 595 case "BOOL": 596 if (defaultValue !== undefined) { 597 return Services.prefs.getBoolPref(name, defaultValue); 598 } 599 return Services.prefs.getBoolPref(name); 600 case "INT": 601 if (defaultValue !== undefined) { 602 return Services.prefs.getIntPref(name, defaultValue); 603 } 604 return Services.prefs.getIntPref(name); 605 case "CHAR": 606 if (defaultValue !== undefined) { 607 return Services.prefs.getCharPref(name, defaultValue); 608 } 609 return Services.prefs.getCharPref(name); 610 case "COMPLEX": 611 return Services.prefs.getComplexValue(name, iid); 612 case "STRING": 613 if (defaultValue !== undefined) { 614 return Services.prefs.getStringPref(name, defaultValue); 615 } 616 return Services.prefs.getStringPref(name); 617 } 618 throw new Error( 619 `Unexpected preference type: ${type} for preference ${name}` 620 ); 621 } 622 623 getBaselinePrefs() { 624 this._basePrefs = this._getAllPreferences(); 625 } 626 627 _comparePrefs(base, target, ignorePrefs, partialMatches) { 628 let failures = []; 629 for (const [key, value] of base) { 630 if (ignorePrefs.includes(key)) { 631 continue; 632 } 633 let partialFind = false; 634 partialMatches.forEach(pm => { 635 if (key.startsWith(pm)) { 636 partialFind = true; 637 } 638 }); 639 if (partialFind) { 640 continue; 641 } 642 643 if (value === target.get(key)) { 644 continue; 645 } 646 if (!failures.includes(key)) { 647 failures.push(key); 648 } 649 } 650 return failures; 651 } 652 653 comparePrefsToBaseline(ignorePrefs) { 654 let newPrefs = this._getAllPreferences(); 655 656 // find all items in ignorePrefs that end in *, add to partialMatch 657 let partialMatch = []; 658 if (ignorePrefs === undefined) { 659 ignorePrefs = []; 660 } 661 ignorePrefs.forEach(pref => { 662 if (pref.endsWith("*")) { 663 partialMatch.push(pref.split("*")[0]); 664 } 665 }); 666 667 // find all new prefs different than old 668 let rv1 = this._comparePrefs( 669 newPrefs, 670 this._basePrefs, 671 ignorePrefs, 672 partialMatch 673 ); 674 675 // find all old prefs different than new (in case we delete) 676 let rv2 = this._comparePrefs( 677 this._basePrefs, 678 newPrefs, 679 ignorePrefs, 680 partialMatch 681 ); 682 683 let failures = [...new Set([...rv1, ...rv2])]; 684 685 // reset failures 686 failures.forEach(f => { 687 if (this._basePrefs.get(f)) { 688 this._setPref( 689 f, 690 PREF_TYPES[Services.prefs.getPrefType(f)], 691 this._basePrefs.get(f) 692 ); 693 } else { 694 Services.prefs.clearUserPref(f); 695 } 696 }); 697 698 if (failures.length) { 699 // Because we can't reset prefs on the default branch, reset our baseline. 700 this.getBaselinePrefs(); 701 } 702 703 if (ignorePrefs.length > 1) { 704 return failures; 705 } 706 return []; 707 } 708 709 _getAllPreferences() { 710 let names = new Map(); 711 for (let prefName of Services.prefs.getChildList("")) { 712 let prefType = PREF_TYPES[Services.prefs.getPrefType(prefName)]; 713 let prefValue = this._getPref(prefName, prefType); 714 names.set(prefName, prefValue); 715 } 716 return names; 717 } 718 719 _toggleMuteAudio(aMuted) { 720 let browser = this.browsingContext.top.embedderElement; 721 if (aMuted) { 722 browser.mute(); 723 } else { 724 browser.unmute(); 725 } 726 } 727 728 _permOp(perm) { 729 switch (perm.op) { 730 case "add": 731 Services.perms.addFromPrincipal( 732 perm.principal, 733 perm.type, 734 perm.permission, 735 perm.expireType, 736 perm.expireTime 737 ); 738 break; 739 case "remove": 740 Services.perms.removeFromPrincipal(perm.principal, perm.type); 741 break; 742 default: 743 throw new Error(`Unexpected permission op: ${perm.op}`); 744 } 745 } 746 747 pushPermissions(inPermissions) { 748 let pendingPermissions = []; 749 let cleanupPermissions = []; 750 751 for (let permission of inPermissions) { 752 let { principal } = permission; 753 if (principal.isSystemPrincipal) { 754 continue; 755 } 756 757 let originalValue = Services.perms.testPermissionFromPrincipal( 758 principal, 759 permission.type 760 ); 761 762 let perm = permission.allow; 763 if (typeof perm === "boolean") { 764 perm = Ci.nsIPermissionManager[perm ? "ALLOW_ACTION" : "DENY_ACTION"]; 765 } 766 767 if (permission.remove) { 768 perm = Ci.nsIPermissionManager.UNKNOWN_ACTION; 769 } 770 771 if (originalValue == perm) { 772 continue; 773 } 774 775 let todo = { 776 op: "add", 777 type: permission.type, 778 permission: perm, 779 value: perm, 780 principal, 781 expireType: 782 typeof permission.expireType === "number" ? permission.expireType : 0, // default: EXPIRE_NEVER 783 expireTime: 784 typeof permission.expireTime === "number" ? permission.expireTime : 0, 785 }; 786 787 var cleanupTodo = Object.assign({}, todo); 788 789 if (permission.remove) { 790 todo.op = "remove"; 791 } 792 793 pendingPermissions.push(todo); 794 795 if (originalValue == Ci.nsIPermissionManager.UNKNOWN_ACTION) { 796 cleanupTodo.op = "remove"; 797 } else { 798 cleanupTodo.value = originalValue; 799 cleanupTodo.permission = originalValue; 800 } 801 cleanupPermissions.push(cleanupTodo); 802 } 803 804 permissionUndoStack.push(cleanupPermissions); 805 806 for (let perm of pendingPermissions) { 807 this._permOp(perm); 808 } 809 } 810 811 popPermissions() { 812 if (permissionUndoStack.length) { 813 for (let perm of permissionUndoStack.pop()) { 814 this._permOp(perm); 815 } 816 } 817 } 818 819 flushPermissions() { 820 while (permissionUndoStack.length) { 821 this.popPermissions(); 822 } 823 } 824 825 _spawnChrome(task, args, caller, imports) { 826 let sb = new lazy.SpecialPowersSandbox( 827 null, 828 data => { 829 this.sendAsyncMessage("Assert", data); 830 }, 831 { imports } 832 ); 833 834 // If more variables are made available, don't forget to update 835 // tools/lint/eslint/eslint-plugin-mozilla/lib/rules/import-content-task-globals.js. 836 for (let [global, prop] of Object.entries({ 837 windowGlobalParent: "manager", 838 browsingContext: "browsingContext", 839 })) { 840 Object.defineProperty(sb.sandbox, global, { 841 get: () => { 842 return this[prop]; 843 }, 844 enumerable: true, 845 }); 846 } 847 848 return sb.execute(task, args, caller); 849 } 850 851 /** 852 * messageManager callback function 853 * This will get requests from our API in the window and process them in chrome for it 854 */ 855 // eslint-disable-next-line complexity 856 async receiveMessage(aMessage) { 857 let startTime = ChromeUtils.now(); 858 // Try block so we can use a finally statement to add a profiler marker 859 // despite all the return statements. 860 try { 861 // We explicitly return values in the below code so that this function 862 // doesn't trigger a flurry of warnings about "does not always return 863 // a value". 864 switch (aMessage.name) { 865 case "SPToggleMuteAudio": 866 return this._toggleMuteAudio(aMessage.data.mute); 867 868 case "Ping": 869 return undefined; 870 871 case "SpecialPowers.Quit": 872 if ( 873 !AppConstants.RELEASE_OR_BETA && 874 !AppConstants.DEBUG && 875 !AppConstants.MOZ_CODE_COVERAGE && 876 !AppConstants.ASAN && 877 !AppConstants.TSAN 878 ) { 879 if (Services.profiler.IsActive()) { 880 let filename = Services.env.get("MOZ_PROFILER_SHUTDOWN"); 881 if (filename) { 882 await Services.profiler.dumpProfileToFileAsync(filename); 883 await Services.profiler.StopProfiler(); 884 } 885 } 886 Cu.exitIfInAutomation(); 887 } else { 888 Services.startup.quit(Ci.nsIAppStartup.eForceQuit); 889 } 890 return undefined; 891 892 case "EnsureFocus": { 893 let bc = aMessage.data.browsingContext; 894 // Send a message to the child telling it to focus the window. 895 // If the message responds with a browsing context, then 896 // a child browsing context in a subframe should be focused. 897 // Iterate until nothing is returned and we get to the most 898 // deeply nested subframe that should be focused. 899 do { 900 let spParent = bc.currentWindowGlobal.getActor("SpecialPowers"); 901 if (spParent) { 902 bc = await spParent.sendQuery("EnsureFocus", { 903 blurSubframe: aMessage.data.blurSubframe, 904 }); 905 } 906 } while (bc && !aMessage.data.blurSubframe); 907 return undefined; 908 } 909 910 case "SpecialPowers.Focus": 911 if (this.manager.rootFrameLoader) { 912 this.manager.rootFrameLoader.ownerElement.focus(); 913 } 914 return undefined; 915 916 case "SpecialPowers.CreateFiles": 917 return (async () => { 918 let filePaths = []; 919 if (!this._createdFiles) { 920 this._createdFiles = []; 921 } 922 let createdFiles = this._createdFiles; 923 924 let promises = []; 925 aMessage.data.forEach(function (request) { 926 const filePerms = 0o666; 927 let testFile = Services.dirsvc.get("ProfD", Ci.nsIFile); 928 if (request.name) { 929 testFile.appendRelativePath(request.name); 930 } else { 931 testFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, filePerms); 932 } 933 let outStream = Cc[ 934 "@mozilla.org/network/file-output-stream;1" 935 ].createInstance(Ci.nsIFileOutputStream); 936 outStream.init( 937 testFile, 938 0x02 | 0x08 | 0x20, // PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE 939 filePerms, 940 0 941 ); 942 if (request.data) { 943 outStream.write(request.data, request.data.length); 944 } 945 outStream.close(); 946 promises.push( 947 File.createFromFileName(testFile.path, request.options).then( 948 function (file) { 949 filePaths.push(file); 950 } 951 ) 952 ); 953 createdFiles.push(testFile); 954 }); 955 956 await Promise.all(promises); 957 return filePaths; 958 })().catch(e => { 959 console.error(e); 960 return Promise.reject(String(e)); 961 }); 962 963 case "SpecialPowers.RemoveFiles": 964 if (this._createdFiles) { 965 this._createdFiles.forEach(function (testFile) { 966 try { 967 testFile.remove(false); 968 } catch (e) {} 969 }); 970 this._createdFiles = null; 971 } 972 return undefined; 973 974 case "Wakeup": 975 return undefined; 976 977 case "EvictAllDocumentViewers": 978 this.browsingContext.top.sessionHistory.evictAllDocumentViewers(); 979 return undefined; 980 981 case "getBaselinePrefs": 982 return this.getBaselinePrefs(); 983 984 case "comparePrefsToBaseline": 985 return this.comparePrefsToBaseline(aMessage.data); 986 987 case "PushPrefEnv": 988 return this.pushPrefEnv(aMessage.data); 989 990 case "PopPrefEnv": 991 return this.popPrefEnv(); 992 993 case "FlushPrefEnv": 994 return this.flushPrefEnv(); 995 996 case "PushPermissions": 997 return this.pushPermissions(aMessage.data); 998 999 case "PopPermissions": 1000 return this.popPermissions(); 1001 1002 case "FlushPermissions": 1003 return this.flushPermissions(); 1004 1005 case "SPPrefService": { 1006 let prefs = Services.prefs; 1007 let prefType = aMessage.json.prefType.toUpperCase(); 1008 let { prefName, prefValue, iid, defaultValue } = aMessage.json; 1009 1010 if (aMessage.json.op == "get") { 1011 if (!prefName || !prefType) { 1012 throw new SpecialPowersError( 1013 "Invalid parameters for get in SPPrefService" 1014 ); 1015 } 1016 1017 // return null if the pref doesn't exist 1018 if ( 1019 defaultValue === undefined && 1020 prefs.getPrefType(prefName) == prefs.PREF_INVALID 1021 ) { 1022 return null; 1023 } 1024 return this._getPref(prefName, prefType, defaultValue, iid); 1025 } else if (aMessage.json.op == "set") { 1026 if (!prefName || !prefType || prefValue === undefined) { 1027 throw new SpecialPowersError( 1028 "Invalid parameters for set in SPPrefService" 1029 ); 1030 } 1031 1032 return this._setPref(prefName, prefType, prefValue, iid); 1033 } else if (aMessage.json.op == "clear") { 1034 if (!prefName) { 1035 throw new SpecialPowersError( 1036 "Invalid parameters for clear in SPPrefService" 1037 ); 1038 } 1039 1040 prefs.clearUserPref(prefName); 1041 } else { 1042 throw new SpecialPowersError("Invalid operation for SPPrefService"); 1043 } 1044 1045 return undefined; // See comment at the beginning of this function. 1046 } 1047 1048 case "SPProcessCrashService": { 1049 switch (aMessage.json.op) { 1050 case "register-observer": 1051 this._addProcessCrashObservers(); 1052 break; 1053 case "unregister-observer": 1054 this._removeProcessCrashObservers(); 1055 break; 1056 case "delete-crash-dump-files": 1057 return this._deleteCrashDumpFiles(aMessage.json.filenames); 1058 case "find-crash-dump-files": 1059 return this._findCrashDumpFiles( 1060 aMessage.json.crashDumpFilesToIgnore 1061 ); 1062 case "delete-pending-crash-dump-files": 1063 return this._deletePendingCrashDumpFiles(); 1064 default: 1065 throw new SpecialPowersError( 1066 "Invalid operation for SPProcessCrashService" 1067 ); 1068 } 1069 return undefined; // See comment at the beginning of this function. 1070 } 1071 1072 case "SPProcessCrashManagerWait": { 1073 let promises = aMessage.json.crashIds.map(crashId => { 1074 return Services.crashmanager.ensureCrashIsPresent(crashId); 1075 }); 1076 return Promise.all(promises); 1077 } 1078 1079 case "SPPermissionManager": { 1080 let msg = aMessage.data; 1081 switch (msg.op) { 1082 case "add": 1083 case "remove": 1084 this._permOp(msg); 1085 break; 1086 case "has": { 1087 let hasPerm = Services.perms.testPermissionFromPrincipal( 1088 msg.principal, 1089 msg.type 1090 ); 1091 return hasPerm == Ci.nsIPermissionManager.ALLOW_ACTION; 1092 } 1093 case "test": { 1094 let testPerm = Services.perms.testPermissionFromPrincipal( 1095 msg.principal, 1096 msg.type 1097 ); 1098 return testPerm == msg.value; 1099 } 1100 default: 1101 throw new SpecialPowersError( 1102 "Invalid operation for SPPermissionManager" 1103 ); 1104 } 1105 return undefined; // See comment at the beginning of this function. 1106 } 1107 1108 case "SPObserverService": { 1109 let topic = aMessage.json.observerTopic; 1110 switch (aMessage.json.op) { 1111 case "notify": { 1112 let data = aMessage.json.observerData; 1113 Services.obs.notifyObservers(null, topic, data); 1114 break; 1115 } 1116 case "add": 1117 this._registerObservers._add(topic); 1118 break; 1119 default: 1120 throw new SpecialPowersError( 1121 "Invalid operation for SPObserverervice" 1122 ); 1123 } 1124 return undefined; // See comment at the beginning of this function. 1125 } 1126 1127 case "SPLoadChromeScript": { 1128 let id = aMessage.json.id; 1129 let scriptName; 1130 1131 let jsScript = aMessage.json.function.body; 1132 if (aMessage.json.url) { 1133 scriptName = aMessage.json.url; 1134 } else if (aMessage.json.function) { 1135 scriptName = 1136 aMessage.json.function.name || 1137 "<loadChromeScript anonymous function>"; 1138 } else { 1139 throw new SpecialPowersError("SPLoadChromeScript: Invalid script"); 1140 } 1141 1142 // Setup a chrome sandbox that has access to sendAsyncMessage 1143 // and {add,remove}MessageListener in order to communicate with 1144 // the mochitest. 1145 let sb = new lazy.SpecialPowersSandbox( 1146 scriptName, 1147 data => { 1148 this.sendAsyncMessage("Assert", data); 1149 }, 1150 aMessage.data 1151 ); 1152 1153 Object.assign(sb.sandbox, { 1154 createWindowlessBrowser, 1155 sendAsyncMessage: (name, message) => { 1156 this.sendAsyncMessage("SPChromeScriptMessage", { 1157 id, 1158 name, 1159 message, 1160 }); 1161 }, 1162 addMessageListener: (name, listener) => { 1163 this._chromeScriptListeners.push({ id, name, listener }); 1164 }, 1165 removeMessageListener: (name, listener) => { 1166 let index = this._chromeScriptListeners.findIndex(function (obj) { 1167 return ( 1168 obj.id == id && obj.name == name && obj.listener == listener 1169 ); 1170 }); 1171 if (index >= 0) { 1172 this._chromeScriptListeners.splice(index, 1); 1173 } 1174 }, 1175 actorParent: this.manager, 1176 console, 1177 }); 1178 1179 // Evaluate the chrome script 1180 try { 1181 Cu.evalInSandbox(jsScript, sb.sandbox, "1.8", scriptName, 1); 1182 } catch (e) { 1183 throw new SpecialPowersError( 1184 "Error while executing chrome script '" + 1185 scriptName + 1186 "':\n" + 1187 e + 1188 "\n" + 1189 e.fileName + 1190 ":" + 1191 e.lineNumber 1192 ); 1193 } 1194 return undefined; // See comment at the beginning of this function. 1195 } 1196 1197 case "SPChromeScriptMessage": { 1198 let id = aMessage.json.id; 1199 let name = aMessage.json.name; 1200 let message = aMessage.json.message; 1201 let result; 1202 for (let listener of this._chromeScriptListeners) { 1203 if (listener.name == name && listener.id == id) { 1204 result = listener.listener(message); 1205 } 1206 } 1207 return result; 1208 } 1209 1210 case "SPCleanUpSTSData": { 1211 let origin = aMessage.data.origin; 1212 let uri = Services.io.newURI(origin); 1213 let sss = Cc["@mozilla.org/ssservice;1"].getService( 1214 Ci.nsISiteSecurityService 1215 ); 1216 sss.resetState(uri); 1217 return undefined; 1218 } 1219 1220 case "SPRequestDumpCoverageCounters": { 1221 return lazy.PerTestCoverageUtils.afterTest(); 1222 } 1223 1224 case "SPRequestResetCoverageCounters": { 1225 return lazy.PerTestCoverageUtils.beforeTest(); 1226 } 1227 1228 case "SPCheckServiceWorkers": { 1229 let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( 1230 Ci.nsIServiceWorkerManager 1231 ); 1232 let regs = swm.getAllRegistrations(); 1233 1234 // XXX This code is shared with specialpowers.js. 1235 let workers = new Array(regs.length); 1236 for (let i = 0; i < regs.length; ++i) { 1237 let { scope, scriptSpec } = regs.queryElementAt( 1238 i, 1239 Ci.nsIServiceWorkerRegistrationInfo 1240 ); 1241 workers[i] = { scope, scriptSpec }; 1242 } 1243 return { workers }; 1244 } 1245 1246 case "SPLoadExtension": { 1247 let id = aMessage.data.id; 1248 let ext = aMessage.data.ext; 1249 if (AppConstants.MOZ_GECKOVIEW) { 1250 // Some extension APIs are partially implemented in Java, and the 1251 // interface between the JS and Java side (GeckoViewWebExtension) 1252 // expects extensions to be registered with the AddonManager. 1253 // 1254 // For simplicity, default to using an Addon Manager (if not null). 1255 if (ext.useAddonManager === undefined) { 1256 ext.useAddonManager = "geckoview-only"; 1257 } 1258 } 1259 // delayedStartup is only supported in xpcshell 1260 if (ext.delayedStartup !== undefined) { 1261 throw new Error( 1262 `delayedStartup is only supported in xpcshell, use "useAddonManager".` 1263 ); 1264 } 1265 1266 let extension = lazy.ExtensionTestCommon.generate(ext); 1267 1268 let resultListener = (...args) => { 1269 this.sendAsyncMessage("SPExtensionMessage", { 1270 id, 1271 type: "testResult", 1272 args, 1273 }); 1274 }; 1275 1276 let messageListener = (...args) => { 1277 args.shift(); 1278 this.sendAsyncMessage("SPExtensionMessage", { 1279 id, 1280 type: "testMessage", 1281 args, 1282 }); 1283 }; 1284 1285 // Register pass/fail handlers. 1286 extension.on("test-result", resultListener); 1287 extension.on("test-eq", resultListener); 1288 extension.on("test-log", resultListener); 1289 extension.on("test-done", resultListener); 1290 // Web Platform Test subtest started and finished events. 1291 extension.on("test-task-start", resultListener); 1292 extension.on("test-task-done", resultListener); 1293 1294 extension.on("test-message", messageListener); 1295 1296 this._extensions.set(id, extension); 1297 return undefined; 1298 } 1299 1300 case "SPStartupExtension": { 1301 let id = aMessage.data.id; 1302 // This is either an Extension, or (if useAddonManager is set) a MockExtension. 1303 let extension = this._extensions.get(id); 1304 extension.on("startup", (eventName, ext) => { 1305 if (AppConstants.platform === "android") { 1306 // We need a way to notify the embedding layer that a new extension 1307 // has been installed, so that the java layer can be updated too. 1308 Services.obs.notifyObservers(null, "testing-installed-addon", id); 1309 } 1310 // ext is always the "real" Extension object, even when "extension" 1311 // is a MockExtension. 1312 this.sendAsyncMessage("SPExtensionMessage", { 1313 id, 1314 type: "extensionSetId", 1315 args: [ext.id, ext.uuid], 1316 }); 1317 }); 1318 1319 // Make sure the extension passes the packaging checks when 1320 // they're run on a bare archive rather than a running instance, 1321 // as the add-on manager runs them. 1322 let extensionData = new lazy.ExtensionData(extension.rootURI); 1323 return extensionData 1324 .loadManifest() 1325 .then( 1326 () => { 1327 return extensionData.initAllLocales().then(() => { 1328 if (extensionData.errors.length) { 1329 return Promise.reject( 1330 "Extension contains packaging errors" 1331 ); 1332 } 1333 return undefined; 1334 }); 1335 }, 1336 () => { 1337 // loadManifest() will throw if we're loading an embedded 1338 // extension, so don't worry about locale errors in that 1339 // case. 1340 } 1341 ) 1342 .then(async () => { 1343 // browser tests do not call startup in ExtensionXPCShellUtils or MockExtension, 1344 // in that case we have an ID here and we need to set the override. 1345 if (extension.id) { 1346 await lazy.ExtensionTestCommon.setIncognitoOverride(extension); 1347 } 1348 return extension.startup().then( 1349 () => {}, 1350 e => { 1351 dump(`Extension startup failed: ${e}\n${e.stack}`); 1352 throw e; 1353 } 1354 ); 1355 }); 1356 } 1357 1358 case "SPExtensionMessage": { 1359 let id = aMessage.data.id; 1360 let extension = this._extensions.get(id); 1361 extension.testMessage(...aMessage.data.args); 1362 return undefined; 1363 } 1364 1365 case "SPExtensionGrantActiveTab": { 1366 let { id, tabId } = aMessage.data; 1367 let { tabManager } = this._extensions.get(id); 1368 tabManager.addActiveTabPermission(tabManager.get(tabId).nativeTab); 1369 return undefined; 1370 } 1371 1372 case "SPUnloadExtension": { 1373 let id = aMessage.data.id; 1374 let extension = this._extensions.get(id); 1375 this._extensions.delete(id); 1376 return lazy.ExtensionTestCommon.unloadTestExtension(extension); 1377 } 1378 1379 case "SPExtensionTerminateBackground": { 1380 let id = aMessage.data.id; 1381 let args = aMessage.data.args; 1382 let extension = this._extensions.get(id); 1383 return extension.terminateBackground(...args); 1384 } 1385 1386 case "SPExtensionWakeupBackground": { 1387 let id = aMessage.data.id; 1388 let extension = this._extensions.get(id); 1389 return extension.wakeupBackground(); 1390 } 1391 1392 case "SetAsDefaultAssertHandler": { 1393 defaultAssertHandler = this; 1394 return undefined; 1395 } 1396 1397 case "Spawn": { 1398 // Use a different variable for the profiler marker start time 1399 // so that a marker isn't added when we return, but instead when 1400 // our promise resolves. 1401 let spawnStartTime = startTime; 1402 startTime = undefined; 1403 let { browsingContext, task, args, caller, hasHarness, imports } = 1404 aMessage.data; 1405 1406 let spParent = 1407 browsingContext.currentWindowGlobal.getActor("SpecialPowers"); 1408 1409 let taskId = nextTaskID++; 1410 if (hasHarness) { 1411 spParent._taskActors.set(taskId, this); 1412 } 1413 1414 return spParent 1415 .sendQuery("Spawn", { task, args, caller, taskId, imports }) 1416 .finally(() => { 1417 ChromeUtils.addProfilerMarker( 1418 "SpecialPowers", 1419 { startTime: spawnStartTime, category: "Test" }, 1420 aMessage.name 1421 ); 1422 return spParent._taskActors.delete(taskId); 1423 }); 1424 } 1425 1426 case "SpawnChrome": { 1427 let { task, args, caller, imports } = aMessage.data; 1428 1429 return this._spawnChrome(task, args, caller, imports); 1430 } 1431 1432 case "Snapshot": { 1433 let { browsingContext, rect, background, resetScrollPosition } = 1434 aMessage.data; 1435 1436 return browsingContext.currentWindowGlobal 1437 .drawSnapshot(rect, 1.0, background, resetScrollPosition) 1438 .then(async image => { 1439 let hiddenFrame = new lazy.HiddenFrame(); 1440 let win = await hiddenFrame.get(); 1441 1442 let canvas = win.document.createElement("canvas"); 1443 canvas.width = image.width; 1444 canvas.height = image.height; 1445 1446 const ctx = canvas.getContext("2d"); 1447 ctx.drawImage(image, 0, 0); 1448 1449 let data = ctx.getImageData(0, 0, image.width, image.height); 1450 hiddenFrame.destroy(); 1451 return data; 1452 }); 1453 } 1454 1455 case "SecurityState": { 1456 let { browsingContext } = aMessage.data; 1457 return browsingContext.secureBrowserUI.state; 1458 } 1459 1460 case "ProxiedAssert": { 1461 let { taskId, data } = aMessage.data; 1462 1463 let actor = this._taskActors.get(taskId) || defaultAssertHandler; 1464 actor.sendAsyncMessage("Assert", data); 1465 1466 return undefined; 1467 } 1468 1469 case "SPRemoveAllServiceWorkers": { 1470 return lazy.ServiceWorkerCleanUp.removeAll(); 1471 } 1472 1473 case "SPRemoveServiceWorkerDataForExampleDomain": { 1474 return lazy.ServiceWorkerCleanUp.removeFromHost("example.com"); 1475 } 1476 1477 case "SPGenerateMediaControlKeyTestEvent": { 1478 // eslint-disable-next-line no-undef 1479 MediaControlService.generateMediaControlKey(aMessage.data.event); 1480 return undefined; 1481 } 1482 1483 default: 1484 throw new SpecialPowersError( 1485 `Unrecognized Special Powers API: ${aMessage.name}` 1486 ); 1487 } 1488 // This should be unreachable. If it ever becomes reachable, ESLint 1489 // will produce an error about inconsistent return values. 1490 } finally { 1491 if (startTime) { 1492 ChromeUtils.addProfilerMarker( 1493 "SpecialPowers", 1494 { startTime, category: "Test" }, 1495 aMessage.name 1496 ); 1497 } 1498 } 1499 } 1500 }