AboutNewTabResourceMapping.sys.mjs (29446B)
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 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 7 8 export const BUILTIN_ADDON_ID = "newtab@mozilla.org"; 9 export const DISABLE_NEWTAB_AS_ADDON_PREF = 10 "browser.newtabpage.disableNewTabAsAddon"; 11 export const TRAINHOP_NIMBUS_FEATURE_ID = "newtabTrainhopAddon"; 12 export const TRAINHOP_NIMBUS_FIRST_STARTUP_FEATURE_ID = 13 "newtabTrainhopFirstStartup"; 14 export const TRAINHOP_XPI_BASE_URL_PREF = 15 "browser.newtabpage.trainhopAddon.xpiBaseURL"; 16 export const TRAINHOP_XPI_VERSION_PREF = 17 "browser.newtabpage.trainhopAddon.version"; 18 export const TRAINHOP_SCHEDULED_UPDATE_STATE_DELAY_PREF = 19 "browser.newtabpage.trainhopAddon.scheduledUpdateState.delay"; 20 export const TRAINHOP_SCHEDULED_UPDATE_STATE_TIMEOUT_PREF = 21 "browser.newtabpage.trainhopAddon.scheduledUpdateState.timeout"; 22 23 const FLUENT_SOURCE_NAME = "newtab"; 24 const TOPIC_LOCALES_CHANGED = "intl:app-locales-changed"; 25 const TOPIC_SHUTDOWN = "profile-before-change"; 26 27 const lazy = XPCOMUtils.declareLazy({ 28 AddonManager: "resource://gre/modules/AddonManager.sys.mjs", 29 AddonSettings: "resource://gre/modules/addons/AddonSettings.sys.mjs", 30 AboutHomeStartupCache: "resource:///modules/AboutHomeStartupCache.sys.mjs", 31 AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs", 32 DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", 33 ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", 34 NewTabGleanUtils: "resource://newtab/lib/NewTabGleanUtils.sys.mjs", 35 NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", 36 37 resProto: { 38 service: "@mozilla.org/network/protocol;1?name=resource", 39 iid: Ci.nsISubstitutingProtocolHandler, 40 }, 41 aomStartup: { 42 service: "@mozilla.org/addons/addon-manager-startup;1", 43 iid: Ci.amIAddonManagerStartup, 44 }, 45 aboutRedirector: { 46 service: "@mozilla.org/network/protocol/about;1?what=newtab", 47 iid: Ci.nsIAboutModule, 48 }, 49 50 // NOTE: the following timeout and delay prefs are used to customize the 51 // DeferredTask that calls updateTrainhopAddonState, and they are meant to 52 // be set for testing, debugging or QA verification. 53 trainhopAddonScheduledUpdateDelay: { 54 pref: TRAINHOP_SCHEDULED_UPDATE_STATE_DELAY_PREF, 55 default: 5000, 56 }, 57 trainhopAddonScheduledUpdateTimeout: { 58 pref: TRAINHOP_SCHEDULED_UPDATE_STATE_TIMEOUT_PREF, 59 default: -1, 60 }, 61 trainhopAddonXPIBaseURL: { 62 pref: TRAINHOP_XPI_BASE_URL_PREF, 63 default: "", 64 }, 65 trainhopAddonXPIVersion: { 66 pref: TRAINHOP_XPI_VERSION_PREF, 67 default: "", 68 }, 69 }); 70 71 /** 72 * AboutNewTabResourceMapping is responsible for creating the mapping between 73 * the built-in add-on newtab code, and the chrome://newtab and resource://newtab 74 * URI prefixes (which are also used by the component mode for newtab, and acts 75 * as a compatibility layer). 76 * 77 * When the built-in add-on newtab is being read in from an XPI, the 78 * AboutNewTabResourceMapping is also responsible for doing dynamic Fluent 79 * and Glean ping/metric registration. 80 */ 81 export var AboutNewTabResourceMapping = { 82 initialized: false, 83 log: null, 84 newTabAsAddonDisabled: false, 85 86 _rootURISpec: null, 87 _addonIsXPI: null, 88 _addonVersion: null, 89 _addonListener: null, 90 _builtinVersion: null, 91 _updateAddonStateDeferredTask: null, 92 _supportedLocales: null, 93 94 /** 95 * Returns the version string for whichever version of New Tab is currently 96 * being used. 97 * 98 * @type {string} 99 */ 100 get addonVersion() { 101 return this._addonVersion; 102 }, 103 104 /** 105 * This should be called early on in the lifetime of the browser, before any 106 * attempt to load a resource from resource://newtab or chrome://newtab. 107 * 108 * This method is a no-op after the first call. 109 */ 110 init() { 111 if (this.initialized) { 112 return; 113 } 114 115 this.logger.debug("Initializing:"); 116 117 // NOTE: this pref is read only once per session on purpose 118 // (and it is expected to be used by the resource mapping logic 119 // on the next application startup if flipped at runtime, e.g. as 120 // part of an emergency pref flip through Nimbus). 121 this.newTabAsAddonDisabled = Services.prefs.getBoolPref( 122 DISABLE_NEWTAB_AS_ADDON_PREF, 123 false 124 ); 125 this.inSafeMode = Services.appinfo.inSafeMode; 126 this.getBuiltinAddonVersion(); 127 this.registerNewTabResources(); 128 this.addAddonListener(); 129 130 this.initialized = true; 131 this.logger.debug("Initialized"); 132 }, 133 134 /** 135 * Adds an add-on listener to detect postponed installations of the newtab add-on 136 * and invalidate the AboutHomeStartupCache. This method is a no-op when the 137 * emergency fallback `browser.newtabpage.disableNewTabAsAddon` about:config pref 138 * is set to true. 139 */ 140 addAddonListener() { 141 if (!this._addonListener && !this.newTabAsAddonDisabled) { 142 // The newtab add-on has a background.js script which defers updating until 143 // the next restart. We still, however, want to blow away the about:home 144 // startup cache when we notice this postponed install, to avoid loading 145 // a cache created with another version of newtab. 146 const addonInstallListener = {}; 147 addonInstallListener.onInstallPostponed = install => { 148 if (install.addon.id === BUILTIN_ADDON_ID) { 149 this.logger.debug( 150 "Invalidating AboutHomeStartupCache on detected newly installed newtab resources" 151 ); 152 lazy.AboutHomeStartupCache.clearCacheAndUninit(); 153 } 154 }; 155 lazy.AddonManager.addInstallListener(addonInstallListener); 156 this._addonListener = addonInstallListener; 157 } 158 }, 159 160 /** 161 * Retrieves the version of the built-in newtab add-on from AddonManager. 162 * If AddonManager.getBuiltinAddonVersion hits an unexpected exception (e.g. 163 * if the method is unexpectedly called before AddonManager and XPIProvider 164 * are being started), it sets the _builtinVersion property to null and logs 165 * a warning message. 166 */ 167 getBuiltinAddonVersion() { 168 try { 169 this._builtinVersion = 170 lazy.AddonManager.getBuiltinAddonVersion(BUILTIN_ADDON_ID); 171 } catch (e) { 172 this._builtinVersion = null; 173 this.logger.warn( 174 "Unexpected failure on retrieving builtin addon version", 175 e 176 ); 177 } 178 }, 179 180 /** 181 * Gets the preferred mapping for newtab resources. This method tries to retrieve 182 * the rootURI from the WebExtensionPolicy instance of the newtab add-on, or falling 183 * back to the URI of the newtab resources bundled in the Desktop omni jar if not found. 184 * The newtab resources bundled in the Desktop omni jar are instead always preferred 185 * while running in safe mode or if the emergency fallback about:config pref 186 * (`browser.newtabpage.disableNewTabAsAddon`) is set to true. 187 * 188 * @returns {{version: ?string, rootURI: nsIURI}} 189 * Returns the preferred newtab root URI for resource://newtab and chrome://newtab, 190 * along with add-on version if using the newtab add-on root URI, or null 191 * when the newtab add-on root URI was not selected as the preferred one. 192 */ 193 getPreferredMapping() { 194 const { inSafeMode, newTabAsAddonDisabled } = this; 195 const policy = WebExtensionPolicy.getByID(BUILTIN_ADDON_ID); 196 // Retrieve the mapping url (but fallback to the known url for the 197 // newtab resources bundled in the Desktop omni jar if that fails). 198 let { version, rootURI } = policy?.extension ?? {}; 199 let isXPI = rootURI?.spec.endsWith(".xpi!/"); 200 201 // If we failed to retrieve the builtin add-on version, avoid mapping 202 // XPI resources as an additional safety measure, because later it 203 // wouldn't be possible to check if the builtin version is more recent 204 // than the train-hop add-on version that may be already installed. 205 if (isXPI && this._builtinVersion === null) { 206 rootURI = null; 207 isXPI = false; 208 } 209 210 // Do not use XPI resources to prepare to uninstall the train-hop add-on xpi 211 // later in the current application session from updateTrainhopAddonState, if: 212 // 213 // - the train-hop add-on version set in the pref is empty (the client has been 214 // unenrolled in the previous browsing session and so we fallback to the 215 // resources bundled in the Desktop omni jar) 216 // - the builtin add-on version is equal or greater than the train-hop add-on 217 // version (and so the application has been updated and the old train-hop 218 // add-on is obsolete and can be uninstalled) 219 // - the train-hop add-on xpi is not system-signed (as specifically required for 220 // newtab xpi being installed in the `extensions` profile subdirectory by 221 // the custom install logic provided by the _installTrainhopAddon method). 222 const shouldUninstallXPI = isXPI 223 ? lazy.trainhopAddonXPIVersion === "" || 224 Services.vc.compare(this._builtinVersion, version) >= 0 || 225 (lazy.AddonSettings.REQUIRE_SIGNING && !policy.isPrivileged) 226 : false; 227 228 if (!rootURI || inSafeMode || newTabAsAddonDisabled || shouldUninstallXPI) { 229 const builtinAddonsURI = lazy.resProto.getSubstitution("builtin-addons"); 230 rootURI = Services.io.newURI("newtab/", null, builtinAddonsURI); 231 version = null; 232 isXPI = false; 233 } 234 return { isXPI, version, rootURI }; 235 }, 236 237 /** 238 * Registers the resource://newtab and chrome://newtab resources, and also 239 * kicks off dynamic Fluent and Glean registration if the add-on is installed 240 * via an XPI. 241 */ 242 registerNewTabResources() { 243 const RES_PATH = "newtab"; 244 try { 245 const { isXPI, version, rootURI } = this.getPreferredMapping(); 246 this._rootURISpec = rootURI.spec; 247 this._addonVersion = version; 248 this._addonIsXPI = isXPI; 249 this.logger.log( 250 this.newTabAsAddonDisabled || !version 251 ? `Mapping newtab resources from ${rootURI.spec}` 252 : `Mapping newtab resources from ${isXPI ? "XPI" : "built-in add-on"} version ${version} ` + 253 `on application version ${AppConstants.MOZ_APP_VERSION_DISPLAY}` 254 ); 255 lazy.resProto.setSubstitutionWithFlags( 256 RES_PATH, 257 rootURI, 258 Ci.nsISubstitutingProtocolHandler.ALLOW_CONTENT_ACCESS 259 ); 260 const manifestURI = Services.io.newURI("manifest.json", null, rootURI); 261 this._chromeHandle = lazy.aomStartup.registerChrome(manifestURI, [ 262 ["content", "newtab", "data/content", "contentaccessible=yes"], 263 ]); 264 265 if (isXPI) { 266 // We must be a train-hopped XPI running in this app. This means we 267 // may have Fluent files or Glean pings/metrics to register dynamically. 268 this.registerFluentSources(rootURI); 269 this.registerMetricsFromJson(); 270 } 271 lazy.aboutRedirector.wrappedJSObject.notifyBuiltInAddonInitialized(); 272 Glean.newtab.addonReadySuccess.set(true); 273 Glean.newtab.addonXpiUsed.set(isXPI); 274 this.logger.debug("Newtab resource mapping completed successfully"); 275 } catch (e) { 276 this.logger.error("Failed to complete resource mapping: ", e); 277 Glean.newtab.addonReadySuccess.set(false); 278 throw e; 279 } 280 }, 281 282 /** 283 * Registers Fluent strings contained within the XPI. 284 * 285 * @param {nsIURI} rootURI 286 * The rootURI for the newtab add-on. 287 * @returns {Promise<undefined>} 288 * Resolves once the Fluent strings have been registered, or even if a 289 * failure to register them has occurred (which will log the error). 290 */ 291 async registerFluentSources(rootURI) { 292 try { 293 // Read in the list of locales included with the XPI. This will prevent 294 // us from accidentally registering a L10nFileSource that wasn't included. 295 this._supportedLocales = new Set( 296 await fetch(rootURI.resolve("/locales/supported-locales.json")).then( 297 r => r.json() 298 ) 299 ); 300 301 // Set up observers so that if the user changes the list of available 302 // locales, we'll re-register. 303 Services.obs.addObserver(this, TOPIC_LOCALES_CHANGED); 304 Services.obs.addObserver(this, TOPIC_SHUTDOWN); 305 // Now actually do the registration. 306 this._updateFluentSourcesRegistration(); 307 } catch (e) { 308 // TODO: consider if we should collect this in telemetry. 309 this.logger.error( 310 `Error on registering fluent files from ${rootURI.spec}:`, 311 e 312 ); 313 } 314 }, 315 316 /** 317 * Sets up the L10nFileSource for the newtab Fluent files included in the 318 * XPI that are in the available locales for the app. If a pre-existing 319 * registration exists, it will be updated. 320 */ 321 _updateFluentSourcesRegistration() { 322 let availableLocales = new Set(Services.locale.availableLocales); 323 let availableSupportedLocales = 324 this._supportedLocales.intersection(availableLocales); 325 326 const newtabFileSource = new L10nFileSource( 327 FLUENT_SOURCE_NAME, 328 "app", 329 [...availableSupportedLocales], 330 `resource://newtab/locales/{locale}/` 331 ); 332 333 let registry = L10nRegistry.getInstance(); 334 if (registry.hasSource(FLUENT_SOURCE_NAME)) { 335 registry.updateSources([newtabFileSource]); 336 this.logger.debug( 337 "Newtab strings updated for ", 338 Array.from(availableSupportedLocales) 339 ); 340 } else { 341 registry.registerSources([newtabFileSource]); 342 this.logger.debug( 343 "Newtab strings registered for ", 344 Array.from(availableSupportedLocales) 345 ); 346 } 347 }, 348 349 observe(_subject, topic, _data) { 350 switch (topic) { 351 case TOPIC_LOCALES_CHANGED: { 352 this._updateFluentSourcesRegistration(); 353 break; 354 } 355 case TOPIC_SHUTDOWN: { 356 Services.obs.removeObserver(this, TOPIC_LOCALES_CHANGED); 357 Services.obs.removeObserver(this, TOPIC_SHUTDOWN); 358 break; 359 } 360 } 361 }, 362 363 /** 364 * Registers any dynamic Glean metrics that have been included with the XPI 365 * version of the add-on. 366 */ 367 registerMetricsFromJson() { 368 // The metrics we need to process were placed in webext-glue/metrics/runtime-metrics-<version>.json 369 // That file will be generated by build script getting implemented with Bug 1960111 370 const version = AppConstants.MOZ_APP_VERSION.match(/\d+/)[0]; 371 const metricsPath = `resource://newtab/webext-glue/metrics/runtime-metrics-${version}.json`; 372 this.logger.debug(`Registering FOG Glean metrics from ${metricsPath}`); 373 lazy.NewTabGleanUtils.registerMetricsAndPings(metricsPath); 374 }, 375 376 scheduleUpdateTrainhopAddonState() { 377 if (!this._updateAddonStateDeferredTask) { 378 this.logger.debug("creating _updateAddonStateDeferredTask"); 379 const delayMs = lazy.trainhopAddonScheduledUpdateDelay; 380 const idleTimeoutMs = lazy.trainhopAddonScheduledUpdateTimeout; 381 this._updateAddonStateDeferredTask = new lazy.DeferredTask( 382 async () => { 383 const isPastShutdownConfirmed = 384 Services.startup.isInOrBeyondShutdownPhase( 385 Ci.nsIAppStartup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED 386 ); 387 if (isPastShutdownConfirmed) { 388 this.logger.debug( 389 "updateAddonStateDeferredTask cancelled after appShutdownConfirmed barrier" 390 ); 391 return; 392 } 393 this.logger.debug("_updateAddonStateDeferredTask running"); 394 await this.updateTrainhopAddonState().catch(e => { 395 this.logger.warn("updateAddonStateDeferredTask failure", e); 396 }); 397 this.logger.debug("_updateAddonStateDeferredTask completed"); 398 }, 399 delayMs, 400 idleTimeoutMs 401 ); 402 lazy.AsyncShutdown.appShutdownConfirmed.addBlocker( 403 `${TRAINHOP_NIMBUS_FEATURE_ID} scheduleUpdateTrainhopAddonState shutting down`, 404 () => this._updateAddonStateDeferredTask.finalize() 405 ); 406 lazy.NimbusFeatures[TRAINHOP_NIMBUS_FEATURE_ID].onUpdate(() => 407 this._updateAddonStateDeferredTask.arm() 408 ); 409 } 410 this.logger.debug("re-arming _updateAddonStateDeferredTask"); 411 this._updateAddonStateDeferredTask.arm(); 412 }, 413 414 /** 415 * Updates the state of the train-hop add-on based on the Nimbus feature variables. 416 * 417 * @returns {Promise<void>} 418 * Resolves once the train-hop add-on has been staged to be installed or uninstalled (e.g. 419 * when the client has been unenrolled from the Nimbus feature), or after it has been 420 * determined that no action was needed (e.g. while running in safemode, or if the same 421 * or an higher add-on version than the train-hop add-on version is already in use, 422 * installed or pending to be installed). Rejects on failures or unexpected cancellations 423 * during installation or uninstallation process. 424 */ 425 async updateTrainhopAddonState(forceRestartlessInstall = false) { 426 if (this.inSafeMode) { 427 this.logger.debug( 428 "train-hop add-on update state disabled while running in SafeMode" 429 ); 430 return; 431 } 432 433 const nimbusFeature = lazy.NimbusFeatures[TRAINHOP_NIMBUS_FEATURE_ID]; 434 await nimbusFeature.ready(); 435 const { addon_version, xpi_download_path } = nimbusFeature.getAllVariables({ 436 defaultValues: { addon_version: null, xpi_download_path: null }, 437 }); 438 439 this.logger.debug("Force restartless install: ", forceRestartlessInstall); 440 this.logger.debug("Received addon version:", addon_version); 441 this.logger.debug("Received XPI download path:", xpi_download_path); 442 443 let addon = await lazy.AddonManager.getAddonByID(BUILTIN_ADDON_ID); 444 445 // Uninstall train-hop add-on xpi if its resources are not currently 446 // being used and the client has been unenrolled from the newtabTrainhopAddon 447 // Nimbus feature. 448 if (!this._addonIsXPI && addon) { 449 let changed = false; 450 if (addon_version === null && xpi_download_path === null) { 451 changed ||= await this.uninstallAddon({ 452 uninstallReason: 453 "uninstalling train-hop add-on version on Nimbus feature unenrolled", 454 }); 455 return; 456 } 457 458 if ( 459 this._builtinVersion && 460 Services.vc.compare(this._builtinVersion, addon.version) >= 0 461 ) { 462 changed ||= await this.uninstallAddon({ 463 uninstallReason: 464 "uninstalling train-hop add-on version on builtin add-on with equal or higher version", 465 }); 466 } 467 468 if ( 469 lazy.AddonSettings.REQUIRE_SIGNING && 470 addon.signedState !== lazy.AddonManager.SIGNEDSTATE_SYSTEM 471 ) { 472 changed ||= await this.uninstallAddon({ 473 uninstallReason: `uninstall train-hop add-on xpi on unexpected signedState ${addon.signedState} (expected ${lazy.AddonManager.SIGNEDSTATE_SYSTEM})`, 474 }); 475 } 476 477 // Retrieve the new add-on wrapper if the xpi version has been uninstalled. 478 if (changed) { 479 addon = await lazy.AddonManager.getAddonByID(BUILTIN_ADDON_ID); 480 481 this.logger.debug( 482 "Invalidating AboutHomeStartupCache after train-hop uninstall" 483 ); 484 lazy.AboutHomeStartupCache.clearCacheAndUninit(); 485 } 486 } 487 488 // Record Nimbus feature newtabTrainhopAddon exposure event if NewTab 489 // is currently using the resources from the train-hop add-on version. 490 if (this._addonIsXPI && this._addonVersion === addon_version) { 491 this.logger.debug( 492 `train-hop add-on version ${addon_version} already in use` 493 ); 494 // Record exposure event for the train hop feature if the train-hop 495 // add-on version is already in use. 496 nimbusFeature.recordExposureEvent({ once: true }); 497 return; 498 } 499 500 // Verify if the train-hop add-on version is already installed. 501 if (addon?.version === addon_version) { 502 this.logger.warn( 503 `train-hop add-on version ${addon_version} already installed but not in use` 504 ); 505 return; 506 } 507 508 if (!lazy.trainhopAddonXPIBaseURL) { 509 this.logger.debug( 510 "train-hop add-on download disabled on empty download base URL" 511 ); 512 return; 513 } 514 515 if (addon_version == null && xpi_download_path == null) { 516 this.logger.debug("train-hop cancelled: client not enrolled"); 517 return; 518 } 519 520 if (addon_version == null) { 521 this.logger.warn("train-hop failure: missing mandatory addon_version"); 522 return; 523 } 524 525 if (xpi_download_path == null) { 526 this.logger.warn( 527 "train-hop failure: missing mandatory xpi_download_path" 528 ); 529 return; 530 } 531 532 const xpiDownloadURL = `${lazy.trainhopAddonXPIBaseURL}${xpi_download_path}`; 533 await this._installTrainhopAddon({ 534 trainhopAddonVersion: addon_version, 535 xpiDownloadURL, 536 forceRestartlessInstall, 537 }); 538 }, 539 540 /** 541 * Downloads and installs the newtab train-hop add-on version based on Nimbus feature configuration, 542 * or record the Nimbus feature exposure event if the newtab train-hop add-on version is already in use. 543 * 544 * @param {object} params 545 * @param {string} params.trainhopAddonVersion - The version of the train-hop add-on to install. 546 * @param {string} params.xpiDownloadURL - The URL from which to download the XPI file. 547 * @param {boolean} params.forceRestartlessInstall 548 * After the XPI is downloaded, attempt to complete a restartless install. Note that if 549 * AboutNewTabResourceMapping.init has been called by the time the XPI has finished 550 * downloading, this directive is ignored, and we fallback to installing on the next 551 * restart. 552 * 553 * @returns {Promise<void>} 554 * Resolves when the train-hop add-on installation is completed or not needed, or rejects 555 * on failures or unexpected cancellations hit during the installation process. 556 */ 557 async _installTrainhopAddon({ 558 trainhopAddonVersion, 559 xpiDownloadURL, 560 forceRestartlessInstall, 561 }) { 562 if ( 563 this._builtinVersion && 564 Services.vc.compare(this._builtinVersion, trainhopAddonVersion) >= 0 565 ) { 566 this.logger.warn( 567 `cancel xpi download on train-hop add-on version ${trainhopAddonVersion} on equal or higher builtin version ${this._builtinVersion}` 568 ); 569 return; 570 } 571 572 let addon = await lazy.AddonManager.getAddonByID(BUILTIN_ADDON_ID); 573 if ( 574 addon?.version && 575 Services.vc.compare(addon.version, trainhopAddonVersion) >= 0 576 ) { 577 this.logger.warn( 578 `cancel xpi download on train-hop add-on version ${trainhopAddonVersion} on equal or higher version ${addon.version} already installed` 579 ); 580 return; 581 } 582 583 // Verify if there is already a pending install for the same or higher add-on version 584 // (in case of multiple pending installations for the same add-on id, the last one wins). 585 let pendingInstall = (await lazy.AddonManager.getAllInstalls()) 586 .filter( 587 install => 588 install.addon?.id === BUILTIN_ADDON_ID && 589 install.state === lazy.AddonManager.STATE_POSTPONED 590 ) 591 .pop(); 592 if ( 593 pendingInstall && 594 Services.vc.compare(pendingInstall.addon.version, trainhopAddonVersion) >= 595 0 596 ) { 597 this.logger.debug( 598 `cancel xpi download on train-hop add-on version ${trainhopAddonVersion} on equal or higher versions ${pendingInstall.addon.version} install already in progress` 599 ); 600 return; 601 } 602 this.logger.log( 603 `downloading train-hop add-on version ${trainhopAddonVersion} from ${xpiDownloadURL}` 604 ); 605 try { 606 let newInstall = await lazy.AddonManager.getInstallForURL( 607 xpiDownloadURL, 608 { 609 telemetryInfo: { source: "nimbus-newtabTrainhopAddon" }, 610 } 611 ); 612 const deferred = Promise.withResolvers(); 613 newInstall.addListener({ 614 onDownloadEnded: () => { 615 if ( 616 newInstall.addon.id !== BUILTIN_ADDON_ID || 617 newInstall.addon.version !== trainhopAddonVersion 618 ) { 619 deferred.reject( 620 new Error( 621 `train-hop add-on install cancelled on mismatching add-on version` + 622 `(actual ${newInstall.addon.version}, expected ${trainhopAddonVersion})` 623 ) 624 ); 625 newInstall.cancel(); 626 } 627 628 if ( 629 lazy.AddonSettings.REQUIRE_SIGNING && 630 newInstall.addon.signedState !== 631 lazy.AddonManager.SIGNEDSTATE_SYSTEM 632 ) { 633 deferred.reject( 634 new Error( 635 `trainhop add-on install cancelled on invalid signed state` + 636 `(actual ${newInstall.addon.signedState}, expected ${lazy.AddonManager.SIGNEDSTATE_SYSTEM})` 637 ) 638 ); 639 newInstall.cancel(); 640 } 641 642 this.logger.debug("Train-hop download ended"); 643 }, 644 onInstallPostponed: () => { 645 this.logger.debug("Train-hop install postponed, as expected"); 646 if (forceRestartlessInstall && !this.initialized) { 647 this.logger.debug("Forcing restartless install of train-hop"); 648 newInstall.continuePostponedInstall(); 649 } else { 650 this.logger.debug("Not forcing restartless install"); 651 if (forceRestartlessInstall) { 652 this.logger.debug( 653 "We must have initialized before the XPI finished downloading." 654 ); 655 } 656 deferred.resolve(); 657 } 658 }, 659 onInstallEnded: () => { 660 this.logger.debug("Train-hop restartless install ended"); 661 if (forceRestartlessInstall) { 662 this.logger.debug("Resolving train-hop install promise"); 663 deferred.resolve(); 664 } 665 }, 666 onDownloadCancelled: () => { 667 deferred.reject( 668 new Error( 669 `Unexpected download cancelled while downloading xpi from ${xpiDownloadURL}` 670 ) 671 ); 672 }, 673 onDownloadFailed: () => { 674 deferred.reject( 675 new Error(`Failed to download xpi from ${xpiDownloadURL}`) 676 ); 677 }, 678 onInstallCancelled: () => { 679 deferred.reject( 680 new Error( 681 `Unexpected install cancelled while installing xpi from ${xpiDownloadURL}` 682 ) 683 ); 684 }, 685 onInstallFailed: () => { 686 deferred.reject( 687 new Error(`Failed to install xpi from ${xpiDownloadURL}`) 688 ); 689 }, 690 }); 691 newInstall.install(); 692 await deferred.promise; 693 694 if (forceRestartlessInstall) { 695 this.logger.debug( 696 `train-hop add-on ${trainhopAddonVersion} downloaded and we will attempt a restartless install` 697 ); 698 } else { 699 this.logger.debug( 700 `train-hop add-on ${trainhopAddonVersion} downloaded and pending install on next startup` 701 ); 702 } 703 } catch (e) { 704 this.logger.error(`train-hop add-on install failure: ${e}`); 705 } 706 }, 707 708 /** 709 * Uninstalls the newtab add-on, if it exists and has the PERM_CAN_UNINSTALL permission, 710 * optionally logs a reason for the add-on being uninstalled. 711 * 712 * @param {object} params 713 * @param {string} [params.uninstallReason] 714 * Reason for uninstalling the add-on to log along with uninstalling 715 * the add-on. 716 * 717 * @returns {Promise<boolean>} 718 * Resolves once the add-on is uninstalled, if it was found and had the 719 * PERM_CAN_UNINSTALL permission, with a boolean set to true if the 720 * add-on was found and uninstalled. 721 */ 722 async uninstallAddon({ uninstallReason } = {}) { 723 let addon = await lazy.AddonManager.getAddonByID(BUILTIN_ADDON_ID); 724 if (addon && addon.permissions & lazy.AddonManager.PERM_CAN_UNINSTALL) { 725 if (uninstallReason) { 726 this.logger.info(uninstallReason); 727 } 728 await addon.uninstall(); 729 return true; 730 } 731 return false; 732 }, 733 734 /** 735 * This is registered to be called on first startup for new profiles on 736 * Windows. It is expected to be called very early on in the lifetime of 737 * new profiles, such that the AboutNewTabResourceMapping.init routine has 738 * not yet had a chance to run. 739 * 740 * @returns {Promise<void>} 741 */ 742 async firstStartupNewProfile() { 743 if (this.initialized) { 744 this.logger.error( 745 "firstStartupNewProfile is being run after AboutNewTabResourceMapping initializes, so we're too late." 746 ); 747 return; 748 } 749 this.logger.debug( 750 "First startup with a new profile. Checking for any train-hops to perform restartless install." 751 ); 752 await lazy.ExperimentAPI.ready(); 753 754 const nimbusFeature = 755 lazy.NimbusFeatures[TRAINHOP_NIMBUS_FIRST_STARTUP_FEATURE_ID]; 756 await nimbusFeature.ready(); 757 const { enabled } = nimbusFeature.getAllVariables({ 758 defaultValues: { enabled: true }, 759 }); 760 if (!enabled) { 761 // We've been configured to bypass the FirstStartup install for 762 // train-hops, so exit now. 763 this.logger.debug( 764 "Not forcing install of any newtab XPIs, as we're currently configured not to." 765 ); 766 return; 767 } 768 769 await lazy.AddonManager.readyPromise; 770 await this.updateTrainhopAddonState(true /* forceRestartlessInstall */); 771 this.logger.debug("First startup - new profile done"); 772 }, 773 }; 774 775 AboutNewTabResourceMapping.logger = console.createInstance({ 776 prefix: "AboutNewTabResourceMapping", 777 maxLogLevel: Services.prefs.getBoolPref( 778 "browser.newtabpage.resource-mapping.log", 779 false 780 ) 781 ? "Debug" 782 : "Warn", 783 });