ActivityStream.sys.mjs (51141B)
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 // We use importESModule here instead of static import so that 6 // the Karma test environment won't choke on this module. This 7 // is because the Karma test environment already stubs out 8 // AppConstants, and overrides importESModule to be a no-op (which 9 // can't be done for a static import statement). 10 11 // eslint-disable-next-line mozilla/use-static-import 12 const { AppConstants } = ChromeUtils.importESModule( 13 "resource://gre/modules/AppConstants.sys.mjs" 14 ); 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 const lazy = {}; 22 23 ChromeUtils.defineESModuleGetters(lazy, { 24 AboutNewTabParent: "resource:///actors/AboutNewTabParent.sys.mjs", 25 AboutPreferences: "resource://newtab/lib/AboutPreferences.sys.mjs", 26 AdsFeed: "resource://newtab/lib/AdsFeed.sys.mjs", 27 InferredPersonalizationFeed: 28 "resource://newtab/lib/InferredPersonalizationFeed.sys.mjs", 29 SmartShortcutsFeed: "resource://newtab/lib/SmartShortcutsFeed.sys.mjs", 30 DEFAULT_SITES: "resource://newtab/lib/DefaultSites.sys.mjs", 31 DefaultPrefs: "resource://newtab/lib/ActivityStreamPrefs.sys.mjs", 32 DiscoveryStreamFeed: "resource://newtab/lib/DiscoveryStreamFeed.sys.mjs", 33 ExternalComponentsFeed: 34 "resource://newtab/lib/ExternalComponentsFeed.sys.mjs", 35 FaviconFeed: "resource://newtab/lib/FaviconFeed.sys.mjs", 36 HighlightsFeed: "resource://newtab/lib/HighlightsFeed.sys.mjs", 37 ListsFeed: "resource://newtab/lib/Widgets/ListsFeed.sys.mjs", 38 NewTabAttributionFeed: "resource://newtab/lib/NewTabAttributionFeed.sys.mjs", 39 NewTabActorRegistry: "resource://newtab/lib/NewTabActorRegistry.sys.mjs", 40 NewTabInit: "resource://newtab/lib/NewTabInit.sys.mjs", 41 NewTabMessaging: "resource://newtab/lib/NewTabMessaging.sys.mjs", 42 NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", 43 PrefsFeed: "resource://newtab/lib/PrefsFeed.sys.mjs", 44 PlacesFeed: "resource://newtab/lib/PlacesFeed.sys.mjs", 45 RecommendationProvider: 46 "resource://newtab/lib/RecommendationProvider.sys.mjs", 47 Region: "resource://gre/modules/Region.sys.mjs", 48 SectionsFeed: "resource://newtab/lib/SectionsManager.sys.mjs", 49 StartupCacheInit: "resource://newtab/lib/StartupCacheInit.sys.mjs", 50 Store: "resource://newtab/lib/Store.sys.mjs", 51 SystemTickFeed: "resource://newtab/lib/SystemTickFeed.sys.mjs", 52 TelemetryFeed: "resource://newtab/lib/TelemetryFeed.sys.mjs", 53 TimerFeed: "resource://newtab/lib/Widgets/TimerFeed.sys.mjs", 54 TopSitesFeed: "resource://newtab/lib/TopSitesFeed.sys.mjs", 55 TopStoriesFeed: "resource://newtab/lib/TopStoriesFeed.sys.mjs", 56 WallpaperFeed: "resource://newtab/lib/Wallpapers/WallpaperFeed.sys.mjs", 57 WeatherFeed: "resource://newtab/lib/WeatherFeed.sys.mjs", 58 }); 59 60 XPCOMUtils.defineLazyServiceGetter( 61 lazy, 62 "ProxyService", 63 "@mozilla.org/network/protocol-proxy-service;1", 64 Ci.nsIProtocolProxyService 65 ); 66 67 // NB: Eagerly load modules that will be loaded/constructed/initialized in the 68 // common case to avoid the overhead of wrapping and detecting lazy loading. 69 import { 70 actionCreators as ac, 71 actionTypes as at, 72 } from "resource://newtab/common/Actions.mjs"; 73 74 const REGION_INFERRED_PERSONALIZATION_CONFIG = 75 "browser.newtabpage.activity-stream.discoverystream.sections.personalization.inferred.region-config"; 76 const LOCALE_INFERRED_PERSONALIZATION_CONFIG = 77 "browser.newtabpage.activity-stream.discoverystream.sections.personalization.inferred.locale-config"; 78 const REGION_SOV_CONFIG = 79 "browser.newtabpage.activity-stream.sov.region-config"; 80 const LOCALE_SOV_CONFIG = 81 "browser.newtabpage.activity-stream.sov.locale-config"; 82 83 const REGION_WEATHER_CONFIG = 84 "browser.newtabpage.activity-stream.discoverystream.region-weather-config"; 85 const LOCALE_WEATHER_CONFIG = 86 "browser.newtabpage.activity-stream.discoverystream.locale-weather-config"; 87 88 const REGION_TOPICS_CONFIG = 89 "browser.newtabpage.activity-stream.discoverystream.topicSelection.region-topics-config"; 90 const LOCALE_TOPICS_CONFIG = 91 "browser.newtabpage.activity-stream.discoverystream.topicSelection.locale-topics-config"; 92 93 const REGION_TOPIC_LABEL_CONFIG = 94 "browser.newtabpage.activity-stream.discoverystream.topicLabels.region-topic-label-config"; 95 const LOCALE_TOPIC_LABEL_CONFIG = 96 "browser.newtabpage.activity-stream.discoverystream.topicLabels.locale-topic-label-config"; 97 const REGION_BASIC_CONFIG = 98 "browser.newtabpage.activity-stream.discoverystream.region-basic-config"; 99 100 const REGION_CONTEXTUAL_AD_CONFIG = 101 "browser.newtabpage.activity-stream.discoverystream.sections.contextualAds.region-config"; 102 const LOCALE_CONTEXTUAL_AD_CONFIG = 103 "browser.newtabpage.activity-stream.discoverystream.sections.contextualAds.locale-config"; 104 105 const REGION_SECTIONS_CONFIG = 106 "browser.newtabpage.activity-stream.discoverystream.sections.region-content-config"; 107 const LOCALE_SECTIONS_CONFIG = 108 "browser.newtabpage.activity-stream.discoverystream.sections.locale-content-config"; 109 110 const PREF_SHOULD_AS_INITIALIZE_FEEDS = 111 "browser.newtabpage.activity-stream.testing.shouldInitializeFeeds"; 112 113 const PREF_INFERRED_ENABLED = 114 "discoverystream.sections.personalization.inferred.enabled"; 115 116 const PREF_IMAGE_PROXY_ENABLED = 117 "browser.newtabpage.activity-stream.discoverystream.imageProxy.enabled"; 118 119 const PREF_IMAGE_PROXY_ENABLED_STORE = "discoverystream.imageProxy.enabled"; 120 121 const PREF_SHOULD_ENABLE_EXTERNAL_COMPONENTS_FEED = 122 "browser.newtabpage.activity-stream.externalComponents.enabled"; 123 124 export const WEATHER_OPTIN_REGIONS = [ 125 "AT", // Austria 126 "BE", // Belgium 127 "BG", // Bulgaria 128 "HR", // Croatia 129 "CY", // Cyprus 130 "CZ", // Czechia 131 "DK", // Denmark 132 "EE", // Estonia 133 "FI", // Finland 134 "FR", // France 135 "DE", // Germany 136 "GB", // United Kingdom 137 "GR", // Greece 138 "HU", // Hungary 139 "IS", // Iceland 140 "IE", // Ireland 141 "IT", // Italy 142 "LV", // Latvia 143 "LI", // Liechtenstein 144 "LT", // Lithuania 145 "MT", // Malta 146 "NL", // Netherlands 147 "NO", // Norway 148 "PL", // Poland 149 "PT", // Portugal 150 "RO", // Romania 151 "SG", // Singapore 152 "SK", // Slovakia 153 "SI", // Slovenia 154 "ES", // Spain 155 "SE", // Sweden 156 "CH", // Switzerland 157 ]; 158 159 export function csvPrefHasValue(stringPrefName, value) { 160 if (typeof stringPrefName !== "string") { 161 throw new Error(`The stringPrefName argument is not a string`); 162 } 163 164 const pref = Services.prefs.getStringPref(stringPrefName, "") || ""; 165 const prefValues = pref 166 .split(",") 167 .map(s => s.trim()) 168 .filter(item => item); 169 170 return prefValues.includes(value); 171 } 172 173 export function shouldInitializeFeeds(defaultValue = true) { 174 // For tests/automation: when false, newtab won't initialize 175 // select feeds in this session. 176 // Flipping after initialization has no effect on the current session. 177 const shouldInitialize = Services.prefs.getBoolPref( 178 PREF_SHOULD_AS_INITIALIZE_FEEDS, 179 defaultValue 180 ); 181 return shouldInitialize; 182 } 183 184 function useInferredPersonalization({ geo, locale }) { 185 return ( 186 csvPrefHasValue(REGION_INFERRED_PERSONALIZATION_CONFIG, geo) && 187 csvPrefHasValue(LOCALE_INFERRED_PERSONALIZATION_CONFIG, locale) 188 ); 189 } 190 191 function useSov({ geo, locale }) { 192 return ( 193 csvPrefHasValue(REGION_SOV_CONFIG, geo) && 194 csvPrefHasValue(LOCALE_SOV_CONFIG, locale) 195 ); 196 } 197 198 function useContextualAds({ geo, locale }) { 199 return ( 200 csvPrefHasValue(REGION_CONTEXTUAL_AD_CONFIG, geo) && 201 csvPrefHasValue(LOCALE_CONTEXTUAL_AD_CONFIG, locale) 202 ); 203 } 204 205 // Determine if spocs should be shown for a geo/locale 206 function showSpocs({ geo }) { 207 const spocsGeoString = 208 lazy.NimbusFeatures.pocketNewtab.getVariable("regionSpocsConfig") || ""; 209 const spocsGeo = spocsGeoString.split(",").map(s => s.trim()); 210 return spocsGeo.includes(geo); 211 } 212 213 function showWeather({ geo, locale }) { 214 return ( 215 csvPrefHasValue(REGION_WEATHER_CONFIG, geo) && 216 csvPrefHasValue(LOCALE_WEATHER_CONFIG, locale) 217 ); 218 } 219 220 function showWeatherOptIn({ geo }) { 221 return WEATHER_OPTIN_REGIONS.includes(geo); 222 } 223 224 function showTopicsSelection({ geo, locale }) { 225 return ( 226 csvPrefHasValue(REGION_TOPICS_CONFIG, geo) && 227 csvPrefHasValue(LOCALE_TOPICS_CONFIG, locale) 228 ); 229 } 230 231 function showTopicLabels({ geo, locale }) { 232 return ( 233 csvPrefHasValue(REGION_TOPIC_LABEL_CONFIG, geo) && 234 csvPrefHasValue(LOCALE_TOPIC_LABEL_CONFIG, locale) 235 ); 236 } 237 238 function showSectionLayout({ geo, locale }) { 239 return ( 240 csvPrefHasValue(REGION_SECTIONS_CONFIG, geo) && 241 csvPrefHasValue(LOCALE_SECTIONS_CONFIG, locale) 242 ); 243 } 244 245 // Configure default Activity Stream prefs with a plain `value` or a `getValue` 246 // that computes a value. A `value_local_dev` is used for development defaults. 247 export const PREFS_CONFIG = new Map([ 248 [ 249 "default.sites", 250 { 251 title: 252 "Comma-separated list of default top sites to fill in behind visited sites", 253 getValue: ({ geo }) => 254 lazy.DEFAULT_SITES.get(lazy.DEFAULT_SITES.has(geo) ? geo : ""), 255 }, 256 ], 257 [ 258 "feeds.topsites", 259 { 260 title: "Displays Top Sites on the New Tab Page", 261 value: true, 262 }, 263 ], 264 [ 265 "hideTopSitesTitle", 266 { 267 title: 268 "Hide the top sites section's title, including the section and collapse icons", 269 value: false, 270 }, 271 ], 272 [ 273 "showSponsored", 274 { 275 title: "User pref for sponsored Pocket content", 276 value: true, 277 }, 278 ], 279 [ 280 "system.showSponsored", 281 { 282 title: "System pref for sponsored Pocket content", 283 // This pref is dynamic as the sponsored content depends on the region 284 getValue: showSpocs, 285 }, 286 ], 287 [ 288 "showSponsoredTopSites", 289 { 290 title: "Show sponsored top sites", 291 value: true, 292 }, 293 ], 294 [ 295 "mobileDownloadModal.enabled", 296 { 297 title: "Boolean flag to show download Firefox for mobile QR code modal", 298 value: false, 299 }, 300 ], 301 [ 302 "mobileDownloadModal.variant-a", 303 { 304 title: 305 "Boolean flag to turn download Firefox for mobile promo variant A on and off", 306 value: false, 307 }, 308 ], 309 [ 310 "mobileDownloadModal.variant-b", 311 { 312 title: 313 "Boolean flag to turn download Firefox for mobile promo variant B on and off", 314 value: false, 315 }, 316 ], 317 [ 318 "mobileDownloadModal.variant-c", 319 { 320 title: 321 "Boolean flag to turn download Firefox for mobile promo variant C on and off", 322 value: false, 323 }, 324 ], 325 [ 326 "discoverystream.refinedCardsLayout.enabled", 327 { 328 title: 329 "Boolean flag enable layout and styling refinements for content and ad cards across different card sizes", 330 value: false, 331 }, 332 ], 333 [ 334 "discoverystream.merino-provider.ohttp.enabled", 335 { 336 title: "Enables the Merino requests and images sent over OHTTP", 337 value: false, 338 }, 339 ], 340 [ 341 "unifiedAds.ohttp.enabled", 342 { 343 title: "Enables the MARS requests and images sent over OHTTP", 344 value: false, 345 }, 346 ], 347 [ 348 "unifiedAds.adsFeed.enabled", 349 { 350 title: 351 "Use AdsFeed.sys.mjs to fetch/cache/serve Mozilla Ad Routing Service (MARS) unified ads ", 352 value: false, 353 }, 354 ], 355 [ 356 "unifiedAds.tiles.enabled", 357 { 358 title: 359 "Use Mozilla Ad Routing Service (MARS) unified ads API for sponsored top sites tiles", 360 value: false, 361 }, 362 ], 363 [ 364 "unifiedAds.spocs.enabled", 365 { 366 title: 367 "Use Mozilla Ad Routing Service (MARS) unified ads API for sponsored content in recommended stories", 368 value: false, 369 }, 370 ], 371 [ 372 "unifiedAds.endpoint", 373 { 374 title: "Mozilla Ad Routing Service (MARS) unified ads API endpoint URL", 375 value: "https://ads.mozilla.org/", 376 }, 377 ], 378 [ 379 "unifiedAds.blockedAds", 380 { 381 title: 382 "CSV list of blocked (dismissed) MARS ads. This payload is sent back every time new ads are fetched.", 383 value: "", 384 }, 385 ], 386 [ 387 "system.showWeather", 388 { 389 title: "system.showWeather", 390 // pref is dynamic 391 getValue: showWeather, 392 }, 393 ], 394 [ 395 "showWeather", 396 { 397 title: "showWeather", 398 value: true, 399 }, 400 ], 401 [ 402 "system.showWeatherOptIn", 403 { 404 title: "system.showWeatherOptIn", 405 // pref is dynamic 406 getValue: showWeatherOptIn, 407 }, 408 ], 409 [ 410 "discoverystream.optIn-region-weather-config", 411 { 412 title: "Regions for weather opt-in.", 413 value: "DE,GB,FR,ES,IT,CH,AT,BE,IE,NL,PL,CZ,SE,SG,HU,SK,FI,DK,NO,PT", 414 }, 415 ], 416 [ 417 "weather.optInDisplayed", 418 { 419 title: 420 "Enable opt-in dialog to display for weather widget in GDPR regions.", 421 value: true, 422 }, 423 ], 424 [ 425 "weather.optInAccepted", 426 { 427 title: 428 "User choice made when prompted with the opt-in dialog for weather.", 429 value: false, 430 }, 431 ], 432 [ 433 "weather.staticData.enabled", 434 { 435 title: 436 "Static weather data shown when user has not set/enabled location from opt-in.", 437 value: true, 438 }, 439 ], 440 [ 441 "weather.query", 442 { 443 title: "weather.query", 444 value: "", 445 }, 446 ], 447 [ 448 "weather.locationSearchEnabled", 449 { 450 title: "Enable the option to search for a specific city", 451 value: false, 452 }, 453 ], 454 [ 455 "weather.temperatureUnits", 456 { 457 title: "Switch the temperature between Celsius and Fahrenheit", 458 getValue: ({ geo }) => (geo === "US" ? "f" : "c"), 459 }, 460 ], 461 [ 462 "weather.display", 463 { 464 title: 465 "Toggle the weather widget to include a text summary of the current conditions", 466 value: "simple", 467 }, 468 ], 469 [ 470 "weather.placement", 471 { 472 title: 473 "weather widget can be rendered in a variety of positions. Either in `header` or `sections`", 474 value: "header", 475 }, 476 ], 477 [ 478 "images.smart", 479 { 480 title: "Smart crop images on newtab", 481 value: false, 482 }, 483 ], 484 [ 485 "pocketCta", 486 { 487 title: "Pocket cta and button for logged out users.", 488 value: JSON.stringify({ 489 cta_button: "", 490 cta_text: "", 491 cta_url: "", 492 use_cta: false, 493 }), 494 }, 495 ], 496 [ 497 "showSearch", 498 { 499 title: "Show the Search bar", 500 value: true, 501 }, 502 ], 503 [ 504 "logowordmark.alwaysVisible", 505 { 506 title: "Show the logo and wordmark", 507 value: true, 508 }, 509 ], 510 [ 511 "topSitesRows", 512 { 513 title: "Number of rows of Top Sites to display", 514 value: 1, 515 }, 516 ], 517 [ 518 "telemetry", 519 { 520 title: "Enable system error and usage data collection", 521 value: true, 522 value_local_dev: false, 523 }, 524 ], 525 [ 526 "telemetry.ut.events", 527 { 528 title: "Enable Unified Telemetry event data collection", 529 value: AppConstants.EARLY_BETA_OR_EARLIER, 530 value_local_dev: false, 531 }, 532 ], 533 [ 534 "telemetry.structuredIngestion.endpoint", 535 { 536 title: "Structured Ingestion telemetry server endpoint", 537 value: "https://incoming.telemetry.mozilla.org/submit", 538 }, 539 ], 540 [ 541 "telemetry.privatePing.enabled", 542 { 543 title: "Enables the private ping sent over OHTTP through Glean", 544 value: false, 545 }, 546 ], 547 [ 548 "telemetry.surfaceId", 549 { 550 title: "surface id", 551 }, 552 ], 553 [ 554 "telemetry.privatePing.redactNewtabPing.enabled", 555 { 556 title: "Redacts content interaction ids from original New Tab ping", 557 value: false, 558 }, 559 ], 560 [ 561 "telemetry.privatePing.inferredInterests.enabled", 562 { 563 title: 564 "Includes interest vector with private ping when user has enabeled inferred personalization", 565 value: false, 566 }, 567 ], 568 [ 569 "section.highlights.includeVisited", 570 { 571 title: 572 "Boolean flag that decides whether or not to show visited pages in highlights.", 573 value: true, 574 }, 575 ], 576 [ 577 "section.highlights.includeBookmarks", 578 { 579 title: 580 "Boolean flag that decides whether or not to show bookmarks in highlights.", 581 value: true, 582 }, 583 ], 584 [ 585 "section.highlights.includeDownloads", 586 { 587 title: 588 "Boolean flag that decides whether or not to show saved recent Downloads in highlights.", 589 value: true, 590 }, 591 ], 592 [ 593 "section.highlights.rows", 594 { 595 title: "Number of rows of Highlights to display", 596 value: 1, 597 }, 598 ], 599 [ 600 "section.topstories.rows", 601 { 602 title: "Number of rows of Top Stories to display", 603 value: 1, 604 }, 605 ], 606 [ 607 "sectionOrder", 608 { 609 title: "The rendering order for the sections", 610 value: "topsites,topstories,highlights", 611 }, 612 ], 613 [ 614 "newtabWallpapers.enabled", 615 { 616 title: "Boolean flag to turn wallpaper functionality on and off", 617 value: false, 618 }, 619 ], 620 [ 621 "newtabWallpapers.customColor.enabled", 622 { 623 title: "Boolean flag to turn show custom color select box", 624 value: false, 625 }, 626 ], 627 [ 628 "newtabWallpapers.customWallpaper.enabled", 629 { 630 title: 631 "Boolean flag to enable custom/user-uploaded wallpaper functionality", 632 value: false, 633 }, 634 ], 635 [ 636 "newtabWallpapers.customWallpaper.uuid", 637 { 638 title: "uuid for uploaded custom wallpaper", 639 value: "", 640 }, 641 ], 642 [ 643 "newtabWallpapers.customWallpaper.uploadedPreviously", 644 { 645 title: 646 "Boolean flag used for telemetry to track if a user has previously uploaded a custom wallpaper", 647 value: false, 648 }, 649 ], 650 [ 651 "newtabWallpapers.customWallpaper.fileSize.enabled", 652 { 653 title: "Boolean flag to enforce a maximum file size for uploaded images", 654 value: false, 655 }, 656 ], 657 [ 658 "newtabWallpapers.customWallpaper.fileSize", 659 { 660 title: "Number pref of maximum file size (in MB) a user can upload", 661 value: 0, 662 }, 663 ], 664 [ 665 "newtabWallpapers.customWallpaper.theme", 666 { 667 title: "theme ('light' | 'dark') of user uploaded wallpaper", 668 value: "", 669 }, 670 ], 671 [ 672 "newtabAdSize.leaderboard", 673 { 674 title: "Boolean flag to turn the leaderboard ad size on and off", 675 value: false, 676 }, 677 ], 678 [ 679 "newtabAdSize.leaderboard.position", 680 { 681 title: 682 "position for leaderboard spoc - should correlate to a row in DS grid", 683 value: "3", 684 }, 685 ], 686 [ 687 "newtabAdSize.billboard", 688 { 689 title: "Boolean flag to turn the billboard ad size on and off", 690 value: false, 691 }, 692 ], 693 [ 694 "newtabAdSize.billboard.position", 695 { 696 title: 697 "position for billboard spoc - should correlate to a row in DS grid", 698 value: "3", 699 }, 700 ], 701 [ 702 "newtabAdSize.mediumRectangle", 703 { 704 title: "Boolean flag to turn the medium (MREC) ad size on and off", 705 value: false, 706 }, 707 ], 708 [ 709 "discoverystream.promoCard.enabled", 710 { 711 title: "Boolean flag to turn the promo card on and off", 712 value: false, 713 }, 714 ], 715 [ 716 "discoverystream.promoCard.visible", 717 { 718 title: "Boolean flag whether the promo card is visible or not", 719 value: false, 720 }, 721 ], 722 [ 723 "discoverystream.sections.enabled", 724 { 725 title: "Boolean flag to enable section layout UI in recommended stories", 726 getValue: showSectionLayout, 727 }, 728 ], 729 [ 730 "discoverystream.sections.personalization.enabled", 731 { 732 title: 733 "Boolean flag to enable personalized sections layout. Allows users to follow/unfollow topic sections.", 734 value: false, 735 }, 736 ], 737 [ 738 "discoverystream.sections.customizeMenuPanel.enabled", 739 { 740 title: 741 "Boolean flag to enable the setions management panel in Customize menu", 742 value: false, 743 }, 744 ], 745 [ 746 "discoverystream.sections.cards.enabled", 747 { 748 title: 749 "Boolean flag to enable revised pocket story card UI in recommended stories", 750 value: false, 751 }, 752 ], 753 [ 754 "discoverystream.sections.contextualAds.enabled", 755 { 756 title: "Boolean flag to enable contextual ads", 757 getValue: useContextualAds, 758 }, 759 ], 760 [ 761 "discoverystream.sections.personalization.inferred.enabled", 762 { 763 title: "Boolean flag to enable inferred personalizaton", 764 // pref is dynamic 765 getValue: useInferredPersonalization, 766 }, 767 ], 768 [ 769 "discoverystream.sections.personalization.inferred.interests.override", 770 { 771 title: 772 "Testing feature to allow specification of specific user interests", 773 }, 774 ], 775 [ 776 "discoverystream.dailyBrief.enabled", 777 { 778 title: "Boolean flag to enable the daily brief section", 779 value: false, 780 }, 781 ], 782 [ 783 "discoverystream.dailyBrief.sectionId", 784 { 785 title: "sectionId for the Daily brief section", 786 value: "top_stories_section", 787 }, 788 ], 789 [ 790 "discoverystream.shortcuts.personalization.enabled", 791 { 792 title: "Boolean flag to enable shortcuts personalization", 793 value: false, 794 }, 795 ], 796 [ 797 "discoverystream.shortcuts.force_log.enabled", 798 { 799 title: 800 "Boolean flag to enable logging shortcuts interactions even if enabled is off", 801 value: false, 802 }, 803 ], 804 [ 805 "discoverystream.attribution.enabled", 806 { 807 title: "Boolean flag to enable newtab attribution", 808 value: false, 809 }, 810 ], 811 [ 812 "discoverystream.sections.personalization.inferred.user.enabled", 813 { 814 title: "User pref to toggle inferred personalizaton", 815 value: false, 816 }, 817 ], 818 [ 819 "discoverystream.sections.personalization.inferred.model.override", 820 { 821 title: 822 "Override inferred personalization model JSON string that typically comes from rec API. Or 'TEST' for a test model", 823 }, 824 ], 825 [ 826 "discoverystream.reportAds.enabled", 827 { 828 title: "Boolean flag to enable reporting ads from the context menu", 829 value: false, 830 }, 831 ], 832 [ 833 "discoverystream.sections.following", 834 { 835 title: "A comma-separated list of strings of followed section topics", 836 value: "", 837 }, 838 ], 839 [ 840 "discoverystream.sections.blocked", 841 { 842 title: "A comma-separated list of strings of blocked section topics", 843 value: "", 844 }, 845 ], 846 [ 847 "discoverystream.sections.interestPicker.enabled", 848 { 849 title: "Boolean flag to enable the inline interest picker", 850 value: false, 851 }, 852 ], 853 [ 854 "discoverystream.sections.interestPicker.visibleSections", 855 { 856 title: "comma separated string of sections that are visible", 857 value: "", 858 }, 859 ], 860 [ 861 "discoverystream.spoc-positions", 862 { 863 title: "CSV string of spoc position indexes on newtab Pocket grid", 864 value: "1,5,7,11,18,20", 865 }, 866 ], 867 [ 868 "discoverystream.placements.contextualSpocs", 869 { 870 title: 871 "CSV string of spoc placement ids on newtab Pocket grid. A placement id tells our ad server where the ads are intended to be displayed.", 872 }, 873 ], 874 [ 875 "discoverystream.placements.contextualSpocs.counts", 876 { 877 title: 878 "CSV string of spoc placement counts on newtab Pocket grid. The count tells the ad server how many ads to return for this position and placement.", 879 }, 880 ], 881 [ 882 "discoverystream.placements.contextualBanners", 883 { 884 title: 885 "CSV string of the banner placement ids on newtab Pocket grid. This placement id tells us which banner is visible when contexual ads are on", 886 value: "", 887 }, 888 ], 889 [ 890 "discoverystream.placements.contextualBanners.counts", 891 { 892 title: 893 "CSV string of AdBanner placement counts on newtab Pocket grid. The count tells the ad server how many banners to return for this position and placement.", 894 value: "", 895 }, 896 ], 897 [ 898 "discoverystream.placements.spocs", 899 { 900 title: 901 "CSV string of spoc placement ids on newtab Pocket grid. A placement id tells our ad server where the ads are intended to be displayed.", 902 }, 903 ], 904 [ 905 "discoverystream.placements.spocs.counts", 906 { 907 title: 908 "CSV string of spoc placement counts on newtab Pocket grid. The count tells the ad server how many ads to return for this position and placement.", 909 }, 910 ], 911 [ 912 "discoverystream.placements.tiles", 913 { 914 title: 915 "CSV string of tiles placement ids on newtab tiles section. A placement id tells our ad server where the ads are intended to be displayed.", 916 }, 917 ], 918 [ 919 "discoverystream.placements.tiles.counts", 920 { 921 title: 922 "CSV string of tiles placement counts on newtab tiles section. The count tells the ad server how many ads to return for this position and placement.", 923 }, 924 ], 925 [ 926 "discoverystream.imageProxy.enabled", 927 { 928 title: "Boolean flag to enable image proxying for images on newtab", 929 value: false, 930 }, 931 ], 932 [ 933 "newtabWallpapers.highlightEnabled", 934 { 935 title: "Boolean flag to show the highlight about the Wallpaper feature", 936 value: false, 937 }, 938 ], 939 [ 940 "newtabWallpapers.highlightDismissed", 941 { 942 title: 943 "Boolean flag to remember if the user has seen the feature highlight", 944 value: false, 945 }, 946 ], 947 [ 948 "newtabWallpapers.highlightSeenCounter", 949 { 950 title: "Count the number of times a user has seen the feature highlight", 951 value: 0, 952 }, 953 ], 954 [ 955 "newtabWallpapers.highlightHeaderText", 956 { 957 title: "Changes the wallpaper feature highlight header text", 958 value: "", 959 }, 960 ], 961 [ 962 "newtabWallpapers.highlightContentText", 963 { 964 title: "Changes the wallpaper feature highlight content text", 965 value: "", 966 }, 967 ], 968 [ 969 "newtabWallpapers.highlightCtaText", 970 { 971 title: "Changes the wallpaper feature highlight cta text", 972 value: "", 973 }, 974 ], 975 [ 976 "newtabWallpapers.wallpaper", 977 { 978 title: "Currently set wallpaper", 979 value: "", 980 }, 981 ], 982 [ 983 "sov.enabled", 984 { 985 title: "Enables share of voice (SOV)", 986 getValue: useSov, 987 }, 988 ], 989 [ 990 "sov.name", 991 { 992 title: 993 "A unique id, usually this is a timestamp for the day it was generated", 994 value: "SOV-20251122215625", 995 }, 996 ], 997 [ 998 "sov.frecency.exposure", 999 { 1000 title: 1001 "Is or was the user eligible for frecency ranked sponsored shortcuts", 1002 value: false, 1003 }, 1004 ], 1005 [ 1006 "sov.amp.allocation", 1007 { 1008 title: "How many positions can be filled from amp", 1009 value: "100, 100, 100", 1010 }, 1011 ], 1012 [ 1013 "sov.frecency.allocation", 1014 { 1015 title: "How many positions can be filled by frecency", 1016 value: "0, 0, 0", 1017 }, 1018 ], 1019 [ 1020 "widgets.system.enabled", 1021 { 1022 title: "Enables visibility of all widgets and controls to enable them", 1023 value: false, 1024 }, 1025 ], 1026 [ 1027 "widgets.enabled", 1028 { 1029 title: "Allows users to toggle all widgets on and off at once", 1030 value: false, 1031 }, 1032 ], 1033 [ 1034 "widgets.lists.enabled", 1035 { 1036 title: "Enables the to-do lists widget", 1037 value: true, 1038 }, 1039 ], 1040 [ 1041 "widgets.lists.maxLists", 1042 { 1043 title: "Maximum number of lists that can be created", 1044 value: 10, 1045 }, 1046 ], 1047 [ 1048 "widgets.lists.maxListItems", 1049 { 1050 title: 1051 "Maximum number of items that can be created on an individual list", 1052 value: 100, 1053 }, 1054 ], 1055 [ 1056 "widgets.system.lists.enabled", 1057 { 1058 title: "Enables the to-do lists widget experiment in Nimbus", 1059 value: false, 1060 }, 1061 ], 1062 [ 1063 "widgets.lists.interaction", 1064 { 1065 title: 1066 "Boolean flag for determining if a user has interacted with the lists widget", 1067 value: false, 1068 }, 1069 ], 1070 [ 1071 "widgets.lists.badge.enabled", 1072 { 1073 title: "Show badge on lists widget to indicate new/beta feature", 1074 value: false, 1075 }, 1076 ], 1077 [ 1078 "widgets.lists.badge.label", 1079 { 1080 title: "Label type for lists widget badge (New or Beta)", 1081 value: "", 1082 }, 1083 ], 1084 [ 1085 "widgets.maximized", 1086 { 1087 title: "Toggles maximized state for all widgets in the widgets section", 1088 value: false, 1089 }, 1090 ], 1091 [ 1092 "widgets.system.maximized", 1093 { 1094 title: "Enables the maximize widget feature experiment in Nimbus", 1095 value: false, 1096 }, 1097 ], 1098 [ 1099 "widgets.focusTimer.enabled", 1100 { 1101 title: "Enables the focus timer widget", 1102 value: true, 1103 }, 1104 ], 1105 [ 1106 "widgets.system.focusTimer.enabled", 1107 { 1108 title: "Enables the focus timer widget experiment in Nimbus", 1109 value: false, 1110 }, 1111 ], 1112 [ 1113 "widgets.focusTimer.interaction", 1114 { 1115 title: 1116 "Boolean flag for determining if a user has interacted with the timer widget", 1117 value: false, 1118 }, 1119 ], 1120 [ 1121 "widgets.focusTimer.showSystemNotifications", 1122 { 1123 title: "Enables the focus timer widget to show system notifications", 1124 value: false, 1125 }, 1126 ], 1127 [ 1128 "widgets.weatherForecast.enabled", 1129 { 1130 title: "Enables the weather forecast widget", 1131 value: true, 1132 }, 1133 ], 1134 [ 1135 "widgets.system.weatherForecast.enabled", 1136 { 1137 title: "Enables the weather forecast widget experiment in Nimbus", 1138 value: false, 1139 }, 1140 ], 1141 [ 1142 "widgets.weatherForecast.interaction", 1143 { 1144 title: 1145 "Boolean flag for determining if a user has interacted with the weather forecast widget", 1146 value: false, 1147 }, 1148 ], 1149 [ 1150 "improvesearch.noDefaultSearchTile", 1151 { 1152 title: "Remove tiles that are the same as the default search", 1153 value: true, 1154 }, 1155 ], 1156 [ 1157 "improvesearch.topSiteSearchShortcuts.searchEngines", 1158 { 1159 title: 1160 "An ordered, comma-delimited list of search shortcuts that we should try and pin", 1161 // This pref is dynamic as the shortcuts vary depending on the region 1162 getValue: ({ geo }) => { 1163 if (!geo) { 1164 return ""; 1165 } 1166 const searchShortcuts = []; 1167 if (geo === "CN") { 1168 searchShortcuts.push("baidu"); 1169 } else if (["BY", "KZ", "RU", "TR"].includes(geo)) { 1170 searchShortcuts.push("yandex"); 1171 } else { 1172 searchShortcuts.push("google"); 1173 } 1174 if (["DE", "FR", "GB", "IT", "JP", "US"].includes(geo)) { 1175 searchShortcuts.push("amazon"); 1176 } 1177 return searchShortcuts.join(","); 1178 }, 1179 }, 1180 ], 1181 [ 1182 "improvesearch.topSiteSearchShortcuts.havePinned", 1183 { 1184 title: 1185 "A comma-delimited list of search shortcuts that have previously been pinned", 1186 value: "", 1187 }, 1188 ], 1189 [ 1190 "asrouter.devtoolsEnabled", 1191 { 1192 title: "Are the asrouter devtools enabled?", 1193 value: false, 1194 }, 1195 ], 1196 [ 1197 "discoverystream.flight.blocks", 1198 { 1199 title: "Track flight blocks", 1200 skipBroadcast: true, 1201 value: "{}", 1202 }, 1203 ], 1204 [ 1205 "discoverystream.config", 1206 { 1207 title: "Configuration for the new pocket new tab", 1208 getValue: () => { 1209 return JSON.stringify({ 1210 collapsible: true, 1211 enabled: true, 1212 }); 1213 }, 1214 }, 1215 ], 1216 [ 1217 "discoverystream.endpoints", 1218 { 1219 title: 1220 "Endpoint prefixes (comma-separated) that are allowed to be requested", 1221 value: 1222 "https://getpocket.cdn.mozilla.net/,https://firefox-api-proxy.cdn.mozilla.net/,https://spocs.getpocket.com/,https://merino.services.mozilla.com/,https://ads.mozilla.org/", 1223 }, 1224 ], 1225 [ 1226 "discoverystream.region-basic-layout", 1227 { 1228 title: "Decision to use basic layout based on region.", 1229 getValue: ({ geo }) => { 1230 const preffedRegionsString = 1231 Services.prefs.getStringPref(REGION_BASIC_CONFIG) || ""; 1232 // If no regions are set to basic, 1233 // we don't need to bother checking against the region. 1234 // We are also not concerned if geo is not set, 1235 // because stories are going to be empty until we have geo. 1236 if (!preffedRegionsString) { 1237 return false; 1238 } 1239 const preffedRegions = preffedRegionsString 1240 .split(",") 1241 .map(s => s.trim()); 1242 1243 return preffedRegions.includes(geo); 1244 }, 1245 }, 1246 ], 1247 [ 1248 "discoverystream.spoc.impressions", 1249 { 1250 title: "Track spoc impressions", 1251 skipBroadcast: true, 1252 value: "{}", 1253 }, 1254 ], 1255 [ 1256 "discoverystream.endpointSpocsClear", 1257 { 1258 title: 1259 "Endpoint for when a user opts-out of sponsored content to delete the user's data from the ad server.", 1260 value: "https://spocs.getpocket.com/user", 1261 }, 1262 ], 1263 [ 1264 "discoverystream.rec.impressions", 1265 { 1266 title: "Track rec impressions", 1267 skipBroadcast: true, 1268 value: "{}", 1269 }, 1270 ], 1271 [ 1272 "discoverystream.topicSelection.enabled", 1273 { 1274 title: "Enables topic selection for discovery stream", 1275 // pref is dynamic 1276 getValue: showTopicsSelection, 1277 }, 1278 ], 1279 [ 1280 "discoverystream.topicSelection.topics", 1281 { 1282 title: "Topics available", 1283 value: 1284 "business, arts, food, health, finance, government, sports, tech, travel, education-science, society", 1285 }, 1286 ], 1287 [ 1288 "discoverystream.topicSelection.selectedTopics", 1289 { 1290 title: "Selected topics", 1291 value: "", 1292 }, 1293 ], 1294 [ 1295 "discoverystream.topicSelection.suggestedTopics", 1296 { 1297 title: "Suggested topics to choose during onboarding for topic selection", 1298 value: "business, arts, government", 1299 }, 1300 ], 1301 [ 1302 "discoverystream.topicSelection.hasBeenUpdatedPreviously", 1303 { 1304 title: "Returns true only if the user has previously selected topics", 1305 value: false, 1306 }, 1307 ], 1308 [ 1309 "discoverystream.topicSelection.onboarding.displayCount", 1310 { 1311 title: "amount of times that topic selection onboarding has been shown", 1312 value: 0, 1313 }, 1314 ], 1315 [ 1316 "discoverystream.topicSelection.onboarding.maybeDisplay", 1317 { 1318 title: 1319 "Whether the onboarding should be shown, based on previous interactions", 1320 value: true, 1321 }, 1322 ], 1323 [ 1324 "discoverystream.topicSelection.onboarding.lastDisplayed", 1325 { 1326 title: 1327 "time in ms that onboarding was last shown (stored as string due to contraits of prefs)", 1328 value: "", 1329 }, 1330 ], 1331 [ 1332 "discoverystream.topicSelection.onboarding.displayTimeout", 1333 { 1334 title: "time in ms that the onboarding show be shown next", 1335 value: 0, 1336 }, 1337 ], 1338 [ 1339 "discoverystream.topicSelection.onboarding.enabled", 1340 { 1341 title: "enabled onboarding experience for topic selection onboarding", 1342 value: false, 1343 }, 1344 ], 1345 [ 1346 "discoverystream.topicLabels.enabled", 1347 { 1348 title: "Enables topic labels for discovery stream", 1349 // pref is dynamic 1350 getValue: showTopicLabels, 1351 }, 1352 ], 1353 [ 1354 "discoverystream.spocs.onDemand", 1355 { 1356 title: "Set sponsored content to only update cache when requested.", 1357 value: false, 1358 }, 1359 ], 1360 [ 1361 "discoverystream.spocs.cacheTimeout", 1362 { 1363 title: "Set sponsored content cache timeout in minutes.", 1364 }, 1365 ], 1366 [ 1367 "discoverystream.spocs.startupCache.enabled", 1368 { 1369 title: "Controls if spocs should be included in startup cache.", 1370 value: false, 1371 }, 1372 ], 1373 [ 1374 "discoverystream.publisherFavicon.enabled", 1375 { 1376 title: "Enables publisher favicons on recommended stories", 1377 value: false, 1378 }, 1379 ], 1380 [ 1381 "discoverystream.sections.clientLayout.enabled", 1382 { 1383 title: "Enables client side layout for recommended stories", 1384 value: false, 1385 }, 1386 ], 1387 [ 1388 "support.url", 1389 { 1390 title: "Link to HNT's support page", 1391 getValue: () => { 1392 // Services.urlFormatter completes the in-product SUMO page URL: 1393 // https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/new-tab 1394 const baseUrl = Services.urlFormatter.formatURLPref( 1395 "app.support.baseURL" 1396 ); 1397 return `${baseUrl}new-tab`; 1398 }, 1399 }, 1400 ], 1401 [ 1402 "caretBlinkCount", 1403 { 1404 title: 1405 "The amount of times the caret blinks. This pref copies the value from the system settings", 1406 getValue: () => { 1407 return Services.appinfo.caretBlinkCount; 1408 }, 1409 }, 1410 ], 1411 [ 1412 "caretBlinkTime", 1413 { 1414 title: 1415 "Rate at which the caret blinks. This pref copies the value from the system settings", 1416 getValue: () => { 1417 return Services.appinfo.caretBlinkTime; 1418 }, 1419 }, 1420 ], 1421 [ 1422 "showSponsoredCheckboxes", 1423 { 1424 title: 1425 "'Support Firefox' pref on 'about:settings#home' page. Toggles all sponsored results on and off at the same time", 1426 value: true, 1427 }, 1428 ], 1429 ]); 1430 1431 // Array of each feed's FEEDS_CONFIG factory and values to add to PREFS_CONFIG 1432 const FEEDS_DATA = [ 1433 { 1434 name: "aboutpreferences", 1435 factory: () => new lazy.AboutPreferences(), 1436 title: "about:preferences rendering", 1437 value: true, 1438 }, 1439 { 1440 name: "newtabinit", 1441 factory: () => new lazy.NewTabInit(), 1442 title: "Sends a copy of the state to each new tab that is opened", 1443 value: true, 1444 }, 1445 { 1446 name: "places", 1447 factory: () => new lazy.PlacesFeed(), 1448 title: "Listens for and relays various Places-related events", 1449 value: true, 1450 }, 1451 { 1452 name: "prefs", 1453 factory: () => new lazy.PrefsFeed(PREFS_CONFIG), 1454 title: "Preferences", 1455 value: true, 1456 }, 1457 { 1458 name: "sections", 1459 factory: () => new lazy.SectionsFeed(), 1460 title: "Manages sections", 1461 value: true, 1462 }, 1463 { 1464 name: "startupcacheinit", 1465 factory: () => new lazy.StartupCacheInit(), 1466 title: "Sends a copy of the state to the startup cache newtab", 1467 value: true, 1468 }, 1469 { 1470 name: "section.highlights", 1471 factory: () => new lazy.HighlightsFeed(), 1472 title: "Fetches content recommendations from places db", 1473 value: false, 1474 }, 1475 { 1476 name: "system.topstories", 1477 factory: () => 1478 new lazy.TopStoriesFeed(PREFS_CONFIG.get("discoverystream.config")), 1479 title: 1480 "System pref that fetches content recommendations from a configurable content provider", 1481 // Dynamically determine if Pocket should be shown for a geo / locale 1482 getValue: ({ geo, locale }) => { 1483 // If we don't have geo, we don't want to flash the screen with stories while geo loads. 1484 // Best to display nothing until geo is ready. 1485 if (!geo) { 1486 return false; 1487 } 1488 const preffedRegionsBlockString = 1489 lazy.NimbusFeatures.pocketNewtab.getVariable("regionStoriesBlock") || 1490 ""; 1491 const preffedRegionsString = 1492 lazy.NimbusFeatures.pocketNewtab.getVariable("regionStoriesConfig") || 1493 ""; 1494 const preffedLocaleListString = 1495 lazy.NimbusFeatures.pocketNewtab.getVariable("localeListConfig") || ""; 1496 const preffedBlockRegions = preffedRegionsBlockString 1497 .split(",") 1498 .map(s => s.trim()); 1499 const preffedRegions = preffedRegionsString.split(",").map(s => s.trim()); 1500 const preffedLocales = preffedLocaleListString 1501 .split(",") 1502 .map(s => s.trim()); 1503 const locales = { 1504 US: ["en-CA", "en-GB", "en-US"], 1505 CA: ["en-CA", "en-GB", "en-US"], 1506 GB: ["en-CA", "en-GB", "en-US"], 1507 AU: ["en-CA", "en-GB", "en-US"], 1508 NZ: ["en-CA", "en-GB", "en-US"], 1509 IN: ["en-CA", "en-GB", "en-US"], 1510 IE: ["en-CA", "en-GB", "en-US"], 1511 ZA: ["en-CA", "en-GB", "en-US"], 1512 CH: ["de"], 1513 BE: ["de"], 1514 DE: ["de"], 1515 AT: ["de"], 1516 IT: ["it"], 1517 FR: ["fr"], 1518 ES: ["es-ES"], 1519 PL: ["pl"], 1520 JP: ["ja", "ja-JP-mac"], 1521 }[geo]; 1522 1523 const regionBlocked = preffedBlockRegions.includes(geo); 1524 const localeEnabled = locale && preffedLocales.includes(locale); 1525 const regionEnabled = 1526 preffedRegions.includes(geo) && !!locales && locales.includes(locale); 1527 return !regionBlocked && (localeEnabled || regionEnabled); 1528 }, 1529 }, 1530 { 1531 name: "systemtick", 1532 factory: () => new lazy.SystemTickFeed(), 1533 title: "Produces system tick events to periodically check for data expiry", 1534 value: true, 1535 }, 1536 { 1537 name: "telemetry", 1538 factory: () => new lazy.TelemetryFeed(), 1539 title: "Relays telemetry-related actions to PingCentre", 1540 value: true, 1541 }, 1542 { 1543 name: "favicon", 1544 factory: () => new lazy.FaviconFeed(), 1545 title: "Fetches tippy top manifests from remote service", 1546 value: true, 1547 }, 1548 { 1549 name: "system.topsites", 1550 factory: () => new lazy.TopSitesFeed(), 1551 title: "Queries places and gets metadata for Top Sites section", 1552 value: true, 1553 }, 1554 { 1555 name: "recommendationprovider", 1556 factory: () => new lazy.RecommendationProvider(), 1557 title: "Handles setup and interaction for the personality provider", 1558 value: true, 1559 }, 1560 { 1561 name: "discoverystreamfeed", 1562 factory: () => new lazy.DiscoveryStreamFeed(), 1563 title: "Handles new pocket ui for the new tab page", 1564 value: true, 1565 }, 1566 { 1567 name: "wallpaperfeed", 1568 factory: () => new lazy.WallpaperFeed(), 1569 title: "Handles fetching and managing wallpaper data from RemoteSettings", 1570 value: true, 1571 }, 1572 { 1573 name: "weatherfeed", 1574 factory: () => new lazy.WeatherFeed(), 1575 title: "Handles fetching and caching weather data", 1576 value: true, 1577 }, 1578 { 1579 name: "adsfeed", 1580 factory: () => new lazy.AdsFeed(), 1581 title: "Handles fetching and caching ads data", 1582 value: true, 1583 }, 1584 { 1585 name: "inferredpersonalizationfeed", 1586 factory: () => new lazy.InferredPersonalizationFeed(), 1587 title: 1588 "Handles generating and caching an interest vector for inferred personalization", 1589 value: true, 1590 }, 1591 { 1592 name: "smartshortcutsfeed", 1593 factory: () => new lazy.SmartShortcutsFeed(), 1594 title: 1595 "Handles generating and caching an interest vector for shortcuts personalization", 1596 value: true, 1597 }, 1598 { 1599 name: "newtabattributionfeed", 1600 factory: () => new lazy.NewTabAttributionFeed(), 1601 title: "Handles a local DB for story and shortcuts clicks and impressions", 1602 value: true, 1603 }, 1604 { 1605 name: "newtabmessaging", 1606 factory: () => new lazy.NewTabMessaging(), 1607 title: "Handles fetching and triggering ASRouter messages in newtab", 1608 value: true, 1609 }, 1610 { 1611 name: "listsfeed", 1612 factory: () => new lazy.ListsFeed(), 1613 title: "Handles the data for the Todo list widget", 1614 value: true, 1615 }, 1616 { 1617 name: "timerfeed", 1618 factory: () => new lazy.TimerFeed(), 1619 title: "Handles the data for the Timer widget", 1620 value: true, 1621 }, 1622 { 1623 name: "externalcomponentsfeed", 1624 factory: () => new lazy.ExternalComponentsFeed(), 1625 title: "Handles updating the registry of external components", 1626 getValue() { 1627 // This feed should only be enabled on versions of the app that have the 1628 // AboutNewTabComponents module. Those versions of the app have this 1629 // preference set to true. 1630 return Services.prefs.getBoolPref( 1631 PREF_SHOULD_ENABLE_EXTERNAL_COMPONENTS_FEED, 1632 false 1633 ); 1634 }, 1635 }, 1636 ]; 1637 1638 const FEEDS_CONFIG = new Map(); 1639 1640 for (const config of FEEDS_DATA) { 1641 const pref = `feeds.${config.name}`; 1642 FEEDS_CONFIG.set(pref, config.factory); 1643 PREFS_CONFIG.set(pref, config); 1644 } 1645 1646 export class ActivityStream { 1647 /** 1648 * constructor - Initializes an instance of ActivityStream 1649 */ 1650 constructor() { 1651 this.initialized = false; 1652 this.store = new lazy.Store(); 1653 this._defaultPrefs = new lazy.DefaultPrefs(PREFS_CONFIG); 1654 this._proxyRegistered = false; 1655 } 1656 1657 get feeds() { 1658 if (shouldInitializeFeeds()) { 1659 return FEEDS_CONFIG; 1660 } 1661 1662 // We currently make excpetions for topsites, and prefs feeds 1663 // because they currently impacts tests timing for places initialization. 1664 // See bug 1999166. 1665 const feeds = new Map([ 1666 ["feeds.system.topsites", FEEDS_CONFIG.get("feeds.system.topsites")], 1667 ["feeds.prefs", FEEDS_CONFIG.get("feeds.prefs")], 1668 ]); 1669 return feeds; 1670 } 1671 1672 init() { 1673 this._updateDynamicPrefs(); 1674 this._defaultPrefs.init(); 1675 Services.obs.addObserver(this, "intl:app-locales-changed"); 1676 Services.prefs.addObserver(PREF_IMAGE_PROXY_ENABLED, this); 1677 lazy.NewTabActorRegistry.init(); 1678 1679 // Hook up the store and let all feeds and pages initialize 1680 this.store.init( 1681 this.feeds, 1682 ac.BroadcastToContent({ 1683 type: at.INIT, 1684 data: { 1685 locale: this.locale, 1686 }, 1687 meta: { 1688 isStartup: true, 1689 }, 1690 }), 1691 { type: at.UNINIT } 1692 ); 1693 1694 this.initialized = true; 1695 1696 this.registerNetworkProxy(); 1697 } 1698 1699 /** 1700 * Registers network proxy channel filter for image requests. 1701 * This enables privacy-preserving image proxy for newtab when 1702 * inferred personalization is enabled. 1703 */ 1704 registerNetworkProxy() { 1705 const enabled = Services.prefs.getBoolPref(PREF_IMAGE_PROXY_ENABLED, false); 1706 if (!this._proxyRegistered && enabled) { 1707 lazy.ProxyService.registerChannelFilter(this, 0); 1708 this._proxyRegistered = true; 1709 } 1710 } 1711 1712 /** 1713 * Unregisters network proxy channel filter. 1714 */ 1715 unregisterNetworkProxy() { 1716 if (this._proxyRegistered) { 1717 lazy.ProxyService.unregisterChannelFilter(this); 1718 this._proxyRegistered = false; 1719 } 1720 } 1721 1722 /** 1723 * Retrieves and validates image proxy configuration from prefs/nimbus. 1724 * 1725 * @returns {object|null} Image proxy config object, or null if disabled/invalid. 1726 */ 1727 getImageProxyConfig() { 1728 try { 1729 if (!this.store || !this.initialized) { 1730 return null; 1731 } 1732 1733 const state = this.store.getState(); 1734 if (!state || !state.Prefs) { 1735 return null; 1736 } 1737 1738 const { values } = state.Prefs; 1739 1740 const config = values?.trainhopConfig?.imageProxy; 1741 if ( 1742 !config || 1743 !config.enabled || 1744 !config.proxyHost || 1745 !config.proxyPort || 1746 !config.proxyAuthHeader || 1747 !values?.[PREF_INFERRED_ENABLED] || 1748 !values?.[PREF_IMAGE_PROXY_ENABLED_STORE] 1749 ) { 1750 return null; 1751 } 1752 return { 1753 proxyHost: config.proxyHost, 1754 proxyPort: config.proxyPort, 1755 proxyAuthHeader: config.proxyAuthHeader, 1756 connectionIsolationKey: config.connectionIsolationKey || "", 1757 failoverProxy: config.failoverProxy, 1758 imageProxyHosts: (config.imageProxyHosts || "") 1759 .split(",") 1760 .map(host => host.trim()), 1761 }; 1762 } catch (e) { 1763 return null; 1764 } 1765 } 1766 1767 /** 1768 * nsIProtocolProxyChannelFilter implementation. Applies MASQUE proxy 1769 * to image requests from newtab when configured. 1770 * 1771 * @param {nsIChannel} channel 1772 * @param {nsIProxyInfo} proxyInfo 1773 * @param {nsIProtocolProxyChannelFilter} callback 1774 */ 1775 applyFilter(channel, proxyInfo, callback) { 1776 const { browsingContext } = channel.loadInfo; 1777 let browser = browsingContext?.top?.embedderElement; 1778 1779 if (!browser || !lazy.AboutNewTabParent.loadedTabs.has(browser)) { 1780 callback.onProxyFilterResult(proxyInfo); 1781 return; 1782 } 1783 1784 const config = this.getImageProxyConfig(); 1785 1786 if (!config) { 1787 callback.onProxyFilterResult(proxyInfo); 1788 return; 1789 } 1790 1791 if ( 1792 config.imageProxyHosts.includes(channel.URI.host) && 1793 channel.URI?.scheme === "https" 1794 ) { 1795 callback.onProxyFilterResult( 1796 lazy.ProxyService.newProxyInfo( 1797 "https" /* aType */, 1798 config.proxyHost /* aHost */, 1799 config.proxyPort /* aPort */, 1800 config.proxyAuthHeader /* aProxyAuthorizationHeader */, 1801 config.connectionIsolationKey /* aConnectionIsolationKey */, 1802 0 /* aFlags */, 1803 5000 /* aFailoverTimeout */, 1804 config.failoverProxy /* aFailoverProxy */ 1805 ) 1806 ); 1807 } else { 1808 callback.onProxyFilterResult(proxyInfo); 1809 } 1810 } 1811 1812 QueryInterface = ChromeUtils.generateQI([Ci.nsIProtocolProxyChannelFilter]); 1813 1814 /** 1815 * Check if an old pref has a custom value to migrate. Clears the pref so that 1816 * it's the default after migrating (to avoid future need to migrate). 1817 * 1818 * @param oldPrefName {string} Pref to check and migrate 1819 * @param cbIfNotDefault {function} Callback that gets the current pref value 1820 */ 1821 _migratePref(oldPrefName, cbIfNotDefault) { 1822 // Nothing to do if the user doesn't have a custom value 1823 if (!Services.prefs.prefHasUserValue(oldPrefName)) { 1824 return; 1825 } 1826 1827 // Figure out what kind of pref getter to use 1828 let prefGetter; 1829 switch (Services.prefs.getPrefType(oldPrefName)) { 1830 case Services.prefs.PREF_BOOL: 1831 prefGetter = "getBoolPref"; 1832 break; 1833 case Services.prefs.PREF_INT: 1834 prefGetter = "getIntPref"; 1835 break; 1836 case Services.prefs.PREF_STRING: 1837 prefGetter = "getStringPref"; 1838 break; 1839 } 1840 1841 // Give the callback the current value then clear the pref 1842 cbIfNotDefault(Services.prefs[prefGetter](oldPrefName)); 1843 Services.prefs.clearUserPref(oldPrefName); 1844 } 1845 1846 uninit() { 1847 if (this.geo === "") { 1848 Services.obs.removeObserver(this, lazy.Region.REGION_TOPIC); 1849 } 1850 delete this.geo; 1851 1852 Services.obs.removeObserver(this, "intl:app-locales-changed"); 1853 Services.prefs.removeObserver(PREF_IMAGE_PROXY_ENABLED, this); 1854 1855 this.store.uninit(); 1856 this.unregisterNetworkProxy(); 1857 this.initialized = false; 1858 } 1859 1860 _updateDynamicPrefs() { 1861 // Save the geo pref if we have it 1862 if (lazy.Region.home) { 1863 if (this.geo === "") { 1864 // The observer has become obsolete. 1865 Services.obs.removeObserver(this, lazy.Region.REGION_TOPIC); 1866 } 1867 this.geo = lazy.Region.home; 1868 } else if (this.geo !== "") { 1869 // Watch for geo changes and use a dummy value for now 1870 Services.obs.addObserver(this, lazy.Region.REGION_TOPIC); 1871 this.geo = ""; 1872 } 1873 1874 this.locale = Services.locale.appLocaleAsBCP47; 1875 1876 // Update the pref config of those with dynamic values 1877 for (const pref of PREFS_CONFIG.keys()) { 1878 // Only need to process dynamic prefs 1879 const prefConfig = PREFS_CONFIG.get(pref); 1880 if (!prefConfig.getValue) { 1881 continue; 1882 } 1883 1884 // Have the dynamic pref just reuse using existing default, e.g., those 1885 // set via Autoconfig or policy 1886 try { 1887 const existingDefault = this._defaultPrefs.get(pref); 1888 if (existingDefault !== undefined && prefConfig.value === undefined) { 1889 prefConfig.getValue = () => existingDefault; 1890 } 1891 } catch (ex) { 1892 // We get NS_ERROR_UNEXPECTED for prefs that have a user value (causing 1893 // default branch to believe there's a type) but no actual default value 1894 } 1895 1896 // Compute the dynamic value (potentially generic based on dummy geo) 1897 const newValue = prefConfig.getValue({ 1898 geo: this.geo, 1899 locale: this.locale, 1900 }); 1901 1902 // If there's an existing value and it has changed, that means we need to 1903 // overwrite the default with the new value. 1904 if (prefConfig.value !== undefined && prefConfig.value !== newValue) { 1905 this._defaultPrefs.set(pref, newValue); 1906 } 1907 1908 prefConfig.value = newValue; 1909 } 1910 } 1911 1912 observe(subject, topic, data) { 1913 switch (topic) { 1914 case "intl:app-locales-changed": 1915 case lazy.Region.REGION_TOPIC: 1916 this._updateDynamicPrefs(); 1917 break; 1918 case "nsPref:changed": 1919 if (data === PREF_IMAGE_PROXY_ENABLED) { 1920 const enabled = Services.prefs.getBoolPref( 1921 PREF_IMAGE_PROXY_ENABLED, 1922 false 1923 ); 1924 if (enabled) { 1925 this.registerNetworkProxy(); 1926 } else { 1927 this.unregisterNetworkProxy(); 1928 } 1929 } 1930 break; 1931 } 1932 } 1933 }