ASRouterTargeting.sys.mjs (48148B)
1 /* This Source Code Form is subject to the terms of the Mozilla PublicddonMa 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 const FXA_ENABLED_PREF = "identity.fxaccounts.enabled"; 5 const TOPIC_SELECTION_MODAL_LAST_DISPLAYED_PREF = 6 "browser.newtabpage.activity-stream.discoverystream.topicSelection.onboarding.lastDisplayed"; 7 const NOTIFICATION_INTERVAL_AFTER_TOPIC_MODAL_MS = 60000; // Assuming avoid notification up to 1 minute after newtab Topic Notification Modal 8 9 // We use importESModule here instead of static import so that 10 // the Karma test environment won't choke on this module. This 11 // is because the Karma test environment already stubs out 12 // XPCOMUtils, AppConstants, NewTabUtils and ShellService, and 13 // overrides importESModule to be a no-op (which can't be done 14 // for a static import statement). 15 16 // eslint-disable-next-line mozilla/use-static-import 17 const { XPCOMUtils } = ChromeUtils.importESModule( 18 "resource://gre/modules/XPCOMUtils.sys.mjs" 19 ); 20 21 // eslint-disable-next-line mozilla/use-static-import 22 const { AppConstants } = ChromeUtils.importESModule( 23 "resource://gre/modules/AppConstants.sys.mjs" 24 ); 25 26 // eslint-disable-next-line mozilla/use-static-import 27 const { NewTabUtils } = ChromeUtils.importESModule( 28 "resource://gre/modules/NewTabUtils.sys.mjs" 29 ); 30 31 // eslint-disable-next-line mozilla/use-static-import 32 const { ShellService } = ChromeUtils.importESModule( 33 "moz-src:///browser/components/shell/ShellService.sys.mjs" 34 ); 35 36 // eslint-disable-next-line mozilla/use-static-import 37 const { ClientID } = ChromeUtils.importESModule( 38 "resource://gre/modules/ClientID.sys.mjs" 39 ); 40 41 // eslint-disable-next-line mozilla/use-static-import 42 const { PlacesUtils } = ChromeUtils.importESModule( 43 "resource://gre/modules/PlacesUtils.sys.mjs" 44 ); 45 46 const lazy = {}; 47 48 ChromeUtils.defineESModuleGetters(lazy, { 49 AboutNewTabResourceMapping: 50 "resource:///modules/AboutNewTabResourceMapping.sys.mjs", 51 AddonManager: "resource://gre/modules/AddonManager.sys.mjs", 52 AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs", 53 ASRouterPreferences: 54 "resource:///modules/asrouter/ASRouterPreferences.sys.mjs", 55 AttributionCode: 56 "moz-src:///browser/components/attribution/AttributionCode.sys.mjs", 57 BackupService: "resource:///modules/backup/BackupService.sys.mjs", 58 BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", 59 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", 60 ClientEnvironment: "resource://normandy/lib/ClientEnvironment.sys.mjs", 61 CustomizableUI: 62 "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs", 63 ExtensionUtils: "resource://gre/modules/ExtensionUtils.sys.mjs", 64 FeatureCalloutBroker: 65 "resource:///modules/asrouter/FeatureCalloutBroker.sys.mjs", 66 HomePage: "resource:///modules/HomePage.sys.mjs", 67 ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs", 68 Region: "resource://gre/modules/Region.sys.mjs", 69 // eslint-disable-next-line mozilla/no-browser-refs-in-toolkit 70 SelectableProfileService: 71 "resource:///modules/profiles/SelectableProfileService.sys.mjs", 72 SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", 73 TargetingContext: "resource://messaging-system/targeting/Targeting.sys.mjs", 74 TaskbarTabs: "resource:///modules/taskbartabs/TaskbarTabs.sys.mjs", 75 TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs", 76 TelemetrySession: "resource://gre/modules/TelemetrySession.sys.mjs", 77 WindowsLaunchOnLogin: "resource://gre/modules/WindowsLaunchOnLogin.sys.mjs", 78 }); 79 80 ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => { 81 return ChromeUtils.importESModule( 82 "resource://gre/modules/FxAccounts.sys.mjs" 83 ).getFxAccountsSingleton(); 84 }); 85 86 XPCOMUtils.defineLazyPreferenceGetter( 87 lazy, 88 "cfrFeaturesUserPref", 89 "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features", 90 true 91 ); 92 XPCOMUtils.defineLazyPreferenceGetter( 93 lazy, 94 "cfrAddonsUserPref", 95 "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons", 96 true 97 ); 98 XPCOMUtils.defineLazyPreferenceGetter( 99 lazy, 100 "hasAccessedFxAPanel", 101 "identity.fxaccounts.toolbar.accessed", 102 false 103 ); 104 XPCOMUtils.defineLazyPreferenceGetter( 105 lazy, 106 "clientsDevicesDesktop", 107 "services.sync.clients.devices.desktop", 108 0 109 ); 110 XPCOMUtils.defineLazyPreferenceGetter( 111 lazy, 112 "clientsDevicesMobile", 113 "services.sync.clients.devices.mobile", 114 0 115 ); 116 XPCOMUtils.defineLazyPreferenceGetter( 117 lazy, 118 "syncNumClients", 119 "services.sync.numClients", 120 0 121 ); 122 XPCOMUtils.defineLazyPreferenceGetter( 123 lazy, 124 "devtoolsSelfXSSCount", 125 "devtools.selfxss.count", 126 0 127 ); 128 XPCOMUtils.defineLazyPreferenceGetter( 129 lazy, 130 "isFxAEnabled", 131 FXA_ENABLED_PREF, 132 true 133 ); 134 XPCOMUtils.defineLazyPreferenceGetter( 135 lazy, 136 "isXPIInstallEnabled", 137 "xpinstall.enabled", 138 true 139 ); 140 XPCOMUtils.defineLazyPreferenceGetter( 141 lazy, 142 "hasMigratedBookmarks", 143 "browser.migrate.interactions.bookmarks", 144 false 145 ); 146 XPCOMUtils.defineLazyPreferenceGetter( 147 lazy, 148 "hasMigratedCSVPasswords", 149 "browser.migrate.interactions.csvpasswords", 150 false 151 ); 152 XPCOMUtils.defineLazyPreferenceGetter( 153 lazy, 154 "hasMigratedHistory", 155 "browser.migrate.interactions.history", 156 false 157 ); 158 XPCOMUtils.defineLazyPreferenceGetter( 159 lazy, 160 "hasMigratedPasswords", 161 "browser.migrate.interactions.passwords", 162 false 163 ); 164 XPCOMUtils.defineLazyPreferenceGetter( 165 lazy, 166 "useEmbeddedMigrationWizard", 167 "browser.migrate.content-modal.about-welcome-behavior", 168 "default", 169 null, 170 behaviorString => { 171 return behaviorString === "embedded"; 172 } 173 ); 174 XPCOMUtils.defineLazyPreferenceGetter( 175 lazy, 176 "totalSearches", 177 "browser.search.totalSearches", 178 0 179 ); 180 XPCOMUtils.defineLazyPreferenceGetter( 181 lazy, 182 "newTabTopicModalLastSeen", 183 TOPIC_SELECTION_MODAL_LAST_DISPLAYED_PREF, 184 null, 185 lastSeenString => { 186 return Number.isInteger(parseInt(lastSeenString, 10)) 187 ? parseInt(lastSeenString, 10) 188 : 0; 189 } 190 ); 191 XPCOMUtils.defineLazyPreferenceGetter( 192 lazy, 193 "profilesCreated", 194 "browser.profiles.created", 195 false 196 ); 197 XPCOMUtils.defineLazyPreferenceGetter( 198 lazy, 199 "didHandleCampaignAction", 200 "trailhead.firstrun.didHandleCampaignAction", 201 false 202 ); 203 204 XPCOMUtils.defineLazyServiceGetters(lazy, { 205 AUS: [ 206 "@mozilla.org/updates/update-service;1", 207 Ci.nsIApplicationUpdateService, 208 ], 209 BrowserHandler: ["@mozilla.org/browser/clh;1", Ci.nsIBrowserHandler], 210 ScreenManager: ["@mozilla.org/gfx/screenmanager;1", Ci.nsIScreenManager], 211 TrackingDBService: [ 212 "@mozilla.org/tracking-db-service;1", 213 Ci.nsITrackingDBService, 214 ], 215 UpdateCheckSvc: [ 216 "@mozilla.org/updates/update-checker;1", 217 Ci.nsIUpdateChecker, 218 ], 219 }); 220 221 const FXA_USERNAME_PREF = "services.sync.username"; 222 223 const { activityStreamProvider: asProvider } = NewTabUtils; 224 225 const FRECENT_SITES_UPDATE_INTERVAL = 6 * 60 * 60 * 1000; // Six hours 226 const FRECENT_SITES_IGNORE_BLOCKED = false; 227 const FRECENT_SITES_NUM_ITEMS = 25; 228 // 2 visits, 30 days ago. 229 const FRECENT_SITES_MIN_FRECENCY = PlacesUtils.history.pageFrecencyThreshold( 230 30, 231 2, 232 false 233 ); 234 235 const CACHE_EXPIRATION = 5 * 60 * 1000; 236 const jexlEvaluationCache = new Map(); 237 238 /** 239 * CachedTargetingGetter 240 * 241 * @param property {string} Name of the method 242 * @param options {any=} Options passed to the method 243 * @param updateInterval {number?} Update interval for query. Defaults to FRECENT_SITES_UPDATE_INTERVAL 244 */ 245 export function CachedTargetingGetter( 246 property, 247 options = null, 248 updateInterval = FRECENT_SITES_UPDATE_INTERVAL, 249 getter = asProvider 250 ) { 251 return { 252 _lastUpdated: 0, 253 _value: null, 254 // For testing 255 expire() { 256 this._lastUpdated = 0; 257 this._value = null; 258 }, 259 async get() { 260 const now = Date.now(); 261 if (now - this._lastUpdated >= updateInterval) { 262 this._value = await getter[property](options); 263 this._lastUpdated = now; 264 } 265 return this._value; 266 }, 267 }; 268 } 269 270 function CacheUnhandledCampaignAction() { 271 return { 272 _lastUpdated: 0, 273 _value: null, 274 expire() { 275 this._lastUpdated = 0; 276 this._value = null; 277 }, 278 get() { 279 const now = Date.now(); 280 // Don't get cached value until the action has been handled to ensure 281 // proper screen targeting in about:welcome 282 if ( 283 now - this._lastUpdated >= FRECENT_SITES_UPDATE_INTERVAL || 284 !lazy.didHandleCampaignAction 285 ) { 286 this._value = null; 287 if (!lazy.didHandleCampaignAction) { 288 const attributionData = 289 lazy.AttributionCode.getCachedAttributionData(); 290 const ALLOWED_CAMPAIGN_ACTIONS = [ 291 "PIN_AND_DEFAULT", 292 "PIN_FIREFOX_TO_TASKBAR", 293 "SET_DEFAULT_BROWSER", 294 ]; 295 const campaign = attributionData?.campaign?.toUpperCase(); 296 if (campaign && ALLOWED_CAMPAIGN_ACTIONS.includes(campaign)) { 297 this._value = campaign; 298 } 299 } 300 this._lastUpdated = now; 301 } 302 return this._value; 303 }, 304 }; 305 } 306 307 function CheckBrowserNeedsUpdate( 308 updateInterval = FRECENT_SITES_UPDATE_INTERVAL 309 ) { 310 const checker = { 311 _lastUpdated: 0, 312 _value: null, 313 // For testing. Avoid update check network call. 314 setUp(value) { 315 this._lastUpdated = Date.now(); 316 this._value = value; 317 }, 318 expire() { 319 this._lastUpdated = 0; 320 this._value = null; 321 }, 322 async get() { 323 const now = Date.now(); 324 if ( 325 !AppConstants.MOZ_UPDATER || 326 now - this._lastUpdated < updateInterval 327 ) { 328 return this._value; 329 } 330 if (!lazy.AUS.canCheckForUpdates) { 331 return false; 332 } 333 this._lastUpdated = now; 334 let check = lazy.UpdateCheckSvc.checkForUpdates( 335 lazy.UpdateCheckSvc.FOREGROUND_CHECK 336 ); 337 let result = await check.result; 338 if (!result.succeeded) { 339 lazy.ASRouterPreferences.console.error( 340 "CheckBrowserNeedsUpdate failed :>> ", 341 result.request 342 ); 343 return false; 344 } 345 checker._value = !!result.updates.length; 346 return checker._value; 347 }, 348 }; 349 350 return checker; 351 } 352 353 export const QueryCache = { 354 expireAll() { 355 Object.keys(this.queries).forEach(query => { 356 this.queries[query].expire(); 357 }); 358 Object.keys(this.getters).forEach(key => { 359 this.getters[key].expire(); 360 }); 361 }, 362 queries: { 363 TopFrecentSites: new CachedTargetingGetter("getTopFrecentSites", { 364 ignoreBlocked: FRECENT_SITES_IGNORE_BLOCKED, 365 numItems: FRECENT_SITES_NUM_ITEMS, 366 topsiteFrecency: FRECENT_SITES_MIN_FRECENCY, 367 onePerDomain: true, 368 includeFavicon: false, 369 }), 370 TotalBookmarksCount: new CachedTargetingGetter("getTotalBookmarksCount"), 371 CheckBrowserNeedsUpdate: new CheckBrowserNeedsUpdate(), 372 RecentBookmarks: new CachedTargetingGetter("getRecentBookmarks"), 373 UserMonthlyActivity: new CachedTargetingGetter("getUserMonthlyActivity"), 374 UnhandledCampaignAction: new CacheUnhandledCampaignAction(), 375 }, 376 getters: { 377 doesAppNeedPin: new CachedTargetingGetter( 378 "doesAppNeedPin", 379 null, 380 FRECENT_SITES_UPDATE_INTERVAL, 381 ShellService 382 ), 383 doesAppNeedPrivatePin: new CachedTargetingGetter( 384 "doesAppNeedPin", 385 true, 386 FRECENT_SITES_UPDATE_INTERVAL, 387 ShellService 388 ), 389 doesAppNeedStartMenuPin: new CachedTargetingGetter( 390 "doesAppNeedStartMenuPin", 391 null, 392 FRECENT_SITES_UPDATE_INTERVAL, 393 ShellService 394 ), 395 isDefaultBrowser: new CachedTargetingGetter( 396 "isDefaultBrowser", 397 null, 398 FRECENT_SITES_UPDATE_INTERVAL, 399 ShellService 400 ), 401 currentThemes: new CachedTargetingGetter( 402 "getAddonsByTypes", 403 ["theme"], 404 FRECENT_SITES_UPDATE_INTERVAL, 405 lazy.AddonManager // eslint-disable-line mozilla/valid-lazy 406 ), 407 isDefaultHTMLHandler: new CachedTargetingGetter( 408 "isDefaultHandlerFor", 409 [".html"], 410 FRECENT_SITES_UPDATE_INTERVAL, 411 ShellService 412 ), 413 isDefaultPDFHandler: new CachedTargetingGetter( 414 "isDefaultHandlerFor", 415 [".pdf"], 416 FRECENT_SITES_UPDATE_INTERVAL, 417 ShellService 418 ), 419 defaultPDFHandler: new CachedTargetingGetter( 420 "getDefaultPDFHandler", 421 null, 422 FRECENT_SITES_UPDATE_INTERVAL, 423 ShellService 424 ), 425 profileGroupId: new CachedTargetingGetter( 426 "getCachedProfileGroupID", 427 null, 428 FRECENT_SITES_UPDATE_INTERVAL, 429 ClientID 430 ), 431 profileGroupProfileCount: new CachedTargetingGetter( 432 "getProfileGroupProfileCount", 433 null, 434 FRECENT_SITES_UPDATE_INTERVAL, 435 { 436 getProfileGroupProfileCount() { 437 if ( 438 !Services.prefs.getBoolPref("browser.profiles.enabled", false) || 439 !Services.prefs.getBoolPref("browser.profiles.created", false) 440 ) { 441 return 0; 442 } 443 444 return lazy.SelectableProfileService.getProfileCount(); 445 }, 446 } 447 ), 448 backupsInfo: new CachedTargetingGetter( 449 "findBackupsInWellKnownLocations", 450 null, 451 FRECENT_SITES_UPDATE_INTERVAL, 452 { 453 async findBackupsInWellKnownLocations() { 454 let bs; 455 try { 456 bs = lazy.BackupService.get(); 457 } catch { 458 bs = lazy.BackupService.init(); 459 } 460 return bs.findBackupsInWellKnownLocations(); 461 }, 462 } 463 ), 464 }, 465 }; 466 467 /** 468 * sortMessagesByWeightedRank 469 * 470 * Each message has an associated weight, which is guaranteed to be strictly 471 * positive. Sort the messages so that higher weighted messages are more likely 472 * to come first. 473 * 474 * Specifically, sort them so that the probability of message x_1 with weight 475 * w_1 appearing before message x_2 with weight w_2 is (w_1 / (w_1 + w_2)). 476 * 477 * This is equivalent to requiring that x_1 appearing before x_2 is (w_1 / w_2) 478 * "times" as likely as x_2 appearing before x_1. 479 * 480 * See Bug 1484996, Comment 2 for a justification of the method. 481 * 482 * @param {Array} messages - A non-empty array of messages to sort, all with 483 * strictly positive weights 484 * @returns the sorted array 485 */ 486 function sortMessagesByWeightedRank(messages) { 487 return messages 488 .map(message => ({ 489 message, 490 rank: Math.pow(Math.random(), 1 / message.weight), 491 })) 492 .sort((a, b) => b.rank - a.rank) 493 .map(({ message }) => message); 494 } 495 496 /** 497 * getSortedMessages - Given an array of Messages, applies sorting and filtering rules 498 * in expected order. 499 * 500 * @param {Array<Message>} messages 501 * @param {{}} options 502 * @param {boolean} options.ordered - Should .order be used instead of random weighted sorting? 503 * @returns {Array<Message>} 504 */ 505 export function getSortedMessages(messages, options = {}) { 506 let { ordered } = { ordered: false, ...options }; 507 let result = messages; 508 509 if (!ordered) { 510 result = sortMessagesByWeightedRank(result); 511 } 512 513 result.sort((a, b) => { 514 // Next, sort by priority 515 if (a.priority > b.priority || (!isNaN(a.priority) && isNaN(b.priority))) { 516 return -1; 517 } 518 if (a.priority < b.priority || (isNaN(a.priority) && !isNaN(b.priority))) { 519 return 1; 520 } 521 522 // Sort messages with targeting expressions higher than those with none 523 if (a.targeting && !b.targeting) { 524 return -1; 525 } 526 if (!a.targeting && b.targeting) { 527 return 1; 528 } 529 530 // Next, sort by order *ascending* if ordered = true 531 if (ordered) { 532 if (a.order > b.order || (!isNaN(a.order) && isNaN(b.order))) { 533 return 1; 534 } 535 if (a.order < b.order || (isNaN(a.order) && !isNaN(b.order))) { 536 return -1; 537 } 538 } 539 540 return 0; 541 }); 542 543 return result; 544 } 545 546 /** 547 * parseAboutPageURL - Parse a URL string retrieved from about:home and about:new, returns 548 * its type (web extenstion or custom url) and the parsed url(s) 549 * 550 * @param {string} url - A URL string for home page or newtab page 551 * @returns {{isWebExt: boolean, isCustomUrl: boolean, urls: {url: string, host: string}[]}} 552 */ 553 function parseAboutPageURL(url) { 554 let ret = { 555 isWebExt: false, 556 isCustomUrl: false, 557 urls: [], 558 }; 559 if (lazy.ExtensionUtils.isExtensionUrl(url)) { 560 ret.isWebExt = true; 561 ret.urls.push({ url, host: "" }); 562 } else { 563 // The home page URL could be either a single URL or a list of "|" separated URLs. 564 // Note that it should work with "about:home" and "about:blank", in which case the 565 // "host" is set as an empty string. 566 for (const _url of url.split("|")) { 567 if (!["about:home", "about:newtab", "about:blank"].includes(_url)) { 568 ret.isCustomUrl = true; 569 } 570 try { 571 const parsedURL = new URL(_url); 572 const host = parsedURL.hostname.replace(/^www\./i, ""); 573 ret.urls.push({ url: _url, host }); 574 } catch (e) {} 575 } 576 // If URL parsing failed, just return the given url with an empty host 577 if (!ret.urls.length) { 578 ret.urls.push({ url, host: "" }); 579 } 580 } 581 582 return ret; 583 } 584 585 /** 586 * Get the number of records in autofill storage, e.g. credit cards/addresses. 587 * 588 * @param {object} [data] 589 * @param {string} [data.collectionName] 590 * The name used to specify which collection to retrieve records. 591 * @param {string} [data.searchString] 592 * The typed string for filtering out the matched records. 593 * @param {string} [data.info] 594 * The input autocomplete property's information. 595 * @returns {Promise<number>} The number of matched records. 596 * @see FormAutofillParent._getRecords 597 */ 598 async function getAutofillRecords(data) { 599 let actor; 600 try { 601 const win = Services.wm.getMostRecentBrowserWindow(); 602 actor = 603 win.gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor( 604 "FormAutofill" 605 ); 606 } catch (error) { 607 // If the actor is not available, we can't get the records. We could import 608 // the records directly from FormAutofillStorage to avoid the messiness of 609 // JSActors, but that would import a lot of code for a targeting attribute. 610 return 0; 611 } 612 let records = await actor?.receiveMessage({ 613 name: "FormAutofill:GetRecords", 614 data, 615 }); 616 return records?.records?.length ?? 0; 617 } 618 619 // Attribution data can be encoded multiple times so we need this function to 620 // get a cleartext value. 621 function decodeAttributionValue(value) { 622 if (!value) { 623 return null; 624 } 625 626 let decodedValue = value; 627 628 while (decodedValue.includes("%")) { 629 try { 630 const result = decodeURIComponent(decodedValue); 631 if (result === decodedValue) { 632 break; 633 } 634 decodedValue = result; 635 } catch (e) { 636 break; 637 } 638 } 639 640 return decodedValue; 641 } 642 643 async function getPinStatus() { 644 return await ShellService.doesAppNeedPin(); 645 } 646 647 const TargetingGetters = { 648 get locale() { 649 return Services.locale.appLocaleAsBCP47; 650 }, 651 get localeLanguageCode() { 652 return ( 653 Services.locale.appLocaleAsBCP47 && 654 Services.locale.appLocaleAsBCP47.substr(0, 2) 655 ); 656 }, 657 get browserSettings() { 658 const { settings } = lazy.TelemetryEnvironment.currentEnvironment; 659 return { 660 update: settings.update, 661 }; 662 }, 663 get attributionData() { 664 // Attribution is determined at startup - so we can use the cached attribution at this point 665 return lazy.AttributionCode.getCachedAttributionData(); 666 }, 667 get currentDate() { 668 return new Date(); 669 }, 670 get canCreateSelectableProfiles() { 671 if (!AppConstants.MOZ_SELECTABLE_PROFILES) { 672 return false; 673 } 674 return lazy.SelectableProfileService?.isEnabled ?? false; 675 }, 676 get hasSelectableProfiles() { 677 return lazy.profilesCreated; 678 }, 679 get profileAgeCreated() { 680 return lazy.ProfileAge().then(times => times.created); 681 }, 682 get profileAgeReset() { 683 return lazy.ProfileAge().then(times => times.reset); 684 }, 685 get usesFirefoxSync() { 686 return Services.prefs.prefHasUserValue(FXA_USERNAME_PREF); 687 }, 688 get isFxAEnabled() { 689 return lazy.isFxAEnabled; 690 }, 691 get isFxASignedIn() { 692 return new Promise(resolve => { 693 if (!lazy.isFxAEnabled) { 694 resolve(false); 695 } 696 if (Services.prefs.getStringPref(FXA_USERNAME_PREF, "")) { 697 resolve(true); 698 } 699 lazy.fxAccounts 700 .getSignedInUser() 701 .then(data => resolve(!!data)) 702 .catch(() => resolve(false)); 703 }); 704 }, 705 get sync() { 706 return { 707 desktopDevices: lazy.clientsDevicesDesktop, 708 mobileDevices: lazy.clientsDevicesMobile, 709 totalDevices: lazy.syncNumClients, 710 }; 711 }, 712 get xpinstallEnabled() { 713 // This is needed for all add-on recommendations, to know if we allow xpi installs in the first place 714 return lazy.isXPIInstallEnabled; 715 }, 716 get addonsInfo() { 717 let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService( 718 Ci.nsIBackgroundTasks 719 ); 720 if (bts?.isBackgroundTaskMode) { 721 return { addons: {}, isFullData: true }; 722 } 723 724 return lazy.AddonManager.getActiveAddons(["extension", "service"]).then( 725 ({ addons, fullData }) => { 726 const info = {}; 727 let hasInstalledAddons = false; 728 for (const addon of addons) { 729 info[addon.id] = { 730 version: addon.version, 731 type: addon.type, 732 isSystem: addon.isSystem, 733 isWebExtension: addon.isWebExtension, 734 hidden: addon.hidden, 735 isBuiltin: addon.isBuiltin, 736 }; 737 if (fullData) { 738 Object.assign(info[addon.id], { 739 name: addon.name, 740 userDisabled: addon.userDisabled, 741 installDate: addon.installDate, 742 }); 743 } 744 // special-powers and mochikit are addons installed in tests that 745 // are not "isSystem" or "isBuiltin" 746 const testAddons = [ 747 "special-powers@mozilla.org", 748 "mochikit@mozilla.org", 749 ]; 750 if ( 751 !addon.isSystem && 752 !addon.isBuiltin && 753 !testAddons.includes(addon.id) 754 ) { 755 hasInstalledAddons = true; 756 } 757 } 758 return { addons: info, isFullData: fullData, hasInstalledAddons }; 759 } 760 ); 761 }, 762 get searchEngines() { 763 const NONE = { installed: [], current: "" }; 764 let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService( 765 Ci.nsIBackgroundTasks 766 ); 767 if (bts?.isBackgroundTaskMode) { 768 return Promise.resolve(NONE); 769 } 770 return new Promise(resolve => { 771 // Note: calling getAppProvidedEngines, calls Services.search.init which 772 // ensures this code is only executed after Search has been initialized. 773 Services.search 774 .getAppProvidedEngines() 775 .then(engines => { 776 let { defaultEngine } = Services.search; 777 resolve({ 778 // Skip reporting the id for third party engines. 779 current: defaultEngine.isAppProvided ? defaultEngine.id : null, 780 // We don't need to filter the id here, as getAppProvidedEngines has 781 // already done that for us. 782 installed: engines.map(engine => engine.id), 783 }); 784 }) 785 .catch(() => resolve(NONE)); 786 }); 787 }, 788 get isDefaultBrowser() { 789 return QueryCache.getters.isDefaultBrowser.get().catch(() => null); 790 }, 791 get isDefaultBrowserUncached() { 792 return ShellService.isDefaultBrowser(); 793 }, 794 get devToolsOpenedCount() { 795 return lazy.devtoolsSelfXSSCount; 796 }, 797 get topFrecentSites() { 798 return QueryCache.queries.TopFrecentSites.get().then(sites => 799 sites.map(site => ({ 800 url: site.url, 801 host: new URL(site.url).hostname, 802 frecency: site.frecency, 803 lastVisitDate: site.lastVisitDate, 804 })) 805 ); 806 }, 807 get recentBookmarks() { 808 return QueryCache.queries.RecentBookmarks.get(); 809 }, 810 get pinnedSites() { 811 return NewTabUtils.pinnedLinks.links.map(site => 812 site 813 ? { 814 url: site.url, 815 host: new URL(site.url).hostname, 816 searchTopSite: site.searchTopSite, 817 } 818 : {} 819 ); 820 }, 821 get providerCohorts() { 822 return lazy.ASRouterPreferences.providers.reduce((prev, current) => { 823 prev[current.id] = current.cohort || ""; 824 return prev; 825 }, {}); 826 }, 827 get totalBookmarksCount() { 828 return QueryCache.queries.TotalBookmarksCount.get(); 829 }, 830 get firefoxVersion() { 831 return parseInt(AppConstants.MOZ_APP_VERSION.match(/\d+/), 10); 832 }, 833 get region() { 834 return lazy.Region.home || ""; 835 }, 836 get needsUpdate() { 837 return QueryCache.queries.CheckBrowserNeedsUpdate.get(); 838 }, 839 get savedTabGroups() { 840 return lazy.SessionStore.getSavedTabGroups().length; 841 }, 842 get currentTabGroups() { 843 let win = lazy.BrowserWindowTracker.getTopWindow({ 844 allowFromInactiveWorkspace: true, 845 }); 846 // If there's no window, there can't be any current tab groups. 847 if (!win) { 848 return 0; 849 } 850 let totalTabGroups = win.gBrowser.getAllTabGroups().length; 851 return totalTabGroups; 852 }, 853 get currentTabInstalledAsWebApp() { 854 let win = lazy.BrowserWindowTracker.getTopWindow({ 855 allowFromInactiveWorkspace: true, 856 }); 857 if (!win) { 858 // There is no active tab, so it isn't a web app. 859 return false; 860 } 861 862 // Note: this is a promise! 863 return ( 864 lazy.TaskbarTabs.findTaskbarTab( 865 win.gBrowser.selectedBrowser.currentURI, 866 win.gBrowser.selectedTab.userContextId 867 ) 868 .then(aTaskbarTab => aTaskbarTab !== null) 869 // If this is not an nsIURL (e.g. if it's about:blank), then this will 870 // throw; in that case there isn't a matching web app. 871 .catch(() => false) 872 ); 873 }, 874 get hasPinnedTabs() { 875 for (let win of Services.wm.getEnumerator("navigator:browser")) { 876 if (win.closed || !win.ownerGlobal.gBrowser) { 877 continue; 878 } 879 if (win.ownerGlobal.gBrowser.visibleTabs.filter(t => t.pinned).length) { 880 return true; 881 } 882 } 883 884 return false; 885 }, 886 get hasAccessedFxAPanel() { 887 return lazy.hasAccessedFxAPanel; 888 }, 889 get userPrefs() { 890 return { 891 cfrFeatures: lazy.cfrFeaturesUserPref, 892 cfrAddons: lazy.cfrAddonsUserPref, 893 }; 894 }, 895 get totalBlockedCount() { 896 return lazy.TrackingDBService.sumAllEvents(); 897 }, 898 get blockedCountByType() { 899 const idToTextMap = new Map([ 900 [Ci.nsITrackingDBService.TRACKERS_ID, "trackerCount"], 901 [Ci.nsITrackingDBService.TRACKING_COOKIES_ID, "cookieCount"], 902 [Ci.nsITrackingDBService.CRYPTOMINERS_ID, "cryptominerCount"], 903 [Ci.nsITrackingDBService.FINGERPRINTERS_ID, "fingerprinterCount"], 904 [Ci.nsITrackingDBService.SOCIAL_ID, "socialCount"], 905 ]); 906 907 const dateTo = new Date(); 908 const dateFrom = new Date(dateTo.getTime() - 42 * 24 * 60 * 60 * 1000); 909 return lazy.TrackingDBService.getEventsByDateRange(dateFrom, dateTo).then( 910 eventsByDate => { 911 let totalEvents = {}; 912 for (let blockedType of idToTextMap.values()) { 913 totalEvents[blockedType] = 0; 914 } 915 916 return eventsByDate.reduce((acc, day) => { 917 const type = day.getResultByName("type"); 918 const count = day.getResultByName("count"); 919 acc[idToTextMap.get(type)] = acc[idToTextMap.get(type)] + count; 920 return acc; 921 }, totalEvents); 922 } 923 ); 924 }, 925 get attachedFxAOAuthClients() { 926 return this.usesFirefoxSync 927 ? new Promise(resolve => 928 lazy.fxAccounts 929 .listAttachedOAuthClients() 930 .then(clients => resolve(clients)) 931 .catch(() => resolve([])) 932 ) 933 : []; 934 }, 935 get platformName() { 936 return AppConstants.platform; 937 }, 938 get isChinaRepack() { 939 return lazy.BrowserUtils.isChinaRepack(); 940 }, 941 get userId() { 942 return lazy.ClientEnvironment.userId; 943 }, 944 get profileRestartCount() { 945 let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService( 946 Ci.nsIBackgroundTasks 947 ); 948 if (bts?.isBackgroundTaskMode) { 949 return 0; 950 } 951 // Counter starts at 1 when a profile is created, substract 1 so the value 952 // returned matches expectations 953 return ( 954 lazy.TelemetrySession.getMetadata("targeting").profileSubsessionCounter - 955 1 956 ); 957 }, 958 get homePageSettings() { 959 const url = lazy.HomePage.get(); 960 const { isWebExt, isCustomUrl, urls } = parseAboutPageURL(url); 961 962 return { 963 isWebExt, 964 isCustomUrl, 965 urls, 966 isDefault: lazy.HomePage.isDefault, 967 isLocked: lazy.HomePage.locked, 968 }; 969 }, 970 get newtabSettings() { 971 const url = lazy.AboutNewTab.newTabURL; 972 const { isWebExt, isCustomUrl, urls } = parseAboutPageURL(url); 973 974 return { 975 isWebExt, 976 isCustomUrl, 977 isDefault: lazy.AboutNewTab.activityStreamEnabled, 978 url: urls[0].url, 979 host: urls[0].host, 980 }; 981 }, 982 get activeNotifications() { 983 let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService( 984 Ci.nsIBackgroundTasks 985 ); 986 if (bts?.isBackgroundTaskMode) { 987 // This might need to hook into the alert service to enumerate relevant 988 // persistent native notifications. 989 return false; 990 } 991 992 let window = lazy.BrowserWindowTracker.getTopWindow({ 993 allowFromInactiveWorkspace: true, 994 }); 995 996 // Technically this doesn't mean we have active notifications, 997 // but because we use !activeNotifications to check for conflicts, this should return true 998 if (!window) { 999 return true; 1000 } 1001 1002 let duration = Date.now() - lazy.newTabTopicModalLastSeen; 1003 let isDialogShowing = 1004 window.gBrowser?.selectedBrowser.hasAttribute("tabDialogShowing") || 1005 window.gDialogBox?.isOpen; 1006 let isFeatureCalloutShowing = lazy.FeatureCalloutBroker.isCalloutShowing; 1007 1008 if ( 1009 isDialogShowing || 1010 isFeatureCalloutShowing || 1011 window.gURLBar?.view.isOpen || 1012 window.gNotificationBox?.currentNotification || 1013 window.gBrowser.readNotificationBox()?.currentNotification || 1014 // Avoid showing messages if the newtab Topic selection modal was shown in 1015 // the past 1 minute 1016 duration <= NOTIFICATION_INTERVAL_AFTER_TOPIC_MODAL_MS 1017 ) { 1018 return true; 1019 } 1020 // use observer service to query Newtab 1021 const subjectWithBrowser = { 1022 browser: window.gBrowser, 1023 activeNewtabMessage: false, 1024 }; 1025 Services.obs.notifyObservers(subjectWithBrowser, "newtab-message-query"); 1026 if (subjectWithBrowser.activeNewtabMessage) { 1027 return true; 1028 } 1029 return false; 1030 }, 1031 1032 get isMajorUpgrade() { 1033 return lazy.BrowserHandler.majorUpgrade; 1034 }, 1035 1036 get hasActiveEnterprisePolicies() { 1037 return Services.policies.status === Services.policies.ACTIVE; 1038 }, 1039 1040 get userMonthlyActivity() { 1041 return QueryCache.queries.UserMonthlyActivity.get(); 1042 }, 1043 1044 get doesAppNeedPin() { 1045 return (async () => { 1046 return ( 1047 (await QueryCache.getters.doesAppNeedPin.get()) || 1048 (await QueryCache.getters.doesAppNeedStartMenuPin.get()) 1049 ); 1050 })(); 1051 }, 1052 1053 get doesAppNeedPinUncached() { 1054 return getPinStatus(); 1055 }, 1056 1057 get doesAppNeedPrivatePin() { 1058 return QueryCache.getters.doesAppNeedPrivatePin.get(); 1059 }, 1060 1061 get launchOnLoginEnabled() { 1062 if (AppConstants.platform !== "win") { 1063 return false; 1064 } 1065 return lazy.WindowsLaunchOnLogin.getLaunchOnLoginEnabled(); 1066 }, 1067 1068 get isMSIX() { 1069 if (AppConstants.platform !== "win") { 1070 return false; 1071 } 1072 // While we can write registry keys using external programs, we have no 1073 // way of cleanup on uninstall. If we are on an MSIX build 1074 // launch on login should never be enabled. 1075 // Default to false so that the feature isn't unnecessarily 1076 // disabled. 1077 // See Bug 1888263. 1078 return Services.sysinfo.getProperty("hasWinPackageId", false); 1079 }, 1080 1081 get packageFamilyName() { 1082 if (AppConstants.platform !== "win") { 1083 // PackageFamilyNames are an MSIX feature, so they won't be available on non-Windows platforms. 1084 return null; 1085 } 1086 1087 let packageFamilyName = Services.sysinfo.getProperty( 1088 "winPackageFamilyName" 1089 ); 1090 if (packageFamilyName === "") { 1091 return null; 1092 } 1093 1094 return packageFamilyName; 1095 }, 1096 1097 /** 1098 * Is this invocation running in background task mode? 1099 * 1100 * @return {boolean} `true` if running in background task mode. 1101 */ 1102 get isBackgroundTaskMode() { 1103 let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService( 1104 Ci.nsIBackgroundTasks 1105 ); 1106 return !!bts?.isBackgroundTaskMode; 1107 }, 1108 1109 /** 1110 * A non-empty task name if this invocation is running in background 1111 * task mode, or `null` if this invocation is not running in 1112 * background task mode. 1113 * 1114 * @return {string|null} background task name or `null`. 1115 */ 1116 get backgroundTaskName() { 1117 let bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService( 1118 Ci.nsIBackgroundTasks 1119 ); 1120 return bts?.backgroundTaskName(); 1121 }, 1122 1123 get userPrefersReducedMotion() { 1124 return Services.appinfo.prefersReducedMotion; 1125 }, 1126 1127 /** 1128 * The distribution id, if any. 1129 * 1130 * @return {string} 1131 */ 1132 get distributionId() { 1133 return Services.prefs 1134 .getDefaultBranch(null) 1135 .getCharPref("distribution.id", ""); 1136 }, 1137 1138 /** 1139 * Where the Firefox View button is shown, if at all. 1140 * 1141 * @return {string} container of the button if it is shown in the toolbar/overflow menu 1142 * @return {string} `null` if the button has been removed 1143 */ 1144 get fxViewButtonAreaType() { 1145 let button = lazy.CustomizableUI.getWidget("firefox-view-button"); 1146 return button.areaType; 1147 }, 1148 1149 get alltabsButtonAreaType() { 1150 let button = lazy.CustomizableUI.getWidget("alltabs-button"); 1151 return button.areaType; 1152 }, 1153 1154 isDefaultHandler: { 1155 get html() { 1156 return QueryCache.getters.isDefaultHTMLHandler.get(); 1157 }, 1158 get pdf() { 1159 return QueryCache.getters.isDefaultPDFHandler.get(); 1160 }, 1161 }, 1162 1163 get defaultPDFHandler() { 1164 return QueryCache.getters.defaultPDFHandler.get(); 1165 }, 1166 1167 get creditCardsSaved() { 1168 return getAutofillRecords({ collectionName: "creditCards" }); 1169 }, 1170 1171 get addressesSaved() { 1172 return getAutofillRecords({ collectionName: "addresses" }); 1173 }, 1174 1175 /** 1176 * Has the user ever used the Migration Wizard to migrate bookmarks? 1177 * 1178 * @return {boolean} `true` if bookmark migration has occurred. 1179 */ 1180 get hasMigratedBookmarks() { 1181 return lazy.hasMigratedBookmarks; 1182 }, 1183 1184 /** 1185 * Has the user ever used the Migration Wizard to migrate passwords from 1186 * a CSV file? 1187 * 1188 * @return {boolean} `true` if CSV passwords have been imported via the 1189 * migration wizard. 1190 */ 1191 get hasMigratedCSVPasswords() { 1192 return lazy.hasMigratedCSVPasswords; 1193 }, 1194 1195 /** 1196 * Has the user ever used the Migration Wizard to migrate history? 1197 * 1198 * @return {boolean} `true` if history migration has occurred. 1199 */ 1200 get hasMigratedHistory() { 1201 return lazy.hasMigratedHistory; 1202 }, 1203 1204 /** 1205 * Has the user ever used the Migration Wizard to migrate passwords? 1206 * 1207 * @return {boolean} `true` if password migration has occurred. 1208 */ 1209 get hasMigratedPasswords() { 1210 return lazy.hasMigratedPasswords; 1211 }, 1212 1213 /** 1214 * Returns true if the user is configured to use the embedded migration 1215 * wizard in about:welcome by having 1216 * "browser.migrate.content-modal.about-welcome-behavior" be equal to 1217 * "embedded". 1218 * 1219 * @return {boolean} `true` if the embedded migration wizard is enabled. 1220 */ 1221 get useEmbeddedMigrationWizard() { 1222 return lazy.useEmbeddedMigrationWizard; 1223 }, 1224 1225 /** 1226 * Returns the version number of the New Tab built-in addon being used 1227 * by the build. 1228 * 1229 * @return {string} 1230 */ 1231 get newtabAddonVersion() { 1232 return lazy.AboutNewTabResourceMapping.addonVersion; 1233 }, 1234 1235 /** 1236 * Whether the user installed Firefox via the RTAMO flow. 1237 * 1238 * @return {boolean} `true` when RTAMO has been used to download Firefox, 1239 * `false` otherwise. 1240 */ 1241 get isRTAMO() { 1242 const { attributionData } = this; 1243 1244 return ( 1245 attributionData?.source === "addons.mozilla.org" && 1246 !!decodeAttributionValue(attributionData?.content)?.startsWith("rta:") 1247 ); 1248 }, 1249 1250 /** 1251 * Whether the user installed via the device migration flow. 1252 * 1253 * @return {boolean} `true` when the link to download the browser was part 1254 * of guidance for device migration. `false` otherwise. 1255 */ 1256 get isDeviceMigration() { 1257 const { attributionData } = this; 1258 1259 return attributionData?.campaign === "migration"; 1260 }, 1261 1262 /** 1263 * Whether the user opted into a special message action represented by an 1264 * installer attribution campaign and this choice still needs to be honored. 1265 * 1266 * @return {string} A special message action to be executed on first-run. For 1267 * example, `"SET_DEFAULT_BROWSER"` when the user selected to set as default 1268 * via the install marketing page and set default has not yet been 1269 * automatically triggered, 'null' otherwise. 1270 */ 1271 get unhandledCampaignAction() { 1272 return QueryCache.queries.UnhandledCampaignAction.get(); 1273 }, 1274 /** 1275 * The values of the height and width available to the browser to display 1276 * web content. The available height and width are each calculated taking 1277 * into account the presence of menu bars, docks, and other similar OS elements 1278 * 1279 * @returns {object} resolution The resolution object containing width and height 1280 * @returns {number} resolution.width The available width of the primary monitor 1281 * @returns {number} resolution.height The available height of the primary monitor 1282 */ 1283 get primaryResolution() { 1284 const { primaryScreen } = lazy.ScreenManager; 1285 const { defaultCSSScaleFactor } = primaryScreen; 1286 let availDeviceLeft = {}; 1287 let availDeviceTop = {}; 1288 let availDeviceWidth = {}; 1289 let availDeviceHeight = {}; 1290 primaryScreen.GetAvailRect( 1291 availDeviceLeft, 1292 availDeviceTop, 1293 availDeviceWidth, 1294 availDeviceHeight 1295 ); 1296 return { 1297 width: Math.floor(availDeviceWidth.value / defaultCSSScaleFactor), 1298 height: Math.floor(availDeviceHeight.value / defaultCSSScaleFactor), 1299 }; 1300 }, 1301 1302 get archBits() { 1303 let bits = null; 1304 try { 1305 bits = Services.sysinfo.getProperty("archbits", null); 1306 } catch (_e) { 1307 // getProperty can throw if the memsize does not exist 1308 } 1309 if (bits) { 1310 bits = Number(bits); 1311 } 1312 return bits; 1313 }, 1314 1315 get systemArch() { 1316 try { 1317 return Services.sysinfo.get("arch"); 1318 } catch (_e) { 1319 return null; 1320 } 1321 }, 1322 1323 get memoryMB() { 1324 let memory = null; 1325 try { 1326 memory = Services.sysinfo.getProperty("memsize", null); 1327 } catch (_e) { 1328 // getProperty can throw if the memsize does not exist 1329 } 1330 if (memory) { 1331 memory = Number(memory) / 1024 / 1024; 1332 } 1333 return memory; 1334 }, 1335 1336 get totalSearches() { 1337 return lazy.totalSearches; 1338 }, 1339 1340 get profileGroupId() { 1341 return QueryCache.getters.profileGroupId.get(); 1342 }, 1343 1344 get currentProfileId() { 1345 if (!lazy.SelectableProfileService.currentProfile) { 1346 return ""; 1347 } 1348 return lazy.SelectableProfileService.currentProfile.id.toString(); 1349 }, 1350 1351 get profileGroupProfileCount() { 1352 return QueryCache.getters.profileGroupProfileCount.get(); 1353 }, 1354 1355 get buildId() { 1356 return parseInt(AppConstants.MOZ_BUILDID, 10); 1357 }, 1358 1359 get backupsInfo() { 1360 return QueryCache.getters.backupsInfo.get().catch(() => null); 1361 }, 1362 1363 get backupArchiveEnabled() { 1364 let bs; 1365 try { 1366 bs = lazy.BackupService.get(); 1367 } catch { 1368 bs = lazy.BackupService.init(); 1369 } 1370 return bs.archiveEnabledStatus.enabled; 1371 }, 1372 1373 get backupRestoreEnabled() { 1374 let bs; 1375 try { 1376 bs = lazy.BackupService.get(); 1377 } catch { 1378 bs = lazy.BackupService.init(); 1379 } 1380 return bs.restoreEnabledStatus.enabled; 1381 }, 1382 1383 get isEncryptedBackup() { 1384 const isEncryptedBackup = 1385 Services.prefs.getStringPref( 1386 "messaging-system-action.backupChooser", 1387 null 1388 ) === "full"; 1389 return isEncryptedBackup; 1390 }, 1391 }; 1392 1393 function addAIWindowTargeting(targeting) { 1394 if (!targeting || targeting === "true") { 1395 // Default behavior: Classic-only if no targeting is specified 1396 return `!isAIWindow`; 1397 } 1398 1399 if (/\bisAIWindow\b/.test(targeting)) { 1400 return targeting; 1401 } 1402 1403 return `((${targeting}) && !isAIWindow)`; 1404 } 1405 1406 export const ASRouterTargeting = { 1407 Environment: TargetingGetters, 1408 1409 /** 1410 * Snapshot the current targeting environment. 1411 * 1412 * Asynchronous getters are handled. Getters that throw or reject 1413 * are ignored. 1414 * 1415 * Leftward (earlier) targets supercede rightward (later) targets, just like 1416 * `TargetingContext.combineContexts`. 1417 * 1418 * @param {object} options - object containing: 1419 * @param {Array<object>|null} options.targets - 1420 * targeting environments to snapshot; (default: `[ASRouterTargeting.Environment]`) 1421 * @return {object} snapshot of target with `environment` object and `version` integer. 1422 */ 1423 async getEnvironmentSnapshot({ 1424 targets = [ASRouterTargeting.Environment], 1425 } = {}) { 1426 async function resolve(object) { 1427 if (typeof object === "object" && object !== null) { 1428 if (Array.isArray(object)) { 1429 return Promise.all(object.map(async item => resolve(await item))); 1430 } 1431 1432 if (object instanceof Date) { 1433 return object; 1434 } 1435 1436 // One promise for each named property. Label promises with property name. 1437 const promises = Object.keys(object).map(async key => { 1438 // Each promise needs to check if we're shutting down when it is evaluated. 1439 if (Services.startup.shuttingDown) { 1440 throw new Error( 1441 "shutting down, so not querying targeting environment" 1442 ); 1443 } 1444 1445 const value = await resolve(await object[key]); 1446 1447 return [key, value]; 1448 }); 1449 1450 const resolved = {}; 1451 for (const result of await Promise.allSettled(promises)) { 1452 // Ignore properties that are rejected. 1453 if (result.status === "fulfilled") { 1454 const [key, value] = result.value; 1455 resolved[key] = value; 1456 } 1457 } 1458 1459 return resolved; 1460 } 1461 1462 return object; 1463 } 1464 1465 // We would like to use `TargetingContext.combineContexts`, but `Proxy` 1466 // instances complicate iterating with `Object.keys`. Instead, merge by 1467 // hand after resolving. 1468 const environment = {}; 1469 for (let target of targets.toReversed()) { 1470 Object.assign(environment, await resolve(target)); 1471 } 1472 1473 // Should we need to migrate in the future. 1474 const snapshot = { environment, version: 1 }; 1475 1476 return snapshot; 1477 }, 1478 1479 isTriggerMatch(trigger = {}, candidateMessageTrigger = {}) { 1480 if (trigger.id !== candidateMessageTrigger.id) { 1481 return false; 1482 } else if ( 1483 !candidateMessageTrigger.params && 1484 !candidateMessageTrigger.patterns 1485 ) { 1486 return true; 1487 } 1488 1489 if (!trigger.param) { 1490 return false; 1491 } 1492 1493 return ( 1494 (candidateMessageTrigger.params && 1495 trigger.param.host && 1496 candidateMessageTrigger.params.includes(trigger.param.host)) || 1497 (candidateMessageTrigger.params && 1498 trigger.param.type && 1499 candidateMessageTrigger.params.filter(t => t === trigger.param.type) 1500 .length) || 1501 (candidateMessageTrigger.params && 1502 trigger.param.type && 1503 candidateMessageTrigger.params.filter( 1504 t => (t & trigger.param.type) === t 1505 ).length) || 1506 (candidateMessageTrigger.patterns && 1507 trigger.param.url && 1508 new MatchPatternSet(candidateMessageTrigger.patterns).matches( 1509 trigger.param.url 1510 )) 1511 ); 1512 }, 1513 1514 /** 1515 * getCachedEvaluation - Return a cached jexl evaluation if available 1516 * 1517 * @param {string} targeting JEXL expression to lookup 1518 * @returns {obj|null} Object with value result or null if not available 1519 */ 1520 getCachedEvaluation(targeting) { 1521 if (jexlEvaluationCache.has(targeting)) { 1522 const { timestamp, value } = jexlEvaluationCache.get(targeting); 1523 if (Date.now() - timestamp <= CACHE_EXPIRATION) { 1524 return { value }; 1525 } 1526 jexlEvaluationCache.delete(targeting); 1527 } 1528 1529 return null; 1530 }, 1531 1532 /** 1533 * checkMessageTargeting - Checks is a message's targeting parameters are satisfied 1534 * 1535 * @param {*} message An AS router message 1536 * @param {obj} targetingContext a TargetingContext instance complete with eval environment 1537 * @param {func} onError A function to handle errors (takes two params; error, message) 1538 * @param {boolean} shouldCache Should the JEXL evaluations be cached and reused. 1539 * @returns 1540 */ 1541 async checkMessageTargeting(message, targetingContext, onError, shouldCache) { 1542 lazy.ASRouterPreferences.console.debug( 1543 "in checkMessageTargeting, arguments = ", 1544 Array.from(arguments) // eslint-disable-line prefer-rest-params 1545 ); 1546 1547 let { targeting } = message; 1548 targeting = addAIWindowTargeting(targeting); 1549 1550 let result; 1551 try { 1552 if (shouldCache) { 1553 result = this.getCachedEvaluation(targeting); 1554 if (result) { 1555 return result.value; 1556 } 1557 } 1558 // Used to report the source of the targeting error in the case of 1559 // undesired events 1560 targetingContext.setTelemetrySource(message.id); 1561 result = await targetingContext.evalWithDefault(targeting); 1562 if (shouldCache) { 1563 jexlEvaluationCache.set(targeting, { 1564 timestamp: Date.now(), 1565 value: result, 1566 }); 1567 } 1568 } catch (error) { 1569 if (onError) { 1570 onError(error, message); 1571 } 1572 console.error(error); 1573 result = false; 1574 } 1575 return result; 1576 }, 1577 1578 _isMessageMatch( 1579 message, 1580 trigger, 1581 targetingContext, 1582 onError, 1583 shouldCache = false 1584 ) { 1585 return ( 1586 message && 1587 (trigger 1588 ? this.isTriggerMatch(trigger, message.trigger) 1589 : !message.trigger) && 1590 // If a trigger expression was passed to this function, the message should match it. 1591 // Otherwise, we should choose a message with no trigger property (i.e. a message that can show up at any time) 1592 this.checkMessageTargeting( 1593 message, 1594 targetingContext, 1595 onError, 1596 shouldCache 1597 ) 1598 ); 1599 }, 1600 1601 /** 1602 * findMatchingMessage - Given an array of messages, returns one message 1603 * whos targeting expression evaluates to true 1604 * 1605 * @param {Array<Message>} messages An array of AS router messages 1606 * @param {trigger} string A trigger expression if a message for that trigger is desired 1607 * @param {obj|null} context A FilterExpression context. Defaults to TargetingGetters above. 1608 * @param {func} onError A function to handle errors (takes two params; error, message) 1609 * @param {func} ordered An optional param when true sort message by order specified in message 1610 * @param {boolean} shouldCache Should the JEXL evaluations be cached and reused. 1611 * @param {boolean} returnAll Should we return all matching messages, not just the first one found. 1612 * @returns {obj|Array<Message>} If returnAll is false, a single message. If returnAll is true, an array of messages. 1613 */ 1614 async findMatchingMessage({ 1615 messages, 1616 trigger = {}, 1617 context = {}, 1618 onError, 1619 ordered = false, 1620 shouldCache = false, 1621 returnAll = false, 1622 }) { 1623 const sortedMessages = getSortedMessages(messages, { ordered }); 1624 lazy.ASRouterPreferences.console.debug( 1625 "in findMatchingMessage, sortedMessages = ", 1626 sortedMessages 1627 ); 1628 const matching = returnAll ? [] : null; 1629 const targetingContext = new lazy.TargetingContext( 1630 lazy.TargetingContext.combineContexts( 1631 context, 1632 this.Environment, 1633 trigger.context || {} 1634 ) 1635 ); 1636 1637 const isMatch = candidate => 1638 this._isMessageMatch( 1639 candidate, 1640 trigger, 1641 targetingContext, 1642 onError, 1643 shouldCache 1644 ); 1645 1646 for (const candidate of sortedMessages) { 1647 if (await isMatch(candidate)) { 1648 // If not returnAll, we should return the first message we find that matches. 1649 if (!returnAll) { 1650 return candidate; 1651 } 1652 1653 matching.push(candidate); 1654 } 1655 } 1656 return matching; 1657 }, 1658 };