From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- browser/components/newtab/.eslintrc.js | 184 + browser/components/newtab/.nvmrc | 1 + .../components/newtab/AboutNewTabService.sys.mjs | 510 + .../newtab/bin/render-activity-stream-html.js | 188 + browser/components/newtab/bin/try-runner.js | 366 + browser/components/newtab/bin/vendor.js | 38 + browser/components/newtab/common/Actions.sys.mjs | 457 + browser/components/newtab/common/Dedupe.sys.mjs | 36 + browser/components/newtab/common/Reducers.sys.mjs | 855 + browser/components/newtab/components.conf | 14 + .../newtab/components/CustomElements/paragraph.js | 72 + .../newtab/content-src/activity-stream.jsx | 57 + .../components/A11yLinkButton/A11yLinkButton.jsx | 18 + .../components/A11yLinkButton/_A11yLinkButton.scss | 13 + .../newtab/content-src/components/Base/Base.jsx | 262 + .../newtab/content-src/components/Base/_Base.scss | 126 + .../newtab/content-src/components/Card/Card.jsx | 362 + .../newtab/content-src/components/Card/_Card.scss | 333 + .../newtab/content-src/components/Card/types.js | 30 + .../CollapsibleSection/CollapsibleSection.jsx | 116 + .../CollapsibleSection/_CollapsibleSection.scss | 108 + .../ComponentPerfTimer/ComponentPerfTimer.jsx | 177 + .../components/ConfirmDialog/ConfirmDialog.jsx | 103 + .../components/ConfirmDialog/_ConfirmDialog.scss | 68 + .../components/ContextMenu/ContextMenu.jsx | 176 + .../components/ContextMenu/ContextMenuButton.jsx | 72 + .../components/ContextMenu/_ContextMenu.scss | 59 + .../BackgroundsSection/BackgroundsSection.jsx | 11 + .../ContentSection/ContentSection.jsx | 270 + .../components/CustomizeMenu/CustomizeMenu.jsx | 85 + .../components/CustomizeMenu/_CustomizeMenu.scss | 244 + .../DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx | 506 + .../DiscoveryStreamAdmin/DiscoveryStreamAdmin.scss | 337 + .../DiscoveryStreamAdmin/SimpleHashRouter.jsx | 35 + .../DiscoveryStreamBase/DiscoveryStreamBase.jsx | 386 + .../DiscoveryStreamBase/_DiscoveryStreamBase.scss | 67 + .../CardGrid/CardGrid.jsx | 542 + .../CardGrid/_CardGrid.scss | 352 + .../CollectionCardGrid/CollectionCardGrid.jsx | 139 + .../CollectionCardGrid/_CollectionCardGrid.scss | 38 + .../DiscoveryStreamComponents/DSCard/DSCard.jsx | 529 + .../DiscoveryStreamComponents/DSCard/_DSCard.scss | 303 + .../DSContextFooter/DSContextFooter.jsx | 145 + .../DSContextFooter/_DSContextFooter.scss | 81 + .../DSDismiss/DSDismiss.jsx | 56 + .../DSDismiss/_DSDismiss.scss | 48 + .../DSEmptyState/DSEmptyState.jsx | 100 + .../DSEmptyState/_DSEmptyState.scss | 83 + .../DiscoveryStreamComponents/DSImage/DSImage.jsx | 263 + .../DSImage/_DSImage.scss | 48 + .../DSLinkMenu/DSLinkMenu.jsx | 70 + .../DSLinkMenu/_DSLinkMenu.scss | 28 + .../DSMessage/DSMessage.jsx | 34 + .../DSMessage/_DSMessage.scss | 37 + .../DSPrivacyModal/DSPrivacyModal.jsx | 72 + .../DSPrivacyModal/_DSPrivacyModal.scss | 48 + .../DSSignup/DSSignup.jsx | 168 + .../DSSignup/DSSignup.scss | 52 + .../DSTextPromo/DSTextPromo.jsx | 143 + .../DSTextPromo/_DSTextPromo.scss | 92 + .../Highlights/Highlights.jsx | 26 + .../Highlights/_Highlights.scss | 47 + .../HorizontalRule/HorizontalRule.jsx | 11 + .../HorizontalRule/_HorizontalRule.scss | 7 + .../Navigation/Navigation.jsx | 112 + .../Navigation/_Navigation.scss | 182 + .../PrivacyLink/PrivacyLink.jsx | 20 + .../PrivacyLink/_PrivacyLink.scss | 10 + .../SafeAnchor/SafeAnchor.jsx | 65 + .../SectionTitle/SectionTitle.jsx | 19 + .../SectionTitle/_SectionTitle.scss | 18 + .../TopSites/_TopSites.scss | 79 + .../TopicsWidget/TopicsWidget.jsx | 125 + .../TopicsWidget/_TopicsWidget.scss | 90 + .../ImpressionStats.jsx | 251 + .../_ImpressionStats.scss | 7 + .../components/ErrorBoundary/ErrorBoundary.jsx | 68 + .../components/ErrorBoundary/_ErrorBoundary.scss | 21 + .../components/FluentOrText/FluentOrText.jsx | 36 + .../content-src/components/LinkMenu/LinkMenu.jsx | 110 + .../components/ModalOverlay/ModalOverlay.jsx | 56 + .../components/ModalOverlay/_ModalOverlay.scss | 103 + .../MoreRecommendations/MoreRecommendations.jsx | 21 + .../MoreRecommendations/_MoreRecommendations.scss | 24 + .../PocketLoggedInCta/PocketLoggedInCta.jsx | 42 + .../PocketLoggedInCta/_PocketLoggedInCta.scss | 42 + .../content-src/components/Search/Search.jsx | 189 + .../content-src/components/Search/_Search.scss | 394 + .../content-src/components/Sections/Sections.jsx | 378 + .../content-src/components/Sections/_Sections.scss | 123 + .../components/TopSites/SearchShortcutsForm.jsx | 192 + .../content-src/components/TopSites/TopSite.jsx | 889 + .../components/TopSites/TopSiteForm.jsx | 323 + .../components/TopSites/TopSiteFormInput.jsx | 111 + .../TopSites/TopSiteImpressionWrapper.jsx | 149 + .../content-src/components/TopSites/TopSites.jsx | 213 + .../components/TopSites/TopSitesConstants.js | 39 + .../content-src/components/TopSites/_TopSites.scss | 631 + .../content-src/components/Topics/Topics.jsx | 33 + .../content-src/components/Topics/_Topics.scss | 24 + .../components/newtab/content-src/lib/constants.js | 38 + .../content-src/lib/detect-user-session-start.js | 82 + .../newtab/content-src/lib/init-store.js | 140 + .../newtab/content-src/lib/link-menu-options.js | 309 + .../newtab/content-src/lib/perf-service.js | 104 + .../newtab/content-src/lib/screenshot-utils.js | 61 + .../newtab/content-src/lib/selectLayoutRender.js | 255 + .../content-src/styles/_activity-stream.scss | 172 + .../newtab/content-src/styles/_icons.scss | 211 + .../newtab/content-src/styles/_mixins.scss | 50 + .../newtab/content-src/styles/_normalize.scss | 29 + .../newtab/content-src/styles/_theme.scss | 97 + .../newtab/content-src/styles/_variables.scss | 215 + .../content-src/styles/activity-stream-linux.scss | 11 + .../content-src/styles/activity-stream-mac.scss | 16 + .../styles/activity-stream-windows.scss | 11 + .../newtab/css/activity-stream-linux.css | 4248 ++++ .../components/newtab/css/activity-stream-mac.css | 4252 ++++ .../newtab/css/activity-stream-windows.css | 4248 ++++ .../data/content/abouthomecache/page.html.template | 46 + .../data/content/abouthomecache/script.js.template | 19 + .../newtab/data/content/activity-stream.bundle.js | 9558 +++++++ .../newtab/data/content/assets/firefox.svg | 168 + .../data/content/assets/glyph-cfr-feature-16.svg | 4 + .../newtab/data/content/assets/glyph-mail-16.svg | 4 + .../data/content/assets/glyph-maximize-16.svg | 4 + .../data/content/assets/glyph-minimize-16.svg | 4 + .../data/content/assets/glyph-modal-delete-20.svg | 8 + .../data/content/assets/glyph-newWindow-16.svg | 4 + .../data/content/assets/glyph-open-file-16.svg | 4 + .../newtab/data/content/assets/glyph-pin-16.svg | 6 + .../content/assets/glyph-pocket-archive-16.svg | 4 + .../data/content/assets/glyph-pocket-delete-16.svg | 4 + .../newtab/data/content/assets/glyph-unpin-16.svg | 4 + .../data/content/assets/glyph-webextension-16.svg | 4 + .../data/content/assets/icon-removed-bookmark.svg | 4 + .../data/content/assets/pocket-onboarding.avif | Bin 0 -> 7462 bytes .../data/content/assets/pocket-onboarding@2x.avif | Bin 0 -> 18590 bytes .../newtab/data/content/assets/pocket-swoosh.svg | 11 + .../newtab/data/content/assets/remote/mountain.svg | 12 + .../newtab/data/content/assets/remote/umbrella.png | Bin 0 -> 4292 bytes .../newtab/data/content/assets/spinner.svg | 4 + .../newtab/data/content/newtab-render.js | 11 + .../data/content/tippytop/favicons/adidas.png | Bin 0 -> 3226 bytes .../content/tippytop/favicons/aliexpress-com.ico | Bin 0 -> 4286 bytes .../data/content/tippytop/favicons/allegro-pl.ico | Bin 0 -> 1150 bytes .../data/content/tippytop/favicons/amazon.ico | Bin 0 -> 1407 bytes .../data/content/tippytop/favicons/avito-ru.ico | Bin 0 -> 5430 bytes .../data/content/tippytop/favicons/baidu-com.png | Bin 0 -> 1983 bytes .../data/content/tippytop/favicons/bbc-uk.ico | Bin 0 -> 958 bytes .../data/content/tippytop/favicons/bing-com.ico | Bin 0 -> 4286 bytes .../data/content/tippytop/favicons/ctrip-com.ico | Bin 0 -> 1150 bytes .../content/tippytop/favicons/duckduckgo-com.ico | Bin 0 -> 2799 bytes .../newtab/data/content/tippytop/favicons/ebay.ico | Bin 0 -> 1455 bytes .../newtab/data/content/tippytop/favicons/etsy.ico | Bin 0 -> 4286 bytes .../content/tippytop/favicons/facebook-com.ico | Bin 0 -> 5430 bytes .../data/content/tippytop/favicons/geico.png | Bin 0 -> 1472 bytes .../data/content/tippytop/favicons/google-com.ico | Bin 0 -> 5430 bytes .../data/content/tippytop/favicons/hrblock.ico | Bin 0 -> 3950 bytes .../data/content/tippytop/favicons/ifeng-com.ico | Bin 0 -> 4038 bytes .../data/content/tippytop/favicons/iqiyi-com.ico | Bin 0 -> 5430 bytes .../content/tippytop/favicons/leboncoin-fr.png | Bin 0 -> 454 bytes .../newtab/data/content/tippytop/favicons/nike.ico | Bin 0 -> 1150 bytes .../data/content/tippytop/favicons/ok-ru.ico | Bin 0 -> 5430 bytes .../data/content/tippytop/favicons/olx-pl.ico | Bin 0 -> 5430 bytes .../data/content/tippytop/favicons/reddit-com.png | Bin 0 -> 2094 bytes .../data/content/tippytop/favicons/samsung.ico | Bin 0 -> 4286 bytes .../data/content/tippytop/favicons/turbotax.png | Bin 0 -> 3744 bytes .../data/content/tippytop/favicons/twitter-com.ico | Bin 0 -> 1650 bytes .../data/content/tippytop/favicons/vk-com.ico | Bin 0 -> 302 bytes .../data/content/tippytop/favicons/vodafone.png | Bin 0 -> 1757 bytes .../data/content/tippytop/favicons/weibo-com.ico | Bin 0 -> 10134 bytes .../content/tippytop/favicons/wikipedia-org.ico | Bin 0 -> 2734 bytes .../newtab/data/content/tippytop/favicons/wix.ico | Bin 0 -> 1061 bytes .../data/content/tippytop/favicons/wykop-pl.png | Bin 0 -> 1705 bytes .../data/content/tippytop/favicons/yandex-com.png | Bin 0 -> 1338 bytes .../data/content/tippytop/favicons/yandex-ru.png | Bin 0 -> 1368 bytes .../data/content/tippytop/favicons/youtube-com.png | Bin 0 -> 348 bytes .../data/content/tippytop/favicons/zhihu-com.ico | Bin 0 -> 6518 bytes .../data/content/tippytop/images/adidas@2x.png | Bin 0 -> 5448 bytes .../content/tippytop/images/aliexpress-com@2x.png | Bin 0 -> 12459 bytes .../data/content/tippytop/images/allegro-pl@2x.png | Bin 0 -> 5041 bytes .../data/content/tippytop/images/amazon@2x.png | Bin 0 -> 6061 bytes .../data/content/tippytop/images/avito-ru@2x.png | Bin 0 -> 1568 bytes .../data/content/tippytop/images/baidu-com@2x.png | Bin 0 -> 8198 bytes .../data/content/tippytop/images/bbc-uk@2x.png | Bin 0 -> 18207 bytes .../data/content/tippytop/images/bing-com@2x.svg | 106 + .../data/content/tippytop/images/ctrip-com@2x.png | Bin 0 -> 15862 bytes .../content/tippytop/images/duckduckgo-com@2x.svg | 12 + .../data/content/tippytop/images/ebay@2x.png | Bin 0 -> 5361 bytes .../data/content/tippytop/images/etsy@2x.jpg | Bin 0 -> 4094 bytes .../content/tippytop/images/facebook-com@2x.png | Bin 0 -> 10780 bytes .../data/content/tippytop/images/geico@2x.jpg | Bin 0 -> 11834 bytes .../data/content/tippytop/images/google-com@2x.png | Bin 0 -> 3035 bytes .../data/content/tippytop/images/hrblock@2x.png | Bin 0 -> 4642 bytes .../data/content/tippytop/images/ifeng-com@2x.png | Bin 0 -> 22282 bytes .../data/content/tippytop/images/iqiyi-com@2x.png | Bin 0 -> 14340 bytes .../content/tippytop/images/leboncoin-fr@2x.png | Bin 0 -> 7146 bytes .../data/content/tippytop/images/nike@2x.jpg | Bin 0 -> 5163 bytes .../data/content/tippytop/images/ok-ru@2x.png | Bin 0 -> 2526 bytes .../data/content/tippytop/images/olx-pl@2x.png | Bin 0 -> 5287 bytes .../data/content/tippytop/images/reddit-com@2x.png | Bin 0 -> 5180 bytes .../data/content/tippytop/images/samsung@2x.jpg | Bin 0 -> 3347 bytes .../data/content/tippytop/images/turbotax@2x.jpg | Bin 0 -> 11930 bytes .../content/tippytop/images/twitter-com@2x.png | Bin 0 -> 1260 bytes .../data/content/tippytop/images/vk-com@2x.png | Bin 0 -> 9897 bytes .../data/content/tippytop/images/vodafone@2x.jpg | Bin 0 -> 7050 bytes .../data/content/tippytop/images/weibo-com@2x.png | Bin 0 -> 15507 bytes .../content/tippytop/images/wikipedia-org@2x.png | Bin 0 -> 19001 bytes .../newtab/data/content/tippytop/images/wix@2x.jpg | Bin 0 -> 8714 bytes .../data/content/tippytop/images/wykop-pl@2x.png | Bin 0 -> 4415 bytes .../data/content/tippytop/images/yandex-com@2x.png | Bin 0 -> 6516 bytes .../data/content/tippytop/images/yandex-ru@2x.png | Bin 0 -> 6638 bytes .../content/tippytop/images/youtube-com@2x.png | Bin 0 -> 2924 bytes .../data/content/tippytop/images/zhihu-com@2x.png | Bin 0 -> 10225 bytes .../newtab/data/content/tippytop/top_sites.json | 182 + browser/components/newtab/docs/index.rst | 119 + .../v2-system-addon/about_home_startup_cache.md | 86 + .../newtab/docs/v2-system-addon/data_events.md | 19 + .../newtab/docs/v2-system-addon/geo_locale.md | 23 + .../newtab/docs/v2-system-addon/mochitests.md | 26 + .../newtab/docs/v2-system-addon/preferences.md | 270 + .../newtab/docs/v2-system-addon/sections.md | 82 + .../newtab/docs/v2-system-addon/telemetry.md | 10 + .../newtab/docs/v2-system-addon/tippytop.md | 40 + .../docs/v2-system-addon/unit_testing_guide.md | 149 + browser/components/newtab/jar.mn | 40 + browser/components/newtab/karma.mc.config.js | 287 + .../components/newtab/lib/AboutPreferences.sys.mjs | 298 + .../components/newtab/lib/ActivityStream.sys.mjs | 700 + .../lib/ActivityStreamMessageChannel.sys.mjs | 333 + .../newtab/lib/ActivityStreamPrefs.sys.mjs | 100 + .../newtab/lib/ActivityStreamStorage.sys.mjs | 119 + browser/components/newtab/lib/DefaultSites.sys.mjs | 46 + .../newtab/lib/DiscoveryStreamFeed.sys.mjs | 2265 ++ .../components/newtab/lib/DownloadsManager.sys.mjs | 188 + browser/components/newtab/lib/FaviconFeed.sys.mjs | 198 + browser/components/newtab/lib/FilterAdult.sys.mjs | 3040 +++ .../components/newtab/lib/HighlightsFeed.sys.mjs | 322 + browser/components/newtab/lib/LinksCache.sys.mjs | 133 + browser/components/newtab/lib/NewTabInit.sys.mjs | 55 + .../components/newtab/lib/PersistentCache.sys.mjs | 90 + .../PersonalityProvider/NaiveBayesTextTagger.mjs | 60 + .../lib/PersonalityProvider/NmfTextTagger.mjs | 58 + .../PersonalityProvider.sys.mjs | 277 + .../PersonalityProvider.worker.mjs | 26 + .../PersonalityProviderWorkerClass.mjs | 306 + .../lib/PersonalityProvider/RecipeExecutor.mjs | 1119 + .../newtab/lib/PersonalityProvider/Tokenize.mjs | 83 + browser/components/newtab/lib/PlacesFeed.sys.mjs | 572 + browser/components/newtab/lib/PrefsFeed.sys.mjs | 273 + .../newtab/lib/RecommendationProvider.sys.mjs | 291 + browser/components/newtab/lib/Screenshots.sys.mjs | 140 + .../components/newtab/lib/SearchShortcuts.sys.mjs | 73 + .../components/newtab/lib/SectionsManager.sys.mjs | 715 + browser/components/newtab/lib/ShortURL.sys.mjs | 88 + .../components/newtab/lib/SiteClassifier.sys.mjs | 103 + browser/components/newtab/lib/Store.sys.mjs | 188 + .../components/newtab/lib/SystemTickFeed.sys.mjs | 70 + .../components/newtab/lib/TelemetryFeed.sys.mjs | 1122 + .../components/newtab/lib/TippyTopProvider.sys.mjs | 60 + browser/components/newtab/lib/TopSitesFeed.sys.mjs | 2007 ++ .../components/newtab/lib/TopStoriesFeed.sys.mjs | 731 + .../components/newtab/lib/UTEventReporting.sys.mjs | 62 + browser/components/newtab/lib/cache.worker.js | 203 + browser/components/newtab/loaders/inject-loader.js | 59 + browser/components/newtab/metrics.yaml | 1589 ++ browser/components/newtab/moz.build | 35 + .../components/newtab/nsIAboutNewTabService.idl | 39 + browser/components/newtab/package-lock.json | 12455 +++++++++ browser/components/newtab/package.json | 115 + browser/components/newtab/pings.yaml | 75 + .../newtab/prerendered/activity-stream-debug.html | 55 + .../prerendered/activity-stream-noscripts.html | 44 + .../newtab/prerendered/activity-stream.html | 55 + .../test/browser/abouthomecache/browser.toml | 52 + .../abouthomecache/browser_basic_endtoend.js | 22 + .../browser/abouthomecache/browser_bump_version.js | 35 + .../browser/abouthomecache/browser_disabled.js | 97 + .../browser_experiments_api_control.js | 63 + .../abouthomecache/browser_locale_change.js | 30 + .../browser/abouthomecache/browser_no_cache.js | 27 + .../browser_no_cache_on_SessionStartup_restore.js | 37 + .../abouthomecache/browser_no_startup_actions.js | 83 + .../abouthomecache/browser_overwrite_cache.js | 38 + .../abouthomecache/browser_process_crash.js | 82 + .../abouthomecache/browser_same_consumer.js | 52 + .../browser/abouthomecache/browser_sanitize.js | 54 + .../abouthomecache/browser_shutdown_timeout.js | 45 + .../newtab/test/browser/abouthomecache/head.js | 365 + .../newtab/test/browser/annotation_first.html | 2 + .../newtab/test/browser/annotation_second.html | 2 + .../newtab/test/browser/annotation_third.html | 2 + .../components/newtab/test/browser/blue_page.html | 6 + .../components/newtab/test/browser/browser.toml | 81 + .../test/browser/browser_as_load_location.js | 44 + .../newtab/test/browser/browser_as_render.js | 83 + .../test/browser/browser_context_menu_item.js | 18 + .../test/browser/browser_customize_menu_content.js | 219 + .../test/browser/browser_customize_menu_render.js | 28 + .../newtab/test/browser/browser_discovery_card.js | 49 + .../test/browser/browser_discovery_render.js | 31 + .../test/browser/browser_enabled_newtabpage.js | 33 + .../test/browser/browser_foxdoodle_set_default.js | 69 + .../newtab/test/browser/browser_getScreenshots.js | 88 + .../test/browser/browser_highlights_section.js | 96 + .../test/browser/browser_multistage_spotlight.js | 90 + .../browser_multistage_spotlight_telemetry.js | 141 + .../newtab/test/browser/browser_newtab_glean.js | 28 + .../newtab/test/browser/browser_newtab_header.js | 76 + .../test/browser/browser_newtab_last_LinkMenu.js | 153 + .../test/browser/browser_newtab_overrides.js | 134 + .../newtab/test/browser/browser_newtab_ping.js | 216 + .../newtab/test/browser/browser_newtab_towindow.js | 45 + .../newtab/test/browser/browser_newtab_trigger.js | 50 + .../newtab/test/browser/browser_open_tab_focus.js | 38 + .../newtab/test/browser/browser_remote_l10n.js | 56 + .../test/browser/browser_topsites_annotation.js | 980 + .../browser_topsites_contextMenu_options.js | 126 + .../test/browser/browser_topsites_section.js | 304 + .../test/browser/browser_trigger_messagesLoaded.js | 153 + .../components/newtab/test/browser/file_pdf.PDF | 12 + browser/components/newtab/test/browser/head.js | 244 + .../components/newtab/test/browser/red_page.html | 6 + .../components/newtab/test/browser/redirect_to.sjs | 9 + .../components/newtab/test/browser/topstories.json | 11 + .../test/schemas/asrouter_event_ping.schema.json | 36 + .../newtab/test/schemas/base_ping.schema.json | 29 + browser/components/newtab/test/schemas/pings.js | 181 + .../newtab/test/schemas/session_ping.schema.json | 122 + .../test/schemas/user_event_ping.schema.json | 75 + .../newtab/test/unit/common/Actions.test.js | 236 + .../newtab/test/unit/common/Dedupe.test.js | 38 + .../newtab/test/unit/common/Reducers.test.js | 1525 ++ .../test/unit/content-src/components/Base.test.jsx | 130 + .../test/unit/content-src/components/Card.test.jsx | 510 + .../components/CollapsibleSection.test.jsx | 67 + .../components/ComponentPerfTimer.test.jsx | 447 + .../content-src/components/ConfirmDialog.test.jsx | 182 + .../content-src/components/ContextMenu.test.jsx | 227 + .../content-src/components/CustomiseMenu.test.jsx | 72 + .../components/DiscoveryStreamAdmin.test.jsx | 267 + .../components/DiscoveryStreamBase.test.jsx | 313 + .../DiscoveryStreamComponents/CardGrid.test.jsx | 354 + .../CollectionCardGrid.test.jsx | 138 + .../DiscoveryStreamComponents/DSCard.test.jsx | 582 + .../DSContextFooter.test.jsx | 138 + .../DiscoveryStreamComponents/DSDismiss.test.jsx | 51 + .../DSEmptyState.test.jsx | 73 + .../DiscoveryStreamComponents/DSImage.test.jsx | 146 + .../DiscoveryStreamComponents/DSLinkMenu.test.jsx | 151 + .../DiscoveryStreamComponents/DSMessage.test.jsx | 57 + .../DSPrivacyModal.test.jsx | 50 + .../DiscoveryStreamComponents/DSSignup.test.jsx | 92 + .../DiscoveryStreamComponents/DSTextPromo.test.jsx | 96 + .../DiscoveryStreamComponents/Highlights.test.jsx | 41 + .../HorizontalRule.test.jsx | 16 + .../ImpressionStats.test.jsx | 276 + .../DiscoveryStreamComponents/Navigation.test.jsx | 131 + .../DiscoveryStreamComponents/PrivacyLink.test.jsx | 29 + .../DiscoveryStreamComponents/SafeAnchor.test.jsx | 56 + .../SectionTitle.test.jsx | 22 + .../TopicsWidget.test.jsx | 238 + .../content-src/components/ErrorBoundary.test.jsx | 110 + .../content-src/components/FluentOrText.test.jsx | 68 + .../unit/content-src/components/LinkMenu.test.jsx | 667 + .../components/MoreRecommendations.test.jsx | 24 + .../components/PocketLoggedInCta.test.jsx | 46 + .../unit/content-src/components/Search.test.jsx | 179 + .../unit/content-src/components/Sections.test.jsx | 600 + .../unit/content-src/components/TopSites.test.jsx | 1930 ++ .../TopSites/SearchShortcutsForm.test.jsx | 56 + .../TopSites/TopSiteImpressionWrapper.test.jsx | 148 + .../unit/content-src/components/Topics.test.jsx | 22 + .../lib/detect-user-session-start.test.js | 120 + .../test/unit/content-src/lib/init-store.test.js | 155 + .../test/unit/content-src/lib/perf-service.test.js | 89 + .../unit/content-src/lib/screenshot-utils.test.js | 147 + .../content-src/lib/selectLayoutRender.test.js | 576 + .../newtab/test/unit/lib/AboutPreferences.test.js | 429 + .../newtab/test/unit/lib/ActivityStream.test.js | 576 + .../unit/lib/ActivityStreamMessageChannel.test.js | 445 + .../test/unit/lib/ActivityStreamPrefs.test.js | 113 + .../test/unit/lib/ActivityStreamStorage.test.js | 161 + .../test/unit/lib/DiscoveryStreamFeed.test.js | 3523 +++ .../newtab/test/unit/lib/DownloadsManager.test.js | 373 + .../newtab/test/unit/lib/FaviconFeed.test.js | 233 + .../newtab/test/unit/lib/FilterAdult.test.js | 112 + .../newtab/test/unit/lib/LinksCache.test.js | 16 + .../newtab/test/unit/lib/NewTabInit.test.js | 81 + .../newtab/test/unit/lib/PersistentCache.test.js | 142 + .../NaiveBayesTextTagger.test.js | 95 + .../lib/PersonalityProvider/NmfTextTagger.test.js | 479 + .../PersonalityProvider.test.js | 356 + .../PersonalityProviderWorkerClass.test.js | 456 + .../lib/PersonalityProvider/RecipeExecutor.test.js | 1543 ++ .../unit/lib/PersonalityProvider/Tokenize.test.js | 134 + .../newtab/test/unit/lib/PrefsFeed.test.js | 357 + .../test/unit/lib/RecommendationProvider.test.js | 331 + .../newtab/test/unit/lib/Screenshots.test.js | 209 + .../newtab/test/unit/lib/SectionsManager.test.js | 897 + .../newtab/test/unit/lib/ShortUrl.test.js | 104 + .../newtab/test/unit/lib/SiteClassifier.test.js | 252 + .../newtab/test/unit/lib/SystemTickFeed.test.js | 79 + .../newtab/test/unit/lib/TippyTopProvider.test.js | 121 + .../newtab/test/unit/lib/UTEventReporting.test.js | 115 + browser/components/newtab/test/unit/unit-entry.js | 733 + browser/components/newtab/test/unit/utils.js | 406 + .../xpcshell/test_AboutHomeStartupCacheChild.js | 33 + .../xpcshell/test_AboutHomeStartupCacheWorker.js | 255 + .../newtab/test/xpcshell/test_AboutNewTab.js | 363 + .../test/xpcshell/test_AboutWelcomeAttribution.js | 69 + .../test/xpcshell/test_AboutWelcomeTelemetry.js | 90 + .../xpcshell/test_AboutWelcomeTelemetry_glean.js | 238 + .../newtab/test/xpcshell/test_HighlightsFeed.js | 1402 ++ .../newtab/test/xpcshell/test_PlacesFeed.js | 1812 ++ .../components/newtab/test/xpcshell/test_Store.js | 453 + .../newtab/test/xpcshell/test_TelemetryFeed.js | 3285 +++ .../newtab/test/xpcshell/test_TopSitesFeed.js | 3397 +++ .../test/xpcshell/test_TopSitesFeed_glean.js | 2023 ++ .../newtab/test/xpcshell/topstories.json | 53 + .../components/newtab/test/xpcshell/xpcshell.toml | 34 + .../components/newtab/tools/resourceUriPlugin.js | 65 + .../components/newtab/vendor/PROP_TYPES_LICENSE | 21 + .../newtab/vendor/REACT_AND_REACT_DOM_LICENSE | 21 + .../components/newtab/vendor/REACT_REDUX_LICENSE | 21 + .../newtab/vendor/REACT_TRANSITION_GROUP_LICENSE | 30 + browser/components/newtab/vendor/REDUX_LICENSE | 21 + browser/components/newtab/vendor/Redux.sys.mjs | 691 + browser/components/newtab/vendor/prop-types.js | 1 + browser/components/newtab/vendor/react-dev.js | 3318 +++ browser/components/newtab/vendor/react-dom-dev.js | 25147 +++++++++++++++++++ .../components/newtab/vendor/react-dom-server.js | 45 + browser/components/newtab/vendor/react-dom.js | 239 + browser/components/newtab/vendor/react-redux.js | 1 + .../newtab/vendor/react-transition-group.js | 9 + browser/components/newtab/vendor/react.js | 32 + browser/components/newtab/vendor/redux.js | 948 + .../newtab/webpack.system-addon.config.js | 65 + browser/components/newtab/yamscripts.yml | 60 + 440 files changed, 146464 insertions(+) create mode 100644 browser/components/newtab/.eslintrc.js create mode 100644 browser/components/newtab/.nvmrc create mode 100644 browser/components/newtab/AboutNewTabService.sys.mjs create mode 100644 browser/components/newtab/bin/render-activity-stream-html.js create mode 100644 browser/components/newtab/bin/try-runner.js create mode 100644 browser/components/newtab/bin/vendor.js create mode 100644 browser/components/newtab/common/Actions.sys.mjs create mode 100644 browser/components/newtab/common/Dedupe.sys.mjs create mode 100644 browser/components/newtab/common/Reducers.sys.mjs create mode 100644 browser/components/newtab/components.conf create mode 100644 browser/components/newtab/components/CustomElements/paragraph.js create mode 100644 browser/components/newtab/content-src/activity-stream.jsx create mode 100644 browser/components/newtab/content-src/components/A11yLinkButton/A11yLinkButton.jsx create mode 100644 browser/components/newtab/content-src/components/A11yLinkButton/_A11yLinkButton.scss create mode 100644 browser/components/newtab/content-src/components/Base/Base.jsx create mode 100644 browser/components/newtab/content-src/components/Base/_Base.scss create mode 100644 browser/components/newtab/content-src/components/Card/Card.jsx create mode 100644 browser/components/newtab/content-src/components/Card/_Card.scss create mode 100644 browser/components/newtab/content-src/components/Card/types.js create mode 100644 browser/components/newtab/content-src/components/CollapsibleSection/CollapsibleSection.jsx create mode 100644 browser/components/newtab/content-src/components/CollapsibleSection/_CollapsibleSection.scss create mode 100644 browser/components/newtab/content-src/components/ComponentPerfTimer/ComponentPerfTimer.jsx create mode 100644 browser/components/newtab/content-src/components/ConfirmDialog/ConfirmDialog.jsx create mode 100644 browser/components/newtab/content-src/components/ConfirmDialog/_ConfirmDialog.scss create mode 100644 browser/components/newtab/content-src/components/ContextMenu/ContextMenu.jsx create mode 100644 browser/components/newtab/content-src/components/ContextMenu/ContextMenuButton.jsx create mode 100644 browser/components/newtab/content-src/components/ContextMenu/_ContextMenu.scss create mode 100644 browser/components/newtab/content-src/components/CustomizeMenu/BackgroundsSection/BackgroundsSection.jsx create mode 100644 browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx create mode 100644 browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx create mode 100644 browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamAdmin/SimpleHashRouter.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamBase/_DiscoveryStreamBase.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/_CardGrid.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/CollectionCardGrid/CollectionCardGrid.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/CollectionCardGrid/_CollectionCardGrid.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/_DSContextFooter.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSDismiss/DSDismiss.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSDismiss/_DSDismiss.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSEmptyState/_DSEmptyState.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSImage/DSImage.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSImage/_DSImage.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/_DSLinkMenu.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSMessage/DSMessage.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSMessage/_DSMessage.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSPrivacyModal/_DSPrivacyModal.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSSignup/DSSignup.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSSignup/DSSignup.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/_DSTextPromo.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/Highlights/Highlights.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/Highlights/_Highlights.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/HorizontalRule/HorizontalRule.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/HorizontalRule/_HorizontalRule.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/Navigation/Navigation.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/Navigation/_Navigation.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/PrivacyLink/PrivacyLink.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/PrivacyLink/_PrivacyLink.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/SectionTitle/SectionTitle.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/SectionTitle/_SectionTitle.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/TopSites/_TopSites.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamComponents/TopicsWidget/_TopicsWidget.scss create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.jsx create mode 100644 browser/components/newtab/content-src/components/DiscoveryStreamImpressionStats/_ImpressionStats.scss create mode 100644 browser/components/newtab/content-src/components/ErrorBoundary/ErrorBoundary.jsx create mode 100644 browser/components/newtab/content-src/components/ErrorBoundary/_ErrorBoundary.scss create mode 100644 browser/components/newtab/content-src/components/FluentOrText/FluentOrText.jsx create mode 100644 browser/components/newtab/content-src/components/LinkMenu/LinkMenu.jsx create mode 100644 browser/components/newtab/content-src/components/ModalOverlay/ModalOverlay.jsx create mode 100644 browser/components/newtab/content-src/components/ModalOverlay/_ModalOverlay.scss create mode 100644 browser/components/newtab/content-src/components/MoreRecommendations/MoreRecommendations.jsx create mode 100644 browser/components/newtab/content-src/components/MoreRecommendations/_MoreRecommendations.scss create mode 100644 browser/components/newtab/content-src/components/PocketLoggedInCta/PocketLoggedInCta.jsx create mode 100644 browser/components/newtab/content-src/components/PocketLoggedInCta/_PocketLoggedInCta.scss create mode 100644 browser/components/newtab/content-src/components/Search/Search.jsx create mode 100644 browser/components/newtab/content-src/components/Search/_Search.scss create mode 100644 browser/components/newtab/content-src/components/Sections/Sections.jsx create mode 100644 browser/components/newtab/content-src/components/Sections/_Sections.scss create mode 100644 browser/components/newtab/content-src/components/TopSites/SearchShortcutsForm.jsx create mode 100644 browser/components/newtab/content-src/components/TopSites/TopSite.jsx create mode 100644 browser/components/newtab/content-src/components/TopSites/TopSiteForm.jsx create mode 100644 browser/components/newtab/content-src/components/TopSites/TopSiteFormInput.jsx create mode 100644 browser/components/newtab/content-src/components/TopSites/TopSiteImpressionWrapper.jsx create mode 100644 browser/components/newtab/content-src/components/TopSites/TopSites.jsx create mode 100644 browser/components/newtab/content-src/components/TopSites/TopSitesConstants.js create mode 100644 browser/components/newtab/content-src/components/TopSites/_TopSites.scss create mode 100644 browser/components/newtab/content-src/components/Topics/Topics.jsx create mode 100644 browser/components/newtab/content-src/components/Topics/_Topics.scss create mode 100644 browser/components/newtab/content-src/lib/constants.js create mode 100644 browser/components/newtab/content-src/lib/detect-user-session-start.js create mode 100644 browser/components/newtab/content-src/lib/init-store.js create mode 100644 browser/components/newtab/content-src/lib/link-menu-options.js create mode 100644 browser/components/newtab/content-src/lib/perf-service.js create mode 100644 browser/components/newtab/content-src/lib/screenshot-utils.js create mode 100644 browser/components/newtab/content-src/lib/selectLayoutRender.js create mode 100644 browser/components/newtab/content-src/styles/_activity-stream.scss create mode 100644 browser/components/newtab/content-src/styles/_icons.scss create mode 100644 browser/components/newtab/content-src/styles/_mixins.scss create mode 100644 browser/components/newtab/content-src/styles/_normalize.scss create mode 100644 browser/components/newtab/content-src/styles/_theme.scss create mode 100644 browser/components/newtab/content-src/styles/_variables.scss create mode 100644 browser/components/newtab/content-src/styles/activity-stream-linux.scss create mode 100644 browser/components/newtab/content-src/styles/activity-stream-mac.scss create mode 100644 browser/components/newtab/content-src/styles/activity-stream-windows.scss create mode 100644 browser/components/newtab/css/activity-stream-linux.css create mode 100644 browser/components/newtab/css/activity-stream-mac.css create mode 100644 browser/components/newtab/css/activity-stream-windows.css create mode 100644 browser/components/newtab/data/content/abouthomecache/page.html.template create mode 100644 browser/components/newtab/data/content/abouthomecache/script.js.template create mode 100644 browser/components/newtab/data/content/activity-stream.bundle.js create mode 100644 browser/components/newtab/data/content/assets/firefox.svg create mode 100644 browser/components/newtab/data/content/assets/glyph-cfr-feature-16.svg create mode 100644 browser/components/newtab/data/content/assets/glyph-mail-16.svg create mode 100644 browser/components/newtab/data/content/assets/glyph-maximize-16.svg create mode 100644 browser/components/newtab/data/content/assets/glyph-minimize-16.svg create mode 100644 browser/components/newtab/data/content/assets/glyph-modal-delete-20.svg create mode 100644 browser/components/newtab/data/content/assets/glyph-newWindow-16.svg create mode 100644 browser/components/newtab/data/content/assets/glyph-open-file-16.svg create mode 100644 browser/components/newtab/data/content/assets/glyph-pin-16.svg create mode 100644 browser/components/newtab/data/content/assets/glyph-pocket-archive-16.svg create mode 100644 browser/components/newtab/data/content/assets/glyph-pocket-delete-16.svg create mode 100644 browser/components/newtab/data/content/assets/glyph-unpin-16.svg create mode 100644 browser/components/newtab/data/content/assets/glyph-webextension-16.svg create mode 100644 browser/components/newtab/data/content/assets/icon-removed-bookmark.svg create mode 100644 browser/components/newtab/data/content/assets/pocket-onboarding.avif create mode 100644 browser/components/newtab/data/content/assets/pocket-onboarding@2x.avif create mode 100644 browser/components/newtab/data/content/assets/pocket-swoosh.svg create mode 100644 browser/components/newtab/data/content/assets/remote/mountain.svg create mode 100644 browser/components/newtab/data/content/assets/remote/umbrella.png create mode 100644 browser/components/newtab/data/content/assets/spinner.svg create mode 100644 browser/components/newtab/data/content/newtab-render.js create mode 100644 browser/components/newtab/data/content/tippytop/favicons/adidas.png create mode 100644 browser/components/newtab/data/content/tippytop/favicons/aliexpress-com.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/allegro-pl.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/amazon.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/avito-ru.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/baidu-com.png create mode 100644 browser/components/newtab/data/content/tippytop/favicons/bbc-uk.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/bing-com.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/ctrip-com.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/duckduckgo-com.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/ebay.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/etsy.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/facebook-com.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/geico.png create mode 100644 browser/components/newtab/data/content/tippytop/favicons/google-com.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/hrblock.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/ifeng-com.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/iqiyi-com.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/leboncoin-fr.png create mode 100644 browser/components/newtab/data/content/tippytop/favicons/nike.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/ok-ru.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/olx-pl.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/reddit-com.png create mode 100644 browser/components/newtab/data/content/tippytop/favicons/samsung.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/turbotax.png create mode 100644 browser/components/newtab/data/content/tippytop/favicons/twitter-com.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/vk-com.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/vodafone.png create mode 100644 browser/components/newtab/data/content/tippytop/favicons/weibo-com.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/wikipedia-org.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/wix.ico create mode 100644 browser/components/newtab/data/content/tippytop/favicons/wykop-pl.png create mode 100644 browser/components/newtab/data/content/tippytop/favicons/yandex-com.png create mode 100644 browser/components/newtab/data/content/tippytop/favicons/yandex-ru.png create mode 100644 browser/components/newtab/data/content/tippytop/favicons/youtube-com.png create mode 100644 browser/components/newtab/data/content/tippytop/favicons/zhihu-com.ico create mode 100644 browser/components/newtab/data/content/tippytop/images/adidas@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/aliexpress-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/allegro-pl@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/amazon@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/avito-ru@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/baidu-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/bbc-uk@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/bing-com@2x.svg create mode 100644 browser/components/newtab/data/content/tippytop/images/ctrip-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/duckduckgo-com@2x.svg create mode 100644 browser/components/newtab/data/content/tippytop/images/ebay@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/etsy@2x.jpg create mode 100644 browser/components/newtab/data/content/tippytop/images/facebook-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/geico@2x.jpg create mode 100644 browser/components/newtab/data/content/tippytop/images/google-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/hrblock@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/ifeng-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/iqiyi-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/leboncoin-fr@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/nike@2x.jpg create mode 100644 browser/components/newtab/data/content/tippytop/images/ok-ru@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/olx-pl@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/reddit-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/samsung@2x.jpg create mode 100644 browser/components/newtab/data/content/tippytop/images/turbotax@2x.jpg create mode 100644 browser/components/newtab/data/content/tippytop/images/twitter-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/vk-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/vodafone@2x.jpg create mode 100644 browser/components/newtab/data/content/tippytop/images/weibo-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/wikipedia-org@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/wix@2x.jpg create mode 100644 browser/components/newtab/data/content/tippytop/images/wykop-pl@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/yandex-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/yandex-ru@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/youtube-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/images/zhihu-com@2x.png create mode 100644 browser/components/newtab/data/content/tippytop/top_sites.json create mode 100644 browser/components/newtab/docs/index.rst create mode 100644 browser/components/newtab/docs/v2-system-addon/about_home_startup_cache.md create mode 100644 browser/components/newtab/docs/v2-system-addon/data_events.md create mode 100644 browser/components/newtab/docs/v2-system-addon/geo_locale.md create mode 100644 browser/components/newtab/docs/v2-system-addon/mochitests.md create mode 100644 browser/components/newtab/docs/v2-system-addon/preferences.md create mode 100644 browser/components/newtab/docs/v2-system-addon/sections.md create mode 100644 browser/components/newtab/docs/v2-system-addon/telemetry.md create mode 100644 browser/components/newtab/docs/v2-system-addon/tippytop.md create mode 100644 browser/components/newtab/docs/v2-system-addon/unit_testing_guide.md create mode 100644 browser/components/newtab/jar.mn create mode 100644 browser/components/newtab/karma.mc.config.js create mode 100644 browser/components/newtab/lib/AboutPreferences.sys.mjs create mode 100644 browser/components/newtab/lib/ActivityStream.sys.mjs create mode 100644 browser/components/newtab/lib/ActivityStreamMessageChannel.sys.mjs create mode 100644 browser/components/newtab/lib/ActivityStreamPrefs.sys.mjs create mode 100644 browser/components/newtab/lib/ActivityStreamStorage.sys.mjs create mode 100644 browser/components/newtab/lib/DefaultSites.sys.mjs create mode 100644 browser/components/newtab/lib/DiscoveryStreamFeed.sys.mjs create mode 100644 browser/components/newtab/lib/DownloadsManager.sys.mjs create mode 100644 browser/components/newtab/lib/FaviconFeed.sys.mjs create mode 100644 browser/components/newtab/lib/FilterAdult.sys.mjs create mode 100644 browser/components/newtab/lib/HighlightsFeed.sys.mjs create mode 100644 browser/components/newtab/lib/LinksCache.sys.mjs create mode 100644 browser/components/newtab/lib/NewTabInit.sys.mjs create mode 100644 browser/components/newtab/lib/PersistentCache.sys.mjs create mode 100644 browser/components/newtab/lib/PersonalityProvider/NaiveBayesTextTagger.mjs create mode 100644 browser/components/newtab/lib/PersonalityProvider/NmfTextTagger.mjs create mode 100644 browser/components/newtab/lib/PersonalityProvider/PersonalityProvider.sys.mjs create mode 100644 browser/components/newtab/lib/PersonalityProvider/PersonalityProvider.worker.mjs create mode 100644 browser/components/newtab/lib/PersonalityProvider/PersonalityProviderWorkerClass.mjs create mode 100644 browser/components/newtab/lib/PersonalityProvider/RecipeExecutor.mjs create mode 100644 browser/components/newtab/lib/PersonalityProvider/Tokenize.mjs create mode 100644 browser/components/newtab/lib/PlacesFeed.sys.mjs create mode 100644 browser/components/newtab/lib/PrefsFeed.sys.mjs create mode 100644 browser/components/newtab/lib/RecommendationProvider.sys.mjs create mode 100644 browser/components/newtab/lib/Screenshots.sys.mjs create mode 100644 browser/components/newtab/lib/SearchShortcuts.sys.mjs create mode 100644 browser/components/newtab/lib/SectionsManager.sys.mjs create mode 100644 browser/components/newtab/lib/ShortURL.sys.mjs create mode 100644 browser/components/newtab/lib/SiteClassifier.sys.mjs create mode 100644 browser/components/newtab/lib/Store.sys.mjs create mode 100644 browser/components/newtab/lib/SystemTickFeed.sys.mjs create mode 100644 browser/components/newtab/lib/TelemetryFeed.sys.mjs create mode 100644 browser/components/newtab/lib/TippyTopProvider.sys.mjs create mode 100644 browser/components/newtab/lib/TopSitesFeed.sys.mjs create mode 100644 browser/components/newtab/lib/TopStoriesFeed.sys.mjs create mode 100644 browser/components/newtab/lib/UTEventReporting.sys.mjs create mode 100644 browser/components/newtab/lib/cache.worker.js create mode 100644 browser/components/newtab/loaders/inject-loader.js create mode 100644 browser/components/newtab/metrics.yaml create mode 100644 browser/components/newtab/moz.build create mode 100644 browser/components/newtab/nsIAboutNewTabService.idl create mode 100644 browser/components/newtab/package-lock.json create mode 100644 browser/components/newtab/package.json create mode 100644 browser/components/newtab/pings.yaml create mode 100644 browser/components/newtab/prerendered/activity-stream-debug.html create mode 100644 browser/components/newtab/prerendered/activity-stream-noscripts.html create mode 100644 browser/components/newtab/prerendered/activity-stream.html create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser.toml create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_basic_endtoend.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_bump_version.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_disabled.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_experiments_api_control.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_locale_change.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_no_cache.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_no_cache_on_SessionStartup_restore.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_no_startup_actions.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_overwrite_cache.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_process_crash.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_same_consumer.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_sanitize.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/browser_shutdown_timeout.js create mode 100644 browser/components/newtab/test/browser/abouthomecache/head.js create mode 100644 browser/components/newtab/test/browser/annotation_first.html create mode 100644 browser/components/newtab/test/browser/annotation_second.html create mode 100644 browser/components/newtab/test/browser/annotation_third.html create mode 100644 browser/components/newtab/test/browser/blue_page.html create mode 100644 browser/components/newtab/test/browser/browser.toml create mode 100644 browser/components/newtab/test/browser/browser_as_load_location.js create mode 100644 browser/components/newtab/test/browser/browser_as_render.js create mode 100644 browser/components/newtab/test/browser/browser_context_menu_item.js create mode 100644 browser/components/newtab/test/browser/browser_customize_menu_content.js create mode 100644 browser/components/newtab/test/browser/browser_customize_menu_render.js create mode 100644 browser/components/newtab/test/browser/browser_discovery_card.js create mode 100644 browser/components/newtab/test/browser/browser_discovery_render.js create mode 100644 browser/components/newtab/test/browser/browser_enabled_newtabpage.js create mode 100644 browser/components/newtab/test/browser/browser_foxdoodle_set_default.js create mode 100644 browser/components/newtab/test/browser/browser_getScreenshots.js create mode 100644 browser/components/newtab/test/browser/browser_highlights_section.js create mode 100644 browser/components/newtab/test/browser/browser_multistage_spotlight.js create mode 100644 browser/components/newtab/test/browser/browser_multistage_spotlight_telemetry.js create mode 100644 browser/components/newtab/test/browser/browser_newtab_glean.js create mode 100644 browser/components/newtab/test/browser/browser_newtab_header.js create mode 100644 browser/components/newtab/test/browser/browser_newtab_last_LinkMenu.js create mode 100644 browser/components/newtab/test/browser/browser_newtab_overrides.js create mode 100644 browser/components/newtab/test/browser/browser_newtab_ping.js create mode 100644 browser/components/newtab/test/browser/browser_newtab_towindow.js create mode 100644 browser/components/newtab/test/browser/browser_newtab_trigger.js create mode 100644 browser/components/newtab/test/browser/browser_open_tab_focus.js create mode 100644 browser/components/newtab/test/browser/browser_remote_l10n.js create mode 100644 browser/components/newtab/test/browser/browser_topsites_annotation.js create mode 100644 browser/components/newtab/test/browser/browser_topsites_contextMenu_options.js create mode 100644 browser/components/newtab/test/browser/browser_topsites_section.js create mode 100644 browser/components/newtab/test/browser/browser_trigger_messagesLoaded.js create mode 100644 browser/components/newtab/test/browser/file_pdf.PDF create mode 100644 browser/components/newtab/test/browser/head.js create mode 100644 browser/components/newtab/test/browser/red_page.html create mode 100644 browser/components/newtab/test/browser/redirect_to.sjs create mode 100644 browser/components/newtab/test/browser/topstories.json create mode 100644 browser/components/newtab/test/schemas/asrouter_event_ping.schema.json create mode 100644 browser/components/newtab/test/schemas/base_ping.schema.json create mode 100644 browser/components/newtab/test/schemas/pings.js create mode 100644 browser/components/newtab/test/schemas/session_ping.schema.json create mode 100644 browser/components/newtab/test/schemas/user_event_ping.schema.json create mode 100644 browser/components/newtab/test/unit/common/Actions.test.js create mode 100644 browser/components/newtab/test/unit/common/Dedupe.test.js create mode 100644 browser/components/newtab/test/unit/common/Reducers.test.js create mode 100644 browser/components/newtab/test/unit/content-src/components/Base.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/Card.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/CollapsibleSection.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/ComponentPerfTimer.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/ConfirmDialog.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/ContextMenu.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/CustomiseMenu.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamAdmin.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamBase.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CollectionCardGrid.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSDismiss.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSEmptyState.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSImage.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSLinkMenu.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSMessage.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSPrivacyModal.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSSignup.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSTextPromo.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/Highlights.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/HorizontalRule.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/Navigation.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/PrivacyLink.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/SafeAnchor.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/SectionTitle.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/TopicsWidget.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/ErrorBoundary.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/FluentOrText.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/LinkMenu.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/MoreRecommendations.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/PocketLoggedInCta.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/Search.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/Sections.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/TopSites.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/TopSites/SearchShortcutsForm.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/TopSites/TopSiteImpressionWrapper.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/components/Topics.test.jsx create mode 100644 browser/components/newtab/test/unit/content-src/lib/detect-user-session-start.test.js create mode 100644 browser/components/newtab/test/unit/content-src/lib/init-store.test.js create mode 100644 browser/components/newtab/test/unit/content-src/lib/perf-service.test.js create mode 100644 browser/components/newtab/test/unit/content-src/lib/screenshot-utils.test.js create mode 100644 browser/components/newtab/test/unit/content-src/lib/selectLayoutRender.test.js create mode 100644 browser/components/newtab/test/unit/lib/AboutPreferences.test.js create mode 100644 browser/components/newtab/test/unit/lib/ActivityStream.test.js create mode 100644 browser/components/newtab/test/unit/lib/ActivityStreamMessageChannel.test.js create mode 100644 browser/components/newtab/test/unit/lib/ActivityStreamPrefs.test.js create mode 100644 browser/components/newtab/test/unit/lib/ActivityStreamStorage.test.js create mode 100644 browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js create mode 100644 browser/components/newtab/test/unit/lib/DownloadsManager.test.js create mode 100644 browser/components/newtab/test/unit/lib/FaviconFeed.test.js create mode 100644 browser/components/newtab/test/unit/lib/FilterAdult.test.js create mode 100644 browser/components/newtab/test/unit/lib/LinksCache.test.js create mode 100644 browser/components/newtab/test/unit/lib/NewTabInit.test.js create mode 100644 browser/components/newtab/test/unit/lib/PersistentCache.test.js create mode 100644 browser/components/newtab/test/unit/lib/PersonalityProvider/NaiveBayesTextTagger.test.js create mode 100644 browser/components/newtab/test/unit/lib/PersonalityProvider/NmfTextTagger.test.js create mode 100644 browser/components/newtab/test/unit/lib/PersonalityProvider/PersonalityProvider.test.js create mode 100644 browser/components/newtab/test/unit/lib/PersonalityProvider/PersonalityProviderWorkerClass.test.js create mode 100644 browser/components/newtab/test/unit/lib/PersonalityProvider/RecipeExecutor.test.js create mode 100644 browser/components/newtab/test/unit/lib/PersonalityProvider/Tokenize.test.js create mode 100644 browser/components/newtab/test/unit/lib/PrefsFeed.test.js create mode 100644 browser/components/newtab/test/unit/lib/RecommendationProvider.test.js create mode 100644 browser/components/newtab/test/unit/lib/Screenshots.test.js create mode 100644 browser/components/newtab/test/unit/lib/SectionsManager.test.js create mode 100644 browser/components/newtab/test/unit/lib/ShortUrl.test.js create mode 100644 browser/components/newtab/test/unit/lib/SiteClassifier.test.js create mode 100644 browser/components/newtab/test/unit/lib/SystemTickFeed.test.js create mode 100644 browser/components/newtab/test/unit/lib/TippyTopProvider.test.js create mode 100644 browser/components/newtab/test/unit/lib/UTEventReporting.test.js create mode 100644 browser/components/newtab/test/unit/unit-entry.js create mode 100644 browser/components/newtab/test/unit/utils.js create mode 100644 browser/components/newtab/test/xpcshell/test_AboutHomeStartupCacheChild.js create mode 100644 browser/components/newtab/test/xpcshell/test_AboutHomeStartupCacheWorker.js create mode 100644 browser/components/newtab/test/xpcshell/test_AboutNewTab.js create mode 100644 browser/components/newtab/test/xpcshell/test_AboutWelcomeAttribution.js create mode 100644 browser/components/newtab/test/xpcshell/test_AboutWelcomeTelemetry.js create mode 100644 browser/components/newtab/test/xpcshell/test_AboutWelcomeTelemetry_glean.js create mode 100644 browser/components/newtab/test/xpcshell/test_HighlightsFeed.js create mode 100644 browser/components/newtab/test/xpcshell/test_PlacesFeed.js create mode 100644 browser/components/newtab/test/xpcshell/test_Store.js create mode 100644 browser/components/newtab/test/xpcshell/test_TelemetryFeed.js create mode 100644 browser/components/newtab/test/xpcshell/test_TopSitesFeed.js create mode 100644 browser/components/newtab/test/xpcshell/test_TopSitesFeed_glean.js create mode 100644 browser/components/newtab/test/xpcshell/topstories.json create mode 100644 browser/components/newtab/test/xpcshell/xpcshell.toml create mode 100644 browser/components/newtab/tools/resourceUriPlugin.js create mode 100644 browser/components/newtab/vendor/PROP_TYPES_LICENSE create mode 100644 browser/components/newtab/vendor/REACT_AND_REACT_DOM_LICENSE create mode 100644 browser/components/newtab/vendor/REACT_REDUX_LICENSE create mode 100644 browser/components/newtab/vendor/REACT_TRANSITION_GROUP_LICENSE create mode 100644 browser/components/newtab/vendor/REDUX_LICENSE create mode 100644 browser/components/newtab/vendor/Redux.sys.mjs create mode 100644 browser/components/newtab/vendor/prop-types.js create mode 100644 browser/components/newtab/vendor/react-dev.js create mode 100644 browser/components/newtab/vendor/react-dom-dev.js create mode 100644 browser/components/newtab/vendor/react-dom-server.js create mode 100644 browser/components/newtab/vendor/react-dom.js create mode 100644 browser/components/newtab/vendor/react-redux.js create mode 100644 browser/components/newtab/vendor/react-transition-group.js create mode 100644 browser/components/newtab/vendor/react.js create mode 100644 browser/components/newtab/vendor/redux.js create mode 100644 browser/components/newtab/webpack.system-addon.config.js create mode 100644 browser/components/newtab/yamscripts.yml (limited to 'browser/components/newtab') diff --git a/browser/components/newtab/.eslintrc.js b/browser/components/newtab/.eslintrc.js new file mode 100644 index 0000000000..5fc7a4dcff --- /dev/null +++ b/browser/components/newtab/.eslintrc.js @@ -0,0 +1,184 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +module.exports = { + // When adding items to this file please check for effects on sub-directories. + plugins: ["import", "react", "jsx-a11y"], + settings: { + react: { + version: "16.2.0", + }, + }, + extends: ["plugin:jsx-a11y/recommended"], + overrides: [ + { + // TODO: Bug 1773467 - Move these to .mjs or figure out a generic way + // to identify these as modules. + files: [ + "content-src/**/*.js", + "test/schemas/**/*.js", + "test/unit/**/*.js", + ], + parserOptions: { + sourceType: "module", + }, + }, + { + // These files use fluent-dom to insert content + files: [ + "content-src/asrouter/templates/OnboardingMessage/**", + "content-src/asrouter/templates/FirstRun/**", + "content-src/components/TopSites/**", + "content-src/components/MoreRecommendations/MoreRecommendations.jsx", + "content-src/components/CollapsibleSection/CollapsibleSection.jsx", + "content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState.jsx", + "content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx", + "content-src/components/CustomizeMenu/**", + ], + rules: { + "jsx-a11y/anchor-has-content": "off", + "jsx-a11y/heading-has-content": "off", + "jsx-a11y/label-has-associated-control": "off", + "jsx-a11y/no-onchange": "off", + }, + }, + { + files: [ + "bin/**", + "content-src/**", + "loaders/**", + "tools/**", + "test/unit/**", + ], + env: { + node: true, + }, + }, + { + // Use a configuration that's appropriate for modules, workers and + // non-production files. + files: ["*.jsm", "lib/cache.worker.js", "test/**"], + rules: { + "no-implicit-globals": "off", + }, + }, + { + files: ["content-src/**", "test/unit/**"], + rules: { + // Disallow commonjs in these directories. + "import/no-commonjs": 2, + }, + }, + { + // These tests simulate the browser environment. + files: "test/unit/**", + env: { + browser: true, + mocha: true, + }, + globals: { + assert: true, + chai: true, + sinon: true, + }, + }, + { + files: "test/**", + rules: { + "func-name-matching": 0, + "lines-between-class-members": 0, + }, + }, + ], + rules: { + "fetch-options/no-fetch-credentials": "error", + + "react/jsx-boolean-value": ["error", "always"], + "react/jsx-key": "error", + "react/jsx-no-bind": [ + "error", + { allowArrowFunctions: true, allowFunctions: true }, + ], + "react/jsx-no-comment-textnodes": "error", + "react/jsx-no-duplicate-props": "error", + "react/jsx-no-target-blank": "error", + "react/jsx-no-undef": "error", + "react/jsx-pascal-case": "error", + "react/jsx-uses-react": "error", + "react/jsx-uses-vars": "error", + "react/no-access-state-in-setstate": "error", + "react/no-danger": "error", + "react/no-deprecated": "error", + "react/no-did-mount-set-state": "error", + "react/no-did-update-set-state": "error", + "react/no-direct-mutation-state": "error", + "react/no-is-mounted": "error", + "react/no-unknown-property": "error", + "react/require-render-return": "error", + + "accessor-pairs": ["error", { setWithoutGet: true, getWithoutSet: false }], + "array-callback-return": "error", + "block-scoped-var": "error", + "consistent-this": ["error", "use-bind"], + eqeqeq: "error", + "func-name-matching": "error", + "getter-return": "error", + "guard-for-in": "error", + "max-depth": ["error", 4], + "max-nested-callbacks": ["error", 4], + "max-params": ["error", 6], + "max-statements": ["error", 50], + "new-cap": ["error", { newIsCap: true, capIsNew: false }], + "no-alert": "error", + "no-console": ["error", { allow: ["error"] }], + "no-div-regex": "error", + "no-duplicate-imports": "error", + "no-eq-null": "error", + "no-extend-native": "error", + "no-extra-label": "error", + "no-implicit-coercion": ["error", { allow: ["!!"] }], + "no-implicit-globals": "error", + "no-loop-func": "error", + "no-multi-assign": "error", + "no-multi-str": "error", + "no-new": "error", + "no-new-func": "error", + "no-octal-escape": "error", + "no-param-reassign": "error", + "no-proto": "error", + "no-prototype-builtins": "error", + "no-return-assign": ["error", "except-parens"], + "no-script-url": "error", + "no-shadow": "error", + "no-template-curly-in-string": "error", + "no-undef-init": "error", + "no-unmodified-loop-condition": "error", + "no-unused-expressions": "error", + "no-use-before-define": "error", + "no-useless-computed-key": "error", + "no-useless-constructor": "error", + "no-useless-rename": "error", + "no-var": "error", + "no-void": ["error", { allowAsStatement: true }], + "one-var": ["error", "never"], + "operator-assignment": ["error", "always"], + "prefer-destructuring": [ + "error", + { + AssignmentExpression: { array: true }, + VariableDeclarator: { array: true, object: true }, + }, + ], + "prefer-numeric-literals": "error", + "prefer-promise-reject-errors": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + "prefer-template": "error", + radix: ["error", "always"], + "sort-vars": "error", + "symbol-description": "error", + "vars-on-top": "error", + yoda: ["error", "never"], + }, +}; diff --git a/browser/components/newtab/.nvmrc b/browser/components/newtab/.nvmrc new file mode 100644 index 0000000000..e65243f2ea --- /dev/null +++ b/browser/components/newtab/.nvmrc @@ -0,0 +1 @@ +16.19.0 diff --git a/browser/components/newtab/AboutNewTabService.sys.mjs b/browser/components/newtab/AboutNewTabService.sys.mjs new file mode 100644 index 0000000000..e73e1b1880 --- /dev/null +++ b/browser/components/newtab/AboutNewTabService.sys.mjs @@ -0,0 +1,510 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/** + * The nsIAboutNewTabService is accessed by the AboutRedirector anytime + * about:home, about:newtab or about:welcome are requested. The primary + * job of an nsIAboutNewTabService is to tell the AboutRedirector what + * resources to actually load for those requests. + * + * The nsIAboutNewTabService is not involved when the user has overridden + * the default about:home or about:newtab pages. + * + * There are two implementations of this service - one for the parent + * process, and one for content processes. Each one has some secondary + * responsibilties that are process-specific. + * + * The need for two implementations is an unfortunate consequence of how + * document loading and process redirection for about: pages currently + * works in Gecko. The commonalities between the two implementations has + * been put into an abstract base class. + */ + +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; +import { E10SUtils } from "resource://gre/modules/E10SUtils.sys.mjs"; +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + BasePromiseWorker: "resource://gre/modules/PromiseWorker.sys.mjs", + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", +}); + +/** + * BEWARE: Do not add variables for holding state in the global scope. + * Any state variables should be properties of the appropriate class + * below. This is to avoid confusion where the state is set in one process, + * but not in another. + * + * Constants are fine in the global scope. + */ + +const PREF_ABOUT_HOME_CACHE_TESTING = + "browser.startup.homepage.abouthome_cache.testing"; +const ABOUT_WELCOME_URL = + "chrome://browser/content/aboutwelcome/aboutwelcome.html"; + +const CACHE_WORKER_URL = "resource://activity-stream/lib/cache.worker.js"; + +const IS_PRIVILEGED_PROCESS = + Services.appinfo.remoteType === E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE; + +const PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS = + "browser.tabs.remote.separatePrivilegedContentProcess"; +const PREF_ACTIVITY_STREAM_DEBUG = "browser.newtabpage.activity-stream.debug"; + +/** + * The AboutHomeStartupCacheChild is responsible for connecting the + * nsIAboutNewTabService with a cached document and script for about:home + * if one happens to exist. The AboutHomeStartupCacheChild is only ever + * handed the streams for those caches when the "privileged about content + * process" first launches, so subsequent loads of about:home do not read + * from this cache. + * + * See https://firefox-source-docs.mozilla.org/browser/components/newtab/docs/v2-system-addon/about_home_startup_cache.html + * for further details. + */ +export const AboutHomeStartupCacheChild = { + _initted: false, + CACHE_REQUEST_MESSAGE: "AboutHomeStartupCache:CacheRequest", + CACHE_RESPONSE_MESSAGE: "AboutHomeStartupCache:CacheResponse", + CACHE_USAGE_RESULT_MESSAGE: "AboutHomeStartupCache:UsageResult", + STATES: { + UNAVAILABLE: 0, + UNCONSUMED: 1, + PAGE_CONSUMED: 2, + PAGE_AND_SCRIPT_CONSUMED: 3, + FAILED: 4, + DISQUALIFIED: 5, + }, + REQUEST_TYPE: { + PAGE: 0, + SCRIPT: 1, + }, + _state: 0, + _consumerBCID: null, + + /** + * Called via a process script very early on in the process lifetime. This + * prepares the AboutHomeStartupCacheChild to pass an nsIChannel back to + * the nsIAboutNewTabService when the initial about:home document is + * eventually requested. + * + * @param pageInputStream (nsIInputStream) + * The stream for the cached page markup. + * @param scriptInputStream (nsIInputStream) + * The stream for the cached script to run on the page. + */ + init(pageInputStream, scriptInputStream) { + if ( + !IS_PRIVILEGED_PROCESS && + !Services.prefs.getBoolPref(PREF_ABOUT_HOME_CACHE_TESTING, false) + ) { + throw new Error( + "Can only instantiate in the privileged about content processes." + ); + } + + if (!lazy.NimbusFeatures.abouthomecache.getVariable("enabled")) { + return; + } + + if (this._initted) { + throw new Error("AboutHomeStartupCacheChild already initted."); + } + + Services.obs.addObserver(this, "memory-pressure"); + Services.cpmm.addMessageListener(this.CACHE_REQUEST_MESSAGE, this); + + this._pageInputStream = pageInputStream; + this._scriptInputStream = scriptInputStream; + this._initted = true; + this.setState(this.STATES.UNCONSUMED); + }, + + /** + * A function that lets us put the AboutHomeStartupCacheChild back into + * its initial state. This is used by tests to let us simulate the startup + * behaviour of the module without having to manually launch a new privileged + * about content process every time. + */ + uninit() { + if (!Services.prefs.getBoolPref(PREF_ABOUT_HOME_CACHE_TESTING, false)) { + throw new Error( + "Cannot uninit AboutHomeStartupCacheChild unless testing." + ); + } + + if (!this._initted) { + return; + } + + Services.obs.removeObserver(this, "memory-pressure"); + Services.cpmm.removeMessageListener(this.CACHE_REQUEST_MESSAGE, this); + + if (this._cacheWorker) { + this._cacheWorker.terminate(); + this._cacheWorker = null; + } + + this._pageInputStream = null; + this._scriptInputStream = null; + this._initted = false; + this._state = this.STATES.UNAVAILABLE; + this._consumerBCID = null; + }, + + /** + * A public method called from nsIAboutNewTabService that attempts + * return an nsIChannel for a cached about:home document that we + * were initialized with. If we failed to be initted with the + * cache, or the input streams that we were sent have no data + * yet available, this function returns null. The caller should + * fall back to generating the page dynamically. + * + * This function will be called when loading about:home, or + * about:home?jscache - the latter returns the cached script. + * + * It is expected that the same BrowsingContext that loads the cached + * page will also load the cached script. + * + * @param uri (nsIURI) + * The URI for the requested page, as passed by nsIAboutNewTabService. + * @param loadInfo (nsILoadInfo) + * The nsILoadInfo for the requested load, as passed by + * nsIAboutNewWTabService. + * @return nsIChannel or null. + */ + maybeGetCachedPageChannel(uri, loadInfo) { + if (!this._initted) { + return null; + } + + if (this._state >= this.STATES.PAGE_AND_SCRIPT_CONSUMED) { + return null; + } + + let requestType = + uri.query === "jscache" + ? this.REQUEST_TYPE.SCRIPT + : this.REQUEST_TYPE.PAGE; + + // If this is a page request, then we need to be in the UNCONSUMED state, + // since we expect the page request to come first. If this is a script + // request, we expect to be in PAGE_CONSUMED state, since the page cache + // stream should he been consumed already. + if ( + (requestType === this.REQUEST_TYPE.PAGE && + this._state !== this.STATES.UNCONSUMED) || + (requestType === this.REQUEST_TYPE_SCRIPT && + this._state !== this.STATES.PAGE_CONSUMED) + ) { + return null; + } + + // If by this point, we don't have anything in the streams, + // then either the cache was too slow to give us data, or the cache + // doesn't exist. The caller should fall back to generating the + // page dynamically. + // + // We only do this on the page request, because by the time + // we get to the script request, we should have already drained + // the page input stream. + if (requestType === this.REQUEST_TYPE.PAGE) { + try { + if ( + !this._scriptInputStream.available() || + !this._pageInputStream.available() + ) { + this.setState(this.STATES.FAILED); + this.reportUsageResult(false /* success */); + return null; + } + } catch (e) { + this.setState(this.STATES.FAILED); + if (e.result === Cr.NS_BASE_STREAM_CLOSED) { + this.reportUsageResult(false /* success */); + return null; + } + throw e; + } + } + + if ( + requestType === this.REQUEST_TYPE.SCRIPT && + this._consumerBCID !== loadInfo.browsingContextID + ) { + // Some other document is somehow requesting the script - one + // that didn't originally request the page. This is not allowed. + this.setState(this.STATES.FAILED); + return null; + } + + let channel = Cc[ + "@mozilla.org/network/input-stream-channel;1" + ].createInstance(Ci.nsIInputStreamChannel); + channel.QueryInterface(Ci.nsIChannel); + channel.setURI(uri); + channel.loadInfo = loadInfo; + channel.contentStream = + requestType === this.REQUEST_TYPE.PAGE + ? this._pageInputStream + : this._scriptInputStream; + + if (requestType === this.REQUEST_TYPE.SCRIPT) { + this.setState(this.STATES.PAGE_AND_SCRIPT_CONSUMED); + this.reportUsageResult(true /* success */); + } else { + this.setState(this.STATES.PAGE_CONSUMED); + // Stash the BrowsingContext ID so that when the script stream + // attempts to be consumed, we ensure that it's from the same + // BrowsingContext that loaded the page. + this._consumerBCID = loadInfo.browsingContextID; + } + + return channel; + }, + + /** + * This function takes the state information required to generate + * the about:home cache markup and script, and then generates that + * markup in script asynchronously. Once that's done, a message + * is sent to the parent process with the nsIInputStream's for the + * markup and script contents. + * + * @param state (Object) + * The Redux state of the about:home document to render. + * @return Promise + * @resolves undefined + * After the message with the nsIInputStream's have been sent to + * the parent. + */ + async constructAndSendCache(state) { + if (!IS_PRIVILEGED_PROCESS) { + throw new Error("Wrong process type."); + } + + let worker = this.getOrCreateWorker(); + + TelemetryStopwatch.start("FX_ABOUTHOME_CACHE_CONSTRUCTION"); + + let { page, script } = await worker + .post("construct", [state]) + .finally(() => { + TelemetryStopwatch.finish("FX_ABOUTHOME_CACHE_CONSTRUCTION"); + }); + + let pageInputStream = Cc[ + "@mozilla.org/io/string-input-stream;1" + ].createInstance(Ci.nsIStringInputStream); + + pageInputStream.setUTF8Data(page); + + let scriptInputStream = Cc[ + "@mozilla.org/io/string-input-stream;1" + ].createInstance(Ci.nsIStringInputStream); + + scriptInputStream.setUTF8Data(script); + + Services.cpmm.sendAsyncMessage(this.CACHE_RESPONSE_MESSAGE, { + pageInputStream, + scriptInputStream, + }); + }, + + _cacheWorker: null, + getOrCreateWorker() { + if (this._cacheWorker) { + return this._cacheWorker; + } + + this._cacheWorker = new lazy.BasePromiseWorker(CACHE_WORKER_URL); + return this._cacheWorker; + }, + + receiveMessage(message) { + if (message.name === this.CACHE_REQUEST_MESSAGE) { + let { state } = message.data; + this.constructAndSendCache(state); + } + }, + + reportUsageResult(success) { + Services.cpmm.sendAsyncMessage(this.CACHE_USAGE_RESULT_MESSAGE, { + success, + }); + }, + + observe(subject, topic, data) { + if (topic === "memory-pressure" && this._cacheWorker) { + this._cacheWorker.terminate(); + this._cacheWorker = null; + } + }, + + /** + * Transitions the AboutHomeStartupCacheChild from one state + * to the next, where each state is defined in this.STATES. + * + * States can only be transitioned in increasing order, otherwise + * an error is logged. + */ + setState(state) { + if (state > this._state) { + this._state = state; + } else { + console.error( + "AboutHomeStartupCacheChild could not transition from state " + + `${this._state} to ${state}`, + new Error().stack + ); + } + }, + + /** + * If the cache hasn't been used, transitions it into the DISQUALIFIED + * state so that it cannot be used. This should be called if it's been + * determined that about:newtab is going to be loaded, which doesn't + * use the cache. + */ + disqualifyCache() { + if (this._state === this.STATES.UNCONSUMED) { + this.setState(this.STATES.DISQUALIFIED); + this.reportUsageResult(false /* success */); + } + }, +}; + +/** + * This is an abstract base class for the nsIAboutNewTabService + * implementations that has some common methods and properties. + */ +class BaseAboutNewTabService { + constructor() { + if (!AppConstants.RELEASE_OR_BETA) { + XPCOMUtils.defineLazyPreferenceGetter( + this, + "activityStreamDebug", + PREF_ACTIVITY_STREAM_DEBUG, + false + ); + } else { + this.activityStreamDebug = false; + } + + XPCOMUtils.defineLazyPreferenceGetter( + this, + "privilegedAboutProcessEnabled", + PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS, + false + ); + + this.classID = Components.ID("{cb36c925-3adc-49b3-b720-a5cc49d8a40e}"); + this.QueryInterface = ChromeUtils.generateQI([ + "nsIAboutNewTabService", + "nsIObserver", + ]); + } + + /** + * Returns the default URL. + * + * This URL depends on various activity stream prefs. Overriding + * the newtab page has no effect on the result of this function. + */ + get defaultURL() { + // Generate the desired activity stream resource depending on state, e.g., + // "resource://activity-stream/prerendered/activity-stream.html" + // "resource://activity-stream/prerendered/activity-stream-debug.html" + // "resource://activity-stream/prerendered/activity-stream-noscripts.html" + return [ + "resource://activity-stream/prerendered/", + "activity-stream", + // Debug version loads dev scripts but noscripts separately loads scripts + this.activityStreamDebug && !this.privilegedAboutProcessEnabled + ? "-debug" + : "", + this.privilegedAboutProcessEnabled ? "-noscripts" : "", + ".html", + ].join(""); + } + + get welcomeURL() { + /* + * Returns the about:welcome URL + * + * This is calculated in the same way the default URL is. + */ + + lazy.NimbusFeatures.aboutwelcome.recordExposureEvent({ once: true }); + if (lazy.NimbusFeatures.aboutwelcome.getVariable("enabled") ?? true) { + return ABOUT_WELCOME_URL; + } + return this.defaultURL; + } + + aboutHomeChannel(uri, loadInfo) { + throw Components.Exception( + "AboutHomeChannel not implemented for this process.", + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } +} + +/** + * The child-process implementation of nsIAboutNewTabService, + * which also does the work of redirecting about:home loads to + * the about:home startup cache if its available. + */ +class AboutNewTabChildService extends BaseAboutNewTabService { + aboutHomeChannel(uri, loadInfo) { + if (IS_PRIVILEGED_PROCESS) { + let cacheChannel = AboutHomeStartupCacheChild.maybeGetCachedPageChannel( + uri, + loadInfo + ); + if (cacheChannel) { + return cacheChannel; + } + } + + let pageURI = Services.io.newURI(this.defaultURL); + let fileChannel = Services.io.newChannelFromURIWithLoadInfo( + pageURI, + loadInfo + ); + fileChannel.originalURI = uri; + return fileChannel; + } + + get defaultURL() { + if (IS_PRIVILEGED_PROCESS) { + // This is a bit of a hack, but attempting to load about:newtab will + // enter this code path in order to get at the expected URL, and we + // can use that to disqualify the about:home cache, since we don't + // use it for about:newtab loads, and we don't want the about:home + // cache to be wildly out of date when about:home is eventually + // loaded (for example, in the first new window). + AboutHomeStartupCacheChild.disqualifyCache(); + } + + return super.defaultURL; + } +} + +/** + * The AboutNewTabStubService is a function called in both the main and + * content processes when trying to get at the nsIAboutNewTabService. This + * function does the job of choosing the appropriate implementation of + * nsIAboutNewTabService for the process type. + */ +export function AboutNewTabStubService() { + if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT) { + return new BaseAboutNewTabService(); + } + return new AboutNewTabChildService(); +} diff --git a/browser/components/newtab/bin/render-activity-stream-html.js b/browser/components/newtab/bin/render-activity-stream-html.js new file mode 100644 index 0000000000..41b77c35db --- /dev/null +++ b/browser/components/newtab/bin/render-activity-stream-html.js @@ -0,0 +1,188 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-disable no-console */ +const fs = require("fs"); +const { mkdir } = require("shelljs"); +const path = require("path"); +const { pathToFileURL } = require("url"); +const chalk = require("chalk"); + +const DEFAULT_OPTIONS = { + // Glob leading from CWD to the parent of the intended prerendered directory. + // Starting in newtab/bin/ and we want to write to newtab/prerendered/ so we + // go up one level. + addonPath: "..", + // depends on the registration in browser/components/newtab/jar.mn + baseUrl: "resource://activity-stream/", +}; + +/** + * templateHTML - Generates HTML for activity stream, given some options and + * prerendered HTML if necessary. + * + * @param {obj} options + * {str} options.baseUrl The base URL for all local assets + * {bool} options.debug Should we use dev versions of JS libraries? + * {bool} options.noscripts Should we include scripts in the prerendered files? + * @return {str} An HTML document as a string + */ +function templateHTML(options) { + const debugString = options.debug ? "-dev" : ""; + // This list must match any similar ones in AboutNewTabChild.sys.mjs + const scripts = [ + "chrome://browser/content/contentSearchUI.js", + "chrome://browser/content/contentSearchHandoffUI.js", + "chrome://browser/content/contentTheme.js", + `${options.baseUrl}vendor/react${debugString}.js`, + `${options.baseUrl}vendor/react-dom${debugString}.js`, + `${options.baseUrl}vendor/prop-types.js`, + `${options.baseUrl}vendor/redux.js`, + `${options.baseUrl}vendor/react-redux.js`, + `${options.baseUrl}vendor/react-transition-group.js`, + `${options.baseUrl}data/content/activity-stream.bundle.js`, + `${options.baseUrl}data/content/newtab-render.js`, + ]; + + // Add spacing and script tags + const scriptRender = `\n${scripts + .map(script => ` `) + .join("\n")}`; + + // The markup below needs to be formatted by Prettier. But any diff after + // running this script should be caught by try-runnner.js + return ` + + + + + + + + + + + + + + + + + + +
${options.noscripts ? "" : scriptRender} + + + +`.trimLeft(); +} + +/** + * writeFiles - Writes to the desired files the result of a template given + * various prerendered data and options. + * + * @param {string} destPath Path to write the files to + * @param {Map} filesMap Mapping of a string file name to templater + * @param {Object} options Various options for the templater + */ +function writeFiles(destPath, filesMap, options) { + for (const [file, templater] of filesMap) { + fs.writeFileSync(path.join(destPath, file), templater({ options })); + console.log(chalk.green(`✓ ${file}`)); + } +} + +const STATIC_FILES = new Map([ + ["activity-stream.html", ({ options }) => templateHTML(options)], + [ + "activity-stream-debug.html", + ({ options }) => templateHTML(Object.assign({}, options, { debug: true })), + ], + [ + "activity-stream-noscripts.html", + ({ options }) => + templateHTML(Object.assign({}, options, { noscripts: true })), + ], +]); + +/** + * main - Parses command line arguments, generates html and js with templates, + * and writes files to their specified locations. + */ +async function main() { + const { default: meow } = await import("meow"); + const fileUrl = pathToFileURL(__filename); + const cli = meow( + ` + Usage + $ node ./bin/render-activity-stream-html.js [options] + + Options + -a PATH, --addon-path PATH Path to the parent of the target directory. + default: "${DEFAULT_OPTIONS.addonPath}" + -b URL, --base-url URL Base URL for assets. + default: "${DEFAULT_OPTIONS.baseUrl}" + --help Show this help message. +`, + { + description: false, + // `pkg` is a tiny optimization. It prevents meow from looking for a package + // that doesn't technically exist. meow searches for a package and changes + // the process name to the package name. It resolves to the newtab + // package.json, which would give a confusing name and be wasteful. + pkg: { + name: "render-activity-stream-html", + version: "0.0.0", + }, + // `importMeta` is required by meow 10+. It was added to support ESM, but + // meow now requires it, and no longer supports CJS style imports. But it + // only uses import.meta.url, which can be polyfilled like this: + importMeta: { url: fileUrl }, + flags: { + addonPath: { + type: "string", + alias: "a", + default: DEFAULT_OPTIONS.addonPath, + }, + baseUrl: { + type: "string", + alias: "b", + default: DEFAULT_OPTIONS.baseUrl, + }, + }, + } + ); + + const options = Object.assign({ debug: false }, cli.flags || {}); + const addonPath = path.resolve(__dirname, options.addonPath); + const prerenderedPath = path.join(addonPath, "prerendered"); + console.log(`Writing prerendered files to ${prerenderedPath}:`); + + mkdir("-p", prerenderedPath); + writeFiles(prerenderedPath, STATIC_FILES, options); +} + +main(); diff --git a/browser/components/newtab/bin/try-runner.js b/browser/components/newtab/bin/try-runner.js new file mode 100644 index 0000000000..93b88fac23 --- /dev/null +++ b/browser/components/newtab/bin/try-runner.js @@ -0,0 +1,366 @@ +/* eslint-disable no-console */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . */ + +/* + * A small test runner/reporter for node-based tests, + * which are run via taskcluster node(debugger). + * + * Forked from + * https://searchfox.org/mozilla-central/rev/c3453c7a0427eb27d467e1582f821f402aed9850/devtools/client/debugger/bin/try-runner.js + */ + +const { execFileSync } = require("child_process"); +const { readFileSync, writeFileSync } = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); +const chalk = require("chalk"); + +function logErrors(tool, errors) { + for (const error of errors) { + console.log(`TEST-UNEXPECTED-FAIL | ${tool} | ${error}`); + } + return errors; +} + +function execOut(...args) { + let exitCode = 0; + let out; + let err; + + try { + out = execFileSync(...args, { + silent: false, + }); + } catch (e) { + // For debugging on (eg) try server... + // + // if (e) { + // logErrors("execOut", ["execFileSync returned exception: ", e]); + // } + + out = e && e.stdout; + err = e && e.stderr; + exitCode = e && e.status; + } + return { exitCode, out: out && out.toString(), err: err && err.toString() }; +} + +function logStart(name) { + console.log(`TEST-START | ${name}`); +} + +function logSkip(name) { + console.log(`TEST-SKIP | ${name}`); +} + +const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm"; + +const tests = { + bundles() { + logStart("bundles"); + + const items = { + "Activity Stream bundle": { + path: path.join("data", "content", "activity-stream.bundle.js"), + }, + "activity-stream.html": { + path: path.join("prerendered", "activity-stream.html"), + }, + "activity-stream-debug.html": { + path: path.join("prerendered", "activity-stream-debug.html"), + }, + "activity-stream-noscripts.html": { + path: path.join("prerendered", "activity-stream-noscripts.html"), + }, + "activity-stream-linux.css": { + path: path.join("css", "activity-stream-linux.css"), + }, + "activity-stream-mac.css": { + path: path.join("css", "activity-stream-mac.css"), + }, + "activity-stream-windows.css": { + path: path.join("css", "activity-stream-windows.css"), + }, + // These should get split out to their own try-runner eventually (bug 1866170). + "about:welcome bundle": { + path: path.join( + "../", + "aboutwelcome", + "content", + "aboutwelcome.bundle.js" + ), + }, + "aboutwelcome.css": { + path: path.join("../", "aboutwelcome", "content", "aboutwelcome.css"), + extraCheck: content => { + if (content.match(/^\s*@import/m)) { + return "aboutwelcome.css contains an @import statement. We should not import styles through the stylesheet, because it is loaded in multiple environments, including the browser chrome for feature callouts. To add other stylesheets to about:welcome or spotlight, add them to aboutwelcome.html or spotlight.html instead."; + } + return null; + }, + }, + // These should get split out to their own try-runner eventually (bug 1866170). + "about:asrouter bundle": { + path: path.join( + "../", + "asrouter", + "content", + "asrouter-admin.bundle.js" + ), + }, + "ASRouterAdmin.css": { + path: path.join( + "../", + "asrouter", + "content", + "components", + "ASRouterAdmin", + "ASRouterAdmin.css" + ), + }, + }; + const errors = []; + + for (const name of Object.keys(items)) { + const item = items[name]; + item.before = readFileSync(item.path, item.encoding || "utf8"); + } + + let newtabBundleExitCode = execOut(npmCommand, ["run", "bundle"]).exitCode; + + // Until we split out the try runner for about:welcome out into its own + // script, we manually run its bundle script. + let cwd = process.cwd(); + process.chdir("../aboutwelcome"); + let welcomeBundleExitCode = execOut(npmCommand, ["run", "bundle"]).exitCode; + process.chdir(cwd); + + // Same thing for about:asrouter + process.chdir("../asrouter"); + let asrouterBundleExitCode = execOut(npmCommand, [ + "run", + "bundle", + ]).exitCode; + process.chdir(cwd); + + for (const name of Object.keys(items)) { + const item = items[name]; + const after = readFileSync(item.path, item.encoding || "utf8"); + + if (item.before !== after) { + errors.push(`${name} out of date`); + } + + if (item.extraCheck) { + const extraError = item.extraCheck(after); + if (extraError) { + errors.push(extraError); + } + } + } + + if (newtabBundleExitCode !== 0) { + errors.push("newtab npm:bundle did not run successfully"); + } + + if (welcomeBundleExitCode !== 0) { + errors.push("about:welcome npm:bundle did not run successfully"); + } + + if (asrouterBundleExitCode !== 0) { + errors.push("about:asrouter npm:bundle did not run successfully"); + } + + logErrors("bundles", errors); + return errors.length === 0; + }, + + karma() { + logStart(`karma ${process.cwd()}`); + + const errors = []; + const { exitCode, out } = execOut(npmCommand, [ + "run", + "testmc:unit", + // , "--", "--log-level", "--verbose", + // to debug the karma integration, uncomment the above line + ]); + + // karma spits everything to stdout, not stderr, so if nothing came back on + // stdout, give up now. + if (!out) { + return false; + } + + // Detect mocha failures + let jsonContent; + try { + // Note that this will be overwritten at each run, but that shouldn't + // matter. + jsonContent = readFileSync(path.join("logs", "karma-run-results.json")); + } catch (ex) { + console.error("exception reading karma-run-results.json: ", ex); + return false; + } + const results = JSON.parse(jsonContent); + // eslint-disable-next-line guard-for-in + for (let testArray in results.result) { + let failedTests = Array.from(results.result[testArray]).filter( + test => !test.success && !test.skipped + ); + + errors.push( + ...failedTests.map( + test => `${test.suite.join(":")} ${test.description}: ${test.log[0]}` + ) + ); + } + + // Detect istanbul failures (coverage thresholds set in karma config) + const coverage = out.match(/ERROR.+coverage-istanbul.+/g); + if (coverage) { + errors.push(...coverage.map(line => line.match(/Coverage.+/)[0])); + } + + logErrors(`karma ${process.cwd()}`, errors); + + console.log("-----karma stdout below this line---"); + console.log(out); + console.log("-----karma stdout above this line---"); + + // Pass if there's no detected errors and nothing unexpected. + return errors.length === 0 && !exitCode; + }, + + welcomekarma() { + let cwd = process.cwd(); + process.chdir("../aboutwelcome"); + const result = this.karma(); + process.chdir(cwd); + return result; + }, + + asrouterkarma() { + let cwd = process.cwd(); + process.chdir("../asrouter"); + const result = this.karma(); + process.chdir(cwd); + return result; + }, + + zipCodeCoverage() { + logStart("zipCodeCoverage"); + + const newtabCoveragePath = "logs/coverage/lcov.info"; + const welcomeCoveragePath = "../aboutwelcome/logs/coverage/lcov.info"; + const asrouterCoveragePath = "../asrouter/logs/coverage/lcov.info"; + + let newtabCoverage = readFileSync(newtabCoveragePath, "utf8"); + const welcomeCoverage = readFileSync(welcomeCoveragePath, "utf8"); + const asrouterCoverage = readFileSync(asrouterCoveragePath, "utf8"); + + newtabCoverage = `${newtabCoverage}${welcomeCoverage}${asrouterCoverage}`; + writeFileSync(newtabCoveragePath, newtabCoverage, "utf8"); + + const { exitCode, out } = execOut("zip", [ + "-j", + "logs/coverage/code-coverage-grcov", + "logs/coverage/lcov.info", + ]); + + console.log("zipCodeCoverage log output: ", out); + + if (!exitCode) { + return true; + } + + return false; + }, +}; + +async function main() { + const { default: meow } = await import("meow"); + const fileUrl = pathToFileURL(__filename); + const cli = meow( + ` + Usage + $ node bin/try-runner.js [options] + + Options + -t NAME, --test NAME Run only the specified test. If not specified, + all tests will be run. Argument can be passed + multiple times to run multiple tests. + --help Show this help message. + + Examples + $ node bin/try-runner.js bundles karma + $ node bin/try-runner.js -t karma -t zip +`, + { + description: false, + // `pkg` is a tiny optimization. It prevents meow from looking for a package + // that doesn't technically exist. meow searches for a package and changes + // the process name to the package name. It resolves to the newtab + // package.json, which would give a confusing name and be wasteful. + pkg: { + name: "try-runner", + version: "1.0.0", + }, + // `importMeta` is required by meow 10+. It was added to support ESM, but + // meow now requires it, and no longer supports CJS style imports. But it + // only uses import.meta.url, which can be polyfilled like this: + importMeta: { url: fileUrl }, + flags: { + test: { + type: "string", + isMultiple: true, + alias: "t", + }, + }, + } + ); + const aliases = { + bundle: "bundles", + build: "bundles", + coverage: "karma", + cov: "karma", + zip: "zipCodeCoverage", + welcomecoverage: "welcomekarma", + welcomecov: "welcomekarma", + asroutercoverage: "asrouterkarma", + asroutercov: "asrouterkarma", + }; + + const inputs = [...cli.input, ...cli.flags.test].map(input => + (aliases[input] || input).toLowerCase() + ); + + function shouldRunTest(name) { + if (inputs.length) { + return inputs.includes(name.toLowerCase()); + } + return true; + } + + const results = []; + for (const name of Object.keys(tests)) { + if (shouldRunTest(name)) { + results.push([name, tests[name]()]); + } else { + logSkip(name); + } + } + + for (const [name, result] of results) { + // colorize output based on result + console.log(result ? chalk.green(`✓ ${name}`) : chalk.red(`✗ ${name}`)); + } + + const success = results.every(([, result]) => result); + process.exitCode = success ? 0 : 1; + console.log("CODE", process.exitCode); +} + +main(); diff --git a/browser/components/newtab/bin/vendor.js b/browser/components/newtab/bin/vendor.js new file mode 100644 index 0000000000..3d929dcf4b --- /dev/null +++ b/browser/components/newtab/bin/vendor.js @@ -0,0 +1,38 @@ +#!/usr/bin/env node +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-disable no-console */ + +const { cp, set } = require("shelljs"); +const path = require("path"); + +const filesToVendor = { + // XXX currently these two licenses are identical. Perhaps we should check + // in case that changes at some point in the future. + "react/LICENSE": "REACT_AND_REACT_DOM_LICENSE", + "react/umd/react.production.min.js": "react.js", + "react/umd/react.development.js": "react-dev.js", + "react-dom/umd/react-dom.production.min.js": "react-dom.js", + "react-dom/umd/react-dom.development.js": "react-dom-dev.js", + "react-dom/umd/react-dom-server.browser.production.min.js": + "react-dom-server.js", + "react-redux/LICENSE.md": "REACT_REDUX_LICENSE", + "react-redux/dist/react-redux.min.js": "react-redux.js", + "react-transition-group/dist/react-transition-group.min.js": + "react-transition-group.js", + "react-transition-group/LICENSE": "REACT_TRANSITION_GROUP_LICENSE", +}; + +set("-v"); // Echo all the copy commands so the user can see what's going on +for (let srcPath of Object.keys(filesToVendor)) { + cp( + path.join("node_modules", srcPath), + path.join("vendor", filesToVendor[srcPath]) + ); +} + +console.log(` +Check to see if any license files have changed, and, if so, be sure to update +https://searchfox.org/mozilla-central/source/toolkit/content/license.html`); diff --git a/browser/components/newtab/common/Actions.sys.mjs b/browser/components/newtab/common/Actions.sys.mjs new file mode 100644 index 0000000000..df5c9f0c91 --- /dev/null +++ b/browser/components/newtab/common/Actions.sys.mjs @@ -0,0 +1,457 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export const MAIN_MESSAGE_TYPE = "ActivityStream:Main"; +export const CONTENT_MESSAGE_TYPE = "ActivityStream:Content"; +export const PRELOAD_MESSAGE_TYPE = "ActivityStream:PreloadedBrowser"; +export const UI_CODE = 1; +export const BACKGROUND_PROCESS = 2; + +/** + * globalImportContext - Are we in UI code (i.e. react, a dom) or some kind of background process? + * Use this in action creators if you need different logic + * for ui/background processes. + */ +export const globalImportContext = + typeof Window === "undefined" ? BACKGROUND_PROCESS : UI_CODE; + +// Create an object that avoids accidental differing key/value pairs: +// { +// INIT: "INIT", +// UNINIT: "UNINIT" +// } +export const actionTypes = {}; + +for (const type of [ + "ABOUT_SPONSORED_TOP_SITES", + "ADDONS_INFO_REQUEST", + "ADDONS_INFO_RESPONSE", + "ARCHIVE_FROM_POCKET", + "AS_ROUTER_INITIALIZED", + "AS_ROUTER_PREF_CHANGED", + "AS_ROUTER_TARGETING_UPDATE", + "AS_ROUTER_TELEMETRY_USER_EVENT", + "BLOCK_URL", + "BOOKMARK_URL", + "CLEAR_PREF", + "COPY_DOWNLOAD_LINK", + "DELETE_BOOKMARK_BY_ID", + "DELETE_FROM_POCKET", + "DELETE_HISTORY_URL", + "DIALOG_CANCEL", + "DIALOG_OPEN", + "DISABLE_SEARCH", + "DISCOVERY_STREAM_COLLECTION_DISMISSIBLE_TOGGLE", + "DISCOVERY_STREAM_CONFIG_CHANGE", + "DISCOVERY_STREAM_CONFIG_RESET", + "DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS", + "DISCOVERY_STREAM_CONFIG_SETUP", + "DISCOVERY_STREAM_CONFIG_SET_VALUE", + "DISCOVERY_STREAM_DEV_EXPIRE_CACHE", + "DISCOVERY_STREAM_DEV_IDLE_DAILY", + "DISCOVERY_STREAM_DEV_SYNC_RS", + "DISCOVERY_STREAM_DEV_SYSTEM_TICK", + "DISCOVERY_STREAM_EXPERIMENT_DATA", + "DISCOVERY_STREAM_FEEDS_UPDATE", + "DISCOVERY_STREAM_FEED_UPDATE", + "DISCOVERY_STREAM_IMPRESSION_STATS", + "DISCOVERY_STREAM_LAYOUT_RESET", + "DISCOVERY_STREAM_LAYOUT_UPDATE", + "DISCOVERY_STREAM_LINK_BLOCKED", + "DISCOVERY_STREAM_LOADED_CONTENT", + "DISCOVERY_STREAM_PERSONALIZATION_INIT", + "DISCOVERY_STREAM_PERSONALIZATION_LAST_UPDATED", + "DISCOVERY_STREAM_PERSONALIZATION_OVERRIDE", + "DISCOVERY_STREAM_PERSONALIZATION_RESET", + "DISCOVERY_STREAM_PERSONALIZATION_TOGGLE", + "DISCOVERY_STREAM_PERSONALIZATION_UPDATED", + "DISCOVERY_STREAM_POCKET_STATE_INIT", + "DISCOVERY_STREAM_POCKET_STATE_SET", + "DISCOVERY_STREAM_PREFS_SETUP", + "DISCOVERY_STREAM_RECENT_SAVES", + "DISCOVERY_STREAM_RETRY_FEED", + "DISCOVERY_STREAM_SPOCS_CAPS", + "DISCOVERY_STREAM_SPOCS_ENDPOINT", + "DISCOVERY_STREAM_SPOCS_PLACEMENTS", + "DISCOVERY_STREAM_SPOCS_UPDATE", + "DISCOVERY_STREAM_SPOC_BLOCKED", + "DISCOVERY_STREAM_SPOC_IMPRESSION", + "DISCOVERY_STREAM_USER_EVENT", + "DOWNLOAD_CHANGED", + "FAKE_FOCUS_SEARCH", + "FILL_SEARCH_TERM", + "HANDOFF_SEARCH_TO_AWESOMEBAR", + "HIDE_PERSONALIZE", + "HIDE_PRIVACY_INFO", + "INIT", + "NEW_TAB_INIT", + "NEW_TAB_INITIAL_STATE", + "NEW_TAB_LOAD", + "NEW_TAB_REHYDRATED", + "NEW_TAB_STATE_REQUEST", + "NEW_TAB_UNLOAD", + "OPEN_DOWNLOAD_FILE", + "OPEN_LINK", + "OPEN_NEW_WINDOW", + "OPEN_PRIVATE_WINDOW", + "OPEN_WEBEXT_SETTINGS", + "PARTNER_LINK_ATTRIBUTION", + "PLACES_BOOKMARKS_REMOVED", + "PLACES_BOOKMARK_ADDED", + "PLACES_HISTORY_CLEARED", + "PLACES_LINKS_CHANGED", + "PLACES_LINKS_DELETED", + "PLACES_LINK_BLOCKED", + "PLACES_SAVED_TO_POCKET", + "POCKET_CTA", + "POCKET_LINK_DELETED_OR_ARCHIVED", + "POCKET_LOGGED_IN", + "POCKET_WAITING_FOR_SPOC", + "PREFS_INITIAL_VALUES", + "PREF_CHANGED", + "PREVIEW_REQUEST", + "PREVIEW_REQUEST_CANCEL", + "PREVIEW_RESPONSE", + "REMOVE_DOWNLOAD_FILE", + "RICH_ICON_MISSING", + "SAVE_SESSION_PERF_DATA", + "SAVE_TO_POCKET", + "SCREENSHOT_UPDATED", + "SECTION_DEREGISTER", + "SECTION_DISABLE", + "SECTION_ENABLE", + "SECTION_MOVE", + "SECTION_OPTIONS_CHANGED", + "SECTION_REGISTER", + "SECTION_UPDATE", + "SECTION_UPDATE_CARD", + "SETTINGS_CLOSE", + "SETTINGS_OPEN", + "SET_PREF", + "SHOW_DOWNLOAD_FILE", + "SHOW_FIREFOX_ACCOUNTS", + "SHOW_PERSONALIZE", + "SHOW_PRIVACY_INFO", + "SHOW_SEARCH", + "SKIPPED_SIGNIN", + "SOV_UPDATED", + "SUBMIT_EMAIL", + "SUBMIT_SIGNIN", + "SYSTEM_TICK", + "TELEMETRY_IMPRESSION_STATS", + "TELEMETRY_USER_EVENT", + "TOP_SITES_CANCEL_EDIT", + "TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL", + "TOP_SITES_EDIT", + "TOP_SITES_INSERT", + "TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL", + "TOP_SITES_ORGANIC_IMPRESSION_STATS", + "TOP_SITES_PIN", + "TOP_SITES_PREFS_UPDATED", + "TOP_SITES_SPONSORED_IMPRESSION_STATS", + "TOP_SITES_UNPIN", + "TOP_SITES_UPDATED", + "TOTAL_BOOKMARKS_REQUEST", + "TOTAL_BOOKMARKS_RESPONSE", + "UNINIT", + "UPDATE_PINNED_SEARCH_SHORTCUTS", + "UPDATE_SEARCH_SHORTCUTS", + "UPDATE_SECTION_PREFS", + "WEBEXT_CLICK", + "WEBEXT_DISMISS", +]) { + actionTypes[type] = type; +} + +// Helper function for creating routed actions between content and main +// Not intended to be used by consumers +function _RouteMessage(action, options) { + const meta = action.meta ? { ...action.meta } : {}; + if (!options || !options.from || !options.to) { + throw new Error( + "Routed Messages must have options as the second parameter, and must at least include a .from and .to property." + ); + } + // For each of these fields, if they are passed as an option, + // add them to the action. If they are not defined, remove them. + ["from", "to", "toTarget", "fromTarget", "skipMain", "skipLocal"].forEach( + o => { + if (typeof options[o] !== "undefined") { + meta[o] = options[o]; + } else if (meta[o]) { + delete meta[o]; + } + } + ); + return { ...action, meta }; +} + +/** + * AlsoToMain - Creates a message that will be dispatched locally and also sent to the Main process. + * + * @param {object} action Any redux action (required) + * @param {object} options + * @param {bool} skipLocal Used by OnlyToMain to skip the main reducer + * @param {string} fromTarget The id of the content port from which the action originated. (optional) + * @return {object} An action with added .meta properties + */ +function AlsoToMain(action, fromTarget, skipLocal) { + return _RouteMessage(action, { + from: CONTENT_MESSAGE_TYPE, + to: MAIN_MESSAGE_TYPE, + fromTarget, + skipLocal, + }); +} + +/** + * OnlyToMain - Creates a message that will be sent to the Main process and skip the local reducer. + * + * @param {object} action Any redux action (required) + * @param {object} options + * @param {string} fromTarget The id of the content port from which the action originated. (optional) + * @return {object} An action with added .meta properties + */ +function OnlyToMain(action, fromTarget) { + return AlsoToMain(action, fromTarget, true); +} + +/** + * BroadcastToContent - Creates a message that will be dispatched to main and sent to ALL content processes. + * + * @param {object} action Any redux action (required) + * @return {object} An action with added .meta properties + */ +function BroadcastToContent(action) { + return _RouteMessage(action, { + from: MAIN_MESSAGE_TYPE, + to: CONTENT_MESSAGE_TYPE, + }); +} + +/** + * AlsoToOneContent - Creates a message that will be will be dispatched to the main store + * and also sent to a particular Content process. + * + * @param {object} action Any redux action (required) + * @param {string} target The id of a content port + * @param {bool} skipMain Used by OnlyToOneContent to skip the main process + * @return {object} An action with added .meta properties + */ +function AlsoToOneContent(action, target, skipMain) { + if (!target) { + throw new Error( + "You must provide a target ID as the second parameter of AlsoToOneContent. If you want to send to all content processes, use BroadcastToContent" + ); + } + return _RouteMessage(action, { + from: MAIN_MESSAGE_TYPE, + to: CONTENT_MESSAGE_TYPE, + toTarget: target, + skipMain, + }); +} + +/** + * OnlyToOneContent - Creates a message that will be sent to a particular Content process + * and skip the main reducer. + * + * @param {object} action Any redux action (required) + * @param {string} target The id of a content port + * @return {object} An action with added .meta properties + */ +function OnlyToOneContent(action, target) { + return AlsoToOneContent(action, target, true); +} + +/** + * AlsoToPreloaded - Creates a message that dispatched to the main reducer and also sent to the preloaded tab. + * + * @param {object} action Any redux action (required) + * @return {object} An action with added .meta properties + */ +function AlsoToPreloaded(action) { + return _RouteMessage(action, { + from: MAIN_MESSAGE_TYPE, + to: PRELOAD_MESSAGE_TYPE, + }); +} + +/** + * UserEvent - A telemetry ping indicating a user action. This should only + * be sent from the UI during a user session. + * + * @param {object} data Fields to include in the ping (source, etc.) + * @return {object} An AlsoToMain action + */ +function UserEvent(data) { + return AlsoToMain({ + type: actionTypes.TELEMETRY_USER_EVENT, + data, + }); +} + +/** + * DiscoveryStreamUserEvent - A telemetry ping indicating a user action from Discovery Stream. This should only + * be sent from the UI during a user session. + * + * @param {object} data Fields to include in the ping (source, etc.) + * @return {object} An AlsoToMain action + */ +function DiscoveryStreamUserEvent(data) { + return AlsoToMain({ + type: actionTypes.DISCOVERY_STREAM_USER_EVENT, + data, + }); +} + +/** + * ASRouterUserEvent - A telemetry ping indicating a user action from AS router. This should only + * be sent from the UI during a user session. + * + * @param {object} data Fields to include in the ping (source, etc.) + * @return {object} An AlsoToMain action + */ +function ASRouterUserEvent(data) { + return AlsoToMain({ + type: actionTypes.AS_ROUTER_TELEMETRY_USER_EVENT, + data, + }); +} + +/** + * ImpressionStats - A telemetry ping indicating an impression stats. + * + * @param {object} data Fields to include in the ping + * @param {int} importContext (For testing) Override the import context for testing. + * #return {object} An action. For UI code, a AlsoToMain action. + */ +function ImpressionStats(data, importContext = globalImportContext) { + const action = { + type: actionTypes.TELEMETRY_IMPRESSION_STATS, + data, + }; + return importContext === UI_CODE ? AlsoToMain(action) : action; +} + +/** + * DiscoveryStreamImpressionStats - A telemetry ping indicating an impression stats in Discovery Stream. + * + * @param {object} data Fields to include in the ping + * @param {int} importContext (For testing) Override the import context for testing. + * #return {object} An action. For UI code, a AlsoToMain action. + */ +function DiscoveryStreamImpressionStats( + data, + importContext = globalImportContext +) { + const action = { + type: actionTypes.DISCOVERY_STREAM_IMPRESSION_STATS, + data, + }; + return importContext === UI_CODE ? AlsoToMain(action) : action; +} + +/** + * DiscoveryStreamLoadedContent - A telemetry ping indicating a content gets loaded in Discovery Stream. + * + * @param {object} data Fields to include in the ping + * @param {int} importContext (For testing) Override the import context for testing. + * #return {object} An action. For UI code, a AlsoToMain action. + */ +function DiscoveryStreamLoadedContent( + data, + importContext = globalImportContext +) { + const action = { + type: actionTypes.DISCOVERY_STREAM_LOADED_CONTENT, + data, + }; + return importContext === UI_CODE ? AlsoToMain(action) : action; +} + +function SetPref(name, value, importContext = globalImportContext) { + const action = { type: actionTypes.SET_PREF, data: { name, value } }; + return importContext === UI_CODE ? AlsoToMain(action) : action; +} + +function WebExtEvent(type, data, importContext = globalImportContext) { + if (!data || !data.source) { + throw new Error( + 'WebExtEvent actions should include a property "source", the id of the webextension that should receive the event.' + ); + } + const action = { type, data }; + return importContext === UI_CODE ? AlsoToMain(action) : action; +} + +export const actionCreators = { + BroadcastToContent, + UserEvent, + DiscoveryStreamUserEvent, + ASRouterUserEvent, + ImpressionStats, + AlsoToOneContent, + OnlyToOneContent, + AlsoToMain, + OnlyToMain, + AlsoToPreloaded, + SetPref, + WebExtEvent, + DiscoveryStreamImpressionStats, + DiscoveryStreamLoadedContent, +}; + +// These are helpers to test for certain kinds of actions +export const actionUtils = { + isSendToMain(action) { + if (!action.meta) { + return false; + } + return ( + action.meta.to === MAIN_MESSAGE_TYPE && + action.meta.from === CONTENT_MESSAGE_TYPE + ); + }, + isBroadcastToContent(action) { + if (!action.meta) { + return false; + } + if (action.meta.to === CONTENT_MESSAGE_TYPE && !action.meta.toTarget) { + return true; + } + return false; + }, + isSendToOneContent(action) { + if (!action.meta) { + return false; + } + if (action.meta.to === CONTENT_MESSAGE_TYPE && action.meta.toTarget) { + return true; + } + return false; + }, + isSendToPreloaded(action) { + if (!action.meta) { + return false; + } + return ( + action.meta.to === PRELOAD_MESSAGE_TYPE && + action.meta.from === MAIN_MESSAGE_TYPE + ); + }, + isFromMain(action) { + if (!action.meta) { + return false; + } + return ( + action.meta.from === MAIN_MESSAGE_TYPE && + action.meta.to === CONTENT_MESSAGE_TYPE + ); + }, + getPortIdOfSender(action) { + return (action.meta && action.meta.fromTarget) || null; + }, + _RouteMessage, +}; diff --git a/browser/components/newtab/common/Dedupe.sys.mjs b/browser/components/newtab/common/Dedupe.sys.mjs new file mode 100644 index 0000000000..eedca8a0ee --- /dev/null +++ b/browser/components/newtab/common/Dedupe.sys.mjs @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export class Dedupe { + constructor(createKey) { + this.createKey = createKey || this.defaultCreateKey; + } + + defaultCreateKey(item) { + return item; + } + + /** + * Dedupe any number of grouped elements favoring those from earlier groups. + * + * @param {Array} groups Contains an arbitrary number of arrays of elements. + * @returns {Array} A matching array of each provided group deduped. + */ + group(...groups) { + const globalKeys = new Set(); + const result = []; + for (const values of groups) { + const valueMap = new Map(); + for (const value of values) { + const key = this.createKey(value); + if (!globalKeys.has(key) && !valueMap.has(key)) { + valueMap.set(key, value); + } + } + result.push(valueMap); + valueMap.forEach((value, key) => globalKeys.add(key)); + } + return result.map(m => Array.from(m.values())); + } +} diff --git a/browser/components/newtab/common/Reducers.sys.mjs b/browser/components/newtab/common/Reducers.sys.mjs new file mode 100644 index 0000000000..d4f879b834 --- /dev/null +++ b/browser/components/newtab/common/Reducers.sys.mjs @@ -0,0 +1,855 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { actionTypes as at } from "resource://activity-stream/common/Actions.sys.mjs"; +import { Dedupe } from "resource://activity-stream/common/Dedupe.sys.mjs"; + +export const TOP_SITES_DEFAULT_ROWS = 1; +export const TOP_SITES_MAX_SITES_PER_ROW = 8; +const PREF_COLLECTION_DISMISSIBLE = "discoverystream.isCollectionDismissible"; + +const dedupe = new Dedupe(site => site && site.url); + +export const INITIAL_STATE = { + App: { + // Have we received real data from the app yet? + initialized: false, + locale: "", + isForStartupCache: false, + customizeMenuVisible: false, + }, + ASRouter: { initialized: false }, + TopSites: { + // Have we received real data from history yet? + initialized: false, + // The history (and possibly default) links + rows: [], + // Used in content only to dispatch action to TopSiteForm. + editForm: null, + // Used in content only to open the SearchShortcutsForm modal. + showSearchShortcutsForm: false, + // The list of available search shortcuts. + searchShortcuts: [], + // The "Share-of-Voice" allocations generated by TopSitesFeed + sov: { + ready: false, + positions: [ + // {position: 0, assignedPartner: "amp"}, + // {position: 1, assignedPartner: "moz-sales"}, + ], + }, + }, + Prefs: { + initialized: false, + values: { featureConfig: {} }, + }, + Dialog: { + visible: false, + data: {}, + }, + Sections: [], + Pocket: { + isUserLoggedIn: null, + pocketCta: {}, + waitingForSpoc: true, + }, + // This is the new pocket configurable layout state. + DiscoveryStream: { + // This is a JSON-parsed copy of the discoverystream.config pref value. + config: { enabled: false }, + layout: [], + isPrivacyInfoModalVisible: false, + isCollectionDismissible: false, + feeds: { + data: { + // "https://foo.com/feed1": {lastUpdated: 123, data: [], personalized: false} + }, + loaded: false, + }, + spocs: { + spocs_endpoint: "", + lastUpdated: null, + data: { + // "spocs": {title: "", context: "", items: [], personalized: false}, + // "placement1": {title: "", context: "", items: [], personalized: false}, + }, + loaded: false, + frequency_caps: [], + blocked: [], + placements: [], + }, + experimentData: { + utmSource: "pocket-newtab", + utmCampaign: undefined, + utmContent: undefined, + }, + recentSavesData: [], + isUserLoggedIn: false, + recentSavesEnabled: false, + }, + Personalization: { + lastUpdated: null, + initialized: false, + }, + Search: { + // When search hand-off is enabled, we render a big button that is styled to + // look like a search textbox. If the button is clicked, we style + // the button as if it was a focused search box and show a fake cursor but + // really focus the awesomebar without the focus styles ("hidden focus"). + fakeFocus: false, + // Hide the search box after handing off to AwesomeBar and user starts typing. + hide: false, + }, +}; + +function App(prevState = INITIAL_STATE.App, action) { + switch (action.type) { + case at.INIT: + return Object.assign({}, prevState, action.data || {}, { + initialized: true, + }); + case at.TOP_SITES_UPDATED: + // Toggle `isForStartupCache` when receiving the `TOP_SITES_UPDATE` action + // so that sponsored tiles can be rendered as usual. See Bug 1826360. + return Object.assign({}, prevState, action.data || {}, { + isForStartupCache: false, + }); + case at.SHOW_PERSONALIZE: + return Object.assign({}, prevState, { + customizeMenuVisible: true, + }); + case at.HIDE_PERSONALIZE: + return Object.assign({}, prevState, { + customizeMenuVisible: false, + }); + default: + return prevState; + } +} + +function ASRouter(prevState = INITIAL_STATE.ASRouter, action) { + switch (action.type) { + case at.AS_ROUTER_INITIALIZED: + return { ...action.data, initialized: true }; + default: + return prevState; + } +} + +/** + * insertPinned - Inserts pinned links in their specified slots + * + * @param {array} a list of links + * @param {array} a list of pinned links + * @return {array} resulting list of links with pinned links inserted + */ +export function insertPinned(links, pinned) { + // Remove any pinned links + const pinnedUrls = pinned.map(link => link && link.url); + let newLinks = links.filter(link => + link ? !pinnedUrls.includes(link.url) : false + ); + newLinks = newLinks.map(link => { + if (link && link.isPinned) { + delete link.isPinned; + delete link.pinIndex; + } + return link; + }); + + // Then insert them in their specified location + pinned.forEach((val, index) => { + if (!val) { + return; + } + let link = Object.assign({}, val, { isPinned: true, pinIndex: index }); + if (index > newLinks.length) { + newLinks[index] = link; + } else { + newLinks.splice(index, 0, link); + } + }); + + return newLinks; +} + +function TopSites(prevState = INITIAL_STATE.TopSites, action) { + let hasMatch; + let newRows; + switch (action.type) { + case at.TOP_SITES_UPDATED: + if (!action.data || !action.data.links) { + return prevState; + } + return Object.assign( + {}, + prevState, + { initialized: true, rows: action.data.links }, + action.data.pref ? { pref: action.data.pref } : {} + ); + case at.TOP_SITES_PREFS_UPDATED: + return Object.assign({}, prevState, { pref: action.data.pref }); + case at.TOP_SITES_EDIT: + return Object.assign({}, prevState, { + editForm: { + index: action.data.index, + previewResponse: null, + }, + }); + case at.TOP_SITES_CANCEL_EDIT: + return Object.assign({}, prevState, { editForm: null }); + case at.TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL: + return Object.assign({}, prevState, { showSearchShortcutsForm: true }); + case at.TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL: + return Object.assign({}, prevState, { showSearchShortcutsForm: false }); + case at.PREVIEW_RESPONSE: + if ( + !prevState.editForm || + action.data.url !== prevState.editForm.previewUrl + ) { + return prevState; + } + return Object.assign({}, prevState, { + editForm: { + index: prevState.editForm.index, + previewResponse: action.data.preview, + previewUrl: action.data.url, + }, + }); + case at.PREVIEW_REQUEST: + if (!prevState.editForm) { + return prevState; + } + return Object.assign({}, prevState, { + editForm: { + index: prevState.editForm.index, + previewResponse: null, + previewUrl: action.data.url, + }, + }); + case at.PREVIEW_REQUEST_CANCEL: + if (!prevState.editForm) { + return prevState; + } + return Object.assign({}, prevState, { + editForm: { + index: prevState.editForm.index, + previewResponse: null, + }, + }); + case at.SCREENSHOT_UPDATED: + newRows = prevState.rows.map(row => { + if (row && row.url === action.data.url) { + hasMatch = true; + return Object.assign({}, row, { screenshot: action.data.screenshot }); + } + return row; + }); + return hasMatch + ? Object.assign({}, prevState, { rows: newRows }) + : prevState; + case at.PLACES_BOOKMARK_ADDED: + if (!action.data) { + return prevState; + } + newRows = prevState.rows.map(site => { + if (site && site.url === action.data.url) { + const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data; + return Object.assign({}, site, { + bookmarkGuid, + bookmarkTitle, + bookmarkDateCreated: dateAdded, + }); + } + return site; + }); + return Object.assign({}, prevState, { rows: newRows }); + case at.PLACES_BOOKMARKS_REMOVED: + if (!action.data) { + return prevState; + } + newRows = prevState.rows.map(site => { + if (site && action.data.urls.includes(site.url)) { + const newSite = Object.assign({}, site); + delete newSite.bookmarkGuid; + delete newSite.bookmarkTitle; + delete newSite.bookmarkDateCreated; + return newSite; + } + return site; + }); + return Object.assign({}, prevState, { rows: newRows }); + case at.PLACES_LINKS_DELETED: + if (!action.data) { + return prevState; + } + newRows = prevState.rows.filter( + site => !action.data.urls.includes(site.url) + ); + return Object.assign({}, prevState, { rows: newRows }); + case at.UPDATE_SEARCH_SHORTCUTS: + return { ...prevState, searchShortcuts: action.data.searchShortcuts }; + case at.SOV_UPDATED: + const sov = { + ready: action.data.ready, + positions: action.data.positions, + }; + return { ...prevState, sov }; + default: + return prevState; + } +} + +function Dialog(prevState = INITIAL_STATE.Dialog, action) { + switch (action.type) { + case at.DIALOG_OPEN: + return Object.assign({}, prevState, { visible: true, data: action.data }); + case at.DIALOG_CANCEL: + return Object.assign({}, prevState, { visible: false }); + case at.DELETE_HISTORY_URL: + return Object.assign({}, INITIAL_STATE.Dialog); + default: + return prevState; + } +} + +function Prefs(prevState = INITIAL_STATE.Prefs, action) { + let newValues; + switch (action.type) { + case at.PREFS_INITIAL_VALUES: + return Object.assign({}, prevState, { + initialized: true, + values: action.data, + }); + case at.PREF_CHANGED: + newValues = Object.assign({}, prevState.values); + newValues[action.data.name] = action.data.value; + return Object.assign({}, prevState, { values: newValues }); + default: + return prevState; + } +} + +function Sections(prevState = INITIAL_STATE.Sections, action) { + let hasMatch; + let newState; + switch (action.type) { + case at.SECTION_DEREGISTER: + return prevState.filter(section => section.id !== action.data); + case at.SECTION_REGISTER: + // If section exists in prevState, update it + newState = prevState.map(section => { + if (section && section.id === action.data.id) { + hasMatch = true; + return Object.assign({}, section, action.data); + } + return section; + }); + // Otherwise, append it + if (!hasMatch) { + const initialized = !!(action.data.rows && !!action.data.rows.length); + const section = Object.assign( + { title: "", rows: [], enabled: false }, + action.data, + { initialized } + ); + newState.push(section); + } + return newState; + case at.SECTION_UPDATE: + newState = prevState.map(section => { + if (section && section.id === action.data.id) { + // If the action is updating rows, we should consider initialized to be true. + // This can be overridden if initialized is defined in the action.data + const initialized = action.data.rows ? { initialized: true } : {}; + + // Make sure pinned cards stay at their current position when rows are updated. + // Disabling a section (SECTION_UPDATE with empty rows) does not retain pinned cards. + if ( + action.data.rows && + !!action.data.rows.length && + section.rows.find(card => card.pinned) + ) { + const rows = Array.from(action.data.rows); + section.rows.forEach((card, index) => { + if (card.pinned) { + // Only add it if it's not already there. + if (rows[index].guid !== card.guid) { + rows.splice(index, 0, card); + } + } + }); + return Object.assign( + {}, + section, + initialized, + Object.assign({}, action.data, { rows }) + ); + } + + return Object.assign({}, section, initialized, action.data); + } + return section; + }); + + if (!action.data.dedupeConfigurations) { + return newState; + } + + action.data.dedupeConfigurations.forEach(dedupeConf => { + newState = newState.map(section => { + if (section.id === dedupeConf.id) { + const dedupedRows = dedupeConf.dedupeFrom.reduce( + (rows, dedupeSectionId) => { + const dedupeSection = newState.find( + s => s.id === dedupeSectionId + ); + const [, newRows] = dedupe.group(dedupeSection.rows, rows); + return newRows; + }, + section.rows + ); + + return Object.assign({}, section, { rows: dedupedRows }); + } + + return section; + }); + }); + + return newState; + case at.SECTION_UPDATE_CARD: + return prevState.map(section => { + if (section && section.id === action.data.id && section.rows) { + const newRows = section.rows.map(card => { + if (card.url === action.data.url) { + return Object.assign({}, card, action.data.options); + } + return card; + }); + return Object.assign({}, section, { rows: newRows }); + } + return section; + }); + case at.PLACES_BOOKMARK_ADDED: + if (!action.data) { + return prevState; + } + return prevState.map(section => + Object.assign({}, section, { + rows: section.rows.map(item => { + // find the item within the rows that is attempted to be bookmarked + if (item.url === action.data.url) { + const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data; + return Object.assign({}, item, { + bookmarkGuid, + bookmarkTitle, + bookmarkDateCreated: dateAdded, + type: "bookmark", + }); + } + return item; + }), + }) + ); + case at.PLACES_SAVED_TO_POCKET: + if (!action.data) { + return prevState; + } + return prevState.map(section => + Object.assign({}, section, { + rows: section.rows.map(item => { + if (item.url === action.data.url) { + return Object.assign({}, item, { + open_url: action.data.open_url, + pocket_id: action.data.pocket_id, + title: action.data.title, + type: "pocket", + }); + } + return item; + }), + }) + ); + case at.PLACES_BOOKMARKS_REMOVED: + if (!action.data) { + return prevState; + } + return prevState.map(section => + Object.assign({}, section, { + rows: section.rows.map(item => { + // find the bookmark within the rows that is attempted to be removed + if (action.data.urls.includes(item.url)) { + const newSite = Object.assign({}, item); + delete newSite.bookmarkGuid; + delete newSite.bookmarkTitle; + delete newSite.bookmarkDateCreated; + if (!newSite.type || newSite.type === "bookmark") { + newSite.type = "history"; + } + return newSite; + } + return item; + }), + }) + ); + case at.PLACES_LINKS_DELETED: + if (!action.data) { + return prevState; + } + return prevState.map(section => + Object.assign({}, section, { + rows: section.rows.filter( + site => !action.data.urls.includes(site.url) + ), + }) + ); + case at.PLACES_LINK_BLOCKED: + if (!action.data) { + return prevState; + } + return prevState.map(section => + Object.assign({}, section, { + rows: section.rows.filter(site => site.url !== action.data.url), + }) + ); + case at.DELETE_FROM_POCKET: + case at.ARCHIVE_FROM_POCKET: + return prevState.map(section => + Object.assign({}, section, { + rows: section.rows.filter( + site => site.pocket_id !== action.data.pocket_id + ), + }) + ); + default: + return prevState; + } +} + +function Pocket(prevState = INITIAL_STATE.Pocket, action) { + switch (action.type) { + case at.POCKET_WAITING_FOR_SPOC: + return { ...prevState, waitingForSpoc: action.data }; + case at.POCKET_LOGGED_IN: + return { ...prevState, isUserLoggedIn: !!action.data }; + case at.POCKET_CTA: + return { + ...prevState, + pocketCta: { + ctaButton: action.data.cta_button, + ctaText: action.data.cta_text, + ctaUrl: action.data.cta_url, + useCta: action.data.use_cta, + }, + }; + default: + return prevState; + } +} + +function Personalization(prevState = INITIAL_STATE.Personalization, action) { + switch (action.type) { + case at.DISCOVERY_STREAM_PERSONALIZATION_LAST_UPDATED: + return { + ...prevState, + lastUpdated: action.data.lastUpdated, + }; + case at.DISCOVERY_STREAM_PERSONALIZATION_INIT: + return { + ...prevState, + initialized: true, + }; + case at.DISCOVERY_STREAM_PERSONALIZATION_RESET: + return { ...INITIAL_STATE.Personalization }; + default: + return prevState; + } +} + +// eslint-disable-next-line complexity +function DiscoveryStream(prevState = INITIAL_STATE.DiscoveryStream, action) { + // Return if action data is empty, or spocs or feeds data is not loaded + const isNotReady = () => + !action.data || !prevState.spocs.loaded || !prevState.feeds.loaded; + + const handlePlacements = handleSites => { + const { data, placements } = prevState.spocs; + const result = {}; + + const forPlacement = placement => { + const placementSpocs = data[placement.name]; + + if ( + !placementSpocs || + !placementSpocs.items || + !placementSpocs.items.length + ) { + return; + } + + result[placement.name] = { + ...placementSpocs, + items: handleSites(placementSpocs.items), + }; + }; + + if (!placements || !placements.length) { + [{ name: "spocs" }].forEach(forPlacement); + } else { + placements.forEach(forPlacement); + } + return result; + }; + + const nextState = handleSites => ({ + ...prevState, + spocs: { + ...prevState.spocs, + data: handlePlacements(handleSites), + }, + feeds: { + ...prevState.feeds, + data: Object.keys(prevState.feeds.data).reduce( + (accumulator, feed_url) => { + accumulator[feed_url] = { + data: { + ...prevState.feeds.data[feed_url].data, + recommendations: handleSites( + prevState.feeds.data[feed_url].data.recommendations + ), + }, + }; + return accumulator; + }, + {} + ), + }, + }); + + switch (action.type) { + case at.DISCOVERY_STREAM_CONFIG_CHANGE: + // Fall through to a separate action is so it doesn't trigger a listener update on init + case at.DISCOVERY_STREAM_CONFIG_SETUP: + return { ...prevState, config: action.data || {} }; + case at.DISCOVERY_STREAM_EXPERIMENT_DATA: + return { ...prevState, experimentData: action.data || {} }; + case at.DISCOVERY_STREAM_LAYOUT_UPDATE: + return { + ...prevState, + layout: action.data.layout || [], + }; + case at.DISCOVERY_STREAM_COLLECTION_DISMISSIBLE_TOGGLE: + return { + ...prevState, + isCollectionDismissible: action.data.value, + }; + case at.DISCOVERY_STREAM_PREFS_SETUP: + return { + ...prevState, + recentSavesEnabled: action.data.recentSavesEnabled, + pocketButtonEnabled: action.data.pocketButtonEnabled, + saveToPocketCard: action.data.saveToPocketCard, + hideDescriptions: action.data.hideDescriptions, + compactImages: action.data.compactImages, + imageGradient: action.data.imageGradient, + newSponsoredLabel: action.data.newSponsoredLabel, + titleLines: action.data.titleLines, + descLines: action.data.descLines, + readTime: action.data.readTime, + }; + case at.DISCOVERY_STREAM_RECENT_SAVES: + return { + ...prevState, + recentSavesData: action.data.recentSaves, + }; + case at.DISCOVERY_STREAM_POCKET_STATE_SET: + return { + ...prevState, + isUserLoggedIn: action.data.isUserLoggedIn, + }; + case at.HIDE_PRIVACY_INFO: + return { + ...prevState, + isPrivacyInfoModalVisible: false, + }; + case at.SHOW_PRIVACY_INFO: + return { + ...prevState, + isPrivacyInfoModalVisible: true, + }; + case at.DISCOVERY_STREAM_LAYOUT_RESET: + return { ...INITIAL_STATE.DiscoveryStream, config: prevState.config }; + case at.DISCOVERY_STREAM_FEEDS_UPDATE: + return { + ...prevState, + feeds: { + ...prevState.feeds, + loaded: true, + }, + }; + case at.DISCOVERY_STREAM_FEED_UPDATE: + const newData = {}; + newData[action.data.url] = action.data.feed; + return { + ...prevState, + feeds: { + ...prevState.feeds, + data: { + ...prevState.feeds.data, + ...newData, + }, + }, + }; + case at.DISCOVERY_STREAM_SPOCS_CAPS: + return { + ...prevState, + spocs: { + ...prevState.spocs, + frequency_caps: [...prevState.spocs.frequency_caps, ...action.data], + }, + }; + case at.DISCOVERY_STREAM_SPOCS_ENDPOINT: + return { + ...prevState, + spocs: { + ...INITIAL_STATE.DiscoveryStream.spocs, + spocs_endpoint: + action.data.url || + INITIAL_STATE.DiscoveryStream.spocs.spocs_endpoint, + }, + }; + case at.DISCOVERY_STREAM_SPOCS_PLACEMENTS: + return { + ...prevState, + spocs: { + ...prevState.spocs, + placements: + action.data.placements || + INITIAL_STATE.DiscoveryStream.spocs.placements, + }, + }; + case at.DISCOVERY_STREAM_SPOCS_UPDATE: + if (action.data) { + return { + ...prevState, + spocs: { + ...prevState.spocs, + lastUpdated: action.data.lastUpdated, + data: action.data.spocs, + loaded: true, + }, + }; + } + return prevState; + case at.DISCOVERY_STREAM_SPOC_BLOCKED: + return { + ...prevState, + spocs: { + ...prevState.spocs, + blocked: [...prevState.spocs.blocked, action.data.url], + }, + }; + case at.DISCOVERY_STREAM_LINK_BLOCKED: + return isNotReady() + ? prevState + : nextState(items => + items.filter(item => item.url !== action.data.url) + ); + + case at.PLACES_SAVED_TO_POCKET: + const addPocketInfo = item => { + if (item.url === action.data.url) { + return Object.assign({}, item, { + open_url: action.data.open_url, + pocket_id: action.data.pocket_id, + context_type: "pocket", + }); + } + return item; + }; + return isNotReady() + ? prevState + : nextState(items => items.map(addPocketInfo)); + + case at.DELETE_FROM_POCKET: + case at.ARCHIVE_FROM_POCKET: + return isNotReady() + ? prevState + : nextState(items => + items.filter(item => item.pocket_id !== action.data.pocket_id) + ); + + case at.PLACES_BOOKMARK_ADDED: + const updateBookmarkInfo = item => { + if (item.url === action.data.url) { + const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data; + return Object.assign({}, item, { + bookmarkGuid, + bookmarkTitle, + bookmarkDateCreated: dateAdded, + context_type: "bookmark", + }); + } + return item; + }; + return isNotReady() + ? prevState + : nextState(items => items.map(updateBookmarkInfo)); + + case at.PLACES_BOOKMARKS_REMOVED: + const removeBookmarkInfo = item => { + if (action.data.urls.includes(item.url)) { + const newSite = Object.assign({}, item); + delete newSite.bookmarkGuid; + delete newSite.bookmarkTitle; + delete newSite.bookmarkDateCreated; + if (!newSite.context_type || newSite.context_type === "bookmark") { + newSite.context_type = "removedBookmark"; + } + return newSite; + } + return item; + }; + return isNotReady() + ? prevState + : nextState(items => items.map(removeBookmarkInfo)); + case at.PREF_CHANGED: + if (action.data.name === PREF_COLLECTION_DISMISSIBLE) { + return { + ...prevState, + isCollectionDismissible: action.data.value, + }; + } + return prevState; + default: + return prevState; + } +} + +function Search(prevState = INITIAL_STATE.Search, action) { + switch (action.type) { + case at.DISABLE_SEARCH: + return Object.assign({ ...prevState, disable: true }); + case at.FAKE_FOCUS_SEARCH: + return Object.assign({ ...prevState, fakeFocus: true }); + case at.SHOW_SEARCH: + return Object.assign({ ...prevState, disable: false, fakeFocus: false }); + default: + return prevState; + } +} + +export const reducers = { + TopSites, + App, + ASRouter, + Prefs, + Dialog, + Sections, + Pocket, + Personalization, + DiscoveryStream, + Search, +}; diff --git a/browser/components/newtab/components.conf b/browser/components/newtab/components.conf new file mode 100644 index 0000000000..7064595496 --- /dev/null +++ b/browser/components/newtab/components.conf @@ -0,0 +1,14 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Classes = [ + { + 'cid': '{dfcd2adc-7867-4d3a-ba70-17501f208142}', + 'contract_ids': ['@mozilla.org/browser/aboutnewtab-service;1'], + 'esModule': 'resource:///modules/AboutNewTabService.sys.mjs', + 'constructor': 'AboutNewTabStubService', + }, +] diff --git a/browser/components/newtab/components/CustomElements/paragraph.js b/browser/components/newtab/components/CustomElements/paragraph.js new file mode 100644 index 0000000000..dce8a229a4 --- /dev/null +++ b/browser/components/newtab/components/CustomElements/paragraph.js @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This is loaded into all XUL windows. Wrap in a block to prevent +// leaking to window scope. +{ + const { RemoteL10n } = ChromeUtils.importESModule( + "resource:///modules/asrouter/RemoteL10n.sys.mjs" + ); + class MozTextParagraph extends HTMLElement { + constructor() { + super(); + + this._content = null; + } + + get fluentAttributeValues() { + const attributes = {}; + for (let name of this.getAttributeNames()) { + if (name.startsWith("fluent-variable-")) { + let value = this.getAttribute(name); + // Attribute value is a string, in some cases that is not useful + // for example instantiating a Date object will fail. We try to + // convert all possible integers back. + if (value.match(/^\d+/)) { + value = parseInt(value, 10); + } + attributes[name.replace(/^fluent-variable-/, "")] = value; + } + } + + return attributes; + } + + render() { + if (this.getAttribute("fluent-remote-id") && this._content) { + RemoteL10n.l10n.setAttributes( + this._content, + this.getAttribute("fluent-remote-id"), + this.fluentAttributeValues + ); + } + } + + static get observedAttributes() { + return ["fluent-remote-id"]; + } + + attributeChangedCallback(name, oldValue, newValue) { + this.render(); + } + + connectedCallback() { + if (this.shadowRoot) { + this.render(); + return; + } + + const shadowRoot = this.attachShadow({ mode: "open" }); + this._content = document.createElement("span"); + shadowRoot.appendChild(this._content); + + this.render(); + RemoteL10n.l10n.translateFragment(this._content); + } + } + + customElements.define("remote-text", MozTextParagraph); +} diff --git a/browser/components/newtab/content-src/activity-stream.jsx b/browser/components/newtab/content-src/activity-stream.jsx new file mode 100644 index 0000000000..c588e8e850 --- /dev/null +++ b/browser/components/newtab/content-src/activity-stream.jsx @@ -0,0 +1,57 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { + actionCreators as ac, + actionTypes as at, +} from "common/Actions.sys.mjs"; +import { Base } from "content-src/components/Base/Base"; +import { DetectUserSessionStart } from "content-src/lib/detect-user-session-start"; +import { initStore } from "content-src/lib/init-store"; +import { Provider } from "react-redux"; +import React from "react"; +import ReactDOM from "react-dom"; +import { reducers } from "common/Reducers.sys.mjs"; + +export const NewTab = ({ store }) => ( + + + +); + +export function renderWithoutState() { + const store = initStore(reducers); + new DetectUserSessionStart(store).sendEventOrAddListener(); + + // If this document has already gone into the background by the time we've reached + // here, we can deprioritize requesting the initial state until the event loop + // frees up. If, however, the visibility changes, we then send the request. + let didRequest = false; + let requestIdleCallbackId = 0; + function doRequest() { + if (!didRequest) { + if (requestIdleCallbackId) { + cancelIdleCallback(requestIdleCallbackId); + } + didRequest = true; + store.dispatch(ac.AlsoToMain({ type: at.NEW_TAB_STATE_REQUEST })); + } + } + + if (document.hidden) { + requestIdleCallbackId = requestIdleCallback(doRequest); + addEventListener("visibilitychange", doRequest, { once: true }); + } else { + doRequest(); + } + + ReactDOM.hydrate(, document.getElementById("root")); +} + +export function renderCache(initialState) { + const store = initStore(reducers, initialState); + new DetectUserSessionStart(store).sendEventOrAddListener(); + + ReactDOM.hydrate(, document.getElementById("root")); +} diff --git a/browser/components/newtab/content-src/components/A11yLinkButton/A11yLinkButton.jsx b/browser/components/newtab/content-src/components/A11yLinkButton/A11yLinkButton.jsx new file mode 100644 index 0000000000..3aab52cdff --- /dev/null +++ b/browser/components/newtab/content-src/components/A11yLinkButton/A11yLinkButton.jsx @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; + +export function A11yLinkButton(props) { + // function for merging classes, if necessary + let className = "a11y-link-button"; + if (props.className) { + className += ` ${props.className}`; + } + return ( + + ); +} diff --git a/browser/components/newtab/content-src/components/A11yLinkButton/_A11yLinkButton.scss b/browser/components/newtab/content-src/components/A11yLinkButton/_A11yLinkButton.scss new file mode 100644 index 0000000000..c87fc93b60 --- /dev/null +++ b/browser/components/newtab/content-src/components/A11yLinkButton/_A11yLinkButton.scss @@ -0,0 +1,13 @@ + +.a11y-link-button { + border: 0; + padding: 0; + cursor: pointer; + text-align: unset; + color: var(--newtab-primary-action-background); + + &:hover, + &:focus { + text-decoration: underline; + } +} diff --git a/browser/components/newtab/content-src/components/Base/Base.jsx b/browser/components/newtab/content-src/components/Base/Base.jsx new file mode 100644 index 0000000000..0580267f26 --- /dev/null +++ b/browser/components/newtab/content-src/components/Base/Base.jsx @@ -0,0 +1,262 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { + actionCreators as ac, + actionTypes as at, +} from "common/Actions.sys.mjs"; +import { DiscoveryStreamAdmin } from "content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin"; +import { ConfirmDialog } from "content-src/components/ConfirmDialog/ConfirmDialog"; +import { connect } from "react-redux"; +import { DiscoveryStreamBase } from "content-src/components/DiscoveryStreamBase/DiscoveryStreamBase"; +import { ErrorBoundary } from "content-src/components/ErrorBoundary/ErrorBoundary"; +import { CustomizeMenu } from "content-src/components/CustomizeMenu/CustomizeMenu"; +import React from "react"; +import { Search } from "content-src/components/Search/Search"; +import { Sections } from "content-src/components/Sections/Sections"; + +export const PrefsButton = ({ onClick, icon }) => ( +
+
+); + +// Returns a function will not be continuously triggered when called. The +// function will be triggered if called again after `wait` milliseconds. +function debounce(func, wait) { + let timer; + return (...args) => { + if (timer) { + return; + } + + let wakeUp = () => { + timer = null; + }; + + timer = setTimeout(wakeUp, wait); + func.apply(this, args); + }; +} + +export class _Base extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + message: {}, + }; + this.notifyContent = this.notifyContent.bind(this); + } + + notifyContent(state) { + this.setState(state); + } + + componentWillUnmount() { + this.updateTheme(); + } + + componentWillUpdate() { + this.updateTheme(); + } + + updateTheme() { + const bodyClassName = [ + "activity-stream", + // If we skipped the about:welcome overlay and removed the CSS classes + // we don't want to add them back to the Activity Stream view + document.body.classList.contains("inline-onboarding") + ? "inline-onboarding" + : "", + ] + .filter(v => v) + .join(" "); + global.document.body.className = bodyClassName; + } + + render() { + const { props } = this; + const { App } = props; + const isDevtoolsEnabled = props.Prefs.values["asrouter.devtoolsEnabled"]; + + if (!App.initialized) { + return null; + } + + return ( + + + + {isDevtoolsEnabled ? ( + + ) : null} + + + ); + } +} + +export class BaseContent extends React.PureComponent { + constructor(props) { + super(props); + this.openPreferences = this.openPreferences.bind(this); + this.openCustomizationMenu = this.openCustomizationMenu.bind(this); + this.closeCustomizationMenu = this.closeCustomizationMenu.bind(this); + this.handleOnKeyDown = this.handleOnKeyDown.bind(this); + this.onWindowScroll = debounce(this.onWindowScroll.bind(this), 5); + this.setPref = this.setPref.bind(this); + this.state = { fixedSearch: false }; + } + + componentDidMount() { + global.addEventListener("scroll", this.onWindowScroll); + global.addEventListener("keydown", this.handleOnKeyDown); + } + + componentWillUnmount() { + global.removeEventListener("scroll", this.onWindowScroll); + global.removeEventListener("keydown", this.handleOnKeyDown); + } + + onWindowScroll() { + const prefs = this.props.Prefs.values; + const SCROLL_THRESHOLD = prefs["logowordmark.alwaysVisible"] ? 179 : 34; + if (global.scrollY > SCROLL_THRESHOLD && !this.state.fixedSearch) { + this.setState({ fixedSearch: true }); + } else if (global.scrollY <= SCROLL_THRESHOLD && this.state.fixedSearch) { + this.setState({ fixedSearch: false }); + } + } + + openPreferences() { + this.props.dispatch(ac.OnlyToMain({ type: at.SETTINGS_OPEN })); + this.props.dispatch(ac.UserEvent({ event: "OPEN_NEWTAB_PREFS" })); + } + + openCustomizationMenu() { + this.props.dispatch({ type: at.SHOW_PERSONALIZE }); + this.props.dispatch(ac.UserEvent({ event: "SHOW_PERSONALIZE" })); + } + + closeCustomizationMenu() { + if (this.props.App.customizeMenuVisible) { + this.props.dispatch({ type: at.HIDE_PERSONALIZE }); + this.props.dispatch(ac.UserEvent({ event: "HIDE_PERSONALIZE" })); + } + } + + handleOnKeyDown(e) { + if (e.key === "Escape") { + this.closeCustomizationMenu(); + } + } + + setPref(pref, value) { + this.props.dispatch(ac.SetPref(pref, value)); + } + + render() { + const { props } = this; + const { App } = props; + const { initialized, customizeMenuVisible } = App; + const prefs = props.Prefs.values; + + const isDiscoveryStream = + props.DiscoveryStream.config && props.DiscoveryStream.config.enabled; + let filteredSections = props.Sections.filter( + section => section.id !== "topstories" + ); + + const pocketEnabled = + prefs["feeds.section.topstories"] && prefs["feeds.system.topstories"]; + const noSectionsEnabled = + !prefs["feeds.topsites"] && + !pocketEnabled && + filteredSections.filter(section => section.enabled).length === 0; + const searchHandoffEnabled = prefs["improvesearch.handoffToAwesomebar"]; + const enabledSections = { + topSitesEnabled: prefs["feeds.topsites"], + pocketEnabled: prefs["feeds.section.topstories"], + highlightsEnabled: prefs["feeds.section.highlights"], + showSponsoredTopSitesEnabled: prefs.showSponsoredTopSites, + showSponsoredPocketEnabled: prefs.showSponsored, + showRecentSavesEnabled: prefs.showRecentSaves, + topSitesRowsCount: prefs.topSitesRows, + }; + + const pocketRegion = prefs["feeds.system.topstories"]; + const mayHaveSponsoredStories = prefs["system.showSponsored"]; + const { mayHaveSponsoredTopSites } = prefs; + + const outerClassName = [ + "outer-wrapper", + isDiscoveryStream && pocketEnabled && "ds-outer-wrapper-search-alignment", + isDiscoveryStream && "ds-outer-wrapper-breakpoint-override", + prefs.showSearch && + this.state.fixedSearch && + !noSectionsEnabled && + "fixed-search", + prefs.showSearch && noSectionsEnabled && "only-search", + prefs["logowordmark.alwaysVisible"] && "visible-logo", + ] + .filter(v => v) + .join(" "); + + return ( +
+ + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions*/} +
+
+ {prefs.showSearch && ( +
+ + + +
+ )} +
+ {isDiscoveryStream ? ( + + + + ) : ( + + )} +
+ +
+
+
+ ); + } +} + +export const Base = connect(state => ({ + App: state.App, + Prefs: state.Prefs, + Sections: state.Sections, + DiscoveryStream: state.DiscoveryStream, + Search: state.Search, +}))(_Base); diff --git a/browser/components/newtab/content-src/components/Base/_Base.scss b/browser/components/newtab/content-src/components/Base/_Base.scss new file mode 100644 index 0000000000..1282173df5 --- /dev/null +++ b/browser/components/newtab/content-src/components/Base/_Base.scss @@ -0,0 +1,126 @@ +.outer-wrapper { + color: var(--newtab-text-primary-color); + display: flex; + flex-grow: 1; + min-height: 100vh; + padding: ($section-spacing + $section-vertical-padding) $base-gutter $base-gutter; + + &.ds-outer-wrapper-breakpoint-override { + padding: 30px 0 32px; + + @media(min-width: $break-point-medium) { + padding: 30px 32px 32px; + } + } + + &.only-search { + display: block; + padding-top: 134px; + } + + a { + color: var(--newtab-primary-action-background); + } +} + +main { + margin: auto; + width: $wrapper-default-width; + padding: 0; + + section { + margin-bottom: $section-spacing; + position: relative; + } + + .hide-main & { + visibility: hidden; + } + + @media (min-width: $break-point-medium) { + width: $wrapper-max-width-medium; + } + + @media (min-width: $break-point-large) { + width: $wrapper-max-width-large; + } + + @media (min-width: $break-point-widest) { + width: $wrapper-max-width-widest; + } +} + +.ds-outer-wrapper-search-alignment { + main { + // This override is to ensure while Discovery Stream loads, + // the search bar does not jump around. (it sticks to the top) + margin: 0 auto; + } +} + +.ds-outer-wrapper-breakpoint-override { + main { + width: 266px; + padding-bottom: 0; + + @media (min-width: $break-point-medium) { + width: 510px; + } + + @media (min-width: $break-point-large) { + width: 746px; + } + + @media (min-width: $break-point-widest) { + width: 986px; + } + } +} + +.base-content-fallback { + // Make the error message be centered against the viewport + height: 100vh; +} + +.body-wrapper { + // Hide certain elements so the page structure is fixed, e.g., placeholders, + // while avoiding flashes of changing content, e.g., icons and text + $selectors-to-hide: '.section-title, .sections-list .section:last-of-type, .topics'; + + #{$selectors-to-hide} { + opacity: 0; + } + + &.on { + #{$selectors-to-hide} { + opacity: 1; + } + } +} + +.non-collapsible-section { + padding: 0 $section-horizontal-padding; +} + +.prefs-button { + button { + background-color: transparent; + border: 0; + border-radius: 2px; + cursor: pointer; + inset-inline-end: 15px; + padding: 15px; + position: fixed; + top: 15px; + z-index: 1000; + + &:hover, + &:focus { + background-color: var(--newtab-element-hover-color); + } + + &:active { + background-color: var(--newtab-element-active-color); + } + } +} diff --git a/browser/components/newtab/content-src/components/Card/Card.jsx b/browser/components/newtab/content-src/components/Card/Card.jsx new file mode 100644 index 0000000000..9d03377f1b --- /dev/null +++ b/browser/components/newtab/content-src/components/Card/Card.jsx @@ -0,0 +1,362 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { + actionCreators as ac, + actionTypes as at, +} from "common/Actions.sys.mjs"; +import { cardContextTypes } from "./types"; +import { connect } from "react-redux"; +import { ContextMenuButton } from "content-src/components/ContextMenu/ContextMenuButton"; +import { LinkMenu } from "content-src/components/LinkMenu/LinkMenu"; +import React from "react"; +import { ScreenshotUtils } from "content-src/lib/screenshot-utils"; + +// Keep track of pending image loads to only request once +const gImageLoading = new Map(); + +/** + * Card component. + * Cards are found within a Section component and contain information about a link such + * as preview image, page title, page description, and some context about if the page + * was visited, bookmarked, trending etc... + * Each Section can make an unordered list of Cards which will create one instane of + * this class. Each card will then get a context menu which reflects the actions that + * can be done on this Card. + */ +export class _Card extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + activeCard: null, + imageLoaded: false, + cardImage: null, + }; + this.onMenuButtonUpdate = this.onMenuButtonUpdate.bind(this); + this.onLinkClick = this.onLinkClick.bind(this); + } + + /** + * Helper to conditionally load an image and update state when it loads. + */ + async maybeLoadImage() { + // No need to load if it's already loaded or no image + const { cardImage } = this.state; + if (!cardImage) { + return; + } + + const imageUrl = cardImage.url; + if (!this.state.imageLoaded) { + // Initialize a promise to share a load across multiple card updates + if (!gImageLoading.has(imageUrl)) { + const loaderPromise = new Promise((resolve, reject) => { + const loader = new Image(); + loader.addEventListener("load", resolve); + loader.addEventListener("error", reject); + loader.src = imageUrl; + }); + + // Save and remove the promise only while it's pending + gImageLoading.set(imageUrl, loaderPromise); + loaderPromise + .catch(ex => ex) + .then(() => gImageLoading.delete(imageUrl)) + .catch(); + } + + // Wait for the image whether just started loading or reused promise + try { + await gImageLoading.get(imageUrl); + } catch (ex) { + // Ignore the failed image without changing state + return; + } + + // Only update state if we're still waiting to load the original image + if ( + ScreenshotUtils.isRemoteImageLocal( + this.state.cardImage, + this.props.link.image + ) && + !this.state.imageLoaded + ) { + this.setState({ imageLoaded: true }); + } + } + } + + /** + * Helper to obtain the next state based on nextProps and prevState. + * + * NOTE: Rename this method to getDerivedStateFromProps when we update React + * to >= 16.3. We will need to update tests as well. We cannot rename this + * method to getDerivedStateFromProps now because there is a mismatch in + * the React version that we are using for both testing and production. + * (i.e. react-test-render => "16.3.2", react => "16.2.0"). + * + * See https://github.com/airbnb/enzyme/blob/master/packages/enzyme-adapter-react-16/package.json#L43. + */ + static getNextStateFromProps(nextProps, prevState) { + const { image } = nextProps.link; + const imageInState = ScreenshotUtils.isRemoteImageLocal( + prevState.cardImage, + image + ); + let nextState = null; + + // Image is updating. + if (!imageInState && nextProps.link) { + nextState = { imageLoaded: false }; + } + + if (imageInState) { + return nextState; + } + + // Since image was updated, attempt to revoke old image blob URL, if it exists. + ScreenshotUtils.maybeRevokeBlobObjectURL(prevState.cardImage); + + nextState = nextState || {}; + nextState.cardImage = ScreenshotUtils.createLocalImageObject(image); + + return nextState; + } + + onMenuButtonUpdate(isOpen) { + if (isOpen) { + this.setState({ activeCard: this.props.index }); + } else { + this.setState({ activeCard: null }); + } + } + + /** + * Report to telemetry additional information about the item. + */ + _getTelemetryInfo() { + // Filter out "history" type for being the default + if (this.props.link.type !== "history") { + return { value: { card_type: this.props.link.type } }; + } + + return null; + } + + onLinkClick(event) { + event.preventDefault(); + const { altKey, button, ctrlKey, metaKey, shiftKey } = event; + if (this.props.link.type === "download") { + this.props.dispatch( + ac.OnlyToMain({ + type: at.OPEN_DOWNLOAD_FILE, + data: Object.assign(this.props.link, { + event: { button, ctrlKey, metaKey, shiftKey }, + }), + }) + ); + } else { + this.props.dispatch( + ac.OnlyToMain({ + type: at.OPEN_LINK, + data: Object.assign(this.props.link, { + event: { altKey, button, ctrlKey, metaKey, shiftKey }, + }), + }) + ); + } + if (this.props.isWebExtension) { + this.props.dispatch( + ac.WebExtEvent(at.WEBEXT_CLICK, { + source: this.props.eventSource, + url: this.props.link.url, + action_position: this.props.index, + }) + ); + } else { + this.props.dispatch( + ac.UserEvent( + Object.assign( + { + event: "CLICK", + source: this.props.eventSource, + action_position: this.props.index, + }, + this._getTelemetryInfo() + ) + ) + ); + + if (this.props.shouldSendImpressionStats) { + this.props.dispatch( + ac.ImpressionStats({ + source: this.props.eventSource, + click: 0, + tiles: [{ id: this.props.link.guid, pos: this.props.index }], + }) + ); + } + } + } + + componentDidMount() { + this.maybeLoadImage(); + } + + componentDidUpdate() { + this.maybeLoadImage(); + } + + // NOTE: Remove this function when we update React to >= 16.3 since React will + // call getDerivedStateFromProps automatically. We will also need to + // rename getNextStateFromProps to getDerivedStateFromProps. + componentWillMount() { + const nextState = _Card.getNextStateFromProps(this.props, this.state); + if (nextState) { + this.setState(nextState); + } + } + + // NOTE: Remove this function when we update React to >= 16.3 since React will + // call getDerivedStateFromProps automatically. We will also need to + // rename getNextStateFromProps to getDerivedStateFromProps. + componentWillReceiveProps(nextProps) { + const nextState = _Card.getNextStateFromProps(nextProps, this.state); + if (nextState) { + this.setState(nextState); + } + } + + componentWillUnmount() { + ScreenshotUtils.maybeRevokeBlobObjectURL(this.state.cardImage); + } + + render() { + const { + index, + className, + link, + dispatch, + contextMenuOptions, + eventSource, + shouldSendImpressionStats, + } = this.props; + const { props } = this; + const title = link.title || link.hostname; + const isContextMenuOpen = this.state.activeCard === index; + // Display "now" as "trending" until we have new strings #3402 + const { icon, fluentID } = + cardContextTypes[link.type === "now" ? "trending" : link.type] || {}; + const hasImage = this.state.cardImage || link.hasImage; + const imageStyle = { + backgroundImage: this.state.cardImage + ? `url(${this.state.cardImage.url})` + : "none", + }; + const outerClassName = [ + "card-outer", + className, + isContextMenuOpen && "active", + props.placeholder && "placeholder", + ] + .filter(v => v) + .join(" "); + + return ( +
  • + +
  • + ); + } +} +_Card.defaultProps = { link: {} }; +export const Card = connect(state => ({ + platform: state.Prefs.values.platform, +}))(_Card); +export const PlaceholderCard = props => ( + +); diff --git a/browser/components/newtab/content-src/components/Card/_Card.scss b/browser/components/newtab/content-src/components/Card/_Card.scss new file mode 100644 index 0000000000..74288ff07f --- /dev/null +++ b/browser/components/newtab/content-src/components/Card/_Card.scss @@ -0,0 +1,333 @@ +@use 'sass:math'; + +/* stylelint-disable max-nesting-depth */ + +.card-outer { + @include context-menu-button; + + background: var(--newtab-background-color-secondary); + border-radius: $border-radius-new; + display: inline-block; + height: $card-height; + margin-inline-end: $base-gutter; + position: relative; + width: 100%; + + &:is(:focus):not(.placeholder) { + @include ds-focus; + + transition: none; + } + + &:hover { + box-shadow: none; + transition: none; + } + + &.placeholder { + background: transparent; + + .card-preview-image-outer, + .card-context { + display: none; + } + } + + .card { + border-radius: $border-radius-new; + box-shadow: $shadow-card; + height: 100%; + } + + > a { + color: inherit; + display: block; + height: 100%; + outline: none; + position: absolute; + width: 100%; + + &:is(:focus) { + .card { + @include ds-focus; + } + } + + &:is(.active, :focus) { + .card { + @include fade-in-card; + } + + .card-title { + color: var(--newtab-primary-action-background); + } + } + } + + &:is(:hover, :focus, .active):not(.placeholder) { + @include context-menu-button-hover; + + outline: none; + + .card-title { + color: var(--newtab-primary-action-background); + } + + .alternate ~ .card-host-name { + display: none; + } + + .card-host-name.alternate { + display: block; + } + } + + .card-preview-image-outer { + background-color: var(--newtab-element-secondary-color); + border-radius: $border-radius-new $border-radius-new 0 0; + height: $card-preview-image-height; + overflow: hidden; + position: relative; + + &::after { + border-bottom: 1px solid var(--newtab-card-hairline-color); + bottom: 0; + content: ''; + position: absolute; + width: 100%; + } + + .card-preview-image { + background-position: center; + background-repeat: no-repeat; + background-size: cover; + height: 100%; + opacity: 0; + transition: opacity 1s $photon-easing; + width: 100%; + + &.loaded { + opacity: 1; + } + } + } + + .card-details { + padding: 15px 16px 12px; + } + + .card-text { + max-height: 4 * $card-text-line-height + $card-title-margin; + overflow: hidden; + + &.no-host-name, + &.no-context { + max-height: 5 * $card-text-line-height + $card-title-margin; + } + + &.no-host-name.no-context { + max-height: 6 * $card-text-line-height + $card-title-margin; + } + + &:not(.no-description) .card-title { + max-height: 3 * $card-text-line-height; + overflow: hidden; + } + } + + .card-host-name { + color: var(--newtab-text-secondary-color); + font-size: 10px; + overflow: hidden; + padding-bottom: 4px; + text-overflow: ellipsis; + text-transform: uppercase; + white-space: nowrap; + } + + .card-host-name.alternate { display: none; } + + .card-title { + font-size: 14px; + font-weight: 600; + line-height: $card-text-line-height; + margin: 0 0 $card-title-margin; + word-wrap: break-word; + } + + .card-description { + font-size: 12px; + line-height: $card-text-line-height; + margin: 0; + overflow: hidden; + word-wrap: break-word; + } + + .card-context { + bottom: 0; + color: var(--newtab-text-secondary-color); + display: flex; + font-size: 11px; + inset-inline-start: 0; + padding: 9px 16px 9px 14px; + position: absolute; + } + + .card-context-icon { + fill: var(--newtab-text-secondary-color); + height: 22px; + margin-inline-end: 6px; + } + + .card-context-label { + flex-grow: 1; + line-height: 22px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} + +.normal-cards { + .card-outer { + // Wide layout styles + @media (min-width: $break-point-widest) { + $line-height: 23px; + + height: $card-height-large; + + .card-preview-image-outer { + height: $card-preview-image-height-large; + } + + .card-details { + padding: 13px 16px 12px; + } + + .card-text { + max-height: 6 * $line-height + $card-title-margin; + } + + .card-host-name { + font-size: 12px; + padding-bottom: 5px; + } + + .card-title { + font-size: 17px; + line-height: $line-height; + margin-bottom: 0; + } + + .card-text:not(.no-description) { + .card-title { + max-height: 3 * $line-height; + } + } + + .card-description { + font-size: 15px; + line-height: $line-height; + } + + .card-context { + bottom: 4px; + font-size: 14px; + } + } + } +} + +.compact-cards { + $card-detail-vertical-spacing: 12px; + $card-title-font-size: 12px; + + .card-outer { + height: $card-height-compact; + + .card-preview-image-outer { + height: $card-preview-image-height-compact; + } + + .card-details { + padding: $card-detail-vertical-spacing 16px; + } + + .card-host-name { + line-height: 10px; + } + + .card-text { + .card-title, + &:not(.no-description) .card-title { + font-size: $card-title-font-size; + line-height: $card-title-font-size + 1; + max-height: $card-title-font-size + 5; + overflow: hidden; + padding: 0 0 4px; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + .card-description { + display: none; + } + + .card-context { + $icon-size: 16px; + $container-size: 32px; + + background-color: var(--newtab-background-color-secondary); + border-radius: math.div($container-size, 2); + clip-path: inset(-1px -1px $container-size - ($card-height-compact - $card-preview-image-height-compact - 2 * $card-detail-vertical-spacing)); + height: $container-size; + width: $container-size; + padding: math.div($container-size - $icon-size, 2); + // The -1 at the end is so both opacity borders don't overlap, which causes bug 1629483 + top: $card-preview-image-height-compact - math.div($container-size, 2) - 1; + inset-inline-end: 12px; + inset-inline-start: auto; + + &::after { + border: 1px solid var(--newtab-card-hairline-color); + border-bottom: 0; + border-radius: math.div($container-size, 2) + 1 math.div($container-size, 2) + 1 0 0; + content: ''; + position: absolute; + height: math.div($container-size + 2, 2); + width: $container-size + 2; + top: -1px; + left: -1px; + } + + .card-context-icon { + margin-inline-end: 0; + height: $icon-size; + width: $icon-size; + + &.icon-bookmark-added { + fill: $bookmark-icon-fill; + } + + &.icon-download { + fill: $download-icon-fill; + } + + &.icon-pocket { + fill: $pocket-icon-fill; + } + } + + .card-context-label { + display: none; + } + } + } + + @media not all and (min-width: $break-point-widest) { + .hide-for-narrow { + display: none; + } + } +} diff --git a/browser/components/newtab/content-src/components/Card/types.js b/browser/components/newtab/content-src/components/Card/types.js new file mode 100644 index 0000000000..0b17eea408 --- /dev/null +++ b/browser/components/newtab/content-src/components/Card/types.js @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export const cardContextTypes = { + history: { + fluentID: "newtab-label-visited", + icon: "history-item", + }, + removedBookmark: { + fluentID: "newtab-label-removed-bookmark", + icon: "bookmark-removed", + }, + bookmark: { + fluentID: "newtab-label-bookmarked", + icon: "bookmark-added", + }, + trending: { + fluentID: "newtab-label-recommended", + icon: "trending", + }, + pocket: { + fluentID: "newtab-label-saved", + icon: "pocket", + }, + download: { + fluentID: "newtab-label-download", + icon: "download", + }, +}; diff --git a/browser/components/newtab/content-src/components/CollapsibleSection/CollapsibleSection.jsx b/browser/components/newtab/content-src/components/CollapsibleSection/CollapsibleSection.jsx new file mode 100644 index 0000000000..679e8e137f --- /dev/null +++ b/browser/components/newtab/content-src/components/CollapsibleSection/CollapsibleSection.jsx @@ -0,0 +1,116 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { ErrorBoundary } from "content-src/components/ErrorBoundary/ErrorBoundary"; +import { FluentOrText } from "content-src/components/FluentOrText/FluentOrText"; +import React from "react"; +import { connect } from "react-redux"; + +/** + * A section that can collapse. As of bug 1710937, it can no longer collapse. + * See bug 1727365 for follow-up work to simplify this component. + */ +export class _CollapsibleSection extends React.PureComponent { + constructor(props) { + super(props); + this.onBodyMount = this.onBodyMount.bind(this); + this.onMenuButtonMouseEnter = this.onMenuButtonMouseEnter.bind(this); + this.onMenuButtonMouseLeave = this.onMenuButtonMouseLeave.bind(this); + this.onMenuUpdate = this.onMenuUpdate.bind(this); + this.state = { + menuButtonHover: false, + showContextMenu: false, + }; + this.setContextMenuButtonRef = this.setContextMenuButtonRef.bind(this); + } + + setContextMenuButtonRef(element) { + this.contextMenuButtonRef = element; + } + + onBodyMount(node) { + this.sectionBody = node; + } + + onMenuButtonMouseEnter() { + this.setState({ menuButtonHover: true }); + } + + onMenuButtonMouseLeave() { + this.setState({ menuButtonHover: false }); + } + + onMenuUpdate(showContextMenu) { + this.setState({ showContextMenu }); + } + + render() { + const { isAnimating, maxHeight, menuButtonHover, showContextMenu } = + this.state; + const { id, collapsed, learnMore, title, subTitle } = this.props; + const active = menuButtonHover || showContextMenu; + let bodyStyle; + if (isAnimating && !collapsed) { + bodyStyle = { maxHeight }; + } else if (!isAnimating && collapsed) { + bodyStyle = { display: "none" }; + } + let titleStyle; + if (this.props.hideTitle) { + titleStyle = { visibility: "hidden" }; + } + const hasSubtitleClassName = subTitle ? `has-subtitle` : ``; + return ( +
    + + +
    + {this.props.children} +
    +
    +
    + ); + } +} + +_CollapsibleSection.defaultProps = { + document: global.document || { + addEventListener: () => {}, + removeEventListener: () => {}, + visibilityState: "hidden", + }, +}; + +export const CollapsibleSection = connect(state => ({ + Prefs: state.Prefs, +}))(_CollapsibleSection); diff --git a/browser/components/newtab/content-src/components/CollapsibleSection/_CollapsibleSection.scss b/browser/components/newtab/content-src/components/CollapsibleSection/_CollapsibleSection.scss new file mode 100644 index 0000000000..10cc58a1b1 --- /dev/null +++ b/browser/components/newtab/content-src/components/CollapsibleSection/_CollapsibleSection.scss @@ -0,0 +1,108 @@ +/* stylelint-disable max-nesting-depth */ + +.collapsible-section { + padding: $section-vertical-padding $section-horizontal-padding; + + .section-title-container { + margin: 0; + + &.has-subtitle { + display: flex; + flex-direction: column; + + @media (min-width: $break-point-large) { + flex-direction: row; + align-items: baseline; + justify-content: space-between; + } + } + } + + .section-title { + font-size: $section-title-font-size; + font-weight: 600; + color: var(--newtab-text-primary-color); + + &.grey-title { + color: var(--newtab-text-primary-color); + display: inline-block; + fill: var(--newtab-text-primary-color); + vertical-align: middle; + } + + .section-title-contents { + // Center "What's Pocket?" for "mobile" viewport + @media (max-width: $break-point-medium - 1) { + display: block; + + .learn-more-link-wrapper { + display: block; + text-align: center; + + .learn-more-link { + margin-inline-start: 0; + } + } + } + + vertical-align: top; + } + } + + .section-sub-title { + font-size: 14px; + line-height: 16px; + color: var(--newtab-text-secondary-color); + opacity: 0.3; + } + + .section-top-bar { + min-height: 19px; + margin-bottom: 13px; + position: relative; + } + + &.active { + background: var(--newtab-element-hover-color); + border-radius: 4px; + } + + .learn-more-link { + font-size: 13px; + margin-inline-start: 12px; + + a { + color: var(--newtab-primary-action-background); + } + } + + .section-body-fallback { + height: $card-height; + } + + .section-body { + // This is so the top sites favicon and card dropshadows don't get clipped during animation: + $horizontal-padding: 7px; + + margin: 0 (-$horizontal-padding); + padding: 0 $horizontal-padding; + + &.animating { + overflow: hidden; + pointer-events: none; + } + } + + &[data-section-id='topsites'] { + .section-top-bar { + display: none; + } + } + + // Hide first story card for the medium breakpoint to prevent orphaned third story + &[data-section-id='topstories'] .card-outer:first-child { + @media (min-width: $break-point-medium) and (max-width: $break-point-large - 1) { + display: none; + } + } +} diff --git a/browser/components/newtab/content-src/components/ComponentPerfTimer/ComponentPerfTimer.jsx b/browser/components/newtab/content-src/components/ComponentPerfTimer/ComponentPerfTimer.jsx new file mode 100644 index 0000000000..4efd8c712e --- /dev/null +++ b/browser/components/newtab/content-src/components/ComponentPerfTimer/ComponentPerfTimer.jsx @@ -0,0 +1,177 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { + actionCreators as ac, + actionTypes as at, +} from "common/Actions.sys.mjs"; +import { perfService as perfSvc } from "content-src/lib/perf-service"; +import React from "react"; + +// Currently record only a fixed set of sections. This will prevent data +// from custom sections from showing up or from topstories. +const RECORDED_SECTIONS = ["highlights", "topsites"]; + +export class ComponentPerfTimer extends React.Component { + constructor(props) { + super(props); + // Just for test dependency injection: + this.perfSvc = this.props.perfSvc || perfSvc; + + this._sendBadStateEvent = this._sendBadStateEvent.bind(this); + this._sendPaintedEvent = this._sendPaintedEvent.bind(this); + this._reportMissingData = false; + this._timestampHandled = false; + this._recordedFirstRender = false; + } + + componentDidMount() { + if (!RECORDED_SECTIONS.includes(this.props.id)) { + return; + } + + this._maybeSendPaintedEvent(); + } + + componentDidUpdate() { + if (!RECORDED_SECTIONS.includes(this.props.id)) { + return; + } + + this._maybeSendPaintedEvent(); + } + + /** + * Call the given callback after the upcoming frame paints. + * + * @note Both setTimeout and requestAnimationFrame are throttled when the page + * is hidden, so this callback may get called up to a second or so after the + * requestAnimationFrame "paint" for hidden tabs. + * + * Newtabs hidden while loading will presumably be fairly rare (other than + * preloaded tabs, which we will be filtering out on the server side), so such + * cases should get lost in the noise. + * + * If we decide that it's important to find out when something that's hidden + * has "painted", however, another option is to post a message to this window. + * That should happen even faster than setTimeout, and, at least as of this + * writing, it's not throttled in hidden windows in Firefox. + * + * @param {Function} callback + * + * @returns void + */ + _afterFramePaint(callback) { + requestAnimationFrame(() => setTimeout(callback, 0)); + } + + _maybeSendBadStateEvent() { + // Follow up bugs: + // https://github.com/mozilla/activity-stream/issues/3691 + if (!this.props.initialized) { + // Remember to report back when data is available. + this._reportMissingData = true; + } else if (this._reportMissingData) { + this._reportMissingData = false; + // Report how long it took for component to become initialized. + this._sendBadStateEvent(); + } + } + + _maybeSendPaintedEvent() { + // If we've already handled a timestamp, don't do it again. + if (this._timestampHandled || !this.props.initialized) { + return; + } + + // And if we haven't, we're doing so now, so remember that. Even if + // something goes wrong in the callback, we can't try again, as we'd be + // sending back the wrong data, and we have to do it here, so that other + // calls to this method while waiting for the next frame won't also try to + // handle it. + this._timestampHandled = true; + this._afterFramePaint(this._sendPaintedEvent); + } + + /** + * Triggered by call to render. Only first call goes through due to + * `_recordedFirstRender`. + */ + _ensureFirstRenderTsRecorded() { + // Used as t0 for recording how long component took to initialize. + if (!this._recordedFirstRender) { + this._recordedFirstRender = true; + // topsites_first_render_ts, highlights_first_render_ts. + const key = `${this.props.id}_first_render_ts`; + this.perfSvc.mark(key); + } + } + + /** + * Creates `SAVE_SESSION_PERF_DATA` with timestamp in ms + * of how much longer the data took to be ready for display than it would + * have been the ideal case. + * https://github.com/mozilla/ping-centre/issues/98 + */ + _sendBadStateEvent() { + // highlights_data_ready_ts, topsites_data_ready_ts. + const dataReadyKey = `${this.props.id}_data_ready_ts`; + this.perfSvc.mark(dataReadyKey); + + try { + const firstRenderKey = `${this.props.id}_first_render_ts`; + // value has to be Int32. + const value = parseInt( + this.perfSvc.getMostRecentAbsMarkStartByName(dataReadyKey) - + this.perfSvc.getMostRecentAbsMarkStartByName(firstRenderKey), + 10 + ); + this.props.dispatch( + ac.OnlyToMain({ + type: at.SAVE_SESSION_PERF_DATA, + // highlights_data_late_by_ms, topsites_data_late_by_ms. + data: { [`${this.props.id}_data_late_by_ms`]: value }, + }) + ); + } catch (ex) { + // If this failed, it's likely because the `privacy.resistFingerprinting` + // pref is true. + } + } + + _sendPaintedEvent() { + // Record first_painted event but only send if topsites. + if (this.props.id !== "topsites") { + return; + } + + // topsites_first_painted_ts. + const key = `${this.props.id}_first_painted_ts`; + this.perfSvc.mark(key); + + try { + const data = {}; + data[key] = this.perfSvc.getMostRecentAbsMarkStartByName(key); + + this.props.dispatch( + ac.OnlyToMain({ + type: at.SAVE_SESSION_PERF_DATA, + data, + }) + ); + } catch (ex) { + // If this failed, it's likely because the `privacy.resistFingerprinting` + // pref is true. We should at least not blow up, and should continue + // to set this._timestampHandled to avoid going through this again. + } + } + + render() { + if (RECORDED_SECTIONS.includes(this.props.id)) { + this._ensureFirstRenderTsRecorded(); + this._maybeSendBadStateEvent(); + } + return this.props.children; + } +} diff --git a/browser/components/newtab/content-src/components/ConfirmDialog/ConfirmDialog.jsx b/browser/components/newtab/content-src/components/ConfirmDialog/ConfirmDialog.jsx new file mode 100644 index 0000000000..f69e540079 --- /dev/null +++ b/browser/components/newtab/content-src/components/ConfirmDialog/ConfirmDialog.jsx @@ -0,0 +1,103 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { actionCreators as ac, actionTypes } from "common/Actions.sys.mjs"; +import { connect } from "react-redux"; +import React from "react"; + +/** + * ConfirmDialog component. + * One primary action button, one cancel button. + * + * Content displayed is controlled by `data` prop the component receives. + * Example: + * data: { + * // Any sort of data needed to be passed around by actions. + * payload: site.url, + * // Primary button AlsoToMain action. + * action: "DELETE_HISTORY_URL", + * // Primary button USerEvent action. + * userEvent: "DELETE", + * // Array of locale ids to display. + * message_body: ["confirm_history_delete_p1", "confirm_history_delete_notice_p2"], + * // Text for primary button. + * confirm_button_string_id: "menu_action_delete" + * }, + */ +export class _ConfirmDialog extends React.PureComponent { + constructor(props) { + super(props); + this._handleCancelBtn = this._handleCancelBtn.bind(this); + this._handleConfirmBtn = this._handleConfirmBtn.bind(this); + } + + _handleCancelBtn() { + this.props.dispatch({ type: actionTypes.DIALOG_CANCEL }); + this.props.dispatch( + ac.UserEvent({ + event: actionTypes.DIALOG_CANCEL, + source: this.props.data.eventSource, + }) + ); + } + + _handleConfirmBtn() { + this.props.data.onConfirm.forEach(this.props.dispatch); + } + + _renderModalMessage() { + const message_body = this.props.data.body_string_id; + + if (!message_body) { + return null; + } + + return ( + + {message_body.map(msg => ( +

    + ))} + + ); + } + + render() { + if (!this.props.visible) { + return null; + } + + return ( +

    +
    +
    +
    + {this.props.data.icon && ( + + )} + {this._renderModalMessage()} +
    +
    +
    +
    +
    + ); + } +} + +export const ConfirmDialog = connect(state => state.Dialog)(_ConfirmDialog); diff --git a/browser/components/newtab/content-src/components/ConfirmDialog/_ConfirmDialog.scss b/browser/components/newtab/content-src/components/ConfirmDialog/_ConfirmDialog.scss new file mode 100644 index 0000000000..ca9940ffc5 --- /dev/null +++ b/browser/components/newtab/content-src/components/ConfirmDialog/_ConfirmDialog.scss @@ -0,0 +1,68 @@ +.confirmation-dialog { + .modal { + box-shadow: $shadow-secondary; + left: 0; + margin: auto; + position: fixed; + right: 0; + top: 20%; + width: 400px; + } + + section { + margin: 0; + } + + .modal-message { + display: flex; + padding: 16px; + padding-bottom: 0; + + p { + margin: 0; + margin-bottom: 16px; + } + } + + .actions { + border: 0; + display: flex; + flex-wrap: nowrap; + padding: 0 16px; + + button { + margin-inline-end: 16px; + padding-inline-end: 18px; + padding-inline-start: 18px; + white-space: normal; + width: 50%; + + &.done { + margin-inline-end: 0; + margin-inline-start: 0; + } + } + } + + .icon { + margin-inline-end: 16px; + } +} + +.modal-overlay { + background: var(--newtab-overlay-color); + height: 100%; + left: 0; + position: fixed; + top: 0; + width: 100%; + z-index: 11001; +} + +.modal { + background: var(--newtab-background-color-secondary); + border: $border-secondary; + border-radius: 5px; + font-size: 15px; + z-index: 11002; +} diff --git a/browser/components/newtab/content-src/components/ContextMenu/ContextMenu.jsx b/browser/components/newtab/content-src/components/ContextMenu/ContextMenu.jsx new file mode 100644 index 0000000000..5ea6a57f71 --- /dev/null +++ b/browser/components/newtab/content-src/components/ContextMenu/ContextMenu.jsx @@ -0,0 +1,176 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; +import { connect } from "react-redux"; + +export class ContextMenu extends React.PureComponent { + constructor(props) { + super(props); + this.hideContext = this.hideContext.bind(this); + this.onShow = this.onShow.bind(this); + this.onClick = this.onClick.bind(this); + } + + hideContext() { + this.props.onUpdate(false); + } + + onShow() { + if (this.props.onShow) { + this.props.onShow(); + } + } + + componentDidMount() { + this.onShow(); + setTimeout(() => { + global.addEventListener("click", this.hideContext); + }, 0); + } + + componentWillUnmount() { + global.removeEventListener("click", this.hideContext); + } + + onClick(event) { + // Eat all clicks on the context menu so they don't bubble up to window. + // This prevents the context menu from closing when clicking disabled items + // or the separators. + event.stopPropagation(); + } + + render() { + // Disabling focus on the menu span allows the first tab to focus on the first menu item instead of the wrapper. + return ( + // eslint-disable-next-line jsx-a11y/interactive-supports-focus + +
      + {this.props.options.map((option, i) => + option.type === "separator" ? ( +
    • + ) : ( + option.type !== "empty" && ( + + ) + ) + )} +
    +
    + ); + } +} + +export class _ContextMenuItem extends React.PureComponent { + constructor(props) { + super(props); + this.onClick = this.onClick.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + this.onKeyUp = this.onKeyUp.bind(this); + this.focusFirst = this.focusFirst.bind(this); + } + + onClick(event) { + this.props.hideContext(); + this.props.option.onClick(event); + } + + // Focus the first menu item if the menu was accessed via the keyboard. + focusFirst(button) { + if (this.props.keyboardAccess && button) { + button.focus(); + } + } + + // This selects the correct node based on the key pressed + focusSibling(target, key) { + const parent = target.parentNode; + const closestSiblingSelector = + key === "ArrowUp" ? "previousSibling" : "nextSibling"; + if (!parent[closestSiblingSelector]) { + return; + } + if (parent[closestSiblingSelector].firstElementChild) { + parent[closestSiblingSelector].firstElementChild.focus(); + } else { + parent[closestSiblingSelector][ + closestSiblingSelector + ].firstElementChild.focus(); + } + } + + onKeyDown(event) { + const { option } = this.props; + switch (event.key) { + case "Tab": + // tab goes down in context menu, shift + tab goes up in context menu + // if we're on the last item, one more tab will close the context menu + // similarly, if we're on the first item, one more shift + tab will close it + if ( + (event.shiftKey && option.first) || + (!event.shiftKey && option.last) + ) { + this.props.hideContext(); + } + break; + case "ArrowUp": + case "ArrowDown": + event.preventDefault(); + this.focusSibling(event.target, event.key); + break; + case "Enter": + case " ": + event.preventDefault(); + this.props.hideContext(); + option.onClick(); + break; + case "Escape": + this.props.hideContext(); + break; + } + } + + // Prevents the default behavior of spacebar + // scrolling the page & auto-triggering buttons. + onKeyUp(event) { + if (event.key === " ") { + event.preventDefault(); + } + } + + render() { + const { option } = this.props; + return ( +
  • + +
  • + ); + } +} + +export const ContextMenuItem = connect(state => ({ + Prefs: state.Prefs, +}))(_ContextMenuItem); diff --git a/browser/components/newtab/content-src/components/ContextMenu/ContextMenuButton.jsx b/browser/components/newtab/content-src/components/ContextMenu/ContextMenuButton.jsx new file mode 100644 index 0000000000..0364f5386a --- /dev/null +++ b/browser/components/newtab/content-src/components/ContextMenu/ContextMenuButton.jsx @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; + +export class ContextMenuButton extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + showContextMenu: false, + contextMenuKeyboard: false, + }; + this.onClick = this.onClick.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + this.onUpdate = this.onUpdate.bind(this); + } + + openContextMenu(isKeyBoard, event) { + if (this.props.onUpdate) { + this.props.onUpdate(true); + } + this.setState({ + showContextMenu: true, + contextMenuKeyboard: isKeyBoard, + }); + } + + onClick(event) { + event.preventDefault(); + this.openContextMenu(false, event); + } + + onKeyDown(event) { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + this.openContextMenu(true, event); + } + } + + onUpdate(showContextMenu) { + if (this.props.onUpdate) { + this.props.onUpdate(showContextMenu); + } + this.setState({ showContextMenu }); + } + + render() { + const { tooltipArgs, tooltip, children, refFunction } = this.props; + const { showContextMenu, contextMenuKeyboard } = this.state; + + return ( + +
    + + ); + } +} diff --git a/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx b/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx new file mode 100644 index 0000000000..3d33f6fde7 --- /dev/null +++ b/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx @@ -0,0 +1,85 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { BackgroundsSection } from "content-src/components/CustomizeMenu/BackgroundsSection/BackgroundsSection"; +import { ContentSection } from "content-src/components/CustomizeMenu/ContentSection/ContentSection"; +import { connect } from "react-redux"; +import React from "react"; +import { CSSTransition } from "react-transition-group"; + +export class _CustomizeMenu extends React.PureComponent { + constructor(props) { + super(props); + this.onEntered = this.onEntered.bind(this); + this.onExited = this.onExited.bind(this); + } + + onEntered() { + if (this.closeButton) { + this.closeButton.focus(); + } + } + + onExited() { + if (this.openButton) { + this.openButton.focus(); + } + } + + render() { + return ( + + + ; + } +} + +export class TogglePrefCheckbox extends React.PureComponent { + constructor(props) { + super(props); + this.onChange = this.onChange.bind(this); + } + + onChange(event) { + this.props.onChange(this.props.pref, event.target.checked); + } + + render() { + return ( + <> + {" "} + {this.props.pref}{" "} + + ); + } +} + +export class Personalization extends React.PureComponent { + constructor(props) { + super(props); + this.togglePersonalization = this.togglePersonalization.bind(this); + } + + togglePersonalization() { + this.props.dispatch( + ac.OnlyToMain({ + type: at.DISCOVERY_STREAM_PERSONALIZATION_TOGGLE, + }) + ); + } + + render() { + const { lastUpdated, initialized } = this.props.state.Personalization; + return ( + + + + + + + + + + + + + + + +
    + + Personalization Last Updated{relativeTime(lastUpdated) || "(no data)"}Personalization Initialized{initialized ? "true" : "false"}
    +
    + ); + } +} + +export class DiscoveryStreamAdminUI extends React.PureComponent { + constructor(props) { + super(props); + this.restorePrefDefaults = this.restorePrefDefaults.bind(this); + this.setConfigValue = this.setConfigValue.bind(this); + this.expireCache = this.expireCache.bind(this); + this.refreshCache = this.refreshCache.bind(this); + this.idleDaily = this.idleDaily.bind(this); + this.systemTick = this.systemTick.bind(this); + this.syncRemoteSettings = this.syncRemoteSettings.bind(this); + this.onStoryToggle = this.onStoryToggle.bind(this); + this.state = { + toggledStories: {}, + }; + } + + setConfigValue(name, value) { + this.props.dispatch( + ac.OnlyToMain({ + type: at.DISCOVERY_STREAM_CONFIG_SET_VALUE, + data: { name, value }, + }) + ); + } + + restorePrefDefaults(event) { + this.props.dispatch( + ac.OnlyToMain({ + type: at.DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS, + }) + ); + } + + refreshCache() { + const { config } = this.props.state.DiscoveryStream; + this.props.dispatch( + ac.OnlyToMain({ + type: at.DISCOVERY_STREAM_CONFIG_CHANGE, + data: config, + }) + ); + } + + dispatchSimpleAction(type) { + this.props.dispatch( + ac.OnlyToMain({ + type, + }) + ); + } + + systemTick() { + this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_SYSTEM_TICK); + } + + expireCache() { + this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_EXPIRE_CACHE); + } + + idleDaily() { + this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_IDLE_DAILY); + } + + syncRemoteSettings() { + this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_SYNC_RS); + } + + renderComponent(width, component) { + return ( + + + + + + + + + + + {component.feed && this.renderFeed(component.feed)} + +
    Type{component.type}Width{width}
    + ); + } + + renderFeedData(url) { + const { feeds } = this.props.state.DiscoveryStream; + const feed = feeds.data[url].data; + return ( + +

    Feed url: {url}

    + + + {feed.recommendations?.map(story => this.renderStoryData(story))} + +
    +
    + ); + } + + renderFeedsData() { + const { feeds } = this.props.state.DiscoveryStream; + return ( + + {Object.keys(feeds.data).map(url => this.renderFeedData(url))} + + ); + } + + renderSpocs() { + const { spocs } = this.props.state.DiscoveryStream; + let spocsData = []; + if (spocs.data && spocs.data.spocs && spocs.data.spocs.items) { + spocsData = spocs.data.spocs.items || []; + } + + return ( + + + + + + + + + + + + +
    spocs_endpoint{spocs.spocs_endpoint}Data last fetched{relativeTime(spocs.lastUpdated)}
    +

    Spoc data

    + + {spocsData.map(spoc => this.renderStoryData(spoc))} +
    +

    Spoc frequency caps

    + + + {spocs.frequency_caps.map(spoc => this.renderStoryData(spoc))} + +
    +
    + ); + } + + onStoryToggle(story) { + const { toggledStories } = this.state; + this.setState({ + toggledStories: { + ...toggledStories, + [story.id]: !toggledStories[story.id], + }, + }); + } + + renderStoryData(story) { + let storyData = ""; + if (this.state.toggledStories[story.id]) { + storyData = JSON.stringify(story, null, 2); + } + return ( + + + + {story.id}
    +
    + + + +
    {storyData}
    + + + ); + } + + renderFeed(feed) { + const { feeds } = this.props.state.DiscoveryStream; + if (!feed.url) { + return null; + } + return ( + + + Feed url + {feed.url} + + + Data last fetched + + {relativeTime( + feeds.data[feed.url] ? feeds.data[feed.url].lastUpdated : null + ) || "(no data)"} + + + + ); + } + + render() { + const prefToggles = "enabled collapsible".split(" "); + const { config, layout } = this.props.state.DiscoveryStream; + const personalized = + this.props.otherPrefs["discoverystream.personalization.enabled"]; + return ( +
    + {" "} + +
    + {" "} + {" "} + +
    + + + + {prefToggles.map(pref => ( + + + + ))} + +
    + +
    +

    Layout

    + {layout.map((row, rowIndex) => ( +
    + {row.components.map((component, componentIndex) => ( +
    + {this.renderComponent(row.width, component)} +
    + ))} +
    + ))} +

    Personalization

    + +

    Spocs

    + {this.renderSpocs()} +

    Feeds Data

    + {this.renderFeedsData()} +
    + ); + } +} + +export class DiscoveryStreamAdminInner extends React.PureComponent { + constructor(props) { + super(props); + this.setState = this.setState.bind(this); + } + + render() { + return ( +
    + ); + } +} + +export class CollapseToggle extends React.PureComponent { + constructor(props) { + super(props); + this.onCollapseToggle = this.onCollapseToggle.bind(this); + this.state = { collapsed: false }; + } + + get renderAdmin() { + const { props } = this; + return props.location.hash && props.location.hash.startsWith("#devtools"); + } + + onCollapseToggle(e) { + e.preventDefault(); + this.setState(state => ({ collapsed: !state.collapsed })); + } + + setBodyClass() { + if (this.renderAdmin && !this.state.collapsed) { + global.document.body.classList.add("no-scroll"); + } else { + global.document.body.classList.remove("no-scroll"); + } + } + + componentDidMount() { + this.setBodyClass(); + } + + componentDidUpdate() { + this.setBodyClass(); + } + + componentWillUnmount() { + global.document.body.classList.remove("no-scroll"); + } + + render() { + const { props } = this; + const { renderAdmin } = this; + const isCollapsed = this.state.collapsed || !renderAdmin; + const label = `${isCollapsed ? "Expand" : "Collapse"} devtools`; + return ( + + + + + {renderAdmin ? ( + + ) : null} + + ); + } +} + +const _DiscoveryStreamAdmin = props => ( + + + +); + +export const DiscoveryStreamAdmin = connect(state => ({ + Sections: state.Sections, + DiscoveryStream: state.DiscoveryStream, + Personalization: state.Personalization, + Prefs: state.Prefs, +}))(_DiscoveryStreamAdmin); diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.scss b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.scss new file mode 100644 index 0000000000..a01227dd3d --- /dev/null +++ b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.scss @@ -0,0 +1,337 @@ +/* stylelint-disable max-nesting-depth */ + +.discoverystream-admin-toggle { + position: fixed; + top: 50px; + inset-inline-end: 15px; + border: 0; + background: none; + z-index: 1; + border-radius: 2px; + + .icon-devtools { + background-image: url('chrome://global/skin/icons/developer.svg'); + padding: 15px; + } + + &:dir(rtl) { + transform: scaleX(-1); + } + + &:hover { + background: var(--newtab-element-hover-color); + } + + &.expanded { + background: $black-20; + } +} + +.discoverystream-admin { + $border-color: var(--newtab-border-color); + $monospace: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', 'Droid Sans Mono', + 'Source Code Pro', monospace; + $sidebar-width: 240px; + + position: fixed; + top: 0; + inset-inline-start: 0; + width: 100%; + background: var(--newtab-background-color); + height: 100%; + overflow-y: scroll; + margin: 0 auto; + font-size: 14px; + padding-inline-start: $sidebar-width; + color: var(--newtab-text-primary-color); + + &.collapsed { + display: none; + } + + .sidebar { + inset-inline-start: 0; + position: fixed; + width: $sidebar-width; + padding: 30px 20px; + + ul { + margin: 0; + padding: 0; + list-style: none; + } + + li a { + padding: 10px 34px; + display: block; + color: var(--lwt-sidebar-text-color); + + &:hover { + background: var(--newtab-background-color-secondary); + } + } + } + + h1 { + font-weight: 200; + font-size: 32px; + } + + h2 .button, + p .button { + font-size: 14px; + padding: 6px 12px; + margin-inline-start: 5px; + margin-bottom: 0; + } + + .general-textarea { + direction: ltr; + width: 740px; + height: 500px; + overflow: auto; + resize: none; + border-radius: 4px; + display: flex; + } + + .wnp-textarea { + direction: ltr; + width: 740px; + height: 500px; + overflow: auto; + resize: none; + border-radius: 4px; + display: flex; + } + + .json-button { + display: inline-flex; + font-size: 10px; + padding: 4px 10px; + margin-bottom: 6px; + margin-inline-end: 4px; + + &:hover { + background-color: var(--newtab-element-hover-color); + box-shadow: none; + } + } + + table { + border-collapse: collapse; + width: 100%; + + &.minimal-table { + border-collapse: collapse; + border: 1px solid $border-color; + + td { + padding: 8px; + } + + td:first-child { + width: 1%; + white-space: nowrap; + } + + td:not(:first-child) { + font-family: $monospace; + } + } + + &.errorReporting { + tr { + border: 1px solid var(--newtab-background-color-secondary); + } + + td { + padding: 4px; + + &[rowspan] { + border: 1px solid var(--newtab-background-color-secondary); + } + } + } + } + + .sourceLabel { + background: var(--newtab-background-color-secondary); + padding: 2px 5px; + border-radius: 3px; + + &.isDisabled { + background: $email-input-invalid; + color: var(--newtab-status-error); + } + } + + .message-item { + &:first-child td { + border-top: 1px solid $border-color; + } + + td { + vertical-align: top; + padding: 8px; + border-bottom: 1px solid $border-color; + + &.min { + width: 1%; + white-space: nowrap; + } + + &.message-summary { + width: 60%; + } + + &.button-column { + width: 15%; + } + + &:first-child { + border-inline-start: 1px solid $border-color; + } + + &:last-child { + border-inline-end: 1px solid $border-color; + } + } + + &.blocked { + .message-id, + .message-summary { + opacity: 0.5; + } + + .message-id { + opacity: 0.5; + } + } + + .message-id { + font-family: $monospace; + font-size: 12px; + } + } + + .providerUrl { + font-size: 12px; + } + + pre { + background: var(--newtab-background-color-secondary); + margin: 0; + padding: 8px; + font-size: 12px; + max-width: 750px; + overflow: auto; + font-family: $monospace; + } + + .errorState { + border: $input-error-border; + } + + .helpLink { + padding: 10px; + display: flex; + background: $black-10; + border-radius: 3px; + align-items: center; + + a { + text-decoration: underline; + } + + .icon { + min-width: 18px; + min-height: 18px; + } + } + + .ds-component { + margin-bottom: 20px; + } + + .modalOverlayInner { + height: 80%; + } + + .clearButton { + border: 0; + padding: 4px; + border-radius: 4px; + display: flex; + + &:hover { + background: var(--newtab-element-hover-color); + } + } + + .collapsed { + display: none; + } + + .icon { + display: inline-table; + cursor: pointer; + width: 18px; + height: 18px; + } + + .button { + &:disabled, + &:disabled:active { + opacity: 0.5; + cursor: unset; + box-shadow: none; + } + } + + .impressions-section { + display: flex; + flex-direction: column; + gap: 16px; + + .impressions-item { + display: flex; + flex-flow: column nowrap; + padding: 8px; + border: 1px solid $border-color; + border-radius: 5px; + + .impressions-inner-box { + display: flex; + flex-flow: row nowrap; + gap: 8px; + } + + .impressions-category { + font-size: 1.15em; + white-space: nowrap; + flex-grow: 0.1; + } + + .impressions-buttons { + display: flex; + flex-direction: column; + gap: 8px; + + button { + margin: 0; + } + } + + .impressions-editor { + display: flex; + flex-grow: 1.5; + + .general-textarea { + width: auto; + flex-grow: 1; + } + } + } + } +} diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/SimpleHashRouter.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/SimpleHashRouter.jsx new file mode 100644 index 0000000000..9c3fd8579c --- /dev/null +++ b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/SimpleHashRouter.jsx @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from "react"; + +export class SimpleHashRouter extends React.PureComponent { + constructor(props) { + super(props); + this.onHashChange = this.onHashChange.bind(this); + this.state = { hash: global.location.hash }; + } + + onHashChange() { + this.setState({ hash: global.location.hash }); + } + + componentWillMount() { + global.addEventListener("hashchange", this.onHashChange); + } + + componentWillUnmount() { + global.removeEventListener("hashchange", this.onHashChange); + } + + render() { + const [, ...routes] = this.state.hash.split("-"); + return React.cloneElement(this.props.children, { + location: { + hash: this.state.hash, + routes, + }, + }); + } +} diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx new file mode 100644 index 0000000000..dff122b366 --- /dev/null +++ b/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx @@ -0,0 +1,386 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { CardGrid } from "content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid"; +import { CollectionCardGrid } from "content-src/components/DiscoveryStreamComponents/CollectionCardGrid/CollectionCardGrid"; +import { CollapsibleSection } from "content-src/components/CollapsibleSection/CollapsibleSection"; +import { connect } from "react-redux"; +import { DSMessage } from "content-src/components/DiscoveryStreamComponents/DSMessage/DSMessage"; +import { DSPrivacyModal } from "content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal"; +import { DSSignup } from "content-src/components/DiscoveryStreamComponents/DSSignup/DSSignup"; +import { DSTextPromo } from "content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo"; +import { Highlights } from "content-src/components/DiscoveryStreamComponents/Highlights/Highlights"; +import { HorizontalRule } from "content-src/components/DiscoveryStreamComponents/HorizontalRule/HorizontalRule"; +import { Navigation } from "content-src/components/DiscoveryStreamComponents/Navigation/Navigation"; +import { PrivacyLink } from "content-src/components/DiscoveryStreamComponents/PrivacyLink/PrivacyLink"; +import React from "react"; +import { SectionTitle } from "content-src/components/DiscoveryStreamComponents/SectionTitle/SectionTitle"; +import { selectLayoutRender } from "content-src/lib/selectLayoutRender"; +import { TopSites } from "content-src/components/TopSites/TopSites"; + +const ALLOWED_CSS_URL_PREFIXES = [ + "chrome://", + "resource://", + "https://img-getpocket.cdn.mozilla.net/", +]; +const DUMMY_CSS_SELECTOR = "DUMMY#CSS.SELECTOR"; + +/** + * Validate a CSS declaration. The values are assumed to be normalized by CSSOM. + */ +export function isAllowedCSS(property, value) { + // Bug 1454823: INTERNAL properties, e.g., -moz-context-properties, are + // exposed but their values aren't resulting in getting nothing. Fortunately, + // we don't care about validating the values of the current set of properties. + if (value === undefined) { + return true; + } + + // Make sure all urls are of the allowed protocols/prefixes + const urls = value.match(/url\("[^"]+"\)/g); + return ( + !urls || + urls.every(url => + ALLOWED_CSS_URL_PREFIXES.some(prefix => url.slice(5).startsWith(prefix)) + ) + ); +} + +export class _DiscoveryStreamBase extends React.PureComponent { + constructor(props) { + super(props); + this.onStyleMount = this.onStyleMount.bind(this); + } + + onStyleMount(style) { + // Unmounting style gets rid of old styles, so nothing else to do + if (!style) { + return; + } + + const { sheet } = style; + const styles = JSON.parse(style.dataset.styles); + styles.forEach((row, rowIndex) => { + row.forEach((component, componentIndex) => { + // Nothing to do without optional styles overrides + if (!component) { + return; + } + + Object.entries(component).forEach(([selectors, declarations]) => { + // Start with a dummy rule to validate declarations and selectors + sheet.insertRule(`${DUMMY_CSS_SELECTOR} {}`); + const [rule] = sheet.cssRules; + + // Validate declarations and remove any offenders. CSSOM silently + // discards invalid entries, so here we apply extra restrictions. + rule.style = declarations; + [...rule.style].forEach(property => { + const value = rule.style[property]; + if (!isAllowedCSS(property, value)) { + console.error(`Bad CSS declaration ${property}: ${value}`); + rule.style.removeProperty(property); + } + }); + + // Set the actual desired selectors scoped to the component + const prefix = `.ds-layout > .ds-column:nth-child(${ + rowIndex + 1 + }) .ds-column-grid > :nth-child(${componentIndex + 1})`; + // NB: Splitting on "," doesn't work with strings with commas, but + // we're okay with not supporting those selectors + rule.selectorText = selectors + .split(",") + .map( + selector => + prefix + + // Assume :pseudo-classes are for component instead of descendant + (selector[0] === ":" ? "" : " ") + + selector + ) + .join(","); + + // CSSOM silently ignores bad selectors, so we'll be noisy instead + if (rule.selectorText === DUMMY_CSS_SELECTOR) { + console.error(`Bad CSS selector ${selectors}`); + } + }); + }); + }); + } + + renderComponent(component, embedWidth) { + switch (component.type) { + case "Highlights": + return ; + case "TopSites": + return ( +
    + +
    + ); + case "TextPromo": + return ( + + ); + case "Signup": + return ( + + ); + case "Message": + return ( + + ); + case "SectionTitle": + return ; + case "Navigation": + return ( + + ); + case "CollectionCardGrid": + const { DiscoveryStream } = this.props; + return ( + + ); + case "CardGrid": + return ( + + ); + case "HorizontalRule": + return ; + case "PrivacyLink": + return ; + default: + return
    {component.type}
    ; + } + } + + renderStyles(styles) { + // Use json string as both the key and styles to render so React knows when + // to unmount and mount a new instance for new styles. + const json = JSON.stringify(styles); + return + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/components/newtab/data/content/assets/glyph-cfr-feature-16.svg b/browser/components/newtab/data/content/assets/glyph-cfr-feature-16.svg new file mode 100644 index 0000000000..871b48ca45 --- /dev/null +++ b/browser/components/newtab/data/content/assets/glyph-cfr-feature-16.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/browser/components/newtab/data/content/assets/glyph-mail-16.svg b/browser/components/newtab/data/content/assets/glyph-mail-16.svg new file mode 100644 index 0000000000..8c211c5567 --- /dev/null +++ b/browser/components/newtab/data/content/assets/glyph-mail-16.svg @@ -0,0 +1,4 @@ + + diff --git a/browser/components/newtab/data/content/assets/glyph-maximize-16.svg b/browser/components/newtab/data/content/assets/glyph-maximize-16.svg new file mode 100644 index 0000000000..2f45557cfa --- /dev/null +++ b/browser/components/newtab/data/content/assets/glyph-maximize-16.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/browser/components/newtab/data/content/assets/glyph-minimize-16.svg b/browser/components/newtab/data/content/assets/glyph-minimize-16.svg new file mode 100644 index 0000000000..6bc93fa5e0 --- /dev/null +++ b/browser/components/newtab/data/content/assets/glyph-minimize-16.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/browser/components/newtab/data/content/assets/glyph-modal-delete-20.svg b/browser/components/newtab/data/content/assets/glyph-modal-delete-20.svg new file mode 100644 index 0000000000..592f9569a1 --- /dev/null +++ b/browser/components/newtab/data/content/assets/glyph-modal-delete-20.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/browser/components/newtab/data/content/assets/glyph-newWindow-16.svg b/browser/components/newtab/data/content/assets/glyph-newWindow-16.svg new file mode 100644 index 0000000000..0b09bfde5a --- /dev/null +++ b/browser/components/newtab/data/content/assets/glyph-newWindow-16.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/browser/components/newtab/data/content/assets/glyph-open-file-16.svg b/browser/components/newtab/data/content/assets/glyph-open-file-16.svg new file mode 100644 index 0000000000..a2a23f09eb --- /dev/null +++ b/browser/components/newtab/data/content/assets/glyph-open-file-16.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/browser/components/newtab/data/content/assets/glyph-pin-16.svg b/browser/components/newtab/data/content/assets/glyph-pin-16.svg new file mode 100644 index 0000000000..c951bc1c9d --- /dev/null +++ b/browser/components/newtab/data/content/assets/glyph-pin-16.svg @@ -0,0 +1,6 @@ + + + + diff --git a/browser/components/newtab/data/content/assets/glyph-pocket-archive-16.svg b/browser/components/newtab/data/content/assets/glyph-pocket-archive-16.svg new file mode 100644 index 0000000000..10cf13c4d2 --- /dev/null +++ b/browser/components/newtab/data/content/assets/glyph-pocket-archive-16.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/browser/components/newtab/data/content/assets/glyph-pocket-delete-16.svg b/browser/components/newtab/data/content/assets/glyph-pocket-delete-16.svg new file mode 100644 index 0000000000..95bb4d3edb --- /dev/null +++ b/browser/components/newtab/data/content/assets/glyph-pocket-delete-16.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/browser/components/newtab/data/content/assets/glyph-unpin-16.svg b/browser/components/newtab/data/content/assets/glyph-unpin-16.svg new file mode 100644 index 0000000000..2352839340 --- /dev/null +++ b/browser/components/newtab/data/content/assets/glyph-unpin-16.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/browser/components/newtab/data/content/assets/glyph-webextension-16.svg b/browser/components/newtab/data/content/assets/glyph-webextension-16.svg new file mode 100644 index 0000000000..b29ea04bf2 --- /dev/null +++ b/browser/components/newtab/data/content/assets/glyph-webextension-16.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/browser/components/newtab/data/content/assets/icon-removed-bookmark.svg b/browser/components/newtab/data/content/assets/icon-removed-bookmark.svg new file mode 100644 index 0000000000..e222da3bfd --- /dev/null +++ b/browser/components/newtab/data/content/assets/icon-removed-bookmark.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/browser/components/newtab/data/content/assets/pocket-onboarding.avif b/browser/components/newtab/data/content/assets/pocket-onboarding.avif new file mode 100644 index 0000000000..9bd1b3e524 Binary files /dev/null and b/browser/components/newtab/data/content/assets/pocket-onboarding.avif differ diff --git a/browser/components/newtab/data/content/assets/pocket-onboarding@2x.avif b/browser/components/newtab/data/content/assets/pocket-onboarding@2x.avif new file mode 100644 index 0000000000..6817e85f2f Binary files /dev/null and b/browser/components/newtab/data/content/assets/pocket-onboarding@2x.avif differ diff --git a/browser/components/newtab/data/content/assets/pocket-swoosh.svg b/browser/components/newtab/data/content/assets/pocket-swoosh.svg new file mode 100644 index 0000000000..0d81c7c453 --- /dev/null +++ b/browser/components/newtab/data/content/assets/pocket-swoosh.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/browser/components/newtab/data/content/assets/remote/mountain.svg b/browser/components/newtab/data/content/assets/remote/mountain.svg new file mode 100644 index 0000000000..4511148820 --- /dev/null +++ b/browser/components/newtab/data/content/assets/remote/mountain.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/browser/components/newtab/data/content/assets/remote/umbrella.png b/browser/components/newtab/data/content/assets/remote/umbrella.png new file mode 100644 index 0000000000..3488d135c6 Binary files /dev/null and b/browser/components/newtab/data/content/assets/remote/umbrella.png differ diff --git a/browser/components/newtab/data/content/assets/spinner.svg b/browser/components/newtab/data/content/assets/spinner.svg new file mode 100644 index 0000000000..2964a31731 --- /dev/null +++ b/browser/components/newtab/data/content/assets/spinner.svg @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/browser/components/newtab/data/content/newtab-render.js b/browser/components/newtab/data/content/newtab-render.js new file mode 100644 index 0000000000..4fd46fad03 --- /dev/null +++ b/browser/components/newtab/data/content/newtab-render.js @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +// exported by activity-stream.bundle.js +if (window.__FROM_STARTUP_CACHE__) { + window.NewtabRenderUtils.renderCache(window.__STARTUP_STATE__); +} else { + window.NewtabRenderUtils.renderWithoutState(); +} diff --git a/browser/components/newtab/data/content/tippytop/favicons/adidas.png b/browser/components/newtab/data/content/tippytop/favicons/adidas.png new file mode 100644 index 0000000000..fd7123958c Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/adidas.png differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/aliexpress-com.ico b/browser/components/newtab/data/content/tippytop/favicons/aliexpress-com.ico new file mode 100644 index 0000000000..99b86e13aa Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/aliexpress-com.ico differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/allegro-pl.ico b/browser/components/newtab/data/content/tippytop/favicons/allegro-pl.ico new file mode 100644 index 0000000000..42b4f90149 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/allegro-pl.ico differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/amazon.ico b/browser/components/newtab/data/content/tippytop/favicons/amazon.ico new file mode 100644 index 0000000000..1c39eaf8fe Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/amazon.ico differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/avito-ru.ico b/browser/components/newtab/data/content/tippytop/favicons/avito-ru.ico new file mode 100644 index 0000000000..c41847b27a Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/avito-ru.ico differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/baidu-com.png b/browser/components/newtab/data/content/tippytop/favicons/baidu-com.png new file mode 100644 index 0000000000..e63737eb30 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/baidu-com.png differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/bbc-uk.ico b/browser/components/newtab/data/content/tippytop/favicons/bbc-uk.ico new file mode 100644 index 0000000000..8f62b07af8 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/bbc-uk.ico differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/bing-com.ico b/browser/components/newtab/data/content/tippytop/favicons/bing-com.ico new file mode 100644 index 0000000000..fdc021cfeb Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/bing-com.ico differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/ctrip-com.ico b/browser/components/newtab/data/content/tippytop/favicons/ctrip-com.ico new file mode 100644 index 0000000000..fa44291d84 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/ctrip-com.ico differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/duckduckgo-com.ico b/browser/components/newtab/data/content/tippytop/favicons/duckduckgo-com.ico new file mode 100644 index 0000000000..3ad20825c1 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/duckduckgo-com.ico differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/ebay.ico b/browser/components/newtab/data/content/tippytop/favicons/ebay.ico new file mode 100644 index 0000000000..3af7a36484 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/ebay.ico differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/etsy.ico b/browser/components/newtab/data/content/tippytop/favicons/etsy.ico new file mode 100644 index 0000000000..a94f3efd4f Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/etsy.ico differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/facebook-com.ico b/browser/components/newtab/data/content/tippytop/favicons/facebook-com.ico new file mode 100644 index 0000000000..8ce319b8f7 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/facebook-com.ico differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/geico.png b/browser/components/newtab/data/content/tippytop/favicons/geico.png new file mode 100644 index 0000000000..3f61497dd8 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/geico.png differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/google-com.ico b/browser/components/newtab/data/content/tippytop/favicons/google-com.ico new file mode 100644 index 0000000000..82339b3b1d Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/google-com.ico differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/hrblock.ico b/browser/components/newtab/data/content/tippytop/favicons/hrblock.ico new file mode 100644 index 0000000000..e0d7be35e0 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/hrblock.ico differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/ifeng-com.ico b/browser/components/newtab/data/content/tippytop/favicons/ifeng-com.ico new file mode 100644 index 0000000000..b0003e058f Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/ifeng-com.ico differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/iqiyi-com.ico b/browser/components/newtab/data/content/tippytop/favicons/iqiyi-com.ico new file mode 100644 index 0000000000..4b179bf4d5 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/iqiyi-com.ico differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/leboncoin-fr.png b/browser/components/newtab/data/content/tippytop/favicons/leboncoin-fr.png new file mode 100644 index 0000000000..e23e2a34b0 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/leboncoin-fr.png differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/nike.ico b/browser/components/newtab/data/content/tippytop/favicons/nike.ico new file mode 100644 index 0000000000..7788d580af Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/nike.ico differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/ok-ru.ico b/browser/components/newtab/data/content/tippytop/favicons/ok-ru.ico new file mode 100644 index 0000000000..7db8914287 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/ok-ru.ico differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/olx-pl.ico b/browser/components/newtab/data/content/tippytop/favicons/olx-pl.ico new file mode 100644 index 0000000000..b2a28638f8 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/olx-pl.ico differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/reddit-com.png b/browser/components/newtab/data/content/tippytop/favicons/reddit-com.png new file mode 100644 index 0000000000..3c09931835 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/reddit-com.png differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/samsung.ico b/browser/components/newtab/data/content/tippytop/favicons/samsung.ico new file mode 100644 index 0000000000..eb8c814256 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/samsung.ico differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/turbotax.png b/browser/components/newtab/data/content/tippytop/favicons/turbotax.png new file mode 100644 index 0000000000..c1d52f99fb Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/turbotax.png differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/twitter-com.ico b/browser/components/newtab/data/content/tippytop/favicons/twitter-com.ico new file mode 100644 index 0000000000..e5aaff4379 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/twitter-com.ico differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/vk-com.ico b/browser/components/newtab/data/content/tippytop/favicons/vk-com.ico new file mode 100644 index 0000000000..0066072c39 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/vk-com.ico differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/vodafone.png b/browser/components/newtab/data/content/tippytop/favicons/vodafone.png new file mode 100644 index 0000000000..1a4ba0089e Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/vodafone.png differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/weibo-com.ico b/browser/components/newtab/data/content/tippytop/favicons/weibo-com.ico new file mode 100644 index 0000000000..11a88045ec Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/weibo-com.ico differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/wikipedia-org.ico b/browser/components/newtab/data/content/tippytop/favicons/wikipedia-org.ico new file mode 100644 index 0000000000..e70021849b Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/wikipedia-org.ico differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/wix.ico b/browser/components/newtab/data/content/tippytop/favicons/wix.ico new file mode 100644 index 0000000000..cabcb650a7 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/wix.ico differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/wykop-pl.png b/browser/components/newtab/data/content/tippytop/favicons/wykop-pl.png new file mode 100644 index 0000000000..5aae5b17f2 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/wykop-pl.png differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/yandex-com.png b/browser/components/newtab/data/content/tippytop/favicons/yandex-com.png new file mode 100644 index 0000000000..d1c3f3f8b1 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/yandex-com.png differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/yandex-ru.png b/browser/components/newtab/data/content/tippytop/favicons/yandex-ru.png new file mode 100644 index 0000000000..eb187398c7 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/yandex-ru.png differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/youtube-com.png b/browser/components/newtab/data/content/tippytop/favicons/youtube-com.png new file mode 100644 index 0000000000..b0c05d0716 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/youtube-com.png differ diff --git a/browser/components/newtab/data/content/tippytop/favicons/zhihu-com.ico b/browser/components/newtab/data/content/tippytop/favicons/zhihu-com.ico new file mode 100644 index 0000000000..c83d0e6d86 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/favicons/zhihu-com.ico differ diff --git a/browser/components/newtab/data/content/tippytop/images/adidas@2x.png b/browser/components/newtab/data/content/tippytop/images/adidas@2x.png new file mode 100644 index 0000000000..f07c17a9a8 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/adidas@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/aliexpress-com@2x.png b/browser/components/newtab/data/content/tippytop/images/aliexpress-com@2x.png new file mode 100644 index 0000000000..76fac2e935 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/aliexpress-com@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/allegro-pl@2x.png b/browser/components/newtab/data/content/tippytop/images/allegro-pl@2x.png new file mode 100644 index 0000000000..7aa6ffd4b3 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/allegro-pl@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/amazon@2x.png b/browser/components/newtab/data/content/tippytop/images/amazon@2x.png new file mode 100644 index 0000000000..fb20eea921 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/amazon@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/avito-ru@2x.png b/browser/components/newtab/data/content/tippytop/images/avito-ru@2x.png new file mode 100644 index 0000000000..9ba32a8d96 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/avito-ru@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/baidu-com@2x.png b/browser/components/newtab/data/content/tippytop/images/baidu-com@2x.png new file mode 100644 index 0000000000..b7662dd21f Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/baidu-com@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/bbc-uk@2x.png b/browser/components/newtab/data/content/tippytop/images/bbc-uk@2x.png new file mode 100644 index 0000000000..e019ac3de6 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/bbc-uk@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/bing-com@2x.svg b/browser/components/newtab/data/content/tippytop/images/bing-com@2x.svg new file mode 100644 index 0000000000..1afdee989a --- /dev/null +++ b/browser/components/newtab/data/content/tippytop/images/bing-com@2x.svg @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/components/newtab/data/content/tippytop/images/ctrip-com@2x.png b/browser/components/newtab/data/content/tippytop/images/ctrip-com@2x.png new file mode 100644 index 0000000000..76a81da5af Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/ctrip-com@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/duckduckgo-com@2x.svg b/browser/components/newtab/data/content/tippytop/images/duckduckgo-com@2x.svg new file mode 100644 index 0000000000..a28cc833cd --- /dev/null +++ b/browser/components/newtab/data/content/tippytop/images/duckduckgo-com@2x.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/browser/components/newtab/data/content/tippytop/images/ebay@2x.png b/browser/components/newtab/data/content/tippytop/images/ebay@2x.png new file mode 100644 index 0000000000..744e2442ff Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/ebay@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/etsy@2x.jpg b/browser/components/newtab/data/content/tippytop/images/etsy@2x.jpg new file mode 100644 index 0000000000..4bd477ca6a Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/etsy@2x.jpg differ diff --git a/browser/components/newtab/data/content/tippytop/images/facebook-com@2x.png b/browser/components/newtab/data/content/tippytop/images/facebook-com@2x.png new file mode 100644 index 0000000000..8827157878 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/facebook-com@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/geico@2x.jpg b/browser/components/newtab/data/content/tippytop/images/geico@2x.jpg new file mode 100644 index 0000000000..938b7948e9 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/geico@2x.jpg differ diff --git a/browser/components/newtab/data/content/tippytop/images/google-com@2x.png b/browser/components/newtab/data/content/tippytop/images/google-com@2x.png new file mode 100644 index 0000000000..263bd973b1 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/google-com@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/hrblock@2x.png b/browser/components/newtab/data/content/tippytop/images/hrblock@2x.png new file mode 100644 index 0000000000..ba66c46b72 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/hrblock@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/ifeng-com@2x.png b/browser/components/newtab/data/content/tippytop/images/ifeng-com@2x.png new file mode 100644 index 0000000000..f7099f334e Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/ifeng-com@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/iqiyi-com@2x.png b/browser/components/newtab/data/content/tippytop/images/iqiyi-com@2x.png new file mode 100644 index 0000000000..901d536ad9 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/iqiyi-com@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/leboncoin-fr@2x.png b/browser/components/newtab/data/content/tippytop/images/leboncoin-fr@2x.png new file mode 100644 index 0000000000..af293fa8c3 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/leboncoin-fr@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/nike@2x.jpg b/browser/components/newtab/data/content/tippytop/images/nike@2x.jpg new file mode 100644 index 0000000000..ac5d639d12 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/nike@2x.jpg differ diff --git a/browser/components/newtab/data/content/tippytop/images/ok-ru@2x.png b/browser/components/newtab/data/content/tippytop/images/ok-ru@2x.png new file mode 100644 index 0000000000..c771bf3ad9 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/ok-ru@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/olx-pl@2x.png b/browser/components/newtab/data/content/tippytop/images/olx-pl@2x.png new file mode 100644 index 0000000000..964cd2df10 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/olx-pl@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/reddit-com@2x.png b/browser/components/newtab/data/content/tippytop/images/reddit-com@2x.png new file mode 100644 index 0000000000..3b8833d6c6 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/reddit-com@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/samsung@2x.jpg b/browser/components/newtab/data/content/tippytop/images/samsung@2x.jpg new file mode 100644 index 0000000000..dec2346e9f Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/samsung@2x.jpg differ diff --git a/browser/components/newtab/data/content/tippytop/images/turbotax@2x.jpg b/browser/components/newtab/data/content/tippytop/images/turbotax@2x.jpg new file mode 100644 index 0000000000..625703943f Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/turbotax@2x.jpg differ diff --git a/browser/components/newtab/data/content/tippytop/images/twitter-com@2x.png b/browser/components/newtab/data/content/tippytop/images/twitter-com@2x.png new file mode 100644 index 0000000000..e5835ff98a Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/twitter-com@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/vk-com@2x.png b/browser/components/newtab/data/content/tippytop/images/vk-com@2x.png new file mode 100644 index 0000000000..b4c14412a4 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/vk-com@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/vodafone@2x.jpg b/browser/components/newtab/data/content/tippytop/images/vodafone@2x.jpg new file mode 100644 index 0000000000..2597063b47 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/vodafone@2x.jpg differ diff --git a/browser/components/newtab/data/content/tippytop/images/weibo-com@2x.png b/browser/components/newtab/data/content/tippytop/images/weibo-com@2x.png new file mode 100644 index 0000000000..e047eaac87 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/weibo-com@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/wikipedia-org@2x.png b/browser/components/newtab/data/content/tippytop/images/wikipedia-org@2x.png new file mode 100644 index 0000000000..53cf1af1c6 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/wikipedia-org@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/wix@2x.jpg b/browser/components/newtab/data/content/tippytop/images/wix@2x.jpg new file mode 100644 index 0000000000..473caf38a3 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/wix@2x.jpg differ diff --git a/browser/components/newtab/data/content/tippytop/images/wykop-pl@2x.png b/browser/components/newtab/data/content/tippytop/images/wykop-pl@2x.png new file mode 100644 index 0000000000..fbde175696 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/wykop-pl@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/yandex-com@2x.png b/browser/components/newtab/data/content/tippytop/images/yandex-com@2x.png new file mode 100644 index 0000000000..ebea409306 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/yandex-com@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/yandex-ru@2x.png b/browser/components/newtab/data/content/tippytop/images/yandex-ru@2x.png new file mode 100644 index 0000000000..3d4ffd15c3 Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/yandex-ru@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/youtube-com@2x.png b/browser/components/newtab/data/content/tippytop/images/youtube-com@2x.png new file mode 100644 index 0000000000..6f1d7a1d7b Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/youtube-com@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/images/zhihu-com@2x.png b/browser/components/newtab/data/content/tippytop/images/zhihu-com@2x.png new file mode 100644 index 0000000000..a1a9db845e Binary files /dev/null and b/browser/components/newtab/data/content/tippytop/images/zhihu-com@2x.png differ diff --git a/browser/components/newtab/data/content/tippytop/top_sites.json b/browser/components/newtab/data/content/tippytop/top_sites.json new file mode 100644 index 0000000000..82764a0527 --- /dev/null +++ b/browser/components/newtab/data/content/tippytop/top_sites.json @@ -0,0 +1,182 @@ +[ + { + "domains": ["adidas.co.uk", "adidas.de", "adidas.fr"], + "image_url": "images/adidas@2x.png", + "favicon_url": "favicons/adidas.png" + }, + { + "domains": ["aliexpress.com"], + "image_url": "images/aliexpress-com@2x.png", + "favicon_url": "favicons/aliexpress-com.ico" + }, + { + "domains": ["allegro.pl"], + "image_url": "images/allegro-pl@2x.png", + "favicon_url": "favicons/allegro-pl.ico" + }, + { + "domains": ["amazon.ae", "amazon.ca", "amazon.cn", "amazon.co.jp", "amazon.co.uk", "amazon.com", "amazon.com.au", "amazon.com.br", "amazon.com.mx", "amazon.de", "amazon.es", "amazon.fr", "amazon.in", "amazon.it", "amazon.nl", "amazon.sa", "amazon.se", "amazon.sg", "amazon.com.tr"], + "image_url": "images/amazon@2x.png", + "favicon_url": "favicons/amazon.ico" + }, + { + "domains": ["avito.ru"], + "image_url": "images/avito-ru@2x.png", + "favicon_url": "favicons/avito-ru.ico" + }, + { + "domains": ["baidu.com"], + "image_url": "images/baidu-com@2x.png", + "favicon_url": "favicons/baidu-com.png" + }, + { + "domains": ["bbc.co.uk"], + "image_url": "images/bbc-uk@2x.png", + "favicon_url": "favicons/bbc-uk.ico" + }, + { + "domains": ["bing.com"], + "image_url": "images/bing-com@2x.svg", + "favicon_url": "favicons/bing-com.ico" + }, + { + "domains": ["ctrip.com"], + "image_url": "images/ctrip-com@2x.png", + "favicon_url": "favicons/ctrip-com.ico" + }, + { + "domains": ["duckduckgo.com"], + "image_url": "images/duckduckgo-com@2x.svg", + "favicon_url": "favicons/duckduckgo-com.ico" + }, + { + "domains": ["mx.ebay.com", "benl.ebay.be", "befr.ebay.be", "ebay.ca", "ebay.ch", "ebay.co.jp", "ebay.co.uk", "ebay.com", "ebay.com.au", "ebay.de", "ebay.es", "ebay.fr", "ebay.ie", "ebay.in", "ebay.it", "ebay.nl"], + "image_url": "images/ebay@2x.png", + "favicon_url": "favicons/ebay.ico" + }, + { + "domains": ["etsy.com"], + "image_url": "images/etsy@2x.jpg", + "favicon_url": "favicons/etsy.ico" + }, + { + "domains": ["facebook.com"], + "image_url": "images/facebook-com@2x.png", + "favicon_url": "favicons/facebook-com.ico" + }, + { + "domains": ["geico.com"], + "image_url": "images/geico@2x.jpg", + "favicon_url": "favicons/geico.png" + }, + { + "domains": ["google.com"], + "image_url": "images/google-com@2x.png", + "favicon_url": "favicons/google-com.ico" + }, + { + "domains": ["hrblock.com"], + "image_url": "images/hrblock@2x.png", + "favicon_url": "favicons/hrblock.ico" + }, + { + "domains": ["ifeng.com"], + "image_url": "images/ifeng-com@2x.png", + "favicon_url": "favicons/ifeng-com.ico" + }, + { + "domains": ["iqiyi.com"], + "image_url": "images/iqiyi-com@2x.png", + "favicon_url": "favicons/iqiyi-com.ico" + }, + { + "domains": ["leboncoin.fr"], + "image_url": "images/leboncoin-fr@2x.png", + "favicon_url": "favicons/leboncoin-fr.png" + }, + { + "domains": ["nike.com"], + "image_url": "images/nike@2x.jpg", + "favicon_url": "favicons/nike.ico" + }, + { + "domains": ["ok.ru"], + "image_url": "images/ok-ru@2x.png", + "favicon_url": "favicons/ok-ru.ico" + }, + { + "domains": ["olx.pl"], + "image_url": "images/olx-pl@2x.png", + "favicon_url": "favicons/olx-pl.ico" + }, + { + "domains": ["reddit.com"], + "image_url": "images/reddit-com@2x.png", + "favicon_url": "favicons/reddit-com.png" + }, + { + "domains": ["samsung.com"], + "image_url": "images/samsung@2x.jpg", + "favicon_url": "favicons/samsung.ico" + }, + { + "domains": ["turbotax.intuit.com"], + "image_url": "images/turbotax@2x.jpg", + "favicon_url": "favicons/turbotax.png" + }, + { + "domains": ["twitter.com"], + "image_url": "images/twitter-com@2x.png", + "favicon_url": "favicons/twitter-com.ico" + }, + { + "domains": ["vk.com"], + "image_url": "images/vk-com@2x.png", + "favicon_url": "favicons/vk-com.ico" + }, + { + "domains": ["vodafone.co.uk"], + "image_url": "images/vodafone@2x.jpg", + "favicon_url": "favicons/vodafone.png" + }, + { + "domains": ["weibo.com"], + "image_url": "images/weibo-com@2x.png", + "favicon_url": "favicons/weibo-com.ico" + }, + { + "domains": ["wikipedia.org"], + "image_url": "images/wikipedia-org@2x.png", + "favicon_url": "favicons/wikipedia-org.ico" + }, + { + "domains": ["wix.com"], + "image_url": "images/wix@2x.jpg", + "favicon_url": "favicons/wix.ico" + }, + { + "domains": ["wykop.pl"], + "image_url": "images/wykop-pl@2x.png", + "favicon_url": "favicons/wykop-pl.png" + }, + { + "domains": ["yandex.com", "yandex.com.tr"], + "image_url": "images/yandex-com@2x.png", + "favicon_url": "favicons/yandex-com.png" + }, + { + "domains": ["yandex.by", "yandex.kz", "yandex.ru", "yandex.ua", "yandex.uz"], + "image_url": "images/yandex-ru@2x.png", + "favicon_url": "favicons/yandex-ru.png" + }, + { + "domains": ["youtube.com"], + "image_url": "images/youtube-com@2x.png", + "favicon_url": "favicons/youtube-com.png" + }, + { + "domains": ["zhihu.com"], + "image_url": "images/zhihu-com@2x.png", + "favicon_url": "favicons/zhihu-com.ico" + } +] diff --git a/browser/components/newtab/docs/index.rst b/browser/components/newtab/docs/index.rst new file mode 100644 index 0000000000..48cf01c331 --- /dev/null +++ b/browser/components/newtab/docs/index.rst @@ -0,0 +1,119 @@ +====================== +Firefox Home (New Tab) +====================== + +All files related to Firefox Home, which includes content that appears on ``about:home`` and +``about:newtab``, can be found in the ``browser/components/newtab`` directory. +Some of these source files (such as ``.js``, ``.jsx``, and ``.scss``) require an additional build step. +We are working on migrating this to work with ``mach``, but in the meantime, please +follow the following steps if you need to make changes in this directory: + +For ``.jsm`` or ``.sys.mjs`` files (system modules) +--------------------------------------------------- + +No build step is necessary. Use ``mach`` and run mochitests according to your regular Firefox workflow. + +For ``.js``, ``.jsx``, ``.scss``, or ``.css`` files +--------------------------------------------------- + +Prerequisites +````````````` + +You will need the following: + +- Node.js 10+ (On Mac, the best way to install Node.js is to use the install link on the `Node.js homepage`_) +- npm (packaged with Node.js) + +To install dependencies, run the following from the root of the mozilla-central repository. +(Using ``mach`` to call ``npm`` and ``node`` commands will ensure you're using the correct versions of Node and npm.) + +.. code-block:: shell + + (cd browser/components/newtab && ../../../mach npm install) + + +Which files should you edit? +```````````````````````````` + +You should not make changes to ``.js`` or ``.css`` files in ``browser/components/newtab/css`` or +``browser/components/newtab/data`` directory. Instead, you should edit the ``.jsx``, ``.js``, and ``.scss`` source files +in ``browser/components/newtab/content-src`` directory. These files will be compiled into the ``.js`` and ``.css`` files. + + +Building assets and running Firefox +----------------------------------- + +To build assets and run Firefox, run the following from the root of the mozilla-central repository: + +.. code-block:: shell + + ./mach npm run bundle --prefix=browser/components/newtab && ./mach build && ./mach run + +Continuous development / debugging +---------------------------------- +Running ``./mach npm run watchmc --prefix=browser/components/newtab`` will start a process that watches files in +``activity-stream`` and rebuilds the bundled files when JS or CSS files change. + +**IMPORTANT NOTE**: This task will add inline source maps to help with debugging, which changes the memory footprint. +Do not use the ``watchmc`` task for profiling or performance testing! + +Running tests +------------- +The majority of New Tab / Messaging unit tests are written using +`mocha `_, and other errors that may show up there are +`SCSS `_ issues flagged by +`stylelint `_. These things are all run using +``npm test`` under the ``newtab`` slug in Treeherder/Try, so if that slug turns +red, these tests are what is failing. To execute them, do this: + +.. code-block:: shell + + ./mach npm test --prefix=browser/components/newtab + +These tests are not currently run by ``mach test``, but there's a +`task filed to fix that `_. + +Windows isn't currently supported by ``npm test`` +(`path/invocation difference `_). +To run newtab specific tests that aren't covered by ``mach lint`` and +``mach test``: + +.. code-block:: shell + + ./mach npm run lint:stylelint --prefix=browser/components/newtab + ./mach npm run testmc:build --prefix=browser/components/newtab + ./mach npm run testmc:unit --prefix=browser/components/newtab + +Mochitests and xpcshell tests run normally, using ``mach test``. + +Code Coverage +------------- +Our testing setup will run code coverage tools in addition to just the unit +tests. It will error out if the code coverage metrics don't meet certain thresholds. + +If you see any missing test coverage, you can inspect the coverage report by +running + +.. code-block:: shell + + ./mach npm test --prefix=browser/components/newtab && + ./mach npm run debugcoverage --prefix=browser/components/newtab + +Discovery Stream Developer tools +-------------------------------- + +You can access the developer tools for the Discovery Stream components of about:newtab by +visiting `about:config` and setting `browser.newtabpage.activity-stream.asrouter.devtoolsEnabled` +to `true`. + +Then, go to any `about:newtab` page and click on the wrench icon in the top-right corner. + +Detailed Docs +------------- +.. toctree:: + :titlesonly: + :glob: + + v2-system-addon/* + +.. _Node.js homepage: https://nodejs.org/ diff --git a/browser/components/newtab/docs/v2-system-addon/about_home_startup_cache.md b/browser/components/newtab/docs/v2-system-addon/about_home_startup_cache.md new file mode 100644 index 0000000000..0366bd3e29 --- /dev/null +++ b/browser/components/newtab/docs/v2-system-addon/about_home_startup_cache.md @@ -0,0 +1,86 @@ +# The `about:home` startup cache + +By default, a user's browser session starts with a single window and a single tab, pointed at about:home. This means that it's important to ensure that `about:home` loads as quickly as possible to provide a fast overall startup experience. + +`about:home`, which is functionally identical to `about:newtab`, is generated dynamically by calculating an appropriate state object in the parent process, and passing it down to a content process into the React library in order to render the final interactive page. This is problematic during the startup sequence, as calculating that initial state can be computationally expensive, and requires multiple reads from the disk. + +The `about:home` startup cache is an attempt to address this expense. It works by assuming that between browser sessions, `about:home` _usually_ doesn't need to change. + +## Components of the `about:home` startup cache mechanism + +There are 3 primary components to the cache mechanism: + +### The HTTP Cache + +The HTTP cache is normally used for caching webpages retrieved over the network, but seemed like the right fit for storage of the `about:home` cache as well. + +The HTTP cache is usually queried by the networking stack when browsing the web. The HTTP cache is, however, not typically queried when accessing `chrome://` or `resource://` URLs, so we have to do it ourselves, manually for the `about:home` case. This means giving `about:home` special capabilities for populating and reading from the HTTP cache. In order to avoid potential security issues, this requires that we sequester `about:home` / `about:newtab` in their own special content process. The "privileged about content process" exists for this purpose, and is also used for `about:logins` and `about:certificate`. + +The HTTP cache lives in the parent process, and so any read and write operations need to be initiated in the parent process. Thankfully, however, the HTTP cache accepts data using `nsIOutputStream` and serves it using `nsIInputStream`. We can send `nsIInputStream` over the message manager, and convert an `nsIInputStream` into an `nsIOutputStream`, so we have everything we need to efficiently communicate with the "privileged about content process" to save and retrieve page data. + +The official documentation for the HTTP cache [can be found here](https://firefox-source-docs.mozilla.org/networking/cache2/doc.html). + +### `AboutHomeStartupCache` + +This singleton component lives inside of `BrowserGlue` to avoid having to load yet another JSM out of the `omni.ja` file in the parent process during startup. + +`AboutHomeStartupCache` is responsible for feeding the "privileged about content process" with the `nsIInputStream`'s that it needs to present the initial `about:home` document. It is also responsible for populating the cache with updated versions of `about:home` that are sent by the "privileged about content process". + +Since accessing the HTTP cache is asynchronous, there is an opportunity for a race, where the cache can either be accessed and available before the initial `about:home` is requested, or after. To accommodate for both cases, the `AboutHomeStartupCache` constructs `nsIPipe` instances, which it sends down to the "privileged about content process" as soon as one launches. + +If the HTTP cache entry is already available when the process launches, and cached data is available, we connect the cache to the `nsIPipe`'s to stream the data down to the "privileged about content process". + +If the HTTP cache is not yet available, we hold references to those `nsIPipe` instances, and wait until the cache entry is available. Only then do we connect the cache entry to the `nsIPipe` instances to send the data down to the "privileged about content process". + +### `AboutNewTabService` + +The `AboutNewTabService` is used by the `AboutRedirector` in both the parent and content processes to determine how to handle attempts to load `about:home` and `about:newtab`. + +There are distinct versions of the `AboutNewTabService` - one for the parent process (`BaseAboutNewTabService`), and one for content processes (`AboutNewTabChildService`, which inherits from `BaseAboutNewTabService`). + +The `AboutRedirector`, when running inside of a "privileged about content process" knows to direct attempts to load `about:home` to `AboutNewTabChildService`'s `aboutHomeCacheChannel` method. This method is then responsible for choosing whether or not to return an `nsIChannel` for the cached document, or for the dynamically generated version of `about:home`. + +### `AboutHomeStartupCacheChild` + +This singleton component lives inside of the "privileged about content process", and is initialized as soon as the message is received from the parent that includes the `nsIInputStream`'s that will be used to potentially load from the cache. + +When the `AboutRedirector` in the "privileged about content process" notices that a request has been made to `about:home`, it asks `nsIAboutNewTabService` to return a new `nsIChannel` for that document. The `AboutNewTabChildService` then checks to see if the `AboutHomeStartupCacheChild` can return an `nsIChannel` for any cached content. + +If, at this point, nothing has been streamed from the parent, we fall back to loading the dynamic `about:home` document. This might occur if the cache doesn't exist yet, or if we were too slow to pull it off of the disk. Subsequent attempts to load `about:home` will bypass the cache and load the dynamic document instead. This is true even if the privileged about content process crashes and a new one is created. + +The `AboutHomeStartupCacheChild` will also be responsible for generating the cache periodically. Periodically, the `AboutNewTabService` will send down the most up-to-date state for `about:home` from the parent process, and then the `AboutHomeStartupCacheChild` will generate document markup using ReactDOMServer within a `ChromeWorker`. After that's generated, the "privileged about content process" will send up `nsIInputStream` instances for both the markup and the script for the initial page state. The `AboutHomeStartupCache` singleton inside of `BrowserGlue` is responsible for receiving those `nsIInputStream`'s and persisting them in the HTTP cache for the next start. + +## What is cached? + +Two things are cached: + +1. The raw HTML mark-up of `about:home`. +2. A small chunk of JavaScript that "hydrates" the markup through the React libraries, allowing the page to become interactive after painting. + +The JavaScript being cached cannot be put directly into the HTML mark-up as inline script due to the CSP of `about:home`, which does not allow inline scripting. Instead, we load a script from `about:home?jscache`. This goes through the same mechanism for retrieving the HTML document from the cache, but instead pulls down the cached script. + +If the HTML mark-up is cached, then we presume that the script is also cached. We cannot cache one and not the other. If only one cache exists, or only one has been sent down to the "privileged about content process" by the time the `about:home` document is requested, then we fallback to loading the dynamic `about:home` document. + +## Refreshing the cache + +The cache is refreshed periodically by having `ActivityStreamMessageChannel` tell `AboutHomeStartupCache` when it has sent any messages down to the preloaded `about:newtab`. In general, such messages are a good hint that something visual has updated for the next `about:newtab`, and that the cache should probably be refreshed. + +`AboutHomeStartupCache` debounces notifications about such messages, since they tend to be bursty. + +## Invalidating the cache + +It's possible that the composition or layout of `about:home` will change over time from release to release. When this occurs, it might be desirable to invalidate any pre-existing cache that might exist for a user, so that they don't see an outdated `about:home` on startup. + +To do this, we set a version number on the cache entry, and ensure that the version number is equal to our expectations on startup. If the version number does not match our expectation, then the cache is discarded and the `about:home` document will be rendered dynamically. + +The version number is currently set to the application build ID. This means that when the application updates, the cache is invalidated on the first restart after a browser update is applied. + +## Handling errors + +`about:home` is typically the first thing that the user sees upon starting the browser. It is critically important that it function quickly and correctly. If anything happens to go wrong when retrieving or saving to the cache, we should fall back to generating the document dynamically. + +As an example, it's theoretically possible for the browser to crash while in the midst of saving to the cache. In that case, we might have a partial document saved, or a partial script saved - neither of which is acceptable. + +Thankfully, the HTTP cache was designed with resilience in mind, so partially written entries are automatically discarded, which allows us to fall back to the dynamic page generation mode. + +As additional redundancy to that resilience, we also make sure to create a new nsICacheEntry every time the cache is populated, and write the version metadata as the last step. Since the version metadata is written last, we know that if it's missing when we try to load the cache that the writing of the page and the script did not complete, and that we should fall back to dynamically rendering the page. diff --git a/browser/components/newtab/docs/v2-system-addon/data_events.md b/browser/components/newtab/docs/v2-system-addon/data_events.md new file mode 100644 index 0000000000..78236bc3b1 --- /dev/null +++ b/browser/components/newtab/docs/v2-system-addon/data_events.md @@ -0,0 +1,19 @@ +# Metrics we collect + +By default, the about:newtab, about:welcome and about:home pages in Firefox (the pages you see when you open a new tab and when you start the browser), will send data back to Mozilla servers about usage of these pages. The intent is to collect data in order to improve the user's experience while using Activity Stream. Data about your specific browsing behaior or the sites you visit is **never transmitted to any Mozilla server**. At any time, it is easy to **turn off** this data collection by [opting out of Firefox telemetry](https://support.mozilla.org/kb/share-telemetry-data-mozilla-help-improve-firefox). + +Data is sent to our servers in the form of discrete HTTPS 'pings' or messages whenever you do some action on the Activity Stream about:home, about:newtab or about:welcome pages. We try to minimize the amount and frequency of pings by batching them together. + +At Mozilla, [we take your privacy very seriously](https://www.mozilla.org/privacy/). The Activity Stream page will never send any data that could personally identify you. We do not transmit what you are browsing, searches you perform or any private settings. Activity Stream does not set or send cookies, and uses [Transport Layer Security](https://en.wikipedia.org/wiki/Transport_Layer_Security) to securely transmit data to Mozilla servers. + +The data collected in the Activity Stream is documented +(along with the other data collected in Firefox Desktop) +in the [Glean Dictionary](https://dictionary.telemetry.mozilla.org/apps/firefox_desktop). + +Ping specifically collected on Firefox Home (New Tab) include: +* ["newtab"](https://dictionary.telemetry.mozilla.org/apps/firefox_desktop/pings/newtab) +* ["pocket-button"](https://dictionary.telemetry.mozilla.org/apps/firefox_desktop/pings/pocket-button) +* ["top-sites"](https://dictionary.telemetry.mozilla.org/apps/firefox_desktop/pings/top-sites) +* ["quick-suggest"](https://dictionary.telemetry.mozilla.org/apps/firefox_desktop/pings/quick-suggest) +* ["messaging-system"](https://dictionary.telemetry.mozilla.org/apps/firefox_desktop/pings/messaging-system) +* ["spoc"](https://dictionary.telemetry.mozilla.org/apps/firefox_desktop/pings/spoc) diff --git a/browser/components/newtab/docs/v2-system-addon/geo_locale.md b/browser/components/newtab/docs/v2-system-addon/geo_locale.md new file mode 100644 index 0000000000..4641e5d001 --- /dev/null +++ b/browser/components/newtab/docs/v2-system-addon/geo_locale.md @@ -0,0 +1,23 @@ +# Custom `geo`, `locale`, and update channels + +There are instances where you may need to change your local build's locale, geo, and update channel (such as changes to the visibility of Discovery Stream on a per-geo/locale basis in `ActivityStream.sys.mjs`). + +## Changing update channel + +- Change `app.update.channel` to desired value (eg: `release`) by editing `LOCAL_BUILD/Contents/Resources/defaults/pref/channel-prefs.js`. (**NOTE:** Changing pref `app.update.channel` from `about:config` seems to have no effect!) + +## Changing geo + +- Set `browser.search.region` to desired geo (eg `CA`) + +## Changing locale + +*Note: These prefs are only configurable on a nightly or local build.* + +- Toggle `extensions.langpacks.signatures.required` to `false` +- Toggle `xpinstall.signatures.required` to `false` +- Toggle `intl.multilingual.downloadEnabled` to `true` +- Toggle `intl.multilingual.enabled` to `true` +- For Mac and Linux builds, open the [langpack](https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central-l10n/linux-x86_64/xpi/) for target locale in your local build (eg `firefox-70.0a1.en-CA.langpack.xpi` if you want an `en-CA` locale). +- For Windows, use [https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central-l10n/](https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central-l10n/) +- In `about:preferences` click "Set Alternatives" under "Language", move desired locale to the top position, click OK, click "Apply And Restart" diff --git a/browser/components/newtab/docs/v2-system-addon/mochitests.md b/browser/components/newtab/docs/v2-system-addon/mochitests.md new file mode 100644 index 0000000000..da77874401 --- /dev/null +++ b/browser/components/newtab/docs/v2-system-addon/mochitests.md @@ -0,0 +1,26 @@ +# Mochitests + +We use [mochitests](https://firefox-source-docs.mozilla.org/testing/browser-chrome/) to do functional (and possibly integration) testing. Mochitests are part of Firefox and allow us to test activity stream literally as you would use it. + +Mochitests live in `test/browser`, and as of this writing, they are all the `browser-chrome` flavor of mochitests. They currently only run against the bootstrapped version of the add-on in system-addon, not the test pilot version at the top level directory. + +## Adding New Tests + +If you add new tests, make sure to list them in the `browser.ini` file. You will see the other tests there. Add a new entry with the same format as the others. You can also add new JS or HTML files by listing in under `support-files`. Make sure to start your test name with "browser_", so that the test suite knows the pick it up. E.g: "browser_as_my_new_test.js". + +## Writing Tests + +Here are a few tips for writing mochitests: + +* Only write mochitests for testing the interaction of multiple components on the page and to make sure that the protocol is working. +* If you need to access the content page, use `ContentTask.spawn`: + +```js +ContentTask.spawn(gBrowser.selectedBrowser, null, function* () { + content.wrappedJSObject.foo(); +}); +``` + +The above calls the function `foo` that exists in the page itself. You can also access the DOM this way: `content.document.querySelector`, if you want to click a button or do other things. You can even you use assertions inside this callback to check DOM state. + +* Nobody likes to see intermittent oranges in their tests, so read the [docs on how to avoid them](https://firefox-source-docs.mozilla.org/testing/intermittent/)! diff --git a/browser/components/newtab/docs/v2-system-addon/preferences.md b/browser/components/newtab/docs/v2-system-addon/preferences.md new file mode 100644 index 0000000000..ec6ba82491 --- /dev/null +++ b/browser/components/newtab/docs/v2-system-addon/preferences.md @@ -0,0 +1,270 @@ +# Preferences + +## Preference branch + +The preference branch for activity stream is `browser.newtabpage.activity-stream.`. +Any preferences defined in the preference configuration will be relative to that +branch. For example, if a preference is defined with the name `foo`, the full +preference as it is displayed in `about:config` will be `browser.newtabpage.activity-stream.foo`. + +## Defining new preferences + +All preferences for Activity Stream should be defined in the `PREFS_CONFIG` Array +found in `lib/ActivityStream.sys.mjs`. +The configuration object should have a `name` (the name of the pref), a `title` +that describes the functionality of the pref, and a `value`, the default value +of the pref. Optionally a `getValue` function can be provided to dynamically +generate a default pref value based on args, e.g., geo and locale. For +developers-specific defaults, an optional `value_local_dev` will be used instead +of `value`. For example: + +```js +{ + name: "telemetry.log", + title: "Log telemetry events in the console", + value: false, + value_local_dev: true, + getValue: ({geo}) => geo === "CA" +} +``` + +### IMPORTANT: Setting test-specific values for Mozilla Central + +If a feed or feature behind a pref makes any network calls or would other be +disruptive for automated tests and that pref is on by default, make sure you +disable it for tests in Mozilla Central. + +You should create a bug in Bugzilla and a patch that adds lines to turn off your +pref in the following files: + +- layout/tools/reftest/reftest-preferences.js +- testing/profiles/prefs_general.js +- testing/talos/talos/config.py + +You can see an example in [this patch](https://github.com/mozilla/activity-stream/pull/2977). + +## Reading, setting, and observing preferences from `.jsm`s + +To read/set/observe Activity Stream preferences, construct a `Prefs` instance found in `lib/ActivityStreamPrefs.sys.mjs`. + +```js +// Import Prefs +const { Prefs } = ChromeUtils.importESModule( + "resource://activity-stream/lib/ActivityStreamPrefs.sys.mjs" +); + +// Create an instance +const prefs = new Prefs(); +``` + +The `Prefs` utility will set the Activity Stream branch for you by default, so you +don't need to worry about prefixing every pref with `browser.newtabpage.activity-stream.`: + +```js +const prefs = new Prefs(); + +// This will return the value of browser.newtabpage.activity-stream.foo +prefs.get("foo"); + +// This will set the value of browser.newtabpage.activity-stream.foo to true +prefs.set("foo", true); + +// This will call aCallback when browser.newtabpage.activity-stream.foo is changed +prefs.observe("foo", aCallback); + +// This will stop listening to browser.newtabpage.activity-stream.foo +prefs.ignore("foo", aCallback); +``` + +See :searchfox:`toolkit/modules/Preferences.sys.mjs ` +for more information about what methods are available. + +## Discovery Stream Preferences + +Preferences specific to the Discovery Stream are nested under the sub-branch `browser.newtabpage.activity-stream.discoverystream` (with the exception of `browser.newtabpage.blocked`). + +### `browser.newtabpage.activity-stream.discoverystream.flight.blocks` + +- Type: `string (JSON)` +- Default: `{}` +- Pref Type: AS + +Not intended for user configuration, but is programmatically updated. Used for tracking blocked flight IDs when a user dismisses a SPOC. Keys are flight IDs. Values don't have a specific meaning. + +### `browser.newtabpage.blocked` + +- Type: `string (JSON)` +- Default: `null` +- Pref Type: AS + +Not intended for user configuration, but is programmatically updated. Used for tracking blocked story IDs when a user dismisses one. Keys are story IDs. Values don't have a specific meaning. + +### `browser.newtabpage.activity-stream.discoverystream.config` + +- Type `string (JSON)` +- Default: + ```json + { + "api_key_pref": "extensions.pocket.oAuthConsumerKey", + "collapsible": true, + "enabled": true, + "personalized": true, + } + ``` + - `api_key_pref` (string): The name of a variable containing the key for the Pocket API. + - `collapsible` (boolean): Controls whether the sections in new tab can be collapsed. + - `enabled` (boolean): Controls whether DS is turned on and is programmatically set based on a user's locale. DS enablement is a logical `AND` of this and the value of `browser.newtabpage.activity-stream.discoverystream.enabled`. + - `personalized` (boolean): When this is `true` personalized content based on browsing history will be displayed. + - `unused_key` (string): This is not set by default and is unused by this codebase. It's a standardized way to differentiate configurations to prevent experiment participants from being unenrolled. + +### `browser.newtabpage.activity-stream.discoverystream.enabled` + +- Type: `boolean` +- Default: `true` +- Pref Type: Firefox + +When this is set to `true` the Discovery Stream experience will show up if `enabled` is also `true` on `browser.newtabpage.activity-stream.discoverystream.config`. Otherwise the old Activity Stream experience will be shown. + +### `browser.newtabpage.activity-stream.discoverystream.endpointSpocsClear` + +- Type: `string (URL)` +- Default: `https://spocs.getpocket.com/user` +- Pref Type: AS + +Endpoint for when a user opts-out of sponsored content to delete the corresponding data from the ad server. + +### `browser.newtabpage.activity-stream.discoverystream.endpoints` + +- Type: `string (URLs, CSV)` +- Default: `https://getpocket.cdn.mozilla.net/,https://spocs.getpocket.com/` +- Pref Type: AS + +A list of endpoints that are allowed to be used by Discovery Stream for remote content (eg: story metadata) and configuration (eg: remote layout definitions for experimentation). + +### `browser.newtabpage.activity-stream.discoverystream.hardcoded-basic-layout` + +- Type: `boolean` +- Default: `false` +- Pref Type: Firefox + +If this is `false` the default hardcoded layout is used, and if it's `true` then an alternate hardcoded layout (that currently simulates the older AS experience) is used. + +### `browser.newtabpage.activity-stream.discoverystream.rec.impressions` + +- Type: `string (JSON)` +- Default: `{}` +- Pref Type: AS + +Programmatically generated hash table where the keys are recommendation IDs and the values are timestamps representing the first impression. + +### `browser.newtabpage.activity-stream.discoverystream.spoc.impressions` + +- Type: `string (JSON)` +- Default: `{}` +- Pref Type: AS + +Programmatically generated hash table where the keys are sponsored content IDs and the values are arrays of timestamps for every impression. + +### `browser.newtabpage.activity-stream.discoverystream.locale-list-config` + +- Type: `string (CSV, locales)` +- Default: `null` +- Pref Type: Firefox + +A comma separated list of locales that by default have stories enabled in newtab. It overrides what might be in region-stories-config. So if I set this to "en-US,en-CA,en-GB", all users with a English browser would see newtab stories, even if their region was not in region-stories-config list. + +### `browser.newtabpage.activity-stream.discoverystream.region-stories-config` + +- Type: `string (CSV, regions)` +- Default: `US,DE,CA,GB,IE,CH,AT,BE` +- Pref Type: Firefox + +A comma separated list of geos that by default have stories enabled in newtab. It matches the client's geo with that list, then looks for a matching locale. + +### `browser.newtabpage.activity-stream.discoverystream.region-spocs-config` + +- Type: `string (CSV, regions)` +- Default: `US,CA,DE` +- Pref Type: Firefox + +A comma separated list of geos that by default have spocs enabled in newtab. It matches the client's geo with that list. + +### `browser.newtabpage.activity-stream.discoverystream.region-layout-config` + +- Type: `string (CSV, regions)` +- Default: `US,CA,GB,DE,IE,CH,AT,BE` +- Pref Type: Firefox + +A comma separated list of geos that have 7 rows of stories enabled in newtab. It matches the client's geo with that list. + +### `browser.newtabpage.activity-stream.discoverystream.region-basic-layout` + +- Type: `boolean` +- Default: false +- Pref Type: AS + +If this is `true` newtabs with stories enabled see 1 row. It is set programmatically based on the result from region-layout-config. + +### `browser.newtabpage.activity-stream.discoverystream.spocs-endpoint` + +- Type: `string (URL)` +- Default: `null` +- Pref Type: Firefox + +Override to specify endpoint for SPOCs. Will take precedence over remote and hardcoded layout SPOC endpoints. + +### `browser.newtabpage.activity-stream.discoverystream.personalization.version` + +- Type: `integer` +- Default: `1` +- Pref Type: Firefox + +This controls what version of personalization we should use to score newtab stories. + +### `browser.newtabpage.activity-stream.discoverystream.personalization.modelKeys` + +- Type: `string (CSV)` +- Default: `nb_model_arts_and_entertainment, nb_model_autos_and_vehicles, nb_model_beauty_and_fitness, nb_model_blogging_resources_and_services, nb_model_books_and_literature, nb_model_business_and_industrial, nb_model_computers_and_electronics, nb_model_finance, nb_model_food_and_drink, nb_model_games, nb_model_health, nb_model_hobbies_and_leisure, nb_model_home_and_garden, nb_model_internet_and_telecom, nb_model_jobs_and_education, nb_model_law_and_government, nb_model_online_communities, nb_model_people_and_society, nb_model_pets_and_animals, nb_model_real_estate, nb_model_reference, nb_model_science, nb_model_shopping, nb_model_sports, nb_model_travel` +- Pref Type: Firefox + +This is a configuration for personalization version 2. It is a list of topics the algorithm uses to score stories by. + +### `browser.newtabpage.activity-stream.discoverystream.recs.personalized` + +- Type: `boolean` +- Default: false +- Pref Type: Firefox + +This controls if newtab story personalization includes regular stories or not. See spocs.personalized for sponsored content. + +### `browser.newtabpage.activity-stream.discoverystream.spocs.personalized` + +- Type: `boolean` +- Default: true +- Pref Type: Firefox + +This controls if newtab story personalization includes sponsored content or not. See recs.personalized for regular stories. + +### `browser.newtabpage.activity-stream.discoverystream.isCollectionDismissible` + +- Type: `boolean` +- Default: true +- Pref Type: Firefox + +This controls if newtab story collections are dismissible or not. + +### `browser.newtabpage.activity-stream.feeds.section.topstories` + +- Type: `boolean` +- Default: true +- Pref Type: Firefox + +This controls if the user should see newtab stories or not. It is set by the user via about:preferences#home + +### `browser.newtabpage.activity-stream.feeds.system.topstories` + +- Type: `boolean` +- Default: false +- Pref Type: AS + +Not intended for user configuration, but is programmatically set. It also controls if the user should see newtab stories or not. It is set at run time, and computed based on the locale/region. diff --git a/browser/components/newtab/docs/v2-system-addon/sections.md b/browser/components/newtab/docs/v2-system-addon/sections.md new file mode 100644 index 0000000000..332cf5b26d --- /dev/null +++ b/browser/components/newtab/docs/v2-system-addon/sections.md @@ -0,0 +1,82 @@ +# Sections + +Each section in Activity Stream displays data from a corresponding section feed +in a standardised `Section` UI component. Each section feed is responsible for +listening to events and updating the section options such as the title, icon, +and rows (the cards for the section to display). + +The `Section` UI component displays the rows provided by the section feed. If no +rows are available it displays an empty state consisting of an icon and a +message. Optionally, the section may have a info option menu that is displayed +when users hover over the info icon. + +On load, `SectionsManager` and `SectionsFeed` in `SectionsManager.sys.mjs` add the +sections configured in the `BUILT_IN_SECTIONS` map to the state. These sections +are initially disabled, so aren't visible. The section's feed may use the +methods provided by the `SectionsManager` to enable its section and update its +properties. + +The section configuration in `BUILT_IN_SECTIONS` consists of a generator +function keyed by the pref name for the section feed. The generator function +takes an `options` argument as the only parameter, which is passed the object +stored as serialised JSON in the pref `{feed_pref_name}.options`, or the empty +object if this doesn't exist. The generator returns a section configuration +object which may have the following properties: + +```{eval-rst} ++--------------------+---------------------+-------------------------------------------------------------------------------------------------------------------+ +| Property | Type | Description | ++====================+=====================+===================================================================================================================+ +| id | String | Non-optional unique id. | ++--------------------+---------------------+-------------------------------------------------------------------------------------------------------------------+ +| title | Localisation object | Has property `id`, the string localisation id, and optionally a `values` object to fill in placeholders. | ++--------------------+---------------------+-------------------------------------------------------------------------------------------------------------------+ +| icon | String | Icon id. New icons should be added in icons.scss. | ++--------------------+---------------------+-------------------------------------------------------------------------------------------------------------------+ +| maxRows | Integer | Maximum number of rows of cards to display. Should be >= 1. | ++--------------------+---------------------+-------------------------------------------------------------------------------------------------------------------+ +| contextMenuOptions | Array of strings | The menu options to provide in the card context menus. | ++--------------------+---------------------+-------------------------------------------------------------------------------------------------------------------+ +| shouldHidePref | Boolean | If true, will the section preference in the preferences pane will not be shown. | ++--------------------+---------------------+-------------------------------------------------------------------------------------------------------------------+ +| pref | Object | Configures the section preference to show in the preferences pane. Has properties `titleString` and `descString`. | ++--------------------+---------------------+-------------------------------------------------------------------------------------------------------------------+ +| emptyState | Object | Configures the empty state of the section. Has properties `message` and `icon`. | ++--------------------+---------------------+-------------------------------------------------------------------------------------------------------------------+ +``` + +## Section feeds + +Each section feed should be controlled by the pref `feeds.section.{section_id}`. + +### Enabling the section + +The section feed must listen for the events `INIT` (dispatched when Activity +Stream is initialised) and `FEED_INIT` (dispatched when a feed is re-enabled +having been turned off, with the feed id as the `data`). On these events it must +call `SectionsManager.enableSection(id)`. Care should be taken that this happens +only once `SectionsManager` has also initialised; the feed can use the method +`SectionsManager.onceInitialized()`. + +### Disabling the section + +The section feed must have an `uninit` method. This is called when the section +feed is disabled by turning the section's pref off. In `uninit` the feed must +call `SectionsManager.disableSection(id)`. This will remove the section's UI +component from every existing Activity Stream page. + +### Updating the section rows + +The section feed can call `SectionsManager.updateSection(id, options)` to update +section options. The `rows` array property of `options` stores the cards of +sites to display. Each card object may have the following properties: + +```js +{ + type, // One of the types in Card/types.js, e.g. "Trending" + title, // Title string + description, // Description string + image, // Image url + url // Site url +} +``` diff --git a/browser/components/newtab/docs/v2-system-addon/telemetry.md b/browser/components/newtab/docs/v2-system-addon/telemetry.md new file mode 100644 index 0000000000..848c931717 --- /dev/null +++ b/browser/components/newtab/docs/v2-system-addon/telemetry.md @@ -0,0 +1,10 @@ +# Telemetry checklist + +Adding telemetry generally involves a few steps: + +1. File a "user story" bug about who wants what question answered. This will be used to track the client-side implementation as well as the data review request. If the server side changes are needed, ask Nan (:nanj / @ncloudio) if in doubt, bugs will be filed separately as dependencies. +1. Implement as usual... +1. Get review from Nan on the data schema and the documentation changes. +1. Request `data-review` of your documentation changes from a [data steward](https://wiki.mozilla.org/Firefox/Data_Collection) to ensure suitability for collection controlled by the opt-out `datareporting.healthreport.uploadEnabled` pref. Download and fill out the [data review request form](https://github.com/mozilla/data-review/blob/master/request.md) and then attach it as a text file on Bugzilla so you can r? a data steward. We've been working with Chris H-C (:chutten) for the Firefox specific telemetry, and Kenny Long (kenny@getpocket.com) for the Pocket specific telemetry, they are the best candidates for the review work as they know well about the context. +1. After landing the implementation, check with Nan to make sure the pings are making it to the database. +1. Once data flows in, you can build dashboard for the new telemetry on [Redash](https://sql.telemetry.mozilla.org/dashboards). If you're looking for some help about Redash or dashboard building, Nan is the guy for that. diff --git a/browser/components/newtab/docs/v2-system-addon/tippytop.md b/browser/components/newtab/docs/v2-system-addon/tippytop.md new file mode 100644 index 0000000000..37111135c7 --- /dev/null +++ b/browser/components/newtab/docs/v2-system-addon/tippytop.md @@ -0,0 +1,40 @@ +# TippyTop in Activity Stream +TippyTop, a collection of icons from the Alexa top sites, provides high quality images for the Top Sites in Activity Stream. The TippyTop manifest is hosted on S3, and then moved to [Remote Settings](https://remote-settings.readthedocs.io/en/latest/index.html) since Firefox 63. In this document, we'll cover how we produce and manage TippyTop manifest for Activity Stream. + +## TippyTop manifest production +TippyTop manifest is produced by [tippy-top-sites](https://github.com/mozilla/tippy-top-sites). + +```sh +# set up the environment, only needed for the first time +$ pip install -r requirements.txt +$ python make_manifest.py --count 2000 > icons.json # Alexa top 2000 sites +``` + +Because the manifest is hosted remotely, we use another repo [tippytop-service](https://github.com/mozilla-services/tippytop-service) for the version control and deployment. Ask :nanj or :r1cky for permission to access this private repo. + +## TippyTop manifest publishing +For each new manifest release, firstly you should tag it in the tippytop-service repo, then publish it as follows: + +### For Firefox 62 and below +File a deploy bug with the tagged version at Bugzilla as [Activity Streams: Application Servers](https://bugzilla.mozilla.org/enter_bug.cgi?product=Firefox&component=Activity%20Streams%3A%20Application%20Servers), assign it to our system engineer :jbuck, he will take care of the rest. + +### For Firefox 63 and beyond +Activity Stream started using Remote Settings to manage TippyTop manifest since Firefox 63. To be able to publish new manifest, you need to be in the author&reviewer group of Remote Settings. See more details in this [mana page](https://mana.mozilla.org/wiki/pages/viewpage.action?pageId=66655528). You can also ask :nanj or :leplatram to get this set up for you. +To publish the manifest to Remote Settings, go to the tippytop-service repo, and run the script as follows, + +```sh +# set up the remote setting, only needed for the first time +$ python3 -m venv .venv +$ source .venv/bin/activate +$ pip install -r requirements.txt + +# publish it to prod +$ source .venv/bin/activate +# It will ask you for your LDAP user name and password. +$ ./upload2remotesettings.py prod +``` + +After uploading it to Remote Setting, you can request for review in the [dashboard](https://remote-settings.mozilla.org/v1/admin/). Note that you will need to log in the Mozilla LDAP VPN for both uploading and accessing Remote Setting's dashboard. Once your request gets approved by the reviewer, the new manifest will be content signed and published to production. + +## TippyTop Viewer +You can use this [viewer](https://mozilla.github.io/tippy-top-sites/manifest-viewer/) to load all the icons in the current manifest. diff --git a/browser/components/newtab/docs/v2-system-addon/unit_testing_guide.md b/browser/components/newtab/docs/v2-system-addon/unit_testing_guide.md new file mode 100644 index 0000000000..c3dd369a2d --- /dev/null +++ b/browser/components/newtab/docs/v2-system-addon/unit_testing_guide.md @@ -0,0 +1,149 @@ +# Unit testing + +## Overview + +Our unit tests in Activity Stream are written with mocha, chai, and sinon, and run +with karma. They include unit tests for both content code (React components, etc.) +and `.jsm`s. + +You can find unit tests in `tests/unit`. + +## Execution + +To run the unit tests once, execute `npm test`. + +To run unit tests continuously (i.e. in "test-driven development" mode), you can +run `npm run tddmc`. + +## Debugging + +To debug tests, you should run them in continuous mode with `npm run tddmc`. +In the Firefox window that is opened (it should say "Karma... - connected"), +click the "debug" button and open your console to see test output, set +breakpoints, etc. + +Unfortunately, source maps for tests do not currently work in Firefox. If you need +to see line numbers, you can run the tests with Chrome by running +`npm install --save-dev karma-chrome-launcher && npm run tddmc -- --browsers Chrome` + +## Where to put new tests + +If you are creating a new test, add it to a subdirectory of the `tests/unit` +that corresponds to the file you are testing. Tests should end with `.test.js` or +`.test.jsx` if the test includes any jsx. + +For example, if the file you are testing is `lib/Foo.jsm`, the test +file should be `test/unit/lib/Foo.test.js` + +## Mocha tests + +All our unit tests are written with [mocha](https://mochajs.org), which injects +globals like `describe`, `it`, `beforeEach`, and others. It can be used to write +synchronous or asynchronous tests: + +```js +describe("FooModule", () => { + // A synchronous test + it("should create an instance", () => { + assert.instanceOf(new FooModule(), FooModule); + }); + describe("#meaningOfLife", () => { + // An asynchronous test + it("should eventually get the meaning of life", async () => { + const foo = new FooModule(); + const result = await foo.meaningOfLife(); + assert.equal(result, 42); + }); + }); +}); +``` + +## Assertions + +To write assertions, use the globally available `assert` object (this is provided +by karma-chai, so you do not need to `require` it). + +For example: + +```js +assert.equal(foo, 3); +assert.propertyVal(someObj, "foo", 3); +assert.calledOnce(someStub); +``` + +You can use any of the assertions from: + +- [`chai`](http://chaijs.com/api/assert/). +- [`sinon-chai`](https://github.com/domenic/sinon-chai#assertions) + +### Custom assertions + +We have some custom assertions for checking various types of actions: + +#### `.isUserEventAction(action)` + +Asserts that a given `action` is a valid User Event, i.e. that it contains only +expected/valid properties for User Events in Activity Stream. + +```js +// This will pass +assert.isUserEventAction(ac.UserEvent({event: "CLICK"})); + +// This will fail +assert.isUserEventAction({type: "FOO"}); + +// This will fail because BLOOP is not a valid event type +assert.isUserEventAction(ac.UserEvent({event: "BLOOP"})); +``` + +## Overriding globals in `.jsm`s + +Most `.jsm`s you will be testing use `Cu.import` or `XPCOMUtils` to inject globals. +In order to add mocks/stubs/fakes for these globals, you should use the `GlobalOverrider` +utility in `test/unit/utils`: + +```js +const {GlobalOverrider} = require("test/unit/utils"); +describe("MyModule", () => { + let globals; + let sandbox; + beforeEach(() => { + globals = new GlobalOverrider(); + sandbox = globals.sandbox; // this is a sinon sandbox + // This will inject a "AboutNewTab" global before each test + globals.set("AboutNewTab", {override: sandbox.stub()}); + }); + // globals.restore() clears any globals you added as well as the sinon sandbox + afterEach(() => globals.restore()); +}); +``` + +## Testing React components + +You should use the [enzyme](https://github.com/airbnb/enzyme) suite of test utilities +to test React Components for Activity Stream. + +Where possible, use the [shallow rendering method](https://github.com/airbnb/enzyme/blob/master/docs/api/shallow.md) (this will avoid unnecessarily +rendering child components): + +```js +const React = require("react"); +const {shallow} = require("enzyme"); + +describe("", () => { + it("should be hidden by default", () => { + const wrapper = shallow(); + assert.isTrue(wrapper.find(".wrapper").props().hidden); + }); +}); +``` + +If you need to, you can also do [Full DOM rendering](https://github.com/airbnb/enzyme/blob/master/docs/api/mount.md) +with enzyme's `mount` utility. + +```js +const React = require("react"); +const {mount} = require("enzyme"); +... +const wrapper = mount(); +``` diff --git a/browser/components/newtab/jar.mn b/browser/components/newtab/jar.mn new file mode 100644 index 0000000000..d7b9912862 --- /dev/null +++ b/browser/components/newtab/jar.mn @@ -0,0 +1,40 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +browser.jar: +% resource activity-stream %res/activity-stream/ contentaccessible=yes +% content activity-stream %content/activity-stream/ contentaccessible=yes + res/activity-stream/lib/ (./lib/*) + res/activity-stream/common/ (./common/*) + res/activity-stream/vendor/Redux.sys.mjs (./vendor/Redux.sys.mjs) + res/activity-stream/vendor/react.js (./vendor/react.js) + res/activity-stream/vendor/react-dom.js (./vendor/react-dom.js) + res/activity-stream/vendor/react-dom-server.js (./vendor/react-dom-server.js) +#ifndef RELEASE_OR_BETA + res/activity-stream/vendor/react-dev.js (./vendor/react-dev.js) + res/activity-stream/vendor/react-dom-dev.js (./vendor/react-dom-dev.js) +#endif + res/activity-stream/vendor/prop-types.js (./vendor/prop-types.js) + res/activity-stream/vendor/react-transition-group.js (./vendor/react-transition-group.js) + res/activity-stream/vendor/redux.js (./vendor/redux.js) + res/activity-stream/vendor/react-redux.js (./vendor/react-redux.js) +* res/activity-stream/data/content/abouthomecache/page.html.template (./data/content/abouthomecache/page.html.template) +* res/activity-stream/data/content/abouthomecache/script.js.template (./data/content/abouthomecache/script.js.template) + content/activity-stream/data/content/assets/ (./data/content/assets/*) + content/activity-stream/data/content/tippytop/ (./data/content/tippytop/*) + res/activity-stream/data/content/activity-stream.bundle.js (./data/content/activity-stream.bundle.js) + res/activity-stream/data/content/newtab-render.js (./data/content/newtab-render.js) + res/activity-stream/data/custom-elements/ (./components/CustomElements/*) +#ifdef XP_MACOSX + content/activity-stream/css/activity-stream.css (./css/activity-stream-mac.css) +#elifdef XP_WIN + content/activity-stream/css/activity-stream.css (./css/activity-stream-windows.css) +#else + content/activity-stream/css/activity-stream.css (./css/activity-stream-linux.css) +#endif + res/activity-stream/prerendered/activity-stream.html (./prerendered/activity-stream.html) +#ifndef RELEASE_OR_BETA + res/activity-stream/prerendered/activity-stream-debug.html (./prerendered/activity-stream-debug.html) +#endif + res/activity-stream/prerendered/activity-stream-noscripts.html (./prerendered/activity-stream-noscripts.html) diff --git a/browser/components/newtab/karma.mc.config.js b/browser/components/newtab/karma.mc.config.js new file mode 100644 index 0000000000..78ef457865 --- /dev/null +++ b/browser/components/newtab/karma.mc.config.js @@ -0,0 +1,287 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const path = require("path"); +const webpack = require("webpack"); +const { ResourceUriPlugin } = require("./tools/resourceUriPlugin"); + +const PATHS = { + // Where is the entry point for the unit tests? + testEntryFile: path.resolve(__dirname, "test/unit/unit-entry.js"), + + // A glob-style pattern matching all unit tests + testFilesPattern: "test/unit/**/*.js", + + // The base directory of all source files (used for path resolution in webpack importing) + moduleResolveDirectory: __dirname, + + // a RegEx matching all Cu.import statements of local files + resourcePathRegEx: /^resource:\/\/activity-stream\//, + + coverageReportingPath: "logs/coverage/", +}; + +// When tweaking here, be sure to review the docs about the execution ordering +// semantics of the preprocessors array, as they are somewhat odd. +const preprocessors = {}; +preprocessors[PATHS.testFilesPattern] = [ + "webpack", // require("karma-webpack") + "sourcemap", // require("karma-sourcemap-loader") +]; + +module.exports = function (config) { + const isTDD = config.tdd; + const browsers = isTDD ? ["Firefox"] : ["FirefoxHeadless"]; // require("karma-firefox-launcher") + config.set({ + singleRun: !isTDD, + browsers, + customLaunchers: { + FirefoxHeadless: { + base: "Firefox", + flags: ["--headless"], + }, + }, + frameworks: [ + "chai", // require("chai") require("karma-chai") + "mocha", // require("mocha") require("karma-mocha") + "sinon", // require("sinon") require("karma-sinon") + ], + reporters: [ + "coverage-istanbul", // require("karma-coverage") + "mocha", // require("karma-mocha-reporter") + + // for bin/try-runner.js to parse the output easily + "json", // require("karma-json-reporter") + ], + jsonReporter: { + // So this doesn't get interleaved with other karma output + stdout: false, + outputFile: path.join("logs", "karma-run-results.json"), + }, + coverageIstanbulReporter: { + reports: ["lcov", "text-summary"], // for some reason "lcov" reallys means "lcov" and "html" + "report-config": { + // so the full m-c path gets printed; needed for https://coverage.moz.tools/ integration + lcov: { + projectRoot: "../../..", + }, + }, + dir: PATHS.coverageReportingPath, + // This will make karma fail if coverage reporting is less than the minimums here + thresholds: !isTDD && { + each: { + statements: 100, + lines: 100, + functions: 100, + branches: 66, + overrides: { + "lib/AboutPreferences.sys.mjs": { + statements: 98, + lines: 98, + functions: 94, + branches: 66, + }, + /** + * TelemetryFeed.sys.mjs is tested via an xpcshell test + */ + "lib/TelemetryFeed.sys.mjs": { + statements: 10, + lines: 10, + functions: 9, + branches: 0, + }, + "content-src/lib/init-store.js": { + statements: 98, + lines: 98, + functions: 100, + branches: 100, + }, + "lib/ActivityStreamStorage.sys.mjs": { + statements: 100, + lines: 100, + functions: 100, + branches: 83, + }, + "lib/DownloadsManager.sys.mjs": { + statements: 100, + lines: 100, + functions: 100, + branches: 78, + }, + /** + * PlacesFeed.sys.mjs is tested via an xpcshell test + */ + "lib/PlacesFeed.sys.mjs": { + statements: 7, + lines: 7, + functions: 8, + branches: 0, + }, + "lib/UTEventReporting.sys.mjs": { + statements: 100, + lines: 100, + functions: 100, + branches: 75, + }, + "lib/Screenshots.sys.mjs": { + statements: 94, + lines: 94, + functions: 75, + branches: 84, + }, + /** + * Store.sys.mjs is tested via an xpcshell test + */ + "lib/Store.sys.mjs": { + statements: 8, + lines: 8, + functions: 0, + branches: 0, + }, + /** + * TopSitesFeed.sys.mjs is tested via an xpcshell test + */ + "lib/TopSitesFeed.sys.mjs": { + statements: 9, + lines: 9, + functions: 5, + branches: 0, + }, + /** + * TopStoresFeed.sys.mjs is not tested in automation and is slated + * for eventual removal. + */ + "lib/TopStoriesFeed.sys.mjs": { + statements: 0, + lines: 0, + functions: 0, + branches: 0, + }, + "content-src/components/DiscoveryStreamComponents/**/*.jsx": { + statements: 90.48, + lines: 90.48, + functions: 85.71, + branches: 68.75, + }, + "content-src/asrouter/**/*.jsx": { + statements: 57, + lines: 58, + functions: 60, + branches: 50, + }, + "content-src/components/DiscoveryStreamAdmin/*.jsx": { + statements: 0, + lines: 0, + functions: 0, + branches: 0, + }, + "content-src/components/CustomizeMenu/**/*.jsx": { + statements: 0, + lines: 0, + functions: 0, + branches: 0, + }, + "content-src/components/CustomizeMenu/*.jsx": { + statements: 0, + lines: 0, + functions: 0, + branches: 0, + }, + "content-src/lib/link-menu-options.js": { + statements: 96, + lines: 96, + functions: 96, + branches: 70, + }, + "content-src/components/**/*.jsx": { + statements: 51.1, + lines: 52.38, + functions: 31.2, + branches: 31.2, + }, + }, + }, + }, + }, + files: [PATHS.testEntryFile], + preprocessors, + webpack: { + mode: "none", + devtool: "inline-source-map", + // This loader allows us to override required files in tests + resolveLoader: { + alias: { inject: path.join(__dirname, "loaders/inject-loader") }, + }, + // This resolve config allows us to import with paths relative to the root directory, e.g. "lib/ActivityStream.sys.mjs" + resolve: { + extensions: [".js", ".jsx"], + modules: [PATHS.moduleResolveDirectory, "node_modules"], + fallback: { + stream: require.resolve("stream-browserify"), + buffer: require.resolve("buffer"), + }, + alias: { + asrouter: path.join(__dirname, "../asrouter"), + }, + }, + plugins: [ + // The ResourceUriPlugin handles translating resource URIs in import + // statements in .mjs files to paths on the filesystem. + new ResourceUriPlugin({ + resourcePathRegExes: [ + [ + new RegExp("^resource://activity-stream/"), + path.join(__dirname, "./"), + ], + ], + }), + new webpack.DefinePlugin({ + "process.env.NODE_ENV": JSON.stringify("development"), + }), + ], + externals: { + // enzyme needs these for backwards compatibility with 0.13. + // see https://github.com/airbnb/enzyme/blob/master/docs/guides/webpack.md#using-enzyme-with-webpack + "react/addons": true, + "react/lib/ReactContext": true, + "react/lib/ExecutionEnvironment": true, + }, + module: { + rules: [ + { + test: /\.js$/, + exclude: [/node_modules\/(?!@fluent\/).*/, /test/], + loader: "babel-loader", + }, + { + test: /\.jsx$/, + exclude: /node_modules/, + loader: "babel-loader", + options: { + presets: ["@babel/preset-react"], + }, + }, + { + test: /\.md$/, + use: "raw-loader", + }, + { + enforce: "post", + test: /\.js[mx]?$/, + loader: "@jsdevtools/coverage-istanbul-loader", + options: { esModules: true }, + include: [ + path.resolve("content-src"), + path.resolve("lib"), + path.resolve("common"), + ], + exclude: [path.resolve("test"), path.resolve("vendor")], + }, + ], + }, + }, + // Silences some overly-verbose logging of individual module builds + webpackMiddleware: { noInfo: true }, + }); +}; diff --git a/browser/components/newtab/lib/AboutPreferences.sys.mjs b/browser/components/newtab/lib/AboutPreferences.sys.mjs new file mode 100644 index 0000000000..33f7ecdaeb --- /dev/null +++ b/browser/components/newtab/lib/AboutPreferences.sys.mjs @@ -0,0 +1,298 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { + actionTypes as at, + actionCreators as ac, +} from "resource://activity-stream/common/Actions.sys.mjs"; + +const HTML_NS = "http://www.w3.org/1999/xhtml"; +export const PREFERENCES_LOADED_EVENT = "home-pane-loaded"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", +}); + +// These "section" objects are formatted in a way to be similar to the ones from +// SectionsManager to construct the preferences view. +const PREFS_BEFORE_SECTIONS = () => [ + { + id: "search", + pref: { + feed: "showSearch", + titleString: "home-prefs-search-header", + }, + icon: "chrome://global/skin/icons/search-glass.svg", + }, + { + id: "topsites", + pref: { + feed: "feeds.topsites", + titleString: "home-prefs-shortcuts-header", + descString: "home-prefs-shortcuts-description", + get nestedPrefs() { + return Services.prefs.getBoolPref("browser.topsites.useRemoteSetting") + ? [ + { + name: "showSponsoredTopSites", + titleString: "home-prefs-shortcuts-by-option-sponsored", + eventSource: "SPONSORED_TOP_SITES", + }, + ] + : []; + }, + }, + icon: "chrome://browser/skin/topsites.svg", + maxRows: 4, + rowsPref: "topSitesRows", + eventSource: "TOP_SITES", + }, +]; + +export class AboutPreferences { + init() { + Services.obs.addObserver(this, PREFERENCES_LOADED_EVENT); + } + + uninit() { + Services.obs.removeObserver(this, PREFERENCES_LOADED_EVENT); + } + + onAction(action) { + switch (action.type) { + case at.INIT: + this.init(); + break; + case at.UNINIT: + this.uninit(); + break; + case at.SETTINGS_OPEN: + action._target.browser.ownerGlobal.openPreferences("paneHome"); + break; + // This is used to open the web extension settings page for an extension + case at.OPEN_WEBEXT_SETTINGS: + action._target.browser.ownerGlobal.BrowserOpenAddonsMgr( + `addons://detail/${encodeURIComponent(action.data)}` + ); + break; + } + } + + handleDiscoverySettings(sections) { + // Deep copy object to not modify original Sections state in store + let sectionsCopy = JSON.parse(JSON.stringify(sections)); + sectionsCopy.forEach(obj => { + if (obj.id === "topstories") { + obj.rowsPref = ""; + } + }); + return sectionsCopy; + } + + setupUserEvent(element, eventSource) { + element.addEventListener("command", e => { + const { checked } = e.target; + if (typeof checked === "boolean") { + this.store.dispatch( + ac.UserEvent({ + event: "PREF_CHANGED", + source: eventSource, + value: { status: checked, menu_source: "ABOUT_PREFERENCES" }, + }) + ); + } + }); + } + + observe(window) { + const discoveryStreamConfig = this.store.getState().DiscoveryStream.config; + let sections = this.store.getState().Sections; + + if (discoveryStreamConfig.enabled) { + sections = this.handleDiscoverySettings(sections); + } + + const featureConfig = lazy.NimbusFeatures.newtab.getAllVariables() || {}; + + this.renderPreferences(window, [ + ...PREFS_BEFORE_SECTIONS(featureConfig), + ...sections, + ]); + } + + /** + * Render preferences to an about:preferences content window with the provided + * preferences structure. + */ + renderPreferences({ document, Preferences, gHomePane }, prefStructure) { + // Helper to create a new element and append it + const createAppend = (tag, parent, options) => + parent.appendChild(document.createXULElement(tag, options)); + + // Helper to get fluentIDs sometimes encase in an object + const getString = message => + typeof message !== "object" ? message : message.id; + + // Helper to link a UI element to a preference for updating + const linkPref = (element, name, type) => { + const fullPref = `browser.newtabpage.activity-stream.${name}`; + element.setAttribute("preference", fullPref); + Preferences.add({ id: fullPref, type }); + + // Prevent changing the UI if the preference can't be changed + element.disabled = Preferences.get(fullPref).locked; + }; + + // Insert a new group immediately after the homepage one + const homeGroup = document.getElementById("homepageGroup"); + const contentsGroup = homeGroup.insertAdjacentElement( + "afterend", + homeGroup.cloneNode() + ); + contentsGroup.id = "homeContentsGroup"; + contentsGroup.setAttribute("data-subcategory", "contents"); + const homeHeader = createAppend("label", contentsGroup).appendChild( + document.createElementNS(HTML_NS, "h2") + ); + document.l10n.setAttributes(homeHeader, "home-prefs-content-header2"); + + const homeDescription = createAppend("description", contentsGroup); + homeDescription.classList.add("description-deemphasized"); + + document.l10n.setAttributes( + homeDescription, + "home-prefs-content-description2" + ); + + // Add preferences for each section + prefStructure.forEach(sectionData => { + const { + id, + pref: prefData, + icon = "webextension", + maxRows, + rowsPref, + shouldHidePref, + eventSource, + } = sectionData; + const { + feed: name, + titleString = {}, + descString, + nestedPrefs = [], + } = prefData || {}; + + // Don't show any sections that we don't want to expose in preferences UI + if (shouldHidePref) { + return; + } + + // Use full icon spec for certain protocols or fall back to packaged icon + const iconUrl = !icon.search(/^(chrome|moz-extension|resource):/) + ? icon + : `chrome://activity-stream/content/data/content/assets/glyph-${icon}-16.svg`; + + // Add the main preference for turning on/off a section + const sectionVbox = createAppend("vbox", contentsGroup); + sectionVbox.setAttribute("data-subcategory", id); + const checkbox = createAppend("checkbox", sectionVbox); + checkbox.classList.add("section-checkbox"); + checkbox.setAttribute("src", iconUrl); + // Setup a user event if we have an event source for this pref. + if (eventSource) { + this.setupUserEvent(checkbox, eventSource); + } + document.l10n.setAttributes( + checkbox, + getString(titleString), + titleString.values + ); + + linkPref(checkbox, name, "bool"); + + // Specially add a link for stories + if (id === "topstories") { + const sponsoredHbox = createAppend("hbox", sectionVbox); + sponsoredHbox.setAttribute("align", "center"); + sponsoredHbox.appendChild(checkbox); + checkbox.classList.add("tail-with-learn-more"); + + const link = createAppend("label", sponsoredHbox, { is: "text-link" }); + link.classList.add("learn-sponsored"); + link.setAttribute("href", sectionData.pref.learnMore.link.href); + document.l10n.setAttributes(link, sectionData.pref.learnMore.link.id); + } + + // Add more details for the section (e.g., description, more prefs) + const detailVbox = createAppend("vbox", sectionVbox); + detailVbox.classList.add("indent"); + if (descString) { + const description = createAppend("description", detailVbox); + description.classList.add("indent", "text-deemphasized"); + document.l10n.setAttributes( + description, + getString(descString), + descString.values + ); + + // Add a rows dropdown if we have a pref to control and a maximum + if (rowsPref && maxRows) { + const detailHbox = createAppend("hbox", detailVbox); + detailHbox.setAttribute("align", "center"); + description.setAttribute("flex", 1); + detailHbox.appendChild(description); + + // Add box so the search tooltip is positioned correctly + const tooltipBox = createAppend("hbox", detailHbox); + + // Add appropriate number of localized entries to the dropdown + const menulist = createAppend("menulist", tooltipBox); + menulist.setAttribute("crop", "none"); + const menupopup = createAppend("menupopup", menulist); + for (let num = 1; num <= maxRows; num++) { + const item = createAppend("menuitem", menupopup); + document.l10n.setAttributes( + item, + "home-prefs-sections-rows-option", + { num } + ); + item.setAttribute("value", num); + } + linkPref(menulist, rowsPref, "int"); + } + } + + const subChecks = []; + const fullName = `browser.newtabpage.activity-stream.${sectionData.pref.feed}`; + const pref = Preferences.get(fullName); + + // Add a checkbox pref for any nested preferences + nestedPrefs.forEach(nested => { + const subcheck = createAppend("checkbox", detailVbox); + // Setup a user event if we have an event source for this pref. + if (nested.eventSource) { + this.setupUserEvent(subcheck, nested.eventSource); + } + subcheck.classList.add("indent"); + document.l10n.setAttributes(subcheck, nested.titleString); + linkPref(subcheck, nested.name, "bool"); + subChecks.push(subcheck); + subcheck.disabled = !pref._value; + subcheck.hidden = nested.hidden; + }); + + // Disable any nested checkboxes if the parent pref is not enabled. + pref.on("change", () => { + subChecks.forEach(subcheck => { + subcheck.disabled = !pref._value; + }); + }); + }); + + // Update the visibility of the Restore Defaults btn based on checked prefs + gHomePane.toggleRestoreDefaultsBtn(); + } +} diff --git a/browser/components/newtab/lib/ActivityStream.sys.mjs b/browser/components/newtab/lib/ActivityStream.sys.mjs new file mode 100644 index 0000000000..f2287fe45e --- /dev/null +++ b/browser/components/newtab/lib/ActivityStream.sys.mjs @@ -0,0 +1,700 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// We use importESModule here instead of static import so that +// the Karma test environment won't choke on this module. This +// is because the Karma test environment already stubs out +// AppConstants, and overrides importESModule to be a no-op (which +// can't be done for a static import statement). + +// eslint-disable-next-line mozilla/use-static-import +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + AboutPreferences: "resource://activity-stream/lib/AboutPreferences.sys.mjs", + DEFAULT_SITES: "resource://activity-stream/lib/DefaultSites.sys.mjs", + DefaultPrefs: "resource://activity-stream/lib/ActivityStreamPrefs.sys.mjs", + DiscoveryStreamFeed: + "resource://activity-stream/lib/DiscoveryStreamFeed.sys.mjs", + FaviconFeed: "resource://activity-stream/lib/FaviconFeed.sys.mjs", + HighlightsFeed: "resource://activity-stream/lib/HighlightsFeed.sys.mjs", + NewTabInit: "resource://activity-stream/lib/NewTabInit.sys.mjs", + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", + PrefsFeed: "resource://activity-stream/lib/PrefsFeed.sys.mjs", + PlacesFeed: "resource://activity-stream/lib/PlacesFeed.sys.mjs", + RecommendationProvider: + "resource://activity-stream/lib/RecommendationProvider.sys.mjs", + Region: "resource://gre/modules/Region.sys.mjs", + SectionsFeed: "resource://activity-stream/lib/SectionsManager.sys.mjs", + Store: "resource://activity-stream/lib/Store.sys.mjs", + SystemTickFeed: "resource://activity-stream/lib/SystemTickFeed.sys.mjs", + TelemetryFeed: "resource://activity-stream/lib/TelemetryFeed.sys.mjs", + TopSitesFeed: "resource://activity-stream/lib/TopSitesFeed.sys.mjs", + TopStoriesFeed: "resource://activity-stream/lib/TopStoriesFeed.sys.mjs", +}); + +// NB: Eagerly load modules that will be loaded/constructed/initialized in the +// common case to avoid the overhead of wrapping and detecting lazy loading. +import { + actionCreators as ac, + actionTypes as at, +} from "resource://activity-stream/common/Actions.sys.mjs"; + +const REGION_BASIC_CONFIG = + "browser.newtabpage.activity-stream.discoverystream.region-basic-config"; + +// Determine if spocs should be shown for a geo/locale +function showSpocs({ geo }) { + const spocsGeoString = + lazy.NimbusFeatures.pocketNewtab.getVariable("regionSpocsConfig") || ""; + const spocsGeo = spocsGeoString.split(",").map(s => s.trim()); + return spocsGeo.includes(geo); +} + +// Configure default Activity Stream prefs with a plain `value` or a `getValue` +// that computes a value. A `value_local_dev` is used for development defaults. +export const PREFS_CONFIG = new Map([ + [ + "default.sites", + { + title: + "Comma-separated list of default top sites to fill in behind visited sites", + getValue: ({ geo }) => + lazy.DEFAULT_SITES.get(lazy.DEFAULT_SITES.has(geo) ? geo : ""), + }, + ], + [ + "feeds.section.topstories.options", + { + title: "Configuration options for top stories feed", + // This is a dynamic pref as it depends on the feed being shown or not + getValue: args => + JSON.stringify({ + api_key_pref: "extensions.pocket.oAuthConsumerKey", + // Use the opposite value as what default value the feed would have used + hidden: !PREFS_CONFIG.get("feeds.system.topstories").getValue(args), + provider_icon: "chrome://global/skin/icons/pocket.svg", + provider_name: "Pocket", + read_more_endpoint: + "https://getpocket.com/explore/trending?src=fx_new_tab", + stories_endpoint: `https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?version=3&consumer_key=$apiKey&locale_lang=${ + args.locale + }&feed_variant=${ + showSpocs(args) ? "default_spocs_on" : "default_spocs_off" + }`, + stories_referrer: "https://getpocket.com/recommendations", + topics_endpoint: `https://getpocket.cdn.mozilla.net/v3/firefox/trending-topics?version=2&consumer_key=$apiKey&locale_lang=${args.locale}`, + show_spocs: showSpocs(args), + }), + }, + ], + [ + "feeds.topsites", + { + title: "Displays Top Sites on the New Tab Page", + value: true, + }, + ], + [ + "hideTopSitesTitle", + { + title: + "Hide the top sites section's title, including the section and collapse icons", + value: false, + }, + ], + [ + "showSponsored", + { + title: "User pref for sponsored Pocket content", + value: true, + }, + ], + [ + "system.showSponsored", + { + title: "System pref for sponsored Pocket content", + // This pref is dynamic as the sponsored content depends on the region + getValue: showSpocs, + }, + ], + [ + "showSponsoredTopSites", + { + title: "Show sponsored top sites", + value: true, + }, + ], + [ + "pocketCta", + { + title: "Pocket cta and button for logged out users.", + value: JSON.stringify({ + cta_button: "", + cta_text: "", + cta_url: "", + use_cta: false, + }), + }, + ], + [ + "showSearch", + { + title: "Show the Search bar", + value: true, + }, + ], + [ + "topSitesRows", + { + title: "Number of rows of Top Sites to display", + value: 1, + }, + ], + [ + "telemetry", + { + title: "Enable system error and usage data collection", + value: true, + value_local_dev: false, + }, + ], + [ + "telemetry.ut.events", + { + title: "Enable Unified Telemetry event data collection", + value: AppConstants.EARLY_BETA_OR_EARLIER, + value_local_dev: false, + }, + ], + [ + "telemetry.structuredIngestion.endpoint", + { + title: "Structured Ingestion telemetry server endpoint", + value: "https://incoming.telemetry.mozilla.org/submit", + }, + ], + [ + "section.highlights.includeVisited", + { + title: + "Boolean flag that decides whether or not to show visited pages in highlights.", + value: true, + }, + ], + [ + "section.highlights.includeBookmarks", + { + title: + "Boolean flag that decides whether or not to show bookmarks in highlights.", + value: true, + }, + ], + [ + "section.highlights.includePocket", + { + title: + "Boolean flag that decides whether or not to show saved Pocket stories in highlights.", + value: true, + }, + ], + [ + "section.highlights.includeDownloads", + { + title: + "Boolean flag that decides whether or not to show saved recent Downloads in highlights.", + value: true, + }, + ], + [ + "section.highlights.rows", + { + title: "Number of rows of Highlights to display", + value: 1, + }, + ], + [ + "section.topstories.rows", + { + title: "Number of rows of Top Stories to display", + value: 1, + }, + ], + [ + "sectionOrder", + { + title: "The rendering order for the sections", + value: "topsites,topstories,highlights", + }, + ], + [ + "improvesearch.noDefaultSearchTile", + { + title: "Remove tiles that are the same as the default search", + value: true, + }, + ], + [ + "improvesearch.topSiteSearchShortcuts.searchEngines", + { + title: + "An ordered, comma-delimited list of search shortcuts that we should try and pin", + // This pref is dynamic as the shortcuts vary depending on the region + getValue: ({ geo }) => { + if (!geo) { + return ""; + } + const searchShortcuts = []; + if (geo === "CN") { + searchShortcuts.push("baidu"); + } else if (["BY", "KZ", "RU", "TR"].includes(geo)) { + searchShortcuts.push("yandex"); + } else { + searchShortcuts.push("google"); + } + if (["DE", "FR", "GB", "IT", "JP", "US"].includes(geo)) { + searchShortcuts.push("amazon"); + } + return searchShortcuts.join(","); + }, + }, + ], + [ + "improvesearch.topSiteSearchShortcuts.havePinned", + { + title: + "A comma-delimited list of search shortcuts that have previously been pinned", + value: "", + }, + ], + [ + "asrouter.devtoolsEnabled", + { + title: "Are the asrouter devtools enabled?", + value: false, + }, + ], + [ + "asrouter.providers.onboarding", + { + title: "Configuration for onboarding provider", + value: JSON.stringify({ + id: "onboarding", + type: "local", + localProvider: "OnboardingMessageProvider", + enabled: true, + // Block specific messages from this local provider + exclude: [], + }), + }, + ], + // See browser/app/profile/firefox.js for other ASR preferences. They must be defined there to enable roll-outs. + [ + "discoverystream.flight.blocks", + { + title: "Track flight blocks", + skipBroadcast: true, + value: "{}", + }, + ], + [ + "discoverystream.config", + { + title: "Configuration for the new pocket new tab", + getValue: ({ geo, locale }) => { + return JSON.stringify({ + api_key_pref: "extensions.pocket.oAuthConsumerKey", + collapsible: true, + enabled: true, + }); + }, + }, + ], + [ + "discoverystream.endpoints", + { + title: + "Endpoint prefixes (comma-separated) that are allowed to be requested", + value: + "https://getpocket.cdn.mozilla.net/,https://firefox-api-proxy.cdn.mozilla.net/,https://spocs.getpocket.com/", + }, + ], + [ + "discoverystream.isCollectionDismissible", + { + title: "Allows Pocket story collections to be dismissed", + value: false, + }, + ], + [ + "discoverystream.onboardingExperience.dismissed", + { + title: "Allows the user to dismiss the new Pocket onboarding experience", + skipBroadcast: true, + alsoToPreloaded: true, + value: false, + }, + ], + [ + "discoverystream.region-basic-layout", + { + title: "Decision to use basic layout based on region.", + getValue: ({ geo }) => { + const preffedRegionsString = + Services.prefs.getStringPref(REGION_BASIC_CONFIG) || ""; + // If no regions are set to basic, + // we don't need to bother checking against the region. + // We are also not concerned if geo is not set, + // because stories are going to be empty until we have geo. + if (!preffedRegionsString) { + return false; + } + const preffedRegions = preffedRegionsString + .split(",") + .map(s => s.trim()); + + return preffedRegions.includes(geo); + }, + }, + ], + [ + "discoverystream.spoc.impressions", + { + title: "Track spoc impressions", + skipBroadcast: true, + value: "{}", + }, + ], + [ + "discoverystream.endpointSpocsClear", + { + title: + "Endpoint for when a user opts-out of sponsored content to delete the user's data from the ad server.", + value: "https://spocs.getpocket.com/user", + }, + ], + [ + "discoverystream.rec.impressions", + { + title: "Track rec impressions", + skipBroadcast: true, + value: "{}", + }, + ], + [ + "showRecentSaves", + { + title: "Control whether a user wants recent saves visible on Newtab", + value: true, + }, + ], +]); + +// Array of each feed's FEEDS_CONFIG factory and values to add to PREFS_CONFIG +const FEEDS_DATA = [ + { + name: "aboutpreferences", + factory: () => new lazy.AboutPreferences(), + title: "about:preferences rendering", + value: true, + }, + { + name: "newtabinit", + factory: () => new lazy.NewTabInit(), + title: "Sends a copy of the state to each new tab that is opened", + value: true, + }, + { + name: "places", + factory: () => new lazy.PlacesFeed(), + title: "Listens for and relays various Places-related events", + value: true, + }, + { + name: "prefs", + factory: () => new lazy.PrefsFeed(PREFS_CONFIG), + title: "Preferences", + value: true, + }, + { + name: "sections", + factory: () => new lazy.SectionsFeed(), + title: "Manages sections", + value: true, + }, + { + name: "section.highlights", + factory: () => new lazy.HighlightsFeed(), + title: "Fetches content recommendations from places db", + value: false, + }, + { + name: "system.topstories", + factory: () => + new lazy.TopStoriesFeed(PREFS_CONFIG.get("discoverystream.config")), + title: + "System pref that fetches content recommendations from a configurable content provider", + // Dynamically determine if Pocket should be shown for a geo / locale + getValue: ({ geo, locale }) => { + // If we don't have geo, we don't want to flash the screen with stories while geo loads. + // Best to display nothing until geo is ready. + if (!geo) { + return false; + } + const preffedRegionsBlockString = + lazy.NimbusFeatures.pocketNewtab.getVariable("regionStoriesBlock") || + ""; + const preffedRegionsString = + lazy.NimbusFeatures.pocketNewtab.getVariable("regionStoriesConfig") || + ""; + const preffedLocaleListString = + lazy.NimbusFeatures.pocketNewtab.getVariable("localeListConfig") || ""; + const preffedBlockRegions = preffedRegionsBlockString + .split(",") + .map(s => s.trim()); + const preffedRegions = preffedRegionsString.split(",").map(s => s.trim()); + const preffedLocales = preffedLocaleListString + .split(",") + .map(s => s.trim()); + const locales = { + US: ["en-CA", "en-GB", "en-US"], + CA: ["en-CA", "en-GB", "en-US"], + GB: ["en-CA", "en-GB", "en-US"], + AU: ["en-CA", "en-GB", "en-US"], + NZ: ["en-CA", "en-GB", "en-US"], + IN: ["en-CA", "en-GB", "en-US"], + IE: ["en-CA", "en-GB", "en-US"], + ZA: ["en-CA", "en-GB", "en-US"], + CH: ["de"], + BE: ["de"], + DE: ["de"], + AT: ["de"], + IT: ["it"], + FR: ["fr"], + ES: ["es-ES"], + PL: ["pl"], + JP: ["ja", "ja-JP-mac"], + }[geo]; + + const regionBlocked = preffedBlockRegions.includes(geo); + const localeEnabled = locale && preffedLocales.includes(locale); + const regionEnabled = + preffedRegions.includes(geo) && !!locales && locales.includes(locale); + return !regionBlocked && (localeEnabled || regionEnabled); + }, + }, + { + name: "systemtick", + factory: () => new lazy.SystemTickFeed(), + title: "Produces system tick events to periodically check for data expiry", + value: true, + }, + { + name: "telemetry", + factory: () => new lazy.TelemetryFeed(), + title: "Relays telemetry-related actions to PingCentre", + value: true, + }, + { + name: "favicon", + factory: () => new lazy.FaviconFeed(), + title: "Fetches tippy top manifests from remote service", + value: true, + }, + { + name: "system.topsites", + factory: () => new lazy.TopSitesFeed(), + title: "Queries places and gets metadata for Top Sites section", + value: true, + }, + { + name: "recommendationprovider", + factory: () => new lazy.RecommendationProvider(), + title: "Handles setup and interaction for the personality provider", + value: true, + }, + { + name: "discoverystreamfeed", + factory: () => new lazy.DiscoveryStreamFeed(), + title: "Handles new pocket ui for the new tab page", + value: true, + }, +]; + +const FEEDS_CONFIG = new Map(); +for (const config of FEEDS_DATA) { + const pref = `feeds.${config.name}`; + FEEDS_CONFIG.set(pref, config.factory); + PREFS_CONFIG.set(pref, config); +} + +export class ActivityStream { + /** + * constructor - Initializes an instance of ActivityStream + */ + constructor() { + this.initialized = false; + this.store = new lazy.Store(); + this.feeds = FEEDS_CONFIG; + this._defaultPrefs = new lazy.DefaultPrefs(PREFS_CONFIG); + } + + init() { + try { + this._updateDynamicPrefs(); + this._defaultPrefs.init(); + Services.obs.addObserver(this, "intl:app-locales-changed"); + + // Look for outdated user pref values that might have been accidentally + // persisted when restoring the original pref value at the end of an + // experiment across versions with a different default value. + const DS_CONFIG = + "browser.newtabpage.activity-stream.discoverystream.config"; + if ( + Services.prefs.prefHasUserValue(DS_CONFIG) && + [ + // Firefox 66 + `{"api_key_pref":"extensions.pocket.oAuthConsumerKey","enabled":false,"show_spocs":true,"layout_endpoint":"https://getpocket.com/v3/newtab/layout?version=1&consumer_key=$apiKey&layout_variant=basic"}`, + // Firefox 67 + `{"api_key_pref":"extensions.pocket.oAuthConsumerKey","enabled":false,"show_spocs":true,"layout_endpoint":"https://getpocket.cdn.mozilla.net/v3/newtab/layout?version=1&consumer_key=$apiKey&layout_variant=basic"}`, + // Firefox 68 + `{"api_key_pref":"extensions.pocket.oAuthConsumerKey","collapsible":true,"enabled":false,"show_spocs":true,"hardcoded_layout":true,"personalized":false,"layout_endpoint":"https://getpocket.cdn.mozilla.net/v3/newtab/layout?version=1&consumer_key=$apiKey&layout_variant=basic"}`, + ].includes(Services.prefs.getStringPref(DS_CONFIG)) + ) { + Services.prefs.clearUserPref(DS_CONFIG); + } + + // Hook up the store and let all feeds and pages initialize + this.store.init( + this.feeds, + ac.BroadcastToContent({ + type: at.INIT, + data: { + locale: this.locale, + }, + meta: { + isStartup: true, + }, + }), + { type: at.UNINIT } + ); + + this.initialized = true; + } catch (e) { + // TelemetryFeed could be unavailable if the telemetry is disabled, or + // the telemetry feed is not yet initialized. + const telemetryFeed = this.store.feeds.get("feeds.telemetry"); + if (telemetryFeed) { + telemetryFeed.handleUndesiredEvent({ + data: { event: "ADDON_INIT_FAILED" }, + }); + } + throw e; + } + } + + /** + * Check if an old pref has a custom value to migrate. Clears the pref so that + * it's the default after migrating (to avoid future need to migrate). + * + * @param oldPrefName {string} Pref to check and migrate + * @param cbIfNotDefault {function} Callback that gets the current pref value + */ + _migratePref(oldPrefName, cbIfNotDefault) { + // Nothing to do if the user doesn't have a custom value + if (!Services.prefs.prefHasUserValue(oldPrefName)) { + return; + } + + // Figure out what kind of pref getter to use + let prefGetter; + switch (Services.prefs.getPrefType(oldPrefName)) { + case Services.prefs.PREF_BOOL: + prefGetter = "getBoolPref"; + break; + case Services.prefs.PREF_INT: + prefGetter = "getIntPref"; + break; + case Services.prefs.PREF_STRING: + prefGetter = "getStringPref"; + break; + } + + // Give the callback the current value then clear the pref + cbIfNotDefault(Services.prefs[prefGetter](oldPrefName)); + Services.prefs.clearUserPref(oldPrefName); + } + + uninit() { + if (this.geo === "") { + Services.obs.removeObserver(this, lazy.Region.REGION_TOPIC); + } + + Services.obs.removeObserver(this, "intl:app-locales-changed"); + + this.store.uninit(); + this.initialized = false; + } + + _updateDynamicPrefs() { + // Save the geo pref if we have it + if (lazy.Region.home) { + this.geo = lazy.Region.home; + } else if (this.geo !== "") { + // Watch for geo changes and use a dummy value for now + Services.obs.addObserver(this, lazy.Region.REGION_TOPIC); + this.geo = ""; + } + + this.locale = Services.locale.appLocaleAsBCP47; + + // Update the pref config of those with dynamic values + for (const pref of PREFS_CONFIG.keys()) { + // Only need to process dynamic prefs + const prefConfig = PREFS_CONFIG.get(pref); + if (!prefConfig.getValue) { + continue; + } + + // Have the dynamic pref just reuse using existing default, e.g., those + // set via Autoconfig or policy + try { + const existingDefault = this._defaultPrefs.get(pref); + if (existingDefault !== undefined && prefConfig.value === undefined) { + prefConfig.getValue = () => existingDefault; + } + } catch (ex) { + // We get NS_ERROR_UNEXPECTED for prefs that have a user value (causing + // default branch to believe there's a type) but no actual default value + } + + // Compute the dynamic value (potentially generic based on dummy geo) + const newValue = prefConfig.getValue({ + geo: this.geo, + locale: this.locale, + }); + + // If there's an existing value and it has changed, that means we need to + // overwrite the default with the new value. + if (prefConfig.value !== undefined && prefConfig.value !== newValue) { + this._defaultPrefs.set(pref, newValue); + } + + prefConfig.value = newValue; + } + } + + observe(subject, topic, data) { + switch (topic) { + case "intl:app-locales-changed": + case lazy.Region.REGION_TOPIC: + this._updateDynamicPrefs(); + break; + } + } +} diff --git a/browser/components/newtab/lib/ActivityStreamMessageChannel.sys.mjs b/browser/components/newtab/lib/ActivityStreamMessageChannel.sys.mjs new file mode 100644 index 0000000000..de9d2cb800 --- /dev/null +++ b/browser/components/newtab/lib/ActivityStreamMessageChannel.sys.mjs @@ -0,0 +1,333 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + AboutHomeStartupCache: "resource:///modules/BrowserGlue.sys.mjs", + AboutNewTabParent: "resource:///actors/AboutNewTabParent.sys.mjs", +}); + +import { + actionCreators as ac, + actionTypes as at, + actionUtils as au, +} from "resource://activity-stream/common/Actions.sys.mjs"; + +const ABOUT_NEW_TAB_URL = "about:newtab"; + +export const DEFAULT_OPTIONS = { + dispatch(action) { + throw new Error( + `\nMessageChannel: Received action ${action.type}, but no dispatcher was defined.\n` + ); + }, + pageURL: ABOUT_NEW_TAB_URL, + outgoingMessageName: "ActivityStream:MainToContent", + incomingMessageName: "ActivityStream:ContentToMain", +}; + +export class ActivityStreamMessageChannel { + /** + * ActivityStreamMessageChannel - This module connects a Redux store to the new tab page actor. + * You should use the BroadcastToContent, AlsoToOneContent, and AlsoToMain action creators + * in common/Actions.sys.mjs to help you create actions that will be automatically routed + * to the correct location. + * + * @param {object} options + * @param {function} options.dispatch The dispatch method from a Redux store + * @param {string} options.pageURL The URL to which the channel is attached, such as about:newtab. + * @param {string} options.outgoingMessageName The name of the message sent to child processes + * @param {string} options.incomingMessageName The name of the message received from child processes + * @return {ActivityStreamMessageChannel} + */ + constructor(options = {}) { + Object.assign(this, DEFAULT_OPTIONS, options); + + this.middleware = this.middleware.bind(this); + this.onMessage = this.onMessage.bind(this); + this.onNewTabLoad = this.onNewTabLoad.bind(this); + this.onNewTabUnload = this.onNewTabUnload.bind(this); + this.onNewTabInit = this.onNewTabInit.bind(this); + } + + /** + * Get an iterator over the loaded tab objects. + */ + get loadedTabs() { + // In the test, AboutNewTabParent is not defined. + return lazy.AboutNewTabParent?.loadedTabs || new Map(); + } + + /** + * middleware - Redux middleware that looks for AlsoToOneContent and BroadcastToContent type + * actions, and sends them out. + * + * @param {object} store A redux store + * @return {function} Redux middleware + */ + middleware(store) { + return next => action => { + const skipMain = action.meta && action.meta.skipMain; + if (au.isSendToOneContent(action)) { + this.send(action); + } else if (au.isBroadcastToContent(action)) { + this.broadcast(action); + } else if (au.isSendToPreloaded(action)) { + this.sendToPreloaded(action); + } + + if (!skipMain) { + next(action); + } + }; + } + + /** + * onActionFromContent - Handler for actions from a content processes + * + * @param {object} action A Redux action + * @param {string} targetId The portID of the port that sent the message + */ + onActionFromContent(action, targetId) { + this.dispatch(ac.AlsoToMain(action, this.validatePortID(targetId))); + } + + /** + * broadcast - Sends an action to all ports + * + * @param {object} action A Redux action + */ + broadcast(action) { + // We're trying to update all tabs, so signal the AboutHomeStartupCache + // that its likely time to refresh the cache. + lazy.AboutHomeStartupCache.onPreloadedNewTabMessage(); + + for (let { actor } of this.loadedTabs.values()) { + try { + actor.sendAsyncMessage(this.outgoingMessageName, action); + } catch (e) { + // The target page is closed/closing by the user or test, so just ignore. + } + } + } + + /** + * send - Sends an action to a specific port + * + * @param {obj} action A redux action; it should contain a portID in the meta.toTarget property + */ + send(action) { + const targetId = action.meta && action.meta.toTarget; + const target = this.getTargetById(targetId); + try { + target.sendAsyncMessage(this.outgoingMessageName, action); + } catch (e) { + // The target page is closed/closing by the user or test, so just ignore. + } + } + + /** + * A valid portID is a combination of process id and a port number. + * It is generated in AboutNewTabChild.sys.mjs. + */ + validatePortID(id) { + if (typeof id !== "string" || !id.includes(":")) { + console.error("Invalid portID"); + } + + return id; + } + + /** + * getTargetById - Retrieve the message target by portID, if it exists + * + * @param {string} id A portID + * @return {obj|null} The message target, if it exists. + */ + getTargetById(id) { + this.validatePortID(id); + + for (let { portID, actor } of this.loadedTabs.values()) { + if (portID === id) { + return actor; + } + } + return null; + } + + /** + * sendToPreloaded - Sends an action to each preloaded browser, if any + * + * @param {obj} action A redux action + */ + sendToPreloaded(action) { + // We're trying to update the preloaded about:newtab, so signal + // the AboutHomeStartupCache that its likely time to refresh + // the cache. + lazy.AboutHomeStartupCache.onPreloadedNewTabMessage(); + + const preloadedActors = this.getPreloadedActors(); + if (preloadedActors && action.data) { + for (let preloadedActor of preloadedActors) { + try { + preloadedActor.sendAsyncMessage(this.outgoingMessageName, action); + } catch (e) { + // The preloaded page is no longer available, so just ignore. + } + } + } + } + + /** + * getPreloadedActors - Retrieve the preloaded actors + * + * @return {Array|null} An array of actors belonging to the preloaded browsers, or null + * if there aren't any preloaded browsers + */ + getPreloadedActors() { + let preloadedActors = []; + for (let { actor, browser } of this.loadedTabs.values()) { + if (this.isPreloadedBrowser(browser)) { + preloadedActors.push(actor); + } + } + return preloadedActors.length ? preloadedActors : null; + } + + /** + * isPreloadedBrowser - Returns true if the passed browser has been preloaded + * for faster rendering of new tabs. + * + * @param {} A to check. + * @return {bool} True if the browser is preloaded. + * if there aren't any preloaded browsers + */ + isPreloadedBrowser(browser) { + return browser.getAttribute("preloadedState") === "preloaded"; + } + + simulateMessagesForExistingTabs() { + // Some pages might have already loaded, so we won't get the usual message + for (const loadedTab of this.loadedTabs.values()) { + let simulatedDetails = { + actor: loadedTab.actor, + browser: loadedTab.browser, + browsingContext: loadedTab.browsingContext, + portID: loadedTab.portID, + url: loadedTab.url, + simulated: true, + }; + + this.onActionFromContent( + { + type: at.NEW_TAB_INIT, + data: simulatedDetails, + }, + loadedTab.portID + ); + + if (loadedTab.loaded) { + this.tabLoaded(simulatedDetails); + } + } + + // It's possible that those existing tabs had sent some messages up + // to us before the feeds / ActivityStreamMessageChannel was ready. + // + // AboutNewTabParent takes care of queueing those for us, so + // now that we're ready, we can flush these queued messages. + lazy.AboutNewTabParent.flushQueuedMessagesFromContent(); + } + + /** + * onNewTabInit - Handler for special RemotePage:Init message fired + * on initialization. + * + * @param {obj} msg The messsage from a page that was just initialized + * @param {obj} tabDetails details about a loaded tab + * + * tabDetails contains: + * actor, browser, browsingContext, portID, url + */ + onNewTabInit(msg, tabDetails) { + this.onActionFromContent( + { + type: at.NEW_TAB_INIT, + data: tabDetails, + }, + msg.data.portID + ); + } + + /** + * onNewTabLoad - Handler for special RemotePage:Load message fired on page load. + * + * @param {obj} msg The messsage from a page that was just loaded + * @param {obj} tabDetails details about a loaded tab, similar to onNewTabInit + */ + onNewTabLoad(msg, tabDetails) { + this.tabLoaded(tabDetails); + } + + tabLoaded(tabDetails) { + tabDetails.loaded = true; + + let { browser } = tabDetails; + if ( + this.isPreloadedBrowser(browser) && + browser.ownerGlobal.windowState !== browser.ownerGlobal.STATE_MINIMIZED && + !browser.ownerGlobal.isFullyOccluded + ) { + // As a perceived performance optimization, if this loaded Activity Stream + // happens to be a preloaded browser in a window that is not minimized or + // occluded, have it render its layers to the compositor now to increase + // the odds that by the time we switch to the tab, the layers are already + // ready to present to the user. + browser.renderLayers = true; + } + + this.onActionFromContent({ type: at.NEW_TAB_LOAD }, tabDetails.portID); + } + + /** + * onNewTabUnloadLoad - Handler for special RemotePage:Unload message fired + * on page unload. + * + * @param {obj} msg The messsage from a page that was just unloaded + * @param {obj} tabDetails details about a loaded tab, similar to onNewTabInit + */ + onNewTabUnload(msg, tabDetails) { + this.onActionFromContent({ type: at.NEW_TAB_UNLOAD }, tabDetails.portID); + } + + /** + * onMessage - Handles custom messages from content. It expects all messages to + * be formatted as Redux actions, and dispatches them to this.store + * + * @param {obj} msg A custom message from content + * @param {obj} msg.action A Redux action (e.g. {type: "HELLO_WORLD"}) + * @param {obj} msg.target A message target + * @param {obj} tabDetails details about a loaded tab, similar to onNewTabInit + */ + onMessage(msg, tabDetails) { + if (!msg.data || !msg.data.type) { + console.error( + new Error( + `Received an improperly formatted message from ${tabDetails.portID}` + ) + ); + return; + } + let action = {}; + Object.assign(action, msg.data); + // target is used to access a browser reference that came from the content + // and should only be used in feeds (not reducers) + action._target = { + browser: tabDetails.browser, + }; + + this.onActionFromContent(action, tabDetails.portID); + } +} diff --git a/browser/components/newtab/lib/ActivityStreamPrefs.sys.mjs b/browser/components/newtab/lib/ActivityStreamPrefs.sys.mjs new file mode 100644 index 0000000000..192ff30288 --- /dev/null +++ b/browser/components/newtab/lib/ActivityStreamPrefs.sys.mjs @@ -0,0 +1,100 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// We use importESModule here instead of static import so that +// the Karma test environment won't choke on this module. This +// is because the Karma test environment already stubs out +// AppConstants, and overrides importESModule to be a no-op (which +// can't be done for a static import statement). + +// eslint-disable-next-line mozilla/use-static-import +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +// eslint-disable-next-line mozilla/use-static-import +const { Preferences } = ChromeUtils.importESModule( + "resource://gre/modules/Preferences.sys.mjs" +); + +const ACTIVITY_STREAM_PREF_BRANCH = "browser.newtabpage.activity-stream."; + +export class Prefs extends Preferences { + /** + * Prefs - A wrapper around Preferences that always sets the branch to + * ACTIVITY_STREAM_PREF_BRANCH + */ + constructor(branch = ACTIVITY_STREAM_PREF_BRANCH) { + super({ branch }); + this._branchObservers = new Map(); + } + + ignoreBranch(listener) { + const observer = this._branchObservers.get(listener); + this._prefBranch.removeObserver("", observer); + this._branchObservers.delete(listener); + } + + observeBranch(listener) { + const observer = (subject, topic, pref) => { + listener.onPrefChanged(pref, this.get(pref)); + }; + this._prefBranch.addObserver("", observer); + this._branchObservers.set(listener, observer); + } +} + +export class DefaultPrefs extends Preferences { + /** + * DefaultPrefs - A helper for setting and resetting default prefs for the add-on + * + * @param {Map} config A Map with {string} key of the pref name and {object} + * value with the following pref properties: + * {string} .title (optional) A description of the pref + * {bool|string|number} .value The default value for the pref + * @param {string} branch (optional) The pref branch (defaults to ACTIVITY_STREAM_PREF_BRANCH) + */ + constructor(config, branch = ACTIVITY_STREAM_PREF_BRANCH) { + super({ + branch, + defaultBranch: true, + }); + this._config = config; + } + + /** + * init - Set default prefs for all prefs in the config + */ + init() { + // Local developer builds (with the default mozconfig) aren't OFFICIAL + const IS_UNOFFICIAL_BUILD = !AppConstants.MOZILLA_OFFICIAL; + + for (const pref of this._config.keys()) { + try { + // Avoid replacing existing valid default pref values, e.g., those set + // via Autoconfig or policy + if (this.get(pref) !== undefined) { + continue; + } + } catch (ex) { + // We get NS_ERROR_UNEXPECTED for prefs that have a user value (causing + // default branch to believe there's a type) but no actual default value + } + + const prefConfig = this._config.get(pref); + let value; + if (IS_UNOFFICIAL_BUILD && "value_local_dev" in prefConfig) { + value = prefConfig.value_local_dev; + } else { + value = prefConfig.value; + } + + try { + this.set(pref, value); + } catch (ex) { + // Potentially the user somehow set an unexpected value type, so we fail + // to set a default of our expected type + } + } + } +} diff --git a/browser/components/newtab/lib/ActivityStreamStorage.sys.mjs b/browser/components/newtab/lib/ActivityStreamStorage.sys.mjs new file mode 100644 index 0000000000..1e128ec3f2 --- /dev/null +++ b/browser/components/newtab/lib/ActivityStreamStorage.sys.mjs @@ -0,0 +1,119 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + IndexedDB: "resource://gre/modules/IndexedDB.sys.mjs", +}); + +export class ActivityStreamStorage { + /** + * @param storeNames Array of strings used to create all the required stores + */ + constructor({ storeNames, telemetry }) { + if (!storeNames) { + throw new Error("storeNames required"); + } + + this.dbName = "ActivityStream"; + this.dbVersion = 3; + this.storeNames = storeNames; + this.telemetry = telemetry; + } + + get db() { + return this._db || (this._db = this.createOrOpenDb()); + } + + /** + * Public method that binds the store required by the consumer and exposes + * the private db getters and setters. + * + * @param storeName String name of desired store + */ + getDbTable(storeName) { + if (this.storeNames.includes(storeName)) { + return { + get: this._get.bind(this, storeName), + getAll: this._getAll.bind(this, storeName), + set: this._set.bind(this, storeName), + }; + } + + throw new Error(`Store name ${storeName} does not exist.`); + } + + async _getStore(storeName) { + return (await this.db).objectStore(storeName, "readwrite"); + } + + _get(storeName, key) { + return this._requestWrapper(async () => + (await this._getStore(storeName)).get(key) + ); + } + + _getAll(storeName) { + return this._requestWrapper(async () => + (await this._getStore(storeName)).getAll() + ); + } + + _set(storeName, key, value) { + return this._requestWrapper(async () => + (await this._getStore(storeName)).put(value, key) + ); + } + + _openDatabase() { + return lazy.IndexedDB.open(this.dbName, { version: this.dbVersion }, db => { + // If provided with array of objectStore names we need to create all the + // individual stores + this.storeNames.forEach(store => { + if (!db.objectStoreNames.contains(store)) { + this._requestWrapper(() => db.createObjectStore(store)); + } + }); + }); + } + + /** + * createOrOpenDb - Open a db (with this.dbName) if it exists. + * If it does not exist, create it. + * If an error occurs, deleted the db and attempt to + * re-create it. + * @returns Promise that resolves with a db instance + */ + async createOrOpenDb() { + try { + const db = await this._openDatabase(); + return db; + } catch (e) { + if (this.telemetry) { + this.telemetry.handleUndesiredEvent({ event: "INDEXEDDB_OPEN_FAILED" }); + } + await lazy.IndexedDB.deleteDatabase(this.dbName); + return this._openDatabase(); + } + } + + async _requestWrapper(request) { + let result = null; + try { + result = await request(); + } catch (e) { + if (this.telemetry) { + this.telemetry.handleUndesiredEvent({ event: "TRANSACTION_FAILED" }); + } + throw e; + } + + return result; + } +} + +export function getDefaultOptions(options) { + return { collapsed: !!options.collapsed }; +} diff --git a/browser/components/newtab/lib/DefaultSites.sys.mjs b/browser/components/newtab/lib/DefaultSites.sys.mjs new file mode 100644 index 0000000000..ea49cccc03 --- /dev/null +++ b/browser/components/newtab/lib/DefaultSites.sys.mjs @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const DEFAULT_SITES_MAP = new Map([ + // This first item is the global list fallback for any unexpected geos + [ + "", + "https://www.youtube.com/,https://www.facebook.com/,https://www.wikipedia.org/,https://www.reddit.com/,https://www.amazon.com/,https://twitter.com/", + ], + [ + "US", + "https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://twitter.com/", + ], + [ + "CA", + "https://www.youtube.com/,https://www.facebook.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://www.amazon.ca/,https://twitter.com/", + ], + [ + "DE", + "https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.de/,https://www.ebay.de/,https://www.wikipedia.org/,https://www.reddit.com/", + ], + [ + "PL", + "https://www.youtube.com/,https://www.facebook.com/,https://allegro.pl/,https://www.wikipedia.org/,https://www.olx.pl/,https://www.wykop.pl/", + ], + [ + "RU", + "https://vk.com/,https://www.youtube.com/,https://ok.ru/,https://www.avito.ru/,https://www.aliexpress.com/,https://www.wikipedia.org/", + ], + [ + "GB", + "https://www.youtube.com/,https://www.facebook.com/,https://www.reddit.com/,https://www.amazon.co.uk/,https://www.bbc.co.uk/,https://www.ebay.co.uk/", + ], + [ + "FR", + "https://www.youtube.com/,https://www.facebook.com/,https://www.wikipedia.org/,https://www.amazon.fr/,https://www.leboncoin.fr/,https://twitter.com/", + ], + [ + "CN", + "https://www.baidu.com/,https://www.zhihu.com/,https://www.ifeng.com/,https://weibo.com/,https://www.ctrip.com/,https://www.iqiyi.com/", + ], +]); + +// Immutable for export. +export const DEFAULT_SITES = Object.freeze(DEFAULT_SITES_MAP); diff --git a/browser/components/newtab/lib/DiscoveryStreamFeed.sys.mjs b/browser/components/newtab/lib/DiscoveryStreamFeed.sys.mjs new file mode 100644 index 0000000000..257036b9da --- /dev/null +++ b/browser/components/newtab/lib/DiscoveryStreamFeed.sys.mjs @@ -0,0 +1,2265 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", + NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", + pktApi: "chrome://pocket/content/pktApi.sys.mjs", + PersistentCache: "resource://activity-stream/lib/PersistentCache.sys.mjs", + Region: "resource://gre/modules/Region.sys.mjs", + RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", +}); + +// We use importESModule here instead of static import so that +// the Karma test environment won't choke on this module. This +// is because the Karma test environment already stubs out +// setTimeout / clearTimeout, and overrides importESModule +// to be a no-op (which can't be done for a static import statement). + +// eslint-disable-next-line mozilla/use-static-import +const { setTimeout, clearTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); +import { + actionTypes as at, + actionCreators as ac, +} from "resource://activity-stream/common/Actions.sys.mjs"; + +const CACHE_KEY = "discovery_stream"; +const STARTUP_CACHE_EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000; // 1 week +const COMPONENT_FEEDS_UPDATE_TIME = 30 * 60 * 1000; // 30 minutes +const SPOCS_FEEDS_UPDATE_TIME = 30 * 60 * 1000; // 30 minutes +const DEFAULT_RECS_EXPIRE_TIME = 60 * 60 * 1000; // 1 hour +const MAX_LIFETIME_CAP = 500; // Guard against misconfiguration on the server +const FETCH_TIMEOUT = 45 * 1000; +const SPOCS_URL = "https://spocs.getpocket.com/spocs"; +const FEED_URL = + "https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?version=3&consumer_key=$apiKey&locale_lang=$locale®ion=$region&count=30"; +const PREF_CONFIG = "discoverystream.config"; +const PREF_ENDPOINTS = "discoverystream.endpoints"; +const PREF_IMPRESSION_ID = "browser.newtabpage.activity-stream.impressionId"; +const PREF_ENABLED = "discoverystream.enabled"; +const PREF_HARDCODED_BASIC_LAYOUT = "discoverystream.hardcoded-basic-layout"; +const PREF_SPOCS_ENDPOINT = "discoverystream.spocs-endpoint"; +const PREF_SPOCS_ENDPOINT_QUERY = "discoverystream.spocs-endpoint-query"; +const PREF_REGION_BASIC_LAYOUT = "discoverystream.region-basic-layout"; +const PREF_USER_TOPSTORIES = "feeds.section.topstories"; +const PREF_SYSTEM_TOPSTORIES = "feeds.system.topstories"; +const PREF_USER_TOPSITES = "feeds.topsites"; +const PREF_SYSTEM_TOPSITES = "feeds.system.topsites"; +const PREF_SPOCS_CLEAR_ENDPOINT = "discoverystream.endpointSpocsClear"; +const PREF_SHOW_SPONSORED = "showSponsored"; +const PREF_SYSTEM_SHOW_SPONSORED = "system.showSponsored"; +const PREF_SHOW_SPONSORED_TOPSITES = "showSponsoredTopSites"; +// Nimbus variable to enable the SOV feature for sponsored tiles. +const NIMBUS_VARIABLE_CONTILE_SOV_ENABLED = "topSitesContileSovEnabled"; +const PREF_SPOC_IMPRESSIONS = "discoverystream.spoc.impressions"; +const PREF_FLIGHT_BLOCKS = "discoverystream.flight.blocks"; +const PREF_REC_IMPRESSIONS = "discoverystream.rec.impressions"; +const PREF_COLLECTIONS_ENABLED = + "discoverystream.sponsored-collections.enabled"; +const PREF_POCKET_BUTTON = "extensions.pocket.enabled"; +const PREF_COLLECTION_DISMISSIBLE = "discoverystream.isCollectionDismissible"; + +let getHardcodedLayout; + +export class DiscoveryStreamFeed { + constructor() { + // Internal state for checking if we've intialized all our data + this.loaded = false; + + // Persistent cache for remote endpoint data. + this.cache = new lazy.PersistentCache(CACHE_KEY, true); + this.locale = Services.locale.appLocaleAsBCP47; + this._impressionId = this.getOrCreateImpressionId(); + // Internal in-memory cache for parsing json prefs. + this._prefCache = {}; + } + + getOrCreateImpressionId() { + let impressionId = Services.prefs.getCharPref(PREF_IMPRESSION_ID, ""); + if (!impressionId) { + impressionId = String(Services.uuid.generateUUID()); + Services.prefs.setCharPref(PREF_IMPRESSION_ID, impressionId); + } + return impressionId; + } + + get config() { + if (this._prefCache.config) { + return this._prefCache.config; + } + try { + this._prefCache.config = JSON.parse( + this.store.getState().Prefs.values[PREF_CONFIG] + ); + } catch (e) { + // istanbul ignore next + this._prefCache.config = {}; + // istanbul ignore next + console.error( + `Could not parse preference. Try resetting ${PREF_CONFIG} in about:config.`, + e + ); + } + this._prefCache.config.enabled = + this._prefCache.config.enabled && + this.store.getState().Prefs.values[PREF_ENABLED]; + + return this._prefCache.config; + } + + resetConfigDefauts() { + this.store.dispatch({ + type: at.CLEAR_PREF, + data: { + name: PREF_CONFIG, + }, + }); + } + + get region() { + return lazy.Region.home; + } + + get isBff() { + if (this._isBff === undefined) { + const pocketConfig = + this.store.getState().Prefs.values?.pocketConfig || {}; + + const preffedRegionBffConfigString = pocketConfig.regionBffConfig || ""; + const preffedRegionBffConfig = preffedRegionBffConfigString + .split(",") + .map(s => s.trim()); + const regionBff = preffedRegionBffConfig.includes(this.region); + this._isBff = regionBff; + } + + return this._isBff; + } + + get showSpocs() { + // High level overall sponsored check, if one of these is true, + // we know we need some sort of spoc control setup. + return this.showSponsoredStories || this.showSponsoredTopsites; + } + + get showSponsoredStories() { + // Combine user-set sponsored opt-out with Mozilla-set config + return ( + this.store.getState().Prefs.values[PREF_SHOW_SPONSORED] && + this.store.getState().Prefs.values[PREF_SYSTEM_SHOW_SPONSORED] + ); + } + + get showSponsoredTopsites() { + const placements = this.getPlacements(); + // Combine user-set sponsored opt-out with placement data + return !!( + this.store.getState().Prefs.values[PREF_SHOW_SPONSORED_TOPSITES] && + placements.find(placement => placement.name === "sponsored-topsites") + ); + } + + get showStories() { + // Combine user-set stories opt-out with Mozilla-set config + return ( + this.store.getState().Prefs.values[PREF_SYSTEM_TOPSTORIES] && + this.store.getState().Prefs.values[PREF_USER_TOPSTORIES] + ); + } + + get showTopsites() { + // Combine user-set topsites opt-out with Mozilla-set config + return ( + this.store.getState().Prefs.values[PREF_SYSTEM_TOPSITES] && + this.store.getState().Prefs.values[PREF_USER_TOPSITES] + ); + } + + get personalized() { + return this.recommendationProvider.personalized; + } + + get recommendationProvider() { + if (this._recommendationProvider) { + return this._recommendationProvider; + } + this._recommendationProvider = this.store.feeds.get( + "feeds.recommendationprovider" + ); + return this._recommendationProvider; + } + + setupConfig(isStartup = false) { + // Send the initial state of the pref on our reducer + this.store.dispatch( + ac.BroadcastToContent({ + type: at.DISCOVERY_STREAM_CONFIG_SETUP, + data: this.config, + meta: { + isStartup, + }, + }) + ); + } + + setupPrefs(isStartup = false) { + const pocketNewtabExperiment = lazy.ExperimentAPI.getExperimentMetaData({ + featureId: "pocketNewtab", + }); + + const pocketNewtabRollout = lazy.ExperimentAPI.getRolloutMetaData({ + featureId: "pocketNewtab", + }); + + // We want to know if the user is in an experiment or rollout, + // but we prioritize experiments over rollouts. + const experimentMetaData = pocketNewtabExperiment || pocketNewtabRollout; + + let utmSource = "pocket-newtab"; + let utmCampaign = experimentMetaData?.slug; + let utmContent = experimentMetaData?.branch?.slug; + + this.store.dispatch( + ac.BroadcastToContent({ + type: at.DISCOVERY_STREAM_EXPERIMENT_DATA, + data: { + utmSource, + utmCampaign, + utmContent, + }, + meta: { + isStartup, + }, + }) + ); + + const pocketButtonEnabled = Services.prefs.getBoolPref(PREF_POCKET_BUTTON); + + const nimbusConfig = this.store.getState().Prefs.values?.pocketConfig || {}; + const { region } = this.store.getState().Prefs.values; + + this.setupSpocsCacheUpdateTime(); + const saveToPocketCardRegions = nimbusConfig.saveToPocketCardRegions + ?.split(",") + .map(s => s.trim()); + const saveToPocketCard = + pocketButtonEnabled && + (nimbusConfig.saveToPocketCard || + saveToPocketCardRegions?.includes(region)); + + const hideDescriptionsRegions = nimbusConfig.hideDescriptionsRegions + ?.split(",") + .map(s => s.trim()); + const hideDescriptions = + nimbusConfig.hideDescriptions || + hideDescriptionsRegions?.includes(region); + + // We don't BroadcastToContent for this, as the changes may + // shift around elements on an open newtab the user is currently reading. + // So instead we AlsoToPreloaded so the next tab is updated. + // This is because setupPrefs is called by the system and not a user interaction. + this.store.dispatch( + ac.AlsoToPreloaded({ + type: at.DISCOVERY_STREAM_PREFS_SETUP, + data: { + recentSavesEnabled: nimbusConfig.recentSavesEnabled, + pocketButtonEnabled, + saveToPocketCard, + hideDescriptions, + compactImages: nimbusConfig.compactImages, + imageGradient: nimbusConfig.imageGradient, + newSponsoredLabel: nimbusConfig.newSponsoredLabel, + titleLines: nimbusConfig.titleLines, + descLines: nimbusConfig.descLines, + readTime: nimbusConfig.readTime, + }, + meta: { + isStartup, + }, + }) + ); + + this.store.dispatch( + ac.BroadcastToContent({ + type: at.DISCOVERY_STREAM_COLLECTION_DISMISSIBLE_TOGGLE, + data: { + value: + this.store.getState().Prefs.values[PREF_COLLECTION_DISMISSIBLE], + }, + meta: { + isStartup, + }, + }) + ); + } + + async setupPocketState(target) { + let dispatch = action => + this.store.dispatch(ac.OnlyToOneContent(action, target)); + const isUserLoggedIn = lazy.pktApi.isUserLoggedIn(); + dispatch({ + type: at.DISCOVERY_STREAM_POCKET_STATE_SET, + data: { + isUserLoggedIn, + }, + }); + + // If we're not logged in, don't bother fetching recent saves, we're done. + if (isUserLoggedIn) { + let recentSaves = await lazy.pktApi.getRecentSavesCache(); + if (recentSaves) { + // We have cache, so we can use those. + dispatch({ + type: at.DISCOVERY_STREAM_RECENT_SAVES, + data: { + recentSaves, + }, + }); + } else { + // We don't have cache, so fetch fresh stories. + lazy.pktApi.getRecentSaves({ + success(data) { + dispatch({ + type: at.DISCOVERY_STREAM_RECENT_SAVES, + data: { + recentSaves: data, + }, + }); + }, + error(error) {}, + }); + } + } + } + + uninitPrefs() { + // Reset in-memory cache + this._prefCache = {}; + } + + async fetchFromEndpoint(rawEndpoint, options = {}) { + if (!rawEndpoint) { + console.error("Tried to fetch endpoint but none was configured."); + return null; + } + + const apiKeyPref = this.config.api_key_pref; + const apiKey = Services.prefs.getCharPref(apiKeyPref, ""); + + const endpoint = rawEndpoint + .replace("$apiKey", apiKey) + .replace("$locale", this.locale) + .replace("$region", this.region); + + try { + // Make sure the requested endpoint is allowed + const allowed = this.store + .getState() + .Prefs.values[PREF_ENDPOINTS].split(","); + if (!allowed.some(prefix => endpoint.startsWith(prefix))) { + throw new Error(`Not one of allowed prefixes (${allowed})`); + } + + const controller = new AbortController(); + const { signal } = controller; + + const fetchPromise = fetch(endpoint, { + ...options, + credentials: "omit", + signal, + }); + // istanbul ignore next + const timeoutId = setTimeout(() => { + controller.abort(); + }, FETCH_TIMEOUT); + + const response = await fetchPromise; + if (!response.ok) { + throw new Error(`Unexpected status (${response.status})`); + } + clearTimeout(timeoutId); + + return response.json(); + } catch (error) { + console.error(`Failed to fetch ${endpoint}:`, error.message); + } + return null; + } + + get spocsCacheUpdateTime() { + if (this._spocsCacheUpdateTime) { + return this._spocsCacheUpdateTime; + } + this.setupSpocsCacheUpdateTime(); + return this._spocsCacheUpdateTime; + } + + setupSpocsCacheUpdateTime() { + const nimbusConfig = this.store.getState().Prefs.values?.pocketConfig || {}; + const { spocsCacheTimeout } = nimbusConfig; + const MAX_TIMEOUT = 30; + const MIN_TIMEOUT = 5; + // We do a bit of min max checking the the configured value is between + // 5 and 30 minutes, to protect against unreasonable values. + if ( + spocsCacheTimeout && + spocsCacheTimeout <= MAX_TIMEOUT && + spocsCacheTimeout >= MIN_TIMEOUT + ) { + // This value is in minutes, but we want ms. + this._spocsCacheUpdateTime = spocsCacheTimeout * 60 * 1000; + } else { + // The const is already in ms. + this._spocsCacheUpdateTime = SPOCS_FEEDS_UPDATE_TIME; + } + } + + /** + * Returns true if data in the cache for a particular key has expired or is missing. + * @param {object} cachedData data returned from cache.get() + * @param {string} key a cache key + * @param {string?} url for "feed" only, the URL of the feed. + * @param {boolean} is this check done at initial browser load + */ + isExpired({ cachedData, key, url, isStartup }) { + const { spocs, feeds } = cachedData; + const updateTimePerComponent = { + spocs: this.spocsCacheUpdateTime, + feed: COMPONENT_FEEDS_UPDATE_TIME, + }; + const EXPIRATION_TIME = isStartup + ? STARTUP_CACHE_EXPIRE_TIME + : updateTimePerComponent[key]; + switch (key) { + case "spocs": + return !spocs || !(Date.now() - spocs.lastUpdated < EXPIRATION_TIME); + case "feed": + return ( + !feeds || + !feeds[url] || + !(Date.now() - feeds[url].lastUpdated < EXPIRATION_TIME) + ); + default: + // istanbul ignore next + throw new Error(`${key} is not a valid key`); + } + } + + async _checkExpirationPerComponent() { + const cachedData = (await this.cache.get()) || {}; + const { feeds } = cachedData; + return { + spocs: this.showSpocs && this.isExpired({ cachedData, key: "spocs" }), + feeds: + this.showStories && + (!feeds || + Object.keys(feeds).some(url => + this.isExpired({ cachedData, key: "feed", url }) + )), + }; + } + + /** + * Returns true if any data for the cached endpoints has expired or is missing. + */ + async checkIfAnyCacheExpired() { + const expirationPerComponent = await this._checkExpirationPerComponent(); + return expirationPerComponent.spocs || expirationPerComponent.feeds; + } + + updatePlacements(sendUpdate, layout, isStartup = false) { + const placements = []; + const placementsMap = {}; + for (const row of layout.filter(r => r.components && r.components.length)) { + for (const component of row.components.filter( + c => c.placement && c.spocs + )) { + // If we find a valid placement, we set it to this value. + let placement; + + // We need to check to see if this placement is on or not. + // If this placement has a prefs array, check against that. + if (component.spocs.prefs) { + // Check every pref in the array to see if this placement is turned on. + if ( + component.spocs.prefs.length && + component.spocs.prefs.every( + p => this.store.getState().Prefs.values[p] + ) + ) { + // This placement is on. + placement = component.placement; + } + } else if (this.showSponsoredStories) { + // If we do not have a prefs array, use old check. + // This is because Pocket spocs uses an old non pref method. + placement = component.placement; + } + + // Validate this placement and check for dupes. + if (placement?.name && !placementsMap[placement.name]) { + placementsMap[placement.name] = placement; + placements.push(placement); + } + } + } + + // Update placements data. + // Even if we have no placements, we still want to update it to clear it. + sendUpdate({ + type: at.DISCOVERY_STREAM_SPOCS_PLACEMENTS, + data: { placements }, + meta: { + isStartup, + }, + }); + } + + /** + * Adds a query string to a URL. + * A query can be any string literal accepted by https://developer.mozilla.org/docs/Web/API/URLSearchParams + * Examples: "?foo=1&bar=2", "&foo=1&bar=2", "foo=1&bar=2", "?bar=2" or "bar=2" + */ + addEndpointQuery(url, query) { + if (!query) { + return url; + } + + const urlObject = new URL(url); + const params = new URLSearchParams(query); + + for (let [key, val] of params.entries()) { + urlObject.searchParams.append(key, val); + } + + return urlObject.toString(); + } + + parseGridPositions(csvPositions) { + let gridPositions; + + // Only accept parseable non-negative integers + try { + gridPositions = csvPositions.map(index => { + let parsedInt = parseInt(index, 10); + + if (!isNaN(parsedInt) && parsedInt >= 0) { + return parsedInt; + } + + throw new Error("Bad input"); + }); + } catch (e) { + // Catch spoc positions that are not numbers or negative, and do nothing. + // We have hard coded backup positions. + gridPositions = undefined; + } + + return gridPositions; + } + + generateFeedUrl(isBff) { + if (isBff) { + return `https://${lazy.NimbusFeatures.saveToPocket.getVariable( + "bffApi" + )}/desktop/v1/recommendations?locale=$locale®ion=$region&count=30`; + } + return FEED_URL; + } + + loadLayout(sendUpdate, isStartup) { + let layoutData = {}; + let url = ""; + + const isBasicLayout = + this.config.hardcoded_basic_layout || + this.store.getState().Prefs.values[PREF_HARDCODED_BASIC_LAYOUT] || + this.store.getState().Prefs.values[PREF_REGION_BASIC_LAYOUT]; + + const sponsoredCollectionsEnabled = + this.store.getState().Prefs.values[PREF_COLLECTIONS_ENABLED]; + + const pocketConfig = this.store.getState().Prefs.values?.pocketConfig || {}; + const onboardingExperience = + this.isBff && pocketConfig.onboardingExperience; + const { spocTopsitesPlacementEnabled } = pocketConfig; + + let items = isBasicLayout ? 3 : 21; + if (pocketConfig.fourCardLayout || pocketConfig.hybridLayout) { + items = isBasicLayout ? 4 : 24; + } + + const ctaButtonSponsors = pocketConfig.ctaButtonSponsors + ?.split(",") + .map(s => s.trim().toLowerCase()); + let ctaButtonVariant = ""; + // We specifically against hard coded values, instead of applying whatever is in the pref. + // This is to ensure random class names from a user modified pref doesn't make it into the class list. + if ( + pocketConfig.ctaButtonVariant === "variant-a" || + pocketConfig.ctaButtonVariant === "variant-b" + ) { + ctaButtonVariant = pocketConfig.ctaButtonVariant; + } + + const prepConfArr = arr => { + return arr + ?.split(",") + .filter(item => item) + .map(item => parseInt(item, 10)); + }; + + const spocAdTypes = prepConfArr(pocketConfig.spocAdTypes); + const spocZoneIds = prepConfArr(pocketConfig.spocZoneIds); + const spocTopsitesAdTypes = prepConfArr(pocketConfig.spocTopsitesAdTypes); + const spocTopsitesZoneIds = prepConfArr(pocketConfig.spocTopsitesZoneIds); + const { spocSiteId } = pocketConfig; + let spocPlacementData; + let spocTopsitesPlacementData; + let spocsUrl; + + if (spocAdTypes?.length && spocZoneIds?.length) { + spocPlacementData = { + ad_types: spocAdTypes, + zone_ids: spocZoneIds, + }; + } + + if (spocTopsitesAdTypes?.length && spocTopsitesZoneIds?.length) { + spocTopsitesPlacementData = { + ad_types: spocTopsitesAdTypes, + zone_ids: spocTopsitesZoneIds, + }; + } + + if (spocSiteId) { + const newUrl = new URL(SPOCS_URL); + newUrl.searchParams.set("site", spocSiteId); + spocsUrl = newUrl.href; + } + + let feedUrl = this.generateFeedUrl(this.isBff); + + // Set layout config. + // Changing values in this layout in memory object is unnecessary. + layoutData = getHardcodedLayout({ + spocsUrl, + feedUrl, + items, + sponsoredCollectionsEnabled, + spocPlacementData, + spocTopsitesPlacementEnabled, + spocTopsitesPlacementData, + spocPositions: this.parseGridPositions( + pocketConfig.spocPositions?.split(`,`) + ), + spocTopsitesPositions: this.parseGridPositions( + pocketConfig.spocTopsitesPositions?.split(`,`) + ), + widgetPositions: this.parseGridPositions( + pocketConfig.widgetPositions?.split(`,`) + ), + widgetData: [ + ...(this.locale.startsWith("en-") ? [{ type: "TopicsWidget" }] : []), + ], + hybridLayout: pocketConfig.hybridLayout, + hideCardBackground: pocketConfig.hideCardBackground, + fourCardLayout: pocketConfig.fourCardLayout, + newFooterSection: pocketConfig.newFooterSection, + compactGrid: pocketConfig.compactGrid, + // For now essentialReadsHeader and editorsPicksHeader are English only. + essentialReadsHeader: + this.locale.startsWith("en-") && pocketConfig.essentialReadsHeader, + editorsPicksHeader: + this.locale.startsWith("en-") && pocketConfig.editorsPicksHeader, + onboardingExperience, + // For now button variants are for experimentation and English only. + ctaButtonSponsors: this.locale.startsWith("en-") ? ctaButtonSponsors : [], + ctaButtonVariant: this.locale.startsWith("en-") ? ctaButtonVariant : "", + }); + + sendUpdate({ + type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, + data: layoutData, + meta: { + isStartup, + }, + }); + + if (layoutData.spocs) { + url = + this.store.getState().Prefs.values[PREF_SPOCS_ENDPOINT] || + this.config.spocs_endpoint || + layoutData.spocs.url; + + const spocsEndpointQuery = + this.store.getState().Prefs.values[PREF_SPOCS_ENDPOINT_QUERY]; + + // For QA, testing, or debugging purposes, there may be a query string to add. + url = this.addEndpointQuery(url, spocsEndpointQuery); + + if ( + url && + url !== this.store.getState().DiscoveryStream.spocs.spocs_endpoint + ) { + sendUpdate({ + type: at.DISCOVERY_STREAM_SPOCS_ENDPOINT, + data: { + url, + }, + meta: { + isStartup, + }, + }); + this.updatePlacements(sendUpdate, layoutData.layout, isStartup); + } + } + } + + /** + * buildFeedPromise - Adds the promise result to newFeeds and + * pushes a promise to newsFeedsPromises. + * @param {Object} Has both newFeedsPromises (Array) and newFeeds (Object) + * @param {Boolean} isStartup We have different cache handling for startup. + * @returns {Function} We return a function so we can contain + * the scope for isStartup and the promises object. + * Combines feed results and promises for each component with a feed. + */ + buildFeedPromise( + { newFeedsPromises, newFeeds }, + isStartup = false, + sendUpdate + ) { + return component => { + const { url } = component.feed; + + if (!newFeeds[url]) { + // We initially stub this out so we don't fetch dupes, + // we then fill in with the proper object inside the promise. + newFeeds[url] = {}; + const feedPromise = this.getComponentFeed(url, isStartup); + + feedPromise + .then(feed => { + // If we stored the result of filter in feed cache as it happened, + // I think we could reduce doing this for cache fetches. + // Bug https://bugzilla.mozilla.org/show_bug.cgi?id=1606277 + newFeeds[url] = this.filterRecommendations(feed); + sendUpdate({ + type: at.DISCOVERY_STREAM_FEED_UPDATE, + data: { + feed: newFeeds[url], + url, + }, + meta: { + isStartup, + }, + }); + }) + .catch( + /* istanbul ignore next */ error => { + console.error( + `Error trying to load component feed ${url}:`, + error + ); + } + ); + newFeedsPromises.push(feedPromise); + } + }; + } + + filterRecommendations(feed) { + if ( + feed && + feed.data && + feed.data.recommendations && + feed.data.recommendations.length + ) { + const { data: recommendations } = this.filterBlocked( + feed.data.recommendations + ); + return { + ...feed, + data: { + ...feed.data, + recommendations, + }, + }; + } + return feed; + } + + /** + * reduceFeedComponents - Filters out components with no feeds, and combines + * all feeds on this component with the feeds from other components. + * @param {Boolean} isStartup We have different cache handling for startup. + * @returns {Function} We return a function so we can contain the scope for isStartup. + * Reduces feeds into promises and feed data. + */ + reduceFeedComponents(isStartup, sendUpdate) { + return (accumulator, row) => { + row.components + .filter(component => component && component.feed) + .forEach(this.buildFeedPromise(accumulator, isStartup, sendUpdate)); + return accumulator; + }; + } + + /** + * buildFeedPromises - Filters out rows with no components, + * and gets us a promise for each unique feed. + * @param {Object} layout This is the Discovery Stream layout object. + * @param {Boolean} isStartup We have different cache handling for startup. + * @returns {Object} An object with newFeedsPromises (Array) and newFeeds (Object), + * we can Promise.all newFeedsPromises to get completed data in newFeeds. + */ + buildFeedPromises(layout, isStartup, sendUpdate) { + const initialData = { + newFeedsPromises: [], + newFeeds: {}, + }; + return layout + .filter(row => row && row.components) + .reduce(this.reduceFeedComponents(isStartup, sendUpdate), initialData); + } + + async loadComponentFeeds(sendUpdate, isStartup = false) { + const { DiscoveryStream } = this.store.getState(); + + if (!DiscoveryStream || !DiscoveryStream.layout) { + return; + } + + // Reset the flag that indicates whether or not at least one API request + // was issued to fetch the component feed in `getComponentFeed()`. + this.componentFeedFetched = false; + const { newFeedsPromises, newFeeds } = this.buildFeedPromises( + DiscoveryStream.layout, + isStartup, + sendUpdate + ); + + // Each promise has a catch already built in, so no need to catch here. + await Promise.all(newFeedsPromises); + + if (this.componentFeedFetched) { + this.cleanUpTopRecImpressionPref(newFeeds); + } + await this.cache.set("feeds", newFeeds); + sendUpdate({ + type: at.DISCOVERY_STREAM_FEEDS_UPDATE, + meta: { + isStartup, + }, + }); + } + + getPlacements() { + const { placements } = this.store.getState().DiscoveryStream.spocs; + return placements; + } + + // I wonder, can this be better as a reducer? + // See Bug https://bugzilla.mozilla.org/show_bug.cgi?id=1606717 + placementsForEach(callback) { + this.getPlacements().forEach(callback); + } + + // Bug 1567271 introduced meta data on a list of spocs. + // This involved moving the spocs array into an items prop. + // However, old data could still be returned, and cached data might also be old. + // For ths reason, we want to ensure if we don't find an items array, + // we use the previous array placement, and then stub out title and context to empty strings. + // We need to do this *after* both fresh fetches and cached data to reduce repetition. + normalizeSpocsItems(spocs) { + const items = spocs.items || spocs; + const title = spocs.title || ""; + const context = spocs.context || ""; + const sponsor = spocs.sponsor || ""; + // We do not stub sponsored_by_override with an empty string. It is an override, and an empty string + // explicitly means to override the client to display an empty string. + // An empty string is not an no op in this case. Undefined is the proper no op here. + const { sponsored_by_override } = spocs; + // Undefined is fine here. It's optional and only used by collections. + // If we leave it out, you get a collection that cannot be dismissed. + const { flight_id } = spocs; + return { + items, + title, + context, + sponsor, + sponsored_by_override, + ...(flight_id ? { flight_id } : {}), + }; + } + + updateSponsoredCollectionsPref(collectionEnabled = false) { + const currentState = + this.store.getState().Prefs.values[PREF_COLLECTIONS_ENABLED]; + + // If the current state does not match the new state, update the pref. + if (currentState !== collectionEnabled) { + this.store.dispatch( + ac.SetPref(PREF_COLLECTIONS_ENABLED, collectionEnabled) + ); + } + } + + async loadSpocs(sendUpdate, isStartup) { + const cachedData = (await this.cache.get()) || {}; + let spocsState = cachedData.spocs; + let placements = this.getPlacements(); + + if ( + this.showSpocs && + placements?.length && + this.isExpired({ cachedData, key: "spocs", isStartup }) + ) { + // We optimistically set this to true, because if SOV is not ready, we fetch them. + let useTopsitesPlacement = true; + + // If SOV is turned off or not available, we optimistically fetch sponsored topsites. + if ( + lazy.NimbusFeatures.pocketNewtab.getVariable( + NIMBUS_VARIABLE_CONTILE_SOV_ENABLED + ) + ) { + let { positions, ready } = this.store.getState().TopSites.sov; + if (ready) { + // We don't need to await here, because we don't need it now. + this.cache.set("sov", positions); + } else { + // If SOV is not available, and there is a SOV cache, use it. + positions = cachedData.sov; + } + + if (positions?.length) { + // If SOV is ready and turned on, we can check if we need moz-sales position. + useTopsitesPlacement = positions.some( + allocation => allocation.assignedPartner === "moz-sales" + ); + } + } + + // We can filter out the topsite placement from the fetch. + if (!useTopsitesPlacement) { + placements = placements.filter( + placement => placement.name !== "sponsored-topsites" + ); + } + + if (placements?.length) { + const endpoint = + this.store.getState().DiscoveryStream.spocs.spocs_endpoint; + + const headers = new Headers(); + headers.append("content-type", "application/json"); + + const apiKeyPref = this.config.api_key_pref; + const apiKey = Services.prefs.getCharPref(apiKeyPref, ""); + + const spocsResponse = await this.fetchFromEndpoint(endpoint, { + method: "POST", + headers, + body: JSON.stringify({ + pocket_id: this._impressionId, + version: 2, + consumer_key: apiKey, + ...(placements.length ? { placements } : {}), + }), + }); + + if (spocsResponse) { + spocsState = { + lastUpdated: Date.now(), + spocs: { + ...spocsResponse, + }, + }; + + if (spocsResponse.settings && spocsResponse.settings.feature_flags) { + this.store.dispatch( + ac.OnlyToMain({ + type: at.DISCOVERY_STREAM_PERSONALIZATION_OVERRIDE, + data: { + override: !spocsResponse.settings.feature_flags.spoc_v2, + }, + }) + ); + this.updateSponsoredCollectionsPref( + spocsResponse.settings.feature_flags.collections + ); + } + + const spocsResultPromises = this.getPlacements().map( + async placement => { + const freshSpocs = spocsState.spocs[placement.name]; + + if (!freshSpocs) { + return; + } + + // spocs can be returns as an array, or an object with an items array. + // We want to normalize this so all our spocs have an items array. + // There can also be some meta data for title and context. + // This is mostly because of backwards compat. + const { + items: normalizedSpocsItems, + title, + context, + sponsor, + sponsored_by_override, + } = this.normalizeSpocsItems(freshSpocs); + + if (!normalizedSpocsItems || !normalizedSpocsItems.length) { + // In the case of old data, we still want to ensure we normalize the data structure, + // even if it's empty. We expect the empty data to be an object with items array, + // and not just an empty array. + spocsState.spocs = { + ...spocsState.spocs, + [placement.name]: { + title, + context, + items: [], + }, + }; + return; + } + + // Migrate flight_id + const { data: migratedSpocs } = + this.migrateFlightId(normalizedSpocsItems); + + const { data: capResult } = this.frequencyCapSpocs(migratedSpocs); + + const { data: blockedResults } = this.filterBlocked(capResult); + + const { data: scoredResults, personalized } = + await this.scoreItems(blockedResults, "spocs"); + + spocsState.spocs = { + ...spocsState.spocs, + [placement.name]: { + title, + context, + sponsor, + sponsored_by_override, + personalized, + items: scoredResults, + }, + }; + } + ); + await Promise.all(spocsResultPromises); + + this.cleanUpFlightImpressionPref(spocsState.spocs); + } else { + console.error("No response for spocs_endpoint prop"); + } + } + } + + // Use good data if we have it, otherwise nothing. + // We can have no data if spocs set to off. + // We can have no data if request fails and there is no good cache. + // We want to send an update spocs or not, so client can render something. + spocsState = + spocsState && spocsState.spocs + ? spocsState + : { + lastUpdated: Date.now(), + spocs: {}, + }; + await this.cache.set("spocs", { + lastUpdated: spocsState.lastUpdated, + spocs: spocsState.spocs, + }); + + sendUpdate({ + type: at.DISCOVERY_STREAM_SPOCS_UPDATE, + data: { + lastUpdated: spocsState.lastUpdated, + spocs: spocsState.spocs, + }, + meta: { + isStartup, + }, + }); + } + + async clearSpocs() { + const endpoint = + this.store.getState().Prefs.values[PREF_SPOCS_CLEAR_ENDPOINT]; + if (!endpoint) { + return; + } + const headers = new Headers(); + headers.append("content-type", "application/json"); + + await this.fetchFromEndpoint(endpoint, { + method: "DELETE", + headers, + body: JSON.stringify({ + pocket_id: this._impressionId, + }), + }); + } + + observe(subject, topic, data) { + switch (topic) { + case "nsPref:changed": + // If the Pocket button was turned on or off, we need to update the cards + // because cards show menu options for the Pocket button that need to be removed. + if (data === PREF_POCKET_BUTTON) { + this.configReset(); + } + break; + } + } + + /* + * This function is used to sort any type of story, both spocs and recs. + * This uses hierarchical sorting, first sorting by priority, then by score within a priority. + * This function could be sorting an array of spocs or an array of recs. + * A rec would have priority undefined, and a spoc would probably have a priority set. + * Priority is sorted ascending, so low numbers are the highest priority. + * Score is sorted descending, so high numbers are the highest score. + * Undefined priority values are considered the lowest priority. + * A negative priority is considered the same as undefined, lowest priority. + * A negative priority is unlikely and not currently supported or expected. + * A negative score is a possible use case. + */ + sortItem(a, b) { + // If the priorities are the same, sort based on score. + // If both item priorities are undefined, + // we can safely sort via score. + if (a.priority === b.priority) { + return b.score - a.score; + } else if (!a.priority || a.priority <= 0) { + // If priority is undefined or an unexpected value, + // consider it lowest priority. + return 1; + } else if (!b.priority || b.priority <= 0) { + // Also consider this case lowest priority. + return -1; + } + // Our primary sort for items with priority. + return a.priority - b.priority; + } + + async scoreItems(items, type) { + const spocsPersonalized = + this.store.getState().Prefs.values?.pocketConfig?.spocsPersonalized; + const recsPersonalized = + this.store.getState().Prefs.values?.pocketConfig?.recsPersonalized; + const personalizedByType = + type === "feed" ? recsPersonalized : spocsPersonalized; + // If this is initialized, we are ready to go. + const personalized = this.store.getState().Personalization.initialized; + + const data = ( + await Promise.all( + items.map(item => this.scoreItem(item, personalizedByType)) + ) + ) + // Sort by highest scores. + .sort(this.sortItem); + + return { data, personalized }; + } + + async scoreItem(item, personalizedByType) { + item.score = item.item_score; + if (item.score !== 0 && !item.score) { + item.score = 1; + } + if (this.personalized && personalizedByType) { + await this.recommendationProvider.calculateItemRelevanceScore(item); + } + return item; + } + + filterBlocked(data) { + if (data && data.length) { + let flights = this.readDataPref(PREF_FLIGHT_BLOCKS); + const filteredItems = data.filter(item => { + const blocked = + lazy.NewTabUtils.blockedLinks.isBlocked({ url: item.url }) || + flights[item.flight_id]; + return !blocked; + }); + return { data: filteredItems }; + } + return { data }; + } + + // For backwards compatibility, older spoc endpoint don't have flight_id, + // but instead had campaign_id we can use + // + // @param {Object} data An object that might have a SPOCS array. + // @returns {Object} An object with a property `data` as the result. + migrateFlightId(spocs) { + if (spocs && spocs.length) { + return { + data: spocs.map(s => { + return { + ...s, + ...(s.flight_id || s.campaign_id + ? { + flight_id: s.flight_id || s.campaign_id, + } + : {}), + ...(s.caps + ? { + caps: { + ...s.caps, + flight: s.caps.flight || s.caps.campaign, + }, + } + : {}), + }; + }), + }; + } + return { data: spocs }; + } + + // Filter spocs based on frequency caps + // + // @param {Object} data An object that might have a SPOCS array. + // @returns {Object} An object with a property `data` as the result, and a property + // `filterItems` as the frequency capped items. + frequencyCapSpocs(spocs) { + if (spocs && spocs.length) { + const impressions = this.readDataPref(PREF_SPOC_IMPRESSIONS); + const caps = []; + const result = spocs.filter(s => { + const isBelow = this.isBelowFrequencyCap(impressions, s); + if (!isBelow) { + caps.push(s); + } + return isBelow; + }); + // send caps to redux if any. + if (caps.length) { + this.store.dispatch({ + type: at.DISCOVERY_STREAM_SPOCS_CAPS, + data: caps, + }); + } + return { data: result, filtered: caps }; + } + return { data: spocs, filtered: [] }; + } + + // Frequency caps are based on flight, which may include multiple spocs. + // We currently support two types of frequency caps: + // - lifetime: Indicates how many times spocs from a flight can be shown in total + // - period: Indicates how many times spocs from a flight can be shown within a period + // + // So, for example, the feed configuration below defines that for flight 1 no more + // than 5 spocs can be shown in total, and no more than 2 per hour. + // "flight_id": 1, + // "caps": { + // "lifetime": 5, + // "flight": { + // "count": 2, + // "period": 3600 + // } + // } + isBelowFrequencyCap(impressions, spoc) { + const flightImpressions = impressions[spoc.flight_id]; + if (!flightImpressions) { + return true; + } + + const lifetime = spoc.caps && spoc.caps.lifetime; + + const lifeTimeCap = Math.min( + lifetime || MAX_LIFETIME_CAP, + MAX_LIFETIME_CAP + ); + const lifeTimeCapExceeded = flightImpressions.length >= lifeTimeCap; + if (lifeTimeCapExceeded) { + return false; + } + + const flightCap = spoc.caps && spoc.caps.flight; + if (flightCap) { + const flightCapExceeded = + flightImpressions.filter(i => Date.now() - i < flightCap.period * 1000) + .length >= flightCap.count; + return !flightCapExceeded; + } + return true; + } + + async retryFeed(feed) { + const { url } = feed; + const result = await this.getComponentFeed(url); + const newFeed = this.filterRecommendations(result); + this.store.dispatch( + ac.BroadcastToContent({ + type: at.DISCOVERY_STREAM_FEED_UPDATE, + data: { + feed: newFeed, + url, + }, + }) + ); + } + + async getComponentFeed(feedUrl, isStartup) { + const cachedData = (await this.cache.get()) || {}; + const { feeds } = cachedData; + + let feed = feeds ? feeds[feedUrl] : null; + if (this.isExpired({ cachedData, key: "feed", url: feedUrl, isStartup })) { + let options = {}; + if (this.isBff) { + const headers = new Headers(); + const oAuthConsumerKey = lazy.NimbusFeatures.saveToPocket.getVariable( + "oAuthConsumerKeyBff" + ); + headers.append("consumer_key", oAuthConsumerKey); + options = { + method: "GET", + headers, + }; + } + + const feedResponse = await this.fetchFromEndpoint(feedUrl, options); + if (feedResponse) { + const { settings = {} } = feedResponse; + let { recommendations } = feedResponse; + if (this.isBff) { + recommendations = feedResponse.data.map(item => ({ + id: item.tileId, + url: item.url, + title: item.title, + excerpt: item.excerpt, + publisher: item.publisher, + time_to_read: item.timeToRead, + raw_image_src: item.imageUrl, + recommendation_id: item.recommendationId, + })); + } + const { data: scoredItems, personalized } = await this.scoreItems( + recommendations, + "feed" + ); + const { recsExpireTime } = settings; + const rotatedItems = this.rotate(scoredItems, recsExpireTime); + this.componentFeedFetched = true; + feed = { + lastUpdated: Date.now(), + personalized, + data: { + settings, + recommendations: rotatedItems, + status: "success", + }, + }; + } else { + console.error("No response for feed"); + } + } + + // If we have no feed at this point, both fetch and cache failed for some reason. + return ( + feed || { + data: { + status: "failed", + }, + } + ); + } + + /** + * Called at startup to update cached data in the background. + */ + async _maybeUpdateCachedData() { + const expirationPerComponent = await this._checkExpirationPerComponent(); + // Pass in `store.dispatch` to send the updates only to main + if (expirationPerComponent.spocs) { + await this.loadSpocs(this.store.dispatch); + } + if (expirationPerComponent.feeds) { + await this.loadComponentFeeds(this.store.dispatch); + } + } + + async scoreFeeds(feedsState) { + if (feedsState.data) { + const feeds = {}; + const feedsPromises = Object.keys(feedsState.data).map(url => { + let feed = feedsState.data[url]; + if (feed.personalized) { + // Feed was previously personalized then cached, we don't need to do this again. + return Promise.resolve(); + } + const feedPromise = this.scoreItems(feed.data.recommendations, "feed"); + feedPromise.then(({ data: scoredItems, personalized }) => { + const { recsExpireTime } = feed.data.settings; + const recommendations = this.rotate(scoredItems, recsExpireTime); + feed = { + ...feed, + personalized, + data: { + ...feed.data, + recommendations, + }, + }; + + feeds[url] = feed; + + this.store.dispatch( + ac.AlsoToPreloaded({ + type: at.DISCOVERY_STREAM_FEED_UPDATE, + data: { + feed, + url, + }, + }) + ); + }); + return feedPromise; + }); + await Promise.all(feedsPromises); + await this.cache.set("feeds", feeds); + } + } + + async scoreSpocs(spocsState) { + const spocsResultPromises = this.getPlacements().map(async placement => { + const nextSpocs = spocsState.data[placement.name] || {}; + const { items } = nextSpocs; + + if (nextSpocs.personalized || !items || !items.length) { + return; + } + + const { data: scoreResult, personalized } = await this.scoreItems( + items, + "spocs" + ); + + spocsState.data = { + ...spocsState.data, + [placement.name]: { + ...nextSpocs, + personalized, + items: scoreResult, + }, + }; + }); + await Promise.all(spocsResultPromises); + + // Update cache here so we don't need to re calculate scores on loads from cache. + // Related Bug 1606276 + await this.cache.set("spocs", { + lastUpdated: spocsState.lastUpdated, + spocs: spocsState.data, + }); + this.store.dispatch( + ac.AlsoToPreloaded({ + type: at.DISCOVERY_STREAM_SPOCS_UPDATE, + data: { + lastUpdated: spocsState.lastUpdated, + spocs: spocsState.data, + }, + }) + ); + } + + /** + * @typedef {Object} RefreshAll + * @property {boolean} updateOpenTabs - Sends updates to open tabs immediately if true, + * updates in background if false + * @property {boolean} isStartup - When the function is called at browser startup + * + * Refreshes component feeds, and spocs in order if caches have expired. + * @param {RefreshAll} options + */ + async refreshAll(options = {}) { + const { updateOpenTabs, isStartup } = options; + + const dispatch = updateOpenTabs + ? action => this.store.dispatch(ac.BroadcastToContent(action)) + : this.store.dispatch; + + this.loadLayout(dispatch, isStartup); + if (this.showStories || this.showTopsites) { + const promises = []; + // We could potentially have either or both sponsored topsites or stories. + // We only make one fetch, and control which to request when we fetch. + // So for now we only care if we need to make this request at all. + const spocsPromise = this.loadSpocs(dispatch, isStartup).catch(error => + console.error("Error trying to load spocs feeds:", error) + ); + promises.push(spocsPromise); + if (this.showStories) { + const storiesPromise = this.loadComponentFeeds( + dispatch, + isStartup + ).catch(error => + console.error("Error trying to load component feeds:", error) + ); + promises.push(storiesPromise); + } + await Promise.all(promises); + if (isStartup) { + // We don't pass isStartup in _maybeUpdateCachedData on purpose, + // because startup loads have a longer cache timer, + // and we want this to update in the background sooner. + await this._maybeUpdateCachedData(); + } + } + } + + // We have to rotate stories on the client so that + // active stories are at the front of the list, followed by stories that have expired + // impressions i.e. have been displayed for longer than recsExpireTime. + rotate(recommendations, recsExpireTime) { + const maxImpressionAge = Math.max( + recsExpireTime * 1000 || DEFAULT_RECS_EXPIRE_TIME, + DEFAULT_RECS_EXPIRE_TIME + ); + const impressions = this.readDataPref(PREF_REC_IMPRESSIONS); + const expired = []; + const active = []; + for (const item of recommendations) { + if ( + impressions[item.id] && + Date.now() - impressions[item.id] >= maxImpressionAge + ) { + expired.push(item); + } else { + active.push(item); + } + } + return active.concat(expired); + } + + enableStories() { + if (this.config.enabled) { + // If stories are being re enabled, ensure we have stories. + this.refreshAll({ updateOpenTabs: true }); + } + } + + async enable(options = {}) { + await this.refreshAll(options); + this.loaded = true; + } + + async reset() { + this.resetDataPrefs(); + await this.resetCache(); + this.resetState(); + } + + async resetCache() { + await this.resetAllCache(); + } + + async resetContentCache() { + await this.cache.set("feeds", {}); + await this.cache.set("spocs", {}); + await this.cache.set("sov", {}); + } + + async resetAllCache() { + await this.resetContentCache(); + // Reset in-memory caches. + this._isBff = undefined; + this._spocsCacheUpdateTime = undefined; + } + + resetDataPrefs() { + this.writeDataPref(PREF_SPOC_IMPRESSIONS, {}); + this.writeDataPref(PREF_REC_IMPRESSIONS, {}); + this.writeDataPref(PREF_FLIGHT_BLOCKS, {}); + } + + resetState() { + // Reset reducer + this.store.dispatch( + ac.BroadcastToContent({ type: at.DISCOVERY_STREAM_LAYOUT_RESET }) + ); + this.setupPrefs(false /* isStartup */); + this.loaded = false; + } + + async onPrefChange() { + // We always want to clear the cache/state if the pref has changed + await this.reset(); + if (this.config.enabled) { + // Load data from all endpoints + await this.enable({ updateOpenTabs: true }); + } + } + + // This is a request to change the config from somewhere. + // Can be from a specific pref related to Discovery Stream, + // or can be a generic request from an external feed that + // something changed. + configReset() { + this._prefCache.config = null; + this.store.dispatch( + ac.BroadcastToContent({ + type: at.DISCOVERY_STREAM_CONFIG_CHANGE, + data: this.config, + }) + ); + } + + recordFlightImpression(flightId) { + let impressions = this.readDataPref(PREF_SPOC_IMPRESSIONS); + + const timeStamps = impressions[flightId] || []; + timeStamps.push(Date.now()); + impressions = { ...impressions, [flightId]: timeStamps }; + + this.writeDataPref(PREF_SPOC_IMPRESSIONS, impressions); + } + + recordTopRecImpressions(recId) { + let impressions = this.readDataPref(PREF_REC_IMPRESSIONS); + if (!impressions[recId]) { + impressions = { ...impressions, [recId]: Date.now() }; + this.writeDataPref(PREF_REC_IMPRESSIONS, impressions); + } + } + + recordBlockFlightId(flightId) { + const flights = this.readDataPref(PREF_FLIGHT_BLOCKS); + if (!flights[flightId]) { + flights[flightId] = 1; + this.writeDataPref(PREF_FLIGHT_BLOCKS, flights); + } + } + + cleanUpFlightImpressionPref(data) { + let flightIds = []; + this.placementsForEach(placement => { + const newSpocs = data[placement.name]; + if (!newSpocs) { + return; + } + + const items = newSpocs.items || []; + flightIds = [...flightIds, ...items.map(s => `${s.flight_id}`)]; + }); + if (flightIds && flightIds.length) { + this.cleanUpImpressionPref( + id => !flightIds.includes(id), + PREF_SPOC_IMPRESSIONS + ); + } + } + + // Clean up rec impression pref by removing all stories that are no + // longer part of the response. + cleanUpTopRecImpressionPref(newFeeds) { + // Need to build a single list of stories. + const activeStories = Object.keys(newFeeds) + .filter(currentValue => newFeeds[currentValue].data) + .reduce((accumulator, currentValue) => { + const { recommendations } = newFeeds[currentValue].data; + return accumulator.concat(recommendations.map(i => `${i.id}`)); + }, []); + this.cleanUpImpressionPref( + id => !activeStories.includes(id), + PREF_REC_IMPRESSIONS + ); + } + + writeDataPref(pref, impressions) { + this.store.dispatch(ac.SetPref(pref, JSON.stringify(impressions))); + } + + readDataPref(pref) { + const prefVal = this.store.getState().Prefs.values[pref]; + return prefVal ? JSON.parse(prefVal) : {}; + } + + cleanUpImpressionPref(isExpired, pref) { + const impressions = this.readDataPref(pref); + let changed = false; + + Object.keys(impressions).forEach(id => { + if (isExpired(id)) { + changed = true; + delete impressions[id]; + } + }); + + if (changed) { + this.writeDataPref(pref, impressions); + } + } + + onCollectionsChanged() { + // Update layout, and reload any off screen tabs. + // This does not change any existing open tabs. + // It also doesn't update any spoc or rec data, just the layout. + const dispatch = action => this.store.dispatch(ac.AlsoToPreloaded(action)); + this.loadLayout(dispatch, false); + } + + async onPrefChangedAction(action) { + switch (action.data.name) { + case PREF_CONFIG: + case PREF_ENABLED: + case PREF_HARDCODED_BASIC_LAYOUT: + case PREF_SPOCS_ENDPOINT: + case PREF_SPOCS_ENDPOINT_QUERY: + case PREF_SPOCS_CLEAR_ENDPOINT: + case PREF_ENDPOINTS: + // This is a config reset directly related to Discovery Stream pref. + this.configReset(); + break; + case PREF_COLLECTIONS_ENABLED: + this.onCollectionsChanged(); + break; + case PREF_USER_TOPSITES: + case PREF_SYSTEM_TOPSITES: + if ( + !( + this.showTopsites || + (this.showStories && this.showSponsoredStories) + ) + ) { + // Ensure we delete any remote data potentially related to spocs. + this.clearSpocs(); + } + break; + case PREF_USER_TOPSTORIES: + case PREF_SYSTEM_TOPSTORIES: + if ( + !( + this.showStories || + (this.showTopsites && this.showSponsoredTopsites) + ) + ) { + // Ensure we delete any remote data potentially related to spocs. + this.clearSpocs(); + } + if (action.data.value) { + this.enableStories(); + } + break; + // Check if spocs was disabled. Remove them if they were. + case PREF_SHOW_SPONSORED: + case PREF_SHOW_SPONSORED_TOPSITES: + const dispatch = update => + this.store.dispatch(ac.BroadcastToContent(update)); + // We refresh placements data because one of the spocs were turned off. + this.updatePlacements( + dispatch, + this.store.getState().DiscoveryStream.layout + ); + // Currently the order of this is important. + // We need to check this after updatePlacements is called, + // because some of the spoc logic depends on the result of placement updates. + if ( + !( + (this.showSponsoredStories || + (this.showTopSites && this.showSponsoredTopSites)) && + (this.showSponsoredTopsites || + (this.showStories && this.showSponsoredStories)) + ) + ) { + // Ensure we delete any remote data potentially related to spocs. + this.clearSpocs(); + } + // Placements have changed so consider spocs expired, and reload them. + await this.cache.set("spocs", {}); + await this.loadSpocs(dispatch); + break; + } + } + + async onAction(action) { + switch (action.type) { + case at.INIT: + // During the initialization of Firefox: + // 1. Set-up listeners and initialize the redux state for config; + this.setupConfig(true /* isStartup */); + this.setupPrefs(true /* isStartup */); + // 2. If config.enabled is true, start loading data. + if (this.config.enabled) { + await this.enable({ updateOpenTabs: true, isStartup: true }); + } + Services.prefs.addObserver(PREF_POCKET_BUTTON, this); + break; + case at.DISCOVERY_STREAM_DEV_SYSTEM_TICK: + case at.SYSTEM_TICK: + // Only refresh if we loaded once in .enable() + if ( + this.config.enabled && + this.loaded && + (await this.checkIfAnyCacheExpired()) + ) { + await this.refreshAll({ updateOpenTabs: false }); + } + break; + case at.DISCOVERY_STREAM_DEV_SYNC_RS: + lazy.RemoteSettings.pollChanges(); + break; + case at.DISCOVERY_STREAM_DEV_EXPIRE_CACHE: + // Personalization scores update at a slower interval than content, so in order to debug, + // we want to be able to expire just content to trigger the earlier expire times. + await this.resetContentCache(); + break; + case at.DISCOVERY_STREAM_CONFIG_SET_VALUE: + // Use the original string pref to then set a value instead of + // this.config which has some modifications + this.store.dispatch( + ac.SetPref( + PREF_CONFIG, + JSON.stringify({ + ...JSON.parse(this.store.getState().Prefs.values[PREF_CONFIG]), + [action.data.name]: action.data.value, + }) + ) + ); + break; + case at.DISCOVERY_STREAM_POCKET_STATE_INIT: + this.setupPocketState(action.meta.fromTarget); + break; + case at.DISCOVERY_STREAM_PERSONALIZATION_UPDATED: + if (this.personalized) { + const { feeds, spocs } = this.store.getState().DiscoveryStream; + const spocsPersonalized = + this.store.getState().Prefs.values?.pocketConfig?.spocsPersonalized; + const recsPersonalized = + this.store.getState().Prefs.values?.pocketConfig?.recsPersonalized; + if (recsPersonalized && feeds.loaded) { + this.scoreFeeds(feeds); + } + if (spocsPersonalized && spocs.loaded) { + this.scoreSpocs(spocs); + } + } + break; + case at.DISCOVERY_STREAM_CONFIG_RESET: + // This is a generic config reset likely related to an external feed pref. + this.configReset(); + break; + case at.DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS: + this.resetConfigDefauts(); + break; + case at.DISCOVERY_STREAM_RETRY_FEED: + this.retryFeed(action.data.feed); + break; + case at.DISCOVERY_STREAM_CONFIG_CHANGE: + // When the config pref changes, load or unload data as needed. + await this.onPrefChange(); + break; + case at.DISCOVERY_STREAM_IMPRESSION_STATS: + if ( + action.data.tiles && + action.data.tiles[0] && + action.data.tiles[0].id + ) { + this.recordTopRecImpressions(action.data.tiles[0].id); + } + break; + case at.DISCOVERY_STREAM_SPOC_IMPRESSION: + if (this.showSpocs) { + this.recordFlightImpression(action.data.flightId); + + // Apply frequency capping to SPOCs in the redux store, only update the + // store if the SPOCs are changed. + const spocsState = this.store.getState().DiscoveryStream.spocs; + + let frequencyCapped = []; + this.placementsForEach(placement => { + const spocs = spocsState.data[placement.name]; + if (!spocs || !spocs.items) { + return; + } + + const { data: capResult, filtered } = this.frequencyCapSpocs( + spocs.items + ); + frequencyCapped = [...frequencyCapped, ...filtered]; + + spocsState.data = { + ...spocsState.data, + [placement.name]: { + ...spocs, + items: capResult, + }, + }; + }); + + if (frequencyCapped.length) { + // Update cache here so we don't need to re calculate frequency caps on loads from cache. + await this.cache.set("spocs", { + lastUpdated: spocsState.lastUpdated, + spocs: spocsState.data, + }); + + this.store.dispatch( + ac.AlsoToPreloaded({ + type: at.DISCOVERY_STREAM_SPOCS_UPDATE, + data: { + lastUpdated: spocsState.lastUpdated, + spocs: spocsState.data, + }, + }) + ); + } + } + break; + // This is fired from the browser, it has no concept of spocs, flight or pocket. + // We match the blocked url with our available spoc urls to see if there is a match. + // I suspect we *could* instead do this in BLOCK_URL but I'm not sure. + case at.PLACES_LINK_BLOCKED: + if (this.showSpocs) { + let blockedItems = []; + const spocsState = this.store.getState().DiscoveryStream.spocs; + + this.placementsForEach(placement => { + const spocs = spocsState.data[placement.name]; + if (spocs && spocs.items && spocs.items.length) { + const blockedResults = []; + const blocks = spocs.items.filter(s => { + const blocked = s.url === action.data.url; + if (!blocked) { + blockedResults.push(s); + } + return blocked; + }); + + blockedItems = [...blockedItems, ...blocks]; + + spocsState.data = { + ...spocsState.data, + [placement.name]: { + ...spocs, + items: blockedResults, + }, + }; + } + }); + + if (blockedItems.length) { + // Update cache here so we don't need to re calculate blocks on loads from cache. + await this.cache.set("spocs", { + lastUpdated: spocsState.lastUpdated, + spocs: spocsState.data, + }); + + // If we're blocking a spoc, we want open tabs to have + // a slightly different treatment from future tabs. + // AlsoToPreloaded updates the source data and preloaded tabs with a new spoc. + // BroadcastToContent updates open tabs with a non spoc instead of a new spoc. + this.store.dispatch( + ac.AlsoToPreloaded({ + type: at.DISCOVERY_STREAM_LINK_BLOCKED, + data: action.data, + }) + ); + this.store.dispatch( + ac.BroadcastToContent({ + type: at.DISCOVERY_STREAM_SPOC_BLOCKED, + data: action.data, + }) + ); + break; + } + } + + this.store.dispatch( + ac.BroadcastToContent({ + type: at.DISCOVERY_STREAM_LINK_BLOCKED, + data: action.data, + }) + ); + break; + case at.UNINIT: + // When this feed is shutting down: + this.uninitPrefs(); + this._recommendationProvider = null; + Services.prefs.removeObserver(PREF_POCKET_BUTTON, this); + break; + case at.BLOCK_URL: { + // If we block a story that also has a flight_id + // we want to record that as blocked too. + // This is because a single flight might have slightly different urls. + action.data.forEach(site => { + const { flight_id } = site; + if (flight_id) { + this.recordBlockFlightId(flight_id); + } + }); + break; + } + case at.PREF_CHANGED: + await this.onPrefChangedAction(action); + if (action.data.name === "pocketConfig") { + await this.onPrefChange(); + this.setupPrefs(false /* isStartup */); + } + break; + } + } +} + +/* This function generates a hardcoded layout each call. + This is because modifying the original object would + persist across pref changes and system_tick updates. + + NOTE: There is some branching logic in the template. + `spocsUrl` Changing the url for spocs is used for adding a siteId query param. + `feedUrl` Where to fetch stories from. + `items` How many items to include in the primary card grid. + `spocPositions` Changes the position of spoc cards. + `spocTopsitesPositions` Changes the position of spoc topsites. + `spocPlacementData` Used to set the spoc content. + `spocTopsitesPlacementEnabled` Tuns on and off the sponsored topsites placement. + `spocTopsitesPlacementData` Used to set spoc content for topsites. + `sponsoredCollectionsEnabled` Tuns on and off the sponsored collection section. + `hybridLayout` Changes cards to smaller more compact cards only for specific breakpoints. + `hideCardBackground` Removes Pocket card background and borders. + `fourCardLayout` Enable four Pocket cards per row. + `newFooterSection` Changes the layout of the topics section. + `compactGrid` Reduce the number of pixels between the Pocket cards. + `essentialReadsHeader` Updates the Pocket section header and title to say "Today’s Essential Reads", moves the "Recommended by Pocket" header to the right side. + `editorsPicksHeader` Updates the Pocket section header and title to say "Editor’s Picks", if used with essentialReadsHeader, creates a second section 2 rows down for editorsPicks. + `onboardingExperience` Show new users some UI explaining Pocket above the Pocket section. + `ctaButtonSponsors` An array of sponsors we want to show a cta button on the card for. + `ctaButtonVariant` Sets the variant for the cta sponsor button. +*/ +getHardcodedLayout = ({ + spocsUrl = SPOCS_URL, + feedUrl = FEED_URL, + items = 21, + spocPositions = [1, 5, 7, 11, 18, 20], + spocTopsitesPositions = [1], + spocPlacementData = { ad_types: [3617], zone_ids: [217758, 217995] }, + spocTopsitesPlacementEnabled = false, + spocTopsitesPlacementData = { ad_types: [3120], zone_ids: [280143] }, + widgetPositions = [], + widgetData = [], + sponsoredCollectionsEnabled = false, + hybridLayout = false, + hideCardBackground = false, + fourCardLayout = false, + newFooterSection = false, + compactGrid = false, + essentialReadsHeader = false, + editorsPicksHeader = false, + onboardingExperience = false, + ctaButtonSponsors = [], + ctaButtonVariant = "", +}) => ({ + lastUpdate: Date.now(), + spocs: { + url: spocsUrl, + }, + layout: [ + { + width: 12, + components: [ + { + type: "TopSites", + header: { + title: { + id: "newtab-section-header-topsites", + }, + }, + ...(spocTopsitesPlacementEnabled && spocTopsitesPlacementData + ? { + placement: { + name: "sponsored-topsites", + ad_types: spocTopsitesPlacementData.ad_types, + zone_ids: spocTopsitesPlacementData.zone_ids, + }, + spocs: { + probability: 1, + prefs: [PREF_SHOW_SPONSORED_TOPSITES], + positions: spocTopsitesPositions.map(position => { + return { index: position }; + }), + }, + } + : {}), + properties: {}, + }, + ...(sponsoredCollectionsEnabled + ? [ + { + type: "CollectionCardGrid", + properties: { + items: 3, + }, + header: { + title: "", + }, + placement: { + name: "sponsored-collection", + ad_types: [3617], + zone_ids: [217759, 218031], + }, + spocs: { + probability: 1, + positions: [ + { + index: 0, + }, + { + index: 1, + }, + { + index: 2, + }, + ], + }, + }, + ] + : []), + { + type: "Message", + essentialReadsHeader, + editorsPicksHeader, + header: { + title: { + id: "newtab-section-header-stories", + }, + subtitle: "", + link_text: { + id: "newtab-pocket-learn-more", + }, + link_url: "https://getpocket.com/firefox/new_tab_learn_more", + icon: "chrome://global/skin/icons/pocket.svg", + }, + properties: {}, + styles: { + ".ds-message": "margin-bottom: -20px", + }, + }, + { + type: "CardGrid", + properties: { + items, + hybridLayout, + hideCardBackground, + fourCardLayout, + compactGrid, + essentialReadsHeader, + editorsPicksHeader, + onboardingExperience, + ctaButtonSponsors, + ctaButtonVariant, + }, + widgets: { + positions: widgetPositions.map(position => { + return { index: position }; + }), + data: widgetData, + }, + cta_variant: "link", + header: { + title: "", + }, + placement: { + name: "spocs", + ad_types: spocPlacementData.ad_types, + zone_ids: spocPlacementData.zone_ids, + }, + feed: { + embed_reference: null, + url: feedUrl, + }, + spocs: { + probability: 1, + positions: spocPositions.map(position => { + return { index: position }; + }), + }, + }, + { + type: "Navigation", + newFooterSection, + properties: { + alignment: "left-align", + links: [ + { + name: "Self improvement", + url: "https://getpocket.com/explore/self-improvement?utm_source=pocket-newtab", + }, + { + name: "Food", + url: "https://getpocket.com/explore/food?utm_source=pocket-newtab", + }, + { + name: "Entertainment", + url: "https://getpocket.com/explore/entertainment?utm_source=pocket-newtab", + }, + { + name: "Health & fitness", + url: "https://getpocket.com/explore/health?utm_source=pocket-newtab", + }, + { + name: "Science", + url: "https://getpocket.com/explore/science?utm_source=pocket-newtab", + }, + { + name: "More recommendations ›", + url: "https://getpocket.com/explore?utm_source=pocket-newtab", + }, + ], + extraLinks: [ + { + name: "Career", + url: "https://getpocket.com/explore/career?utm_source=pocket-newtab", + }, + { + name: "Technology", + url: "https://getpocket.com/explore/technology?utm_source=pocket-newtab", + }, + ], + privacyNoticeURL: { + url: "https://www.mozilla.org/privacy/firefox/#recommend-relevant-content", + title: { + id: "newtab-section-menu-privacy-notice", + }, + }, + }, + header: { + title: { + id: "newtab-pocket-read-more", + }, + }, + styles: { + ".ds-navigation": "margin-top: -10px;", + }, + }, + ...(newFooterSection + ? [ + { + type: "PrivacyLink", + properties: { + url: "https://www.mozilla.org/privacy/firefox/", + title: { + id: "newtab-section-menu-privacy-notice", + }, + }, + }, + ] + : []), + ], + }, + ], +}); diff --git a/browser/components/newtab/lib/DownloadsManager.sys.mjs b/browser/components/newtab/lib/DownloadsManager.sys.mjs new file mode 100644 index 0000000000..f095645d41 --- /dev/null +++ b/browser/components/newtab/lib/DownloadsManager.sys.mjs @@ -0,0 +1,188 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { actionTypes as at } from "resource://activity-stream/common/Actions.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + DownloadsCommon: "resource:///modules/DownloadsCommon.sys.mjs", + DownloadsViewUI: "resource:///modules/DownloadsViewUI.sys.mjs", + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", + NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", +}); + +const DOWNLOAD_CHANGED_DELAY_TIME = 1000; // time in ms to delay timer for downloads changed events + +export class DownloadsManager { + constructor(store) { + this._downloadData = null; + this._store = null; + this._downloadItems = new Map(); + this._downloadTimer = null; + } + + setTimeout(callback, delay) { + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback(callback, delay, Ci.nsITimer.TYPE_ONE_SHOT); + return timer; + } + + formatDownload(download) { + let referrer = download.source.referrerInfo?.originalReferrer?.spec || null; + return { + hostname: new URL(download.source.url).hostname, + url: download.source.url, + path: download.target.path, + title: lazy.DownloadsViewUI.getDisplayName(download), + description: + lazy.DownloadsViewUI.getSizeWithUnits(download) || + lazy.DownloadsCommon.strings.sizeUnknown, + referrer, + date_added: download.endTime, + }; + } + + init(store) { + this._store = store; + this._downloadData = lazy.DownloadsCommon.getData( + null /* null for non-private downloads */, + true, + false, + true + ); + this._downloadData.addView(this); + } + + onDownloadAdded(download) { + if (!this._downloadItems.has(download.source.url)) { + this._downloadItems.set(download.source.url, download); + + // On startup, all existing downloads fire this notification, so debounce them + if (this._downloadTimer) { + this._downloadTimer.delay = DOWNLOAD_CHANGED_DELAY_TIME; + } else { + this._downloadTimer = this.setTimeout(() => { + this._downloadTimer = null; + this._store.dispatch({ type: at.DOWNLOAD_CHANGED }); + }, DOWNLOAD_CHANGED_DELAY_TIME); + } + } + } + + onDownloadRemoved(download) { + if (this._downloadItems.has(download.source.url)) { + this._downloadItems.delete(download.source.url); + this._store.dispatch({ type: at.DOWNLOAD_CHANGED }); + } + } + + async getDownloads( + threshold, + { + numItems = this._downloadItems.size, + onlySucceeded = false, + onlyExists = false, + } + ) { + if (!threshold) { + return []; + } + let results = []; + + // Only get downloads within the time threshold specified and sort by recency + const downloadThreshold = Date.now() - threshold; + let downloads = [...this._downloadItems.values()] + .filter(download => download.endTime > downloadThreshold) + .sort((download1, download2) => download1.endTime < download2.endTime); + + for (const download of downloads) { + // Ignore blocked links, but allow long (data:) uris to avoid high CPU + if ( + download.source.url.length < 10000 && + lazy.NewTabUtils.blockedLinks.isBlocked(download.source) + ) { + continue; + } + + // Only include downloads where the file still exists + if (onlyExists) { + // Refresh download to ensure the 'exists' attribute is up to date + await download.refresh(); + if (!download.target.exists) { + continue; + } + } + // Only include downloads that were completed successfully + if (onlySucceeded) { + if (!download.succeeded) { + continue; + } + } + const formattedDownloadForHighlights = this.formatDownload(download); + results.push(formattedDownloadForHighlights); + if (results.length === numItems) { + break; + } + } + return results; + } + + uninit() { + if (this._downloadData) { + this._downloadData.removeView(this); + this._downloadData = null; + } + if (this._downloadTimer) { + this._downloadTimer.cancel(); + this._downloadTimer = null; + } + } + + onAction(action) { + let doDownloadAction = callback => { + let download = this._downloadItems.get(action.data.url); + if (download) { + callback(download); + } + }; + + switch (action.type) { + case at.COPY_DOWNLOAD_LINK: + doDownloadAction(download => { + lazy.DownloadsCommon.copyDownloadLink(download); + }); + break; + case at.REMOVE_DOWNLOAD_FILE: + doDownloadAction(download => { + lazy.DownloadsCommon.deleteDownload(download).catch(console.error); + }); + break; + case at.SHOW_DOWNLOAD_FILE: + doDownloadAction(download => { + lazy.DownloadsCommon.showDownloadedFile( + new lazy.FileUtils.File(download.target.path) + ); + }); + break; + case at.OPEN_DOWNLOAD_FILE: + const win = action._target.browser.ownerGlobal; + const openWhere = + action.data.event && win.whereToOpenLink(action.data.event); + doDownloadAction(download => { + lazy.DownloadsCommon.openDownload(download, { + // Replace "current" or unknown value with "tab" as the default behavior + // for opening downloads when handled internally + openWhere: ["window", "tab", "tabshifted"].includes(openWhere) + ? openWhere + : "tab", + }); + }); + break; + case at.UNINIT: + this.uninit(); + break; + } + } +} diff --git a/browser/components/newtab/lib/FaviconFeed.sys.mjs b/browser/components/newtab/lib/FaviconFeed.sys.mjs new file mode 100644 index 0000000000..a76566d3e8 --- /dev/null +++ b/browser/components/newtab/lib/FaviconFeed.sys.mjs @@ -0,0 +1,198 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { actionTypes as at } from "resource://activity-stream/common/Actions.sys.mjs"; +import { getDomain } from "resource://activity-stream/lib/TippyTopProvider.sys.mjs"; + +// We use importESModule here instead of static import so that +// the Karma test environment won't choke on this module. This +// is because the Karma test environment already stubs out +// RemoteSettings, and overrides importESModule to be a no-op (which +// can't be done for a static import statement). + +// eslint-disable-next-line mozilla/use-static-import +const { RemoteSettings } = ChromeUtils.importESModule( + "resource://services-settings/remote-settings.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", +}); + +const MIN_FAVICON_SIZE = 96; + +/** + * Get favicon info (uri and size) for a uri from Places. + * + * @param uri {nsIURI} Page to check for favicon data + * @returns A promise of an object (possibly null) containing the data + */ +function getFaviconInfo(uri) { + return new Promise(resolve => + lazy.PlacesUtils.favicons.getFaviconDataForPage( + uri, + // Package up the icon data in an object if we have it; otherwise null + (iconUri, faviconLength, favicon, mimeType, faviconSize) => + resolve(iconUri ? { iconUri, faviconSize } : null), + lazy.NewTabUtils.activityStreamProvider.THUMB_FAVICON_SIZE + ) + ); +} + +/** + * Fetches visit paths for a given URL from its most recent visit in Places. + * + * Note that this includes the URL itself as well as all the following + * permenent&temporary redirected URLs if any. + * + * @param {String} a URL string + * + * @returns {Array} Returns an array containing objects as + * {int} visit_id: ID of the visit in moz_historyvisits. + * {String} url: URL of the redirected URL. + */ +async function fetchVisitPaths(url) { + const query = ` + WITH RECURSIVE path(visit_id) + AS ( + SELECT v.id + FROM moz_places h + JOIN moz_historyvisits v + ON v.place_id = h.id + WHERE h.url_hash = hash(:url) AND h.url = :url + AND v.visit_date = h.last_visit_date + + UNION + + SELECT id + FROM moz_historyvisits + JOIN path + ON visit_id = from_visit + WHERE visit_type IN + (${lazy.PlacesUtils.history.TRANSITIONS.REDIRECT_PERMANENT}, + ${lazy.PlacesUtils.history.TRANSITIONS.REDIRECT_TEMPORARY}) + ) + SELECT visit_id, ( + SELECT ( + SELECT url + FROM moz_places + WHERE id = place_id) + FROM moz_historyvisits + WHERE id = visit_id) AS url + FROM path + `; + + const visits = + await lazy.NewTabUtils.activityStreamProvider.executePlacesQuery(query, { + columns: ["visit_id", "url"], + params: { url }, + }); + return visits; +} + +/** + * Fetch favicon for a url by following its redirects in Places. + * + * This can improve the rich icon coverage for Top Sites since Places only + * associates the favicon to the final url if the original one gets redirected. + * Note this is not an urgent request, hence it is dispatched to the main + * thread idle handler to avoid any possible performance impact. + */ +export async function fetchIconFromRedirects(url) { + const visitPaths = await fetchVisitPaths(url); + if (visitPaths.length > 1) { + const lastVisit = visitPaths.pop(); + const redirectedUri = Services.io.newURI(lastVisit.url); + const iconInfo = await getFaviconInfo(redirectedUri); + if (iconInfo && iconInfo.faviconSize >= MIN_FAVICON_SIZE) { + lazy.PlacesUtils.favicons.setAndFetchFaviconForPage( + Services.io.newURI(url), + iconInfo.iconUri, + false, + lazy.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + null, + Services.scriptSecurityManager.getSystemPrincipal() + ); + } + } +} + +export class FaviconFeed { + constructor() { + this._queryForRedirects = new Set(); + } + + /** + * fetchIcon attempts to fetch a rich icon for the given url from two sources. + * First, it looks up the tippy top feed, if it's still missing, then it queries + * the places for rich icon with its most recent visit in order to deal with + * the redirected visit. See Bug 1421428 for more details. + */ + async fetchIcon(url) { + // Avoid initializing and fetching icons if prefs are turned off + if (!this.shouldFetchIcons) { + return; + } + + const site = await this.getSite(getDomain(url)); + if (!site) { + if (!this._queryForRedirects.has(url)) { + this._queryForRedirects.add(url); + Services.tm.idleDispatchToMainThread(() => fetchIconFromRedirects(url)); + } + return; + } + + let iconUri = Services.io.newURI(site.image_url); + // The #tippytop is to be able to identify them for telemetry. + iconUri = iconUri.mutate().setRef("tippytop").finalize(); + lazy.PlacesUtils.favicons.setAndFetchFaviconForPage( + Services.io.newURI(url), + iconUri, + false, + lazy.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + null, + Services.scriptSecurityManager.getSystemPrincipal() + ); + } + + /** + * Get the site tippy top data from Remote Settings. + */ + async getSite(domain) { + const sites = await this.tippyTop.get({ + filters: { domain }, + syncIfEmpty: false, + }); + return sites.length ? sites[0] : null; + } + + /** + * Get the tippy top collection from Remote Settings. + */ + get tippyTop() { + if (!this._tippyTop) { + this._tippyTop = RemoteSettings("tippytop"); + } + return this._tippyTop; + } + + /** + * Determine if we should be fetching and saving icons. + */ + get shouldFetchIcons() { + return Services.prefs.getBoolPref("browser.chrome.site_icons"); + } + + onAction(action) { + switch (action.type) { + case at.RICH_ICON_MISSING: + this.fetchIcon(action.data.url); + break; + } + } +} diff --git a/browser/components/newtab/lib/FilterAdult.sys.mjs b/browser/components/newtab/lib/FilterAdult.sys.mjs new file mode 100644 index 0000000000..a60ba3baa6 --- /dev/null +++ b/browser/components/newtab/lib/FilterAdult.sys.mjs @@ -0,0 +1,3040 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// We use importESModule here instead of static import so that +// the Karma test environment won't choke on this module. This +// is because the Karma test environment already stubs out +// XPCOMUtils, and overrides importESModule to be a no-op (which +// can't be done for a static import statement). + +// eslint-disable-next-line mozilla/use-static-import +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "gFilterAdultEnabled", + "browser.newtabpage.activity-stream.filterAdult", + true +); + +// Keep a Set of adult base domains for lookup (initialized at end of file) +let gAdultSet; + +// Keep a hasher for repeated hashings +let gCryptoHash = null; + +/** + * Run some text through md5 and return the base64 result. + */ +function md5Hash(text) { + // Lazily create a reusable hasher + if (gCryptoHash === null) { + gCryptoHash = Cc["@mozilla.org/security/hash;1"].createInstance( + Ci.nsICryptoHash + ); + } + + gCryptoHash.init(gCryptoHash.MD5); + + // Convert the text to a byte array for hashing + gCryptoHash.update( + text.split("").map(c => c.charCodeAt(0)), + text.length + ); + + // Request the has result as ASCII base64 + return gCryptoHash.finish(true); +} + +export const FilterAdult = { + /** + * Filter out any link objects that have a url with an adult base domain. + * + * @param {string[]} links + * An array of links to test. + * @returns {string[]} + * A filtered array without adult links. + */ + filter(links) { + if (!lazy.gFilterAdultEnabled) { + return links; + } + + return links.filter(({ url }) => { + try { + const uri = Services.io.newURI(url); + return !gAdultSet.has(md5Hash(Services.eTLD.getBaseDomain(uri))); + } catch (ex) { + return true; + } + }); + }, + + /** + * Determine if the supplied url is an adult url or not. + * + * @param {string} url + * The url to test. + * @returns {boolean} + * True if it is an adult url. + */ + isAdultUrl(url) { + if (!lazy.gFilterAdultEnabled) { + return false; + } + try { + const uri = Services.io.newURI(url); + return gAdultSet.has(md5Hash(Services.eTLD.getBaseDomain(uri))); + } catch (ex) { + return false; + } + }, + + /** + * For tests, adds a domain to the adult list. + */ + addDomainToList(url) { + gAdultSet.add( + md5Hash(Services.eTLD.getBaseDomain(Services.io.newURI(url))) + ); + }, + + /** + * For tests, removes a domain to the adult list. + */ + removeDomainFromList(url) { + gAdultSet.delete( + md5Hash(Services.eTLD.getBaseDomain(Services.io.newURI(url))) + ); + }, +}; + +// These are md5 hashes of base domains to be filtered out. Originally from: +// https://hg.mozilla.org/mozilla-central/log/default/browser/base/content/newtab/newTab.inadjacent.json +gAdultSet = new Set([ + "+/UCpAhZhz368iGioEO8aQ==", + "+1e7jvUo8f2/2l0TFrQqfA==", + "+1gcqAqaRZwCj5BGiZp3CA==", + "+25t/2lo0FUEtWYK8LdQZQ==", + "+8PiQt6O7pJI/nIvQpDaAg==", + "+CLf5witKkuOvPCulTlkqw==", + "+CvLiih/gf2ugXAF+LgWqw==", + "+DWs0vvFGt6d3mzdcsdsyA==", + "+H0Rglt/HnhZwdty2hsDHg==", + "+L1FDsr5VQtuYc2Is5QGjw==", + "+LJYVZl1iPrdMU3L5+nxZw==", + "+Mp+JIyO0XC5urvMyi3wvQ==", + "+NMUaQ7XPsAi0rk7tTT9wQ==", + "+NmjwjsPhGJh9bM10SFkLw==", + "+OERSmo7OQUUjudkccSMOA==", + "+OLntmlsMBBYPREPnS6iVw==", + "+OXdvbTxHtSoLg7bZMho4w==", + "+P5q4YD1Rr5SX26Xr+tzlw==", + "+PUVXkoTqHxJHO18z4KMfw==", + "+Pl0bSMBAdXpRIA+zE02JA==", + "+QosBAnSM2h4lsKuBlqEZw==", + "+S+WXgVDSU1oGmCzGwuT3g==", + "+SclwwY8R2RPrnX54Z+A6w==", + "+VfRcTBQ80KSeJRdg0cDfw==", + "+WpF8+poKmHPUBB4UYh/ig==", + "+YVxSyViJfrme/ENe1zA7A==", + "+YrqTEJlJCv0A2RHQ8tr1A==", + "+ZozWaPWw8ws1cE5DJACeg==", + "+aF4ilbjQbLpAuFXQEYMWQ==", + "+dBv88reDrjEz6a2xX3Hzw==", + "+dIEf5FBrHpkjmwUmGS6eg==", + "+edqJYGvcy1AH2mEjJtSIg==", + "+fcjH2kZKNj8quOytUk4nQ==", + "+gO0bg8LY+py2dLM1sM7Ag==", + "+gbitI/gpxebN/rK7qj8Fw==", + "+gpHnUj2GWocP74t5XWz4w==", + "+jVN/3ASc2O44sX6ab8/cg==", + "+mJLK+6qq8xFv7O/mbILTw==", + "+n0K7OB2ItzhySZ4rhUrMg==", + "+p8pofUlwn8vV6Rp6+sz9g==", + "+tuUmnRDRWVLA+1k0dcUvg==", + "+zBkeHF4P8vLzk1iO1Zn3Q==", + "//eHwmDOQRSrv+k9C/k3ZQ==", + "/2Chaw2M9DzsadFFkCu6WQ==", + "/2c4oNniwhL3z5IOngfggg==", + "/2jGyMekNu7U136K+2N3Jg==", + "/Bwpt5fllzDHq2Ul6v86fA==", + "/DJgKE9ouibewuZ2QEnk6w==", + "/DiUApY7cVp5W9o24rkgRA==", + "/FchS2nPezycB8Bcqc2dbg==", + "/FdZzSprPnNDPwbhV1C0Cg==", + "/FsJYFNe+7UvsSkiotNJEQ==", + "/G26n5Xoviqldr5sg/Jl3w==", + "/HU2+fBqfWTEuqINc0UZSA==", + "/IarsLzJB8bf0AupJJ+/Eg==", + "/KYZdUWrkfxSsIrp46xxow==", + "/MEOgAhwb7F0nBnV4tIRZA==", + "/MeHciFhvFzQsCIw39xIZA==", + "/Ph/6l/lFNVqxAje1+PgFA==", + "/SP6pOdYFzcAl2OL05z4uQ==", + "/TSsi/AwKHtP6kQaeReI3w==", + "/VnKh/NDv7y/bfO6CWsLaQ==", + "/XC/FmMIOdhMTPqmy4DfUA==", + "/XjB6c5fxFGcKVAQ4o+OMw==", + "/YuQw7oAF08KDptxJEBS9g==", + "/a+bLXOq02sa/s8h7PhUTg==", + "/a9O7kWeXa0le45ab3+nVw==", + "/c34NtdUZAHWIwGl3JM8Tw==", + "/cJ0Nn5YbXeUpOHMfWXNHQ==", + "/cdR1i5TuQvO+u3Ov3b0KQ==", + "/gi3UZmunVOIXhZSktZ8zQ==", + "/hFhjFGJx2wRfz6hyrIpvA==", + "/jDVt9dRIn+o4IQ1DPwbsg==", + "/jH6imhTPZ/tHI4gYz2+HA==", + "/kGxvyEokQsVz0xlKzCn2A==", + "/mFp3GFkGNLhx2CiDvJv4A==", + "/mrqas0eDX+sFUNJvCQY8g==", + "/n1RLTTVpygre1dl36PDwQ==", + "/ngbFuKIAVpdSwsA3VxvNw==", + "/p/aCTIhi1bU0/liuO/a2Q==", + "/u5W2Gab4GgCMIc4KTp2mg==", + "/wIZAye9h1TUiZmDW0ZmYA==", + "/wiA2ltAuWyBhIvQAYBTQw==", + "/y/jHHEpUu5TR+R2o96kXA==", + "/zFLRvi75UL8qvg+a6zqGg==", + "00TVKawojyqrJkC7YqT41Q==", + "022B0oiRMx8Xb4Af98mTvQ==", + "02im2RooJQ/9UfUrh5LO+A==", + "0G93AxGPVwmr66ZOleM90A==", + "0HN6MIGtkdzNPsrGs611xA==", + "0K4NBxqEa3RYpnrkrD/XjQ==", + "0L0FVcH5Dlj3oL8+e9Na7g==", + "0NrvBuyjcJ2q6yaHpz/FOA==", + "0ODJyWKJSfObo+FNdRQkkA==", + "0QB0OUW5x2JLHfrtmpZQ+w==", + "0QCQORCYfLuSbq94Sbt0bQ==", + "0QbH4oI8IjZ9BRcqRyvvDQ==", + "0QxPAqRF8inBuFEEzNmLjA==", + "0SkC/4PtnX1bMYgD6r6CLA==", + "0TxcYwG72dT7Tg+eG8pP1w==", + "0UeRwDID2RBIikInqFI7uw==", + "0VsaJHR0Ms8zegsCpAKoyg==", + "0Y6iiZjCwPDwD/CwJzfioQ==", + "0ZEC3hy411LkOhKblvTcqg==", + "0ZRGz+oj2infCAkuKKuHiQ==", + "0a4SafpDIe8V4FlFWYkMHw==", + "0b/xj6fd0x+aB8EB0LC4SA==", + "0bj069wXgEJbw7dpiPr8Tg==", + "0dIeIM5Zvm5nSVWLy94LWg==", + "0e8hM3E5tnABRyy29A8yFw==", + "0egBaMnAf0CQEXf1pCIKnA==", + "0fN+eHlbRS6mVZBbH/B9FQ==", + "0fnruVOCxEczscBuv4yL9A==", + "0fpe9E6m3eLp/5j5rLrz2Q==", + "0klouNfZRHFFpdHi4ZR2hA==", + "0nOg18ZJ/NicqVUz5Jr0Hg==", + "0ofMbUCA3/v5L8lHnX4S5w==", + "0p1jMr06OyBoXQuSLYN4aQ==", + "0p8YbEMxeb73HbAfvPLQRw==", + "0q+erphtrB+6HBnnYg7O6w==", + "0rTYcuVYdilO7zEfKrxY3A==", + "0rfG4gRugAwVP0i3AGVxxg==", + "0u+0WHr7WI6IlVBBgiRi6w==", + "0yJ7TQYzcp3DXVSvwavr+w==", + "1+A9FCGP3bZhk6gU3LQtNg==", + "1+XWdu4qCqLLVjqkKz3nmA==", + "1+qmrbC8c7MJ6pxmDMcKuA==", + "1/Hxu8M9N/oNwk8bCj4FNQ==", + "1/SGIab+NnizimUmNDC4wA==", + "1/ZheMsbojazxt31j/l3iA==", + "10OltdxPXOvfatJuwPVKbQ==", + "11FE2kknwYi2Qu0JUKMn3A==", + "11U5XEwfMI7avx014LfC8g==", + "16d+fhFlgayu3ttKVV/pbg==", + "16iT/jCcPDrJEfi2bE5F+Q==", + "18RKixTv12q3xoBLz6eKiA==", + "18ndtDM9UaNfBR1cr3SHdA==", + "19yQHaBemtlgo2QkU5M6jQ==", + "1AeReq55UQotRQVKJ66pmg==", + "1ApqwW7pE+XUB2Cs2M6y7g==", + "1B5gxGQSGzVKoNd5Ol4N7g==", + "1BjsijOzgHt/0i36ZGffoQ==", + "1C50kisi9nvyVJNfq2hOEQ==", + "1E3pMgAHOnHx3ALdNoHr8Q==", + "1EI9aa955ejNo1dJepcZJw==", + "1FSrgkUXgZot2CsmbAtkPw==", + "1Gpj4TPXhdPEI4zfQFsOCg==", + "1HDgfU7xU7LWO/BXsODZAQ==", + "1I+UVx3krrD4NhzO7dgfHQ==", + "1JI9bT92UzxI8txjhst9LQ==", + "1JRgSHnfAQFQtSkFTttkqQ==", + "1LPC0BzhJbepHTSAiZ3QTw==", + "1MIn73MLroxXirrb+vyg2Q==", + "1Oykse0jQVbuR3MvW5ot4A==", + "1Pmnur6TbZ9cmemvu0+dSA==", + "1PvTn90xwZJPoVfyT5/uIQ==", + "1QGhj9NONF2rC44UdO+Izw==", + "1RQZ2pWSxT+RKyhBigtSFg==", + "1Vtrv6QUAfiYQjlLTpNovg==", + "1WIi4I62GqkjDXOYqHWJfQ==", + "1Wc8jQlDSB4Dp32wkL2odw==", + "1X14kHeKwGmLeYqpe60XEA==", + "1YO9G8qAhLIu2rShvekedw==", + "1Ym0lyBJ9aFjhJb/GdUPvQ==", + "1b2uf+CdVjufqiVpUShvHw==", + "1buQEv2YlH/ljTgH0uJEtw==", + "1cj1Fpd3+UiBAOahEhsluA==", + "1d7RPHdZ9qzAbG3Vi9BdFA==", + "1dhq3ozNCx0o4dV1syLVDA==", + "1dsKN1nG6upj7kKTKuJWsQ==", + "1eCHcz4swFH+uRhiilOinQ==", + "1eRUCdIJe3YGD5jOMbkkOg==", + "1fztTtQWNMIMSAc5Hr6jMQ==", + "1gA65t5FiBTEgMELTQFUPQ==", + "1jBaRO8Bg5l6TH7qJ8EPiw==", + "1k8tL2xmGFVYMgKUcmDcEw==", + "1lCcQWGDePPYco4vYrA5vw==", + "1m1yD4L9A7Q1Ot+wCsrxJQ==", + "1mw6LfTiirFyfjejf8QNGA==", + "1nXByug2eKq0kR3H3VjnWQ==", + "1tpM0qgdo7JDFwvT0TD78g==", + "1vqRt79ukuvdJNyIlIag8Q==", + "1wBuHqS1ciup31WTfm3NPg==", + "1xWx5V3G9murZP7srljFmA==", + "1zDfWw5LdG20ClNP1HYxgw==", + "203EqmJI9Q4tWxTJaBdSzA==", + "23C4eh3yBb5n/RNZeTyJkA==", + "23d9B9Gz5kUOi1I//EYsSQ==", + "24H9q+E8pgCEdFS7JO5kzQ==", + "25w3ZRUzCvJwAVHYCIO5uw==", + "26+yXbqI+fmIZsYl4UhUzw==", + "26Wmdp6SkKN74W0/XPcnmA==", + "29EybnMEO95Ng4l/qK4NWQ==", + "2Ct+pLXrK6Ku1f4qehjurQ==", + "2D6yhuABiaFFoXz0Lh0C+w==", + "2DNbXVgesUa7PgYQ4zX5Lw==", + "2E41e0MgM3WhFx2oasIQeA==", + "2HHqeGRMfzf3RXwVybx+ZQ==", + "2Hc5oyl0AYRy2VzcDKy+VA==", + "2QQtKtBAm2AjJ5c0WQ6BQA==", + "2QS/6OBA1T01NlIbfkTYJg==", + "2RFaMPlSbVuoEqKXgkIa5A==", + "2SI4F7Vvde2yjzMLAwxOog==", + "2SwIiUwT4vRZPrg7+vZqDA==", + "2W6lz1Z7PhkvObEAg2XKJw==", + "2Wvk/kouEEOY0evUkQLhOQ==", + "2XrR2hjDEvx8MQpHk9dnjw==", + "2aDK0tGNgMLyxT+BQPDE8Q==", + "2aIx9UdMxxZWvrfeJ+DcTw==", + "2abfl3N46tznOpr+94VONQ==", + "2bsIpvnGcFhTCSrK9EW1FQ==", + "2hEzujfG3mR5uQJXbvOPTQ==", + "2j83jrPwPfYlpJJ2clEBYQ==", + "2ksediOVrh4asSBxKcudTg==", + "2melaInV0wnhBpiI3da6/A==", + "2nSTEYzLK77h5Rgyti+ULQ==", + "2os5s7j7Tl46ZmoZJH8FjA==", + "2rOkEVl90EPqfHOF5q2FYw==", + "2rhjiY0O0Lo36wTHjmlNyw==", + "2vm7g3rk1ACJOTCXkLB3zA==", + "2wesXiib76wM9sqRZ7JYwQ==", + "2ywo4t5PPSVUCWDwUlOVwQ==", + "3++dZXzZ6AFEz7hK+i5hww==", + "3+9nURtBK3FKn0J9DQDa3g==", + "3+zsjCi7TnJhti//YXK35w==", + "3/1puZTGSrD9qNKPGaUZww==", + "300hoYyMR/mk1mfWJxS8/w==", + "301utVPZ93AnPLYbsiJggw==", + "312g8iTB9oJgk/OqcgR7Cw==", + "342VOUOxoLHUqtHANt83Hw==", + "36XDmX6j542q+Oei1/x0gw==", + "37Nkh06O979nt7xzspOFyQ==", + "3AKEYQqpkfW7CZMFQZoxOw==", + "3AVYtcIv7A5mVbVnQMaCeA==", + "3BjLFon1Il0SsjxHE2A1LQ==", + "3CJbrUdW68E3Drhe4ahUnQ==", + "3EhLkC9NqD3A6ApV6idmgg==", + "3Ejtsqw3Iep/UQd0tXnSlg==", + "3FH4D31nKV13sC9RpRZFIg==", + "3Gg9N7vjAfQEYOtQKuF/Eg==", + "3HPOzIZxoaQAmWRy9OkoSg==", + "3JhnM6G4L06NHt31lR0zXA==", + "3L3KEBHhgDwH615w4OvgZA==", + "3Leu2Sc+YOntJFlrvhaXeg==", + "3P2aJxV8Trll2GH9ptElYA==", + "3RTtSaMp1TZegJo5gFtwwA==", + "3TbRZtFtsh9ez8hqZuTDeA==", + "3TjntNWtpG7VqBt3729L6Q==", + "3UBYBMejKInSbCHRoJJ7dg==", + "3UNJ37f+gnNyYk9yLFeoYA==", + "3WVBP9fyAiBPZAq3DpMwOQ==", + "3Wfj05vCLFAB9vII5AU9tw==", + "3WwITQML938W9+MUM56a3A==", + "3XyoREdvhmSbyvAbgw2y/A==", + "3Y4w0nETru3SiSVUMcWXqw==", + "3Y6/HqS1trYc9Dh778sefg==", + "3YXp1PmMldUjBz3hC6ItbA==", + "3djRJvkZk9O2bZeUTe+7xQ==", + "3go7bJ9WqH/PPUTjNP3q/Q==", + "3hVslsq98QCDIiO40JNOuA==", + "3iC21ByW/YVL+pSyppanWw==", + "3itfXtlLPRmPCSYaSvc39Q==", + "3j0kFUZ6g+yeeEljx+WXGg==", + "3jmCreW5ytSuGfmeLv7NfQ==", + "3jqsY8/xTWELmu/az3Daug==", + "3kREs/qaMX0AwFXN0LO5ow==", + "3ltw31yJuAl4VT6MieEXXw==", + "3nthUmLZ30HxQrzr2d7xFA==", + "3oMTbWf7Bv83KRlfjNWQZA==", + "3pi3aNVq1QNJmu1j0iyL0g==", + "3rbml1D0gfXnwOs5jRZ3gA==", + "3sNJJIx1NnjYcgJhjOLJOg==", + "3v09RHCPTLUztqapThYaHg==", + "3xw8+0/WU51Yz4TWIMK8mw==", + "3y5Xk65ShGvWFbQxcZaQAQ==", + "3yDD+xT8iRfUVdxcc7RxKw==", + "3yavzOJ1mM44pOSFLLszgA==", + "4+htiqjEz9oq0YcI/ErBVg==", + "40HzgVKYnqIb6NJhpSIF0A==", + "40gCrW4YWi+2lkqMSPKBPg==", + "41WEjhYUlG6jp2UPGj11eQ==", + "444F9T6Y7J67Y9sULG81qg==", + "46FCwqh+eMkf+czjhjworw==", + "46piyANQVvvLqcoMq5G8tQ==", + "49jZr/mEW6fvnyzskyN40w==", + "49z/15Nx9Og7dN9ebVqIzg==", + "4A+RHIw+aDzw0rSRYfbc7g==", + "4BkqgraeXY7yaI1FE07Evw==", + "4CfEP8TeMKX33ktwgifGgA==", + "4DIPP/yWRgRuFqVeqIyxMQ==", + "4FBBtWPvqJ3dv4w25tRHiQ==", + "4ID0PHTzIMZz2rQqDGBVfA==", + "4KJZPCE9NKTfzFxl76GWjg==", + "4LtQrahKXVtsbXrEzYU1zQ==", + "4LvQSicqsgxQFWauqlcEjw==", + "4NHQwbb3zWq2klqbT/pG6g==", + "4NP8EFFJyPcuQKnBSxzKgQ==", + "4PBaoeEwUj79njftnYYqLg==", + "4Qinl7cWmVeLJgah8bcNkw==", + "4SdHWowXgCpCDL28jEFpAw==", + "4TQkMnRsXBobbtnBmfPKnA==", + "4VR5LiXLew6Nyn91zH9L4w==", + "4WO6eT0Rh6sokb29zSJQnQ==", + "4WRdAjiUmOQg2MahsunjAg==", + "4WcFEswYU/HHQPw77DYnyA==", + "4XNUmgwxsqDYsNmPkgNQYQ==", + "4Xh/B3C16rrjbES+FM1W8g==", + "4ZFYKa7ZgvHyZLS6WpM8gA==", + "4aPU6053cfMLHgLwAZJRNg==", + "4ekt4m38G9m599xJCmhlug==", + "4erEA42TqGA9K4iFKkxMMA==", + "4ifNsmjYf1iOn2YpMfzihg==", + "4iiCq+HhC+hPMldNQMt0NA==", + "4itEKfbRCJvqlgKnyEdIOQ==", + "4jeOFKuKpCmMXUVJSh9y0g==", + "4kXlJNuT79XXf1HuuFOlHw==", + "4kj0S8XlmhHXoUP7dQItUw==", + "4mQVNv7FHj+/O6XFqWFt/Q==", + "4mig4AMLUw+T/ect9p4CfA==", + "4qMSNAxichi3ori/pR+o0w==", + "4rrSL6N0wyucuxeRELfAmw==", + "4u3eyKc+y3uRnkASrgBVUw==", + "4wnUAbPT3AHRJrPwTTEjyw==", + "4xojeUxTFmMLGm6jiMYh/Q==", + "4yEkKp2FYZ09mAhw2IcrrA==", + "4yVqq66iHYQjiTSxGgX2oA==", + "4yrFNgqWq17zVCyffULocA==", + "50jASqzGm4VyHJbFv8qVRA==", + "50xwiYvGQytEDyVgeeOnMg==", + "51yLpfEdvqXmtB6+q27/AQ==", + "520wTzrysiRi2Td92Zq0HQ==", + "53UccFNzMi9mKmdeD82vAw==", + "54XELlPm8gBvx8D5bN3aUg==", + "59ipbMH7cKBsF9bNf4PLeQ==", + "5CMadLqS2KWwwMCpzlDmLw==", + "5DDb7fFJQEb3XTc3YyOTjg==", + "5HovoyHtul8lXh+z8ywq9A==", + "5I/heFSQG/UpWGx0uhAqGQ==", + "5KOgetfZR+O2wHQSKt41BQ==", + "5LJqHFRyIwQKA4HbtqAYQQ==", + "5LuFDNKzMd2BzpWEIYO2Ww==", + "5M3dFrAOemzQ0MAbA8bI5w==", + "5N2oi2pB69NxeNt08yPLhw==", + "5NEP7Xt7ynj6xCzWzt21hQ==", + "5Nk2Z94DhlIdfG5HNgvBbQ==", + "5PfGtbH9fmVuNnq83xIIgQ==", + "5Q/Y2V0iSVTK8HE8JerEig==", + "5S5/asYfWjOwnzYpbK6JDw==", + "5SbwLDNT6sBOy6nONtUcTg==", + "5T39s5CtSrK5awMPUcEWJg==", + "5VO1inwXMvLDBQSOahT6rg==", + "5VY++KiWgo7jXSdFJsPN3A==", + "5Wcq+6hgnWsQZ/bojERpUw==", + "5Yrj6uevT8wHRyqqgnSfeg==", + "5dUry23poD+0wxZ3hH6WmA==", + "5eHStFN7wEmIE+uuRwIlPQ==", + "5eXpiczlRdmqMYSaodOUiQ==", + "5gGoDPTc/sOIDLngmlEq4A==", + "5jHgQF4SfO/zy9xy9t+9dw==", + "5jyuDp82Fux+B0+zlx8EXw==", + "5kvyy902llnYGQdn2Py04w==", + "5l6kDfjtZjkTZPJvNNOVFw==", + "5lfLJAk1L3QzGMML3fOuSw==", + "5m1ijXEW+4RTNGZsDA/rxQ==", + "5oD/aGqoakxaezq43x0Tvw==", + "5pje7qyz8BRsa8U4a4rmoA==", + "5pqqzC/YmRIMA9tMFPi7rg==", + "5r1ZsGkrzNQEpgt/gENibw==", + "5u2PdDcIY3RQgtchSGDCGg==", + "5ugVOraop5P5z5XLlYPJyQ==", + "5w/c9WkI/FA+4lOtdPxoww==", + "5w4FbRhWACP7k2WnNitiHg==", + "6+jhreeBLfw64tJ+Nhyipw==", + "600bwlyhcy754W1E6tuyYg==", + "600mjiWke4u0CDaSQKLOOg==", + "60suecbWRfexSh7C67RENA==", + "61V74uIjaSfZM8au1dxr1A==", + "62RHCbpGU8Hb+Ubn+SCTBg==", + "63OTPaKM0xCfJOy9EDto+Q==", + "64AA4jLHXc1Dp15aMaGVcA==", + "64QzHOYX0A9++FqRzZRHlQ==", + "64YsV2qeDxk2Q6WK/h7OqA==", + "65KhGKUBFQubRRIEdh9SwQ==", + "6706ncrH1OANFnaK6DUMqQ==", + "68jPYo3znYoU4uWI7FH3/g==", + "68nqDtXOuxF7DSw6muEZvg==", + "6ACvJNfryPSjGOK39ov8Qg==", + "6CjtF1S2Y6RCbhl7hMsD+g==", + "6G2bD3Y7qbGmfPqH9TqLFA==", + "6GXHGF62/+jZ7PfIBlMxZw==", + "6HGeEPyTAu9oiKhNVLjQnA==", + "6HnWgYNKohqhoa1tnjjU3A==", + "6M6QapJ5xtMXfiD3bMaiLA==", + "6NP81geiL14BeQW6TpLnUA==", + "6PzjncEw2wHZg7SP7SQk9w==", + "6QAtjOK9enNLRhcVa2iaTg==", + "6QUGE2S8oFYx4T4nW56cCw==", + "6W79FmpUN1ByNtv5IEXY4w==", + "6WhHPWlqEUqXC52rHGRHjA==", + "6XYqR2WvDzx4fWO7BIOTjA==", + "6Z9myGCF5ylWljgIYAmhqw==", + "6ZKmm7IW7IdWuVytLr68CQ==", + "6ZMs9vCzK9lsbS6eyzZlIA==", + "6b7ue29cBDsvmj1VSa5njw==", + "6c0iuya20Ys8BsvoI4iQaQ==", + "6cTETZ9iebhWl+4W5CB+YQ==", + "6dshA8knH5qqD+KmR/kdSQ==", + "6e8boFcyc8iF0/tHVje4eQ==", + "6erpZS36qZRXeZ9RN9L+kw==", + "6fWom3YoKvW6NIg6y9o9CQ==", + "6k2cuk0McTThSMW/QRHfjA==", + "6lVSzYUQ/r0ep4W2eCzFpg==", + "6leyDVmC5jglAa98NQ3+Hg==", + "6nwR+e9Qw0qp8qIwH9S/Mg==", + "6o5g9JfKLKQ2vBPqKs6kjg==", + "6rIWazDEWU5WPZHLkqznuQ==", + "6rqK8sjLPJUIp7ohkEwfZg==", + "6sBemZt4qY/TBwqk3YcLOQ==", + "6sNP0rzCCm3w976I2q2s/w==", + "6tfM6dx3R5TiVKaqYQjnCg==", + "6txm8z4/LGCH0cpaet/Hsg==", + "6uMF5i0b/xsk55DlPumT7A==", + "6uT7LZiWjLnnqnnSEW4e/Q==", + "6v3eTZtPYBfKFSjfOo2UaA==", + "6wkfN8hyKmKU6tG3YetCmw==", + "6z8CRivao3IMyV4p4gMh7g==", + "71w3aSvuh2mBLtdqJCN3wA==", + "734u4Y1R3u7UNUnD+wWUoA==", + "74FW/QYTzr/P1k6QwVHMcw==", + "778O1hdVKHLG2q9dycUS0Q==", + "78b8sDBp28zUlYPV5UTnYw==", + "79uTykH43voFC3XhHHUzKg==", + "7E6V6/zSjbtqraG7Umj+Jw==", + "7Ephy+mklG2Y3MFdqmXqlA==", + "7Eqzyb+Kep+dIahYJWNNxQ==", + "7GgNLBppgAKcgJCDSsRqOQ==", + "7J3FoFGuTIW36q0PZkgBiw==", + "7K8l6KoP0BH82/WMLntfrg==", + "7R5rFaXCxM3moIUtoCfM2g==", + "7Tauesu7bgs5lJmQROVFiQ==", + "7VHlLw20dWck+I8tCEZilA==", + "7W9aF7dxnL+E8lbS/F7brg==", + "7XRiYvytcwscemlxd9iXIQ==", + "7Y87wVJok20UfuwkGbXxLg==", + "7b0oo4+qphu6HRvJq6qkHQ==", + "7bM/pn4G7g7Zl6Xf1r62Lg==", + "7br49X11xc2GxQLSpZWjKQ==", + "7btpMFgeGkUsiTtsmNxGQA==", + "7cnUHeaPO8txZGGWHL9tKg==", + "7dz+W494zwU5sg63v5flCg==", + "7k5rBuh8FbTTI4TP87wBPQ==", + "7l0RMKbONGS/goW/M+gnMQ==", + "7mxU5fJl/c6dXss9H3vGcQ==", + "7nr3zyWL+HHtJhRrCPhYZA==", + "7p4NpnoNSQR7ISg+w+4yFg==", + "7pkUY2UzSbGnwLvyRrbxfA==", + "7sCJ4RxbxRqVnF4MBoKfuQ==", + "7w3b73nN/fIBvuLuGZDCYQ==", + "7w4PDRJxptG8HMe/ijL6cQ==", + "7wgT9WIiMVcrj48PVAMIgw==", + "7xDIG/80SnhgxAYPL9YJtg==", + "7xTKFcog69nTmMfr5qFUTA==", + "80C9TB9/XT1gGFfQDJxRoA==", + "80PCwYh4llIKAplcDvMj4g==", + "80UE+Ivby3nwplO/HA7cPw==", + "81ZH3SO0NrOO+xoR/Ngw1g==", + "81iQLU+YwxNwq4of6e9z7A==", + "81nkjWtpBhqhvOp6K8dcWg==", + "81pAhreEPxcKse+++h1qBg==", + "82hTTe1Nr4N2g7zwgGjxkw==", + "83ERX2XJV3ST4XwvN7YWCg==", + "83WGpQGWyt6mCV+emaomog==", + "83wtvSoSP9FVBsdWaiWfpA==", + "861mBNvjIkVgkBiocCUj/Q==", + "88PNi9+yn3Bp4/upgxtWGA==", + "88tB/HgUIUnqWXEX++b5Aw==", + "897ptlztTjr7yk+pk8MT0Q==", + "8AfCSZC0uasVON9Y/0P2Pw==", + "8B12CamjOGzJDnQ+RkUf4w==", + "8BLkvEkfnOizJq0OTCYGzw==", + "8CjmgWQSAAGcXX9kz3kssw==", + "8Cm19vJW8ivhFPy0oQXVNA==", + "8DtgIyYiNFqDc5qVrpFUng==", + "8GyPup4QAiolFJ9v80/Nkw==", + "8JVHFRwAd/SCLU0CRJYofg==", + "8LNNoHe6rEQyJ0ebl151Mw==", + "8M0kSvjn5KN8bjsMdUqKZQ==", + "8N3mhHt29FZDHn1P2WH1wQ==", + "8OFxXwnPmrogpNoueZlC4Q==", + "8QK7emHS6rAcAF5QQemW/A==", + "8RtLlzkGEiisy1v9Xo0sbw==", + "8VqeoQELbCs232+Mu+HblA==", + "8WU1vLKV1GhrL7oS9PpABg==", + "8ZBiwr842ZMKphlqmNngHw==", + "8ZFPMJJYVJHsfRpU4DigSg==", + "8ZqmPJDnQSOFXvNMRQYG2Q==", + "8c+lvG5sZNimvx9NKNH3ug==", + "8cXqZub6rjgJXmh1CYJBOg==", + "8dBIsHMEAk7aoArLZKDZtg==", + "8dUcSkd2qnX5lD9B+fUe+Q==", + "8dbyfox/isKLsnVjQNsEXg==", + "8fJLQeIHaTnJ8wGqUiKU6g==", + "8g08gjG/QtvAYer32xgNAg==", + "8hsfXqi4uiuL+bV1VrHqCw==", + "8iYdEleTXGM+Wc85/7vU9w==", + "8j9GVPiFdfIRm/+ho7hpoA==", + "8nOTDhFyZ8YUA4b6M5p84w==", + "8snljTGo/uICl9q0Hxy7/A==", + "8uP4HUnSodw88yoiWXOIcw==", + "8vLA9MOdmLTo3Qg+/2GzLA==", + "8vr+ERVrM99dp+IGnCWDGQ==", + "8ylI1AS3QJpAi3I/NLMYdg==", + "9+hjTVMQUsvVKs7Tmp52tg==", + "90dtIMq0ozJXezT2r79vMQ==", + "91+Yms6Oy/rP0rVjha5z9w==", + "91LQuW6bMSxl10J/UDX23A==", + "91SdBFJEZ65M+ixGaprY/A==", + "91VcAVv7YDzkC1XtluPigw==", + "91vfsZ7Lx9x5gqWTOdM4sg==", + "96ORaz1JRHY1Gk8H74+C2g==", + "99+SBN45LwKCPfrjUKRPmw==", + "9Bet5waJF5/ZvsYaHUVEjQ==", + "9DRHdyX8ECKHUoEsGuqR4Q==", + "9DtM1vls4rFTdrSnQ7uWXw==", + "9FdpxlIFu11qIPdO7WC5nw==", + "9Gkw+hvsR/tFY1cO89topg==", + "9J53kk+InE3CKa7cPyCXMw==", + "9JKIJrlQjhNSC46H3Cstcw==", + "9L6yLO93sRN70+3qq3ObfA==", + "9MDG0WeBPpjGJLEmUJgBWg==", + "9QFYrCXsGsInUb4SClS3cQ==", + "9RGIQ2qyevNbSSEF36xk/A==", + "9RXymE9kCkDvBzWGyMgIWA==", + "9SUOfKtfKmkGICJnvbIDMg==", + "9SgfpAY0UhNC6sYGus9GgQ==", + "9T7gB0ZkdWB0VpbKIXiujQ==", + "9TalxEyFgy6hFCM73hgb7Q==", + "9UhKmKtr4vMzXTEn74BEhg==", + "9W57pTzc572EvSURqwrRhw==", + "9Y1ZmfiHJd9vCiZ6KfO1xQ==", + "9aKH1u5+4lgYhhLztQ4KWA==", + "9ajIS45NTicqRANzRhDWFA==", + "9bAWYElyRN1oJ6eJwPtCtQ==", + "9cvHJmim9e0pOaoUEtiM6A==", + "9dbn0Kzwr9adCEfBJh78uQ==", + "9iB7+VwXRbi6HLkWyh9/kg==", + "9inw7xzbqAnZDKOl/MfCqA==", + "9jxA/t3TQx8dQ+FBsn/YCg==", + "9k17UqdR1HzlF7OBAjpREA==", + "9k1u/5TgPmXrsx3/NsYUhg==", + "9lLhHcrPWI4EsA4fHIIXuw==", + "9nMltdrrBmM5ESBY2FRjGA==", + "9oQ/SVNJ4Ye9lq8AaguGAQ==", + "9oUawSwUGOmb0sDn3XS6og==", + "9onh6QKp70glZk9cX3s34A==", + "9pdeedz1UZUlv8jPfPeZ1g==", + "9pk75mBzhmcdT+koHvgDlw==", + "9qWLbRLXWIBJUXYjYhY2pg==", + "9rL8nC/VbSqrvnUtH9WsxQ==", + "9reBKZ1Rp6xcdH1pFQacjw==", + "9s3ar9q32Y5A3tla5GW/2Q==", + "9sYLg75/hudZaBA3FrzKHw==", + "9tiibT8V9VwnPOErWGNT3w==", + "9vEgJVJLEfed6wJ7hBUGgQ==", + "9viAzLFGYYudBYFu7kFamg==", + "9vmJUS7WIVOlhMqwipAknQ==", + "9wUIeSgNN36SFxy8v2unVg==", + "9xIgKpZGqq0/OU6wM5ZSHw==", + "9xmtuClkFlpz/X5E9JBWBA==", + "A+DLpIlYyCb9DaarpLN76g==", + "A2ODff+ImIkreJtDPUVrlg==", + "A3dX2ShyL9+WOi6MNJBoYQ==", + "A6TLWhipfymkjPYq8kaoDQ==", + "AChOz8avRYsvxlbWcorQ3w==", + "AEpTVUQhIEJGlXJB6rS26A==", + "AFdelaqvxRj6T3YdLgCFyg==", + "AGd0rcLnQ0n+meYyJur1Pw==", + "AGoVLd0QPcXnTedT5T95JQ==", + "ALJWKUImVE40MbEooqsrng==", + "ALlGgVDO8So71ccX0D6u2g==", + "AMfL0rH+g8c0VqOUSgNzQw==", + "ARCWkHAnVgBOIkCDQ19ZuA==", + "ARKIvf4+zRF8eCvUITWPng==", + "ATmMzriwGLl+M3ppkfcZNA==", + "AUGmvZkpkKBry5bHZn4DJA==", + "AV/YJfdoDUdRcrXVwinhQg==", + "AVjwqrTBQH1VREuBlOyUOg==", + "AX1HxQKXD12Yv5HWi39aPQ==", + "AYxGETZs477n2sa1Ulu/RQ==", + "AZs3v4KJYxdi8T1gjVjI2Q==", + "AcKwfS8FRVqb72uSkDNY/Q==", + "AcbG0e6xN8pZfYAv7QJe1Q==", + "Af9j1naGtnZf0u1LyYmK1w==", + "AfVPdxD3FyfwwNrQnVNQ7A==", + "AgDJsaW0LkpGE65Kxk5+IA==", + "Ahpi9+nl13kPTdzL+jgqMw==", + "AiMtfedwGcddA+XYNc+21g==", + "AjHz9GkRTFPjrqBokCDzFw==", + "Ak3rlzEOds6ykivfg39xmw==", + "AkAes5oErTaJiGD2I4A1Pw==", + "AklOdt9/2//3ylUhWebHRw==", + "Al8+d/dlOA5BXsUc5GL8Tg==", + "Ao1Zc0h5AdSHtYt1caWZnQ==", + "AoN/pnK4KEUaGw4V9SFjpg==", + "ApiuEPWr8UjuRyJjsYZQBw==", + "AqHVaj3JcR44hnMzUPvVYg==", + "Ar1Eb/f/LtuIjXnnVPYQlA==", + "Ar9N1VYgE7riwmcrM3bA2Q==", + "AsAHrIkMgc3RRWnklY9lJw==", + "AvdeYb9XNOUFWiiz+XGfng==", + "AwPTZpC28NJQhf5fNiJuLA==", + "AxEjImKz4tMFieSo7m60Sg==", + "AyWlT+EGzIXc395zTlEU5Q==", + "B+TsxQZf0IiQrU8X9S4dsQ==", + "B0TaUQ6dKhPfSc5V/MjLEQ==", + "B1VVUbl8pU0Phyl1RYrmBg==", + "B6reUwMkQFaCHb9BYZExpw==", + "BA18GEAOOyVXO2yZt2U35w==", + "BAJ+/jbk2HyobezZyB9LiQ==", + "BB/R8oQOcoE4j63Hrh8ifg==", + "BB9PTlwKAWkExt3kKC/Wog==", + "BDNM1u/9mefjuW1YM2DuBg==", + "BDbfe/xa9Mz1lVD82ZYRGA==", + "BH+rkZWQjTp7au6vtll/CQ==", + "BL3buzSCV78rCXNEhUhuKQ==", + "BLJk9wA88z6e0IQNrWJIVw==", + "BLbTFLSb4mkxMaq4/B2khg==", + "BMOi5JmFUg5sCkbTTffXHw==", + "BMZB1FwvAuEqyrd0rZrEzw==", + "BPT4PQxeQcsZsUQl33VGmg==", + "BTiGLT6XdZIpFBc91IJY6g==", + "BV1moliPL15M14xkL+H1zw==", + "BW0A06zoQw7S+YMGaegT7g==", + "BXGlq54wIH6R3OdYfSSDRw==", + "BYpHADmEnzBsegdYTv8B5Q==", + "BYz52gYI/Z6AbYbjWefcEA==", + "BZTzHJGhzhs3mCXHDqMjnQ==", + "BaRwTrc5ulyKbW4+QqD0dw==", + "BhKO1s1O693Fjy1LItR/Jw==", + "BjfOelfc1IBgmUxMJFjlbQ==", + "BlCgDd7EYDIqnoAiKOXX6Q==", + "BophnnMszW5o+ywgb+3Qbw==", + "Bq82MoMcDjIo/exqd/6UoA==", + "BuDVDLl0OGdomEcr+73XhQ==", + "BuENxPg7JNrWXcCxBltOPg==", + "Bv4mNIC72KppYw/nHQxfpQ==", + "Bvk8NX4l6WktLcRDRKsK/A==", + "BwRA+tMtwEvth28IwpZx+w==", + "BxFP+4o6PSlGN78eSVT1pA==", + "BxsDnI8jXr4lBwDbyHaYXw==", + "Byhi4ymFqqH8uIeoMRvPug==", + "BzkNYH03gF/mQY71RwO3VA==", + "C+Ssp+v1r+00+qiTy2d7kA==", + "C4QEzQKGxyRi2rjwioHttA==", + "C65PZm8rZxJ6tTEb6d08Eg==", + "C7UaoIEXsVRxjeA0u99Qmw==", + "CBAGa5l95f3hVzNi6MPWeQ==", + "CCK+6Dr72G3WlNCzV7nmqw==", + "CDsanJz7e3r/eQe+ZYFeVQ==", + "CF1sAlhjDQY/KWOBnSSveA==", + "CHLHizLruvCrVi9chj9sXA==", + "CHsFJfsvZkPWDXkA6ZMsDQ==", + "CJoZn5wdTXbhrWO5LkiW0g==", + "CLPzjXKGGpJ0VrkSJp7wPQ==", + "CPDs+We/1wvsGdaiqxzeCQ==", + "CQ0PPwgdG3N6Ohfwx1C8xA==", + "CQpJFrpOvcQhsTXIlJli+Q==", + "CRiL6zpjfznhGXhCIbz8pQ==", + "CRmAj3JcasAb4iZ9ZbNIbw==", + "CT3ldhWpS1SEEmPtjejR/Q==", + "CT9g8mKsIN/VeHLSTFJcNQ==", + "CUCjG2UaEBmiYWQc6+AS1Q==", + "CUEueo8QXRxkfVdfNIk/gg==", + "CWBGcRFYwZ0va6115vV/oQ==", + "CX/N/lHckmAtHKysYtGdZA==", + "CXMKIdGvm60bgfsNc+Imvg==", + "CYJB3qy5GalPLAv1KGFEZA==", + "CZNoTy26VUQirvYxSPc/5A==", + "CZbd+UoTz0Qu1kkCS3k8Xg==", + "CazLJMJjQMeHhYLwXW7YNg==", + "Ci7sS7Yi1+IwAM3VMAB4ew==", + "CiiUeJ0LeWfm7+gmEmYXtg==", + "CkDIoAFLlIRXra78bxT/ZA==", + "CkZUmKBAGu0FLpgPDrybpw==", + "Cl1u5nGyXaoGyDmNdt38Bw==", + "CmBf5qchS1V3C2mS6Rl4bw==", + "CmVD6nh8b/04/6JV9SovlA==", + "CmkmWcMK4eqPBcRbdnQvhw==", + "CnIwpRVC2URVfoiymnsdYQ==", + "CoLvjQDQGldGDqRxfQo+WQ==", + "CrJDgdfzOea2M2hVedTrIg==", + "CsPkyTZADMnKcgSuNu1qxg==", + "CtDj/h2Q/lRey20G8dzSgA==", + "CuGIxWhRLN7AalafBZLCKQ==", + "Cv079ZF55RnbsDT27MOQIA==", + "Cz1G77hsDtAjpe0WzEgQog==", + "CzP13PM/mNpJcJg8JD3s6w==", + "CzSumIcYrZlxOUwUnLR2Zw==", + "CzWhuxwYbNB/Ffj/uSCtbw==", + "D09afzGpwCEH0EgZUSmIZA==", + "D0Qt9sRlMaPnOv1xaq+XUg==", + "D0W5F7gKMljoG5rlue1jrg==", + "D175i+2bZ7aWa4quSSkQpA==", + "D2JcY4zWwqaCKebLM8lPiQ==", + "D31ZticrjGWAO45l5hFh7A==", + "D5ibbo8UJMfFZ48RffuhgQ==", + "D5jaV+HtXkSpSxJPmaBDXg==", + "D66Suu3tWBD+eurBpPXfjA==", + "D7piVoB2NJlBxK5owyo4+g==", + "D7wN7b5u5PKkMaLJBP9Ksw==", + "DA+3fjr7mgpwf6BZcExj0w==", + "DB706G73NpBSRS8TKQOVZw==", + "DBKrdpCE0awppxST4o/zzg==", + "DCjgaGV5hgSVtFY5tcwkuA==", + "DCvI9byhw0wOFwF1uP6xIQ==", + "DDitrRSvovaiXe2nfAtp4g==", + "DEaZD/8aWV6+zkiLSVN/gA==", + "DG2Qe2DqPs5MkZPOqX363Q==", + "DJ+a37tCaGF5OgUhG+T0NA==", + "DJmrmNRKARzsTCKSMLmcNA==", + "DJoy1NSZZw87oxWGlNHhfg==", + "DJscTYNFPyPmTb57g/1w+Q==", + "DKApp/alXiaPSRNm3MfSuA==", + "DLzHkTjjuH6LpWHo2ITD0Q==", + "DMHmyn2U2n+UXxkqdvKpnA==", + "DO1/jfP/xBI9N0RJNqB2Rw==", + "DQJRsUwO1fOuGlkgJavcwQ==", + "DQQB/l55iPN9XcySieNX3A==", + "DQeib845UqBMEl96sqsaSg==", + "DQlZWBgdTCoYB1tJrNS5YQ==", + "DRiFNojs7wM8sfkWcmLnhQ==", + "DWKsPfKDAtfuwgmc2dKUNg==", + "DY0IolKTYlW+jbKLPAlYjQ==", + "DYWCPUq/hpjr6puBE7KBHg==", + "DbWQI3H2tcJsVJThszfHGA==", + "DdaT4JLC7U0EkF50LzIj9w==", + "DdiNGiOSoIZxrMrGNvqkXw==", + "DinJuuBX9OKsK5fUtcaTcQ==", + "DjHszpS8Dgocv3oQkW/VZQ==", + "DjeSrUoWW2QAZOAybeLGJg==", + "Dk0L/lQizPEb3Qud6VHb1Q==", + "DmxgZsQg+Qy1GP0fPkW3VA==", + "Dmyb+a7/QFsU4d2cVQsxDw==", + "DnF6TYSJxlc+cwdfevLYng==", + "Do3aqbRKtmlQI2fXtSZfxQ==", + "DoiItHSms0B9gYmunVbRkQ==", + "DqzWt1gfyu/e7RQl5zWnuQ==", + "Dt6hvhPJu94CJpiyJ5uUkg==", + "Dt8Q5ORzTmpPR2Wdk0k+Aw==", + "DuEKxykezAvyaFO2/5ZmKQ==", + "Dulw855DfgIwiK7hr3X8vg==", + "Duz/8Ebbd0w6oHwOs0Wnwg==", + "DwOTyyCoUfaSShHZx9u6xg==", + "DwP0MQf71VsqvAbAMtC3QQ==", + "DwrNdmU5VFFf3TwCCcptPA==", + "Dz90OhYEjpaJ/pxwg1Qxhg==", + "E+02smwQGBIxv42LIF2Y4Q==", + "E1CvxFbuu9AYW604mnpGTw==", + "E2LR1aZ3DcdCBuVT7BhReA==", + "E2v8Kk60qVpQ232YzjS2ow==", + "E3jMjAgXwvwR8PA53g4+PQ==", + "E4NtzxQruLcetC23zKVIng==", + "E4ojRDwGsIiyuxBuXHsKBA==", + "E8yMPK7W0SIGTK6gIqhxiQ==", + "E9IlDyULLdeaVUzN6eky8g==", + "E9ajQQMe02gyUiW3YLjO/A==", + "E9yeifEZtpqlD0N3pomnGw==", + "EATnlYm0p3h04cLAL95JgA==", + "EC0+iUdSZvmIEzipXgj7Gg==", + "EGLOaMe6Nvzs/cmb7pNpbg==", + "EJgedRYsZPc4cT9rlwaZhg==", + "EKU3OVlT4b/8j3MTBqpMNg==", + "ENFfP93LA257G6pXQkmIdg==", + "EUXQZwLgnDG+C8qxVoBNdw==", + "EXveRXjzsjh8zbbQY2pM9g==", + "EZVQGsXTZvht1qedRLF8bQ==", + "EbGG4X18upaiVQmPfwKytg==", + "EdvIAKdRAXj7e42mMlFOGQ==", + "Ee4A3lTMLQ7iDQ7b8QP8Qg==", + "EfXDc6h69aBPE6qsB+6+Ig==", + "Egs14xVbRWjfBBX7X5Z60g==", + "Ej7W3+67kCIng3yulXGpRQ==", + "ElTNyMR4Rg8ApKrPw88WPg==", + "Epm0d/DvXkOFeM4hoPCBrg==", + "EqMlrz1to7HG4GIFTPaehQ==", + "EqYq2aVOrdX5r7hBqUJP7g==", + "Err1mbWJud80JNsDEmXcYg==", + "EuGWtIbyKToOe6DN3NkVpQ==", + "Ev/xjTi7akYBI7IeZJ4Igw==", + "EvSB+rCggob2RBeXyDQRvQ==", + "Ex3x5HeDPhgO2S9jjCFy4g==", + "EyIsYQxgFa4huyo/Lomv7g==", + "EzjbinBHx3Wr08eXpH3HXA==", + "F50iXjRo1aSTr37GQQXuJA==", + "F58ktE4O0f7C9HdsXYm+lw==", + "F5FcNti7lUa9DyF2iEpBug==", + "F5bs0GGWBx9eBwcJJpXbqg==", + "F8l+Qd9TZgzV+r8G584lKA==", + "F8tEIT5EhcvLNRU5f0zlXQ==", + "FA+nK6mpFWdD0kLFcEdhxA==", + "FAXzjjIr8l1nsQFPpgxM/g==", + "FCLQocqxxhJeleARZ6kSPg==", + "FH5Z60RXXUiDk+dSZBxD3g==", + "FHvI0IVNvih8tC7JgzvCOw==", + "FI2WhaSMb3guFLe3e9il8Q==", + "FIOCTEbzb2+KMCnEdJ7jZw==", + "FL/j3GJBuXdAo54JYiWklQ==", + "FLvED9nB9FEl9LqPn7OOrA==", + "FN7oLGBQGHXXn5dLnr/ElA==", + "FNvQqYoe0s/SogpAB7Hr1Q==", + "FUQySDFodnRhr+NUsWt0KA==", + "FV/D5uSco+Iz8L+5t7E8SA==", + "FWphIPZMumqnXr1glnbK4w==", + "FXzaxi3nAXBc8WZfFElQeA==", + "FbxScyuRacAQkdQ034ShTA==", + "FcFcn4qmPse5mJCX5yNlsA==", + "FcKjlHKfQAGoovtpf+DxWQ==", + "Fd0c8f2eykUp9GYhqOcKoA==", + "Fd2fYFs8vtjws2kx1gf6Rw==", + "FeRovookFQIsXmHXUJhGOw==", + "FhthAO5IkMyW4dFwpFS7RA==", + "Fiy3hkcGZQjNKSQP9vRqyA==", + "FltEN+7NKvzt+XAktHpfHA==", + "FnVNxl5AFH1AieYru2ZG+A==", + "FoJZ61VrU8i084pAuoWhDQ==", + "FpWDTLTDmkUhH/Sgo+g1Gg==", + "FpgdsQ2OG+bVEy3AeuLXFQ==", + "FqWLkhWl0iiD/u2cp+XK9A==", + "FrTgaF5YZCNkyfR1kVzTLQ==", + "Ft2wXUokFdUf6d2Y/lwriw==", + "FtxpWdhEmC6MT61qQv4DGA==", + "FuWspiqu5g8Eeli5Az+BkA==", + "FxnbKnuDct4OWcnFMT/a5w==", + "Fz8EI+ZpYlbcttSHs5PfpA==", + "FzqIpOcTsckSNHExrl+9jg==", + "Fzuq+Wg7clo6DTujNrxsSA==", + "G+sGF13VXPH4Ih6XgFEXxg==", + "G/PA+kt0N+jXDVKjR/054A==", + "G0LChrb0OE5YFqsfTpIL1Q==", + "G0MlFNCbRjXk4ekcPO/chQ==", + "G2UponGde3/Z+9b2m9abpQ==", + "G37U8XTFyshfCs7qzFxATg==", + "G3PmmPGHaWHpPW30xQgm3Q==", + "G4qzBI1sFP2faN+tlRL/Bw==", + "G736AX070whraDxChqUrqw==", + "G7J/za99BFbAZH+Q+/B8WA==", + "G8LFBop8u6IIng+gQuVg3w==", + "GA8k6GQ20DGduVoC+gieRA==", + "GCYI9Dn1h3gOuueKc7pdKA==", + "GDMqfhPQN0PxfJPnK1Bb9A==", + "GF0lY77rx1NQzAsZpFtXIQ==", + "GF2yvI9UWf1WY7V7HXmKPA==", + "GFRJoPcXlkKSvJRuBOAYHQ==", + "GG8a3BlwGrYIwZH9j3cnPA==", + "GHEdXgGWOeOa6RuPMF0xXg==", + "GIHKW6plyLra0BmMOurFgA==", + "GKzs8mlnQQc58CyOBTlfIg==", + "GLDNTSwygNBmuFwCIm7HtA==", + "GLmWLXURlUOJ+PMjpWEXVA==", + "GLnS9wDCje7TOMvBX9jJVA==", + "GNak/LFeoHWlTdLW1iU4eg==", + "GNrMvNXQkW7PydlyJa+f1w==", + "GQJxu1SoMBH14KPV/G/KrQ==", + "GSWncBq4nwomZCBoxCULww==", + "GT6WUDXiheKAM7tPg3he9A==", + "GTNttXfMniNhrbhn92Aykg==", + "GUiinC3vgBjbQC2ybMrMNQ==", + "GW1Uaq622QamiiF24QUA0g==", + "GWwJ32SZqD5wldrXUdNTLA==", + "GdTanUprpE3X/YjJDPpkhQ==", + "Gdf4VEDLBrKJNQ8qzDsIyw==", + "GglPoW5fvr4JSM3Zv99oiA==", + "GhpJfRSWZigLg/azTssyVA==", + "Ghuj9hAyfehmYgebBktfgA==", + "GmC+0rNDMIR+YbUudoNUXw==", + "GnJKlRzmgKN9vWyGfMq3aA==", + "GncGQgmWpI/fZyb/6zaFCg==", + "GrSbnecYAC3j5gtoKntL0A==", + "Gt4/MMrLBErhbFjGbiNqQQ==", + "GzbeM7snhe+M+J7X+gAsQw==", + "H+NHjk/GJDh/GaNzMQSzjg==", + "H+yPRiooEh5J7lAJB4RZ7Q==", + "H0UMAUfHFQH92A2AXRCBKA==", + "H1NJEI+fvOQbI51kaNQQjQ==", + "H1y2iXVaQYwP0SakN6sa+Q==", + "H1zH9I8RwfEy5DGz3z+dHw==", + "H6HPFAcdHFbQUNrYnB74dA==", + "H6j2nPbBaxHecXruxiWYkA==", + "HBRzLacCVYfwUVGzrefZYg==", + "HCbHUfsTDl6+bxPjT57lrA==", + "HCu4ZMrcLMZbPXbTlWuvvQ==", + "HDxGhvdQwGh0aLRYEGFqnw==", + "HEcOaEd9zCoOVbEmroSvJg==", + "HEghmKg3GN60K7otpeNhaA==", + "HFCQEiZf7/SNc+oNSkkwlA==", + "HFHMGgfOeO0UPrray1G+Zw==", + "HGxe+5/kkh6R9GXzEOOFHA==", + "HHxn4iIQ7m0tF1rSd+BZBg==", + "HI4ZIE5s8ez8Rb+Mv39FxA==", + "HITIVoFoWNg04NExe13dNA==", + "HJYgUxFZ66fRT8Ka73RaUg==", + "HK0yf7F97bkf1VYCrEFoWA==", + "HK9xG03FjgCy8vSR+hx8+Q==", + "HLesnV3DL+FhWF3h6RXe8g==", + "HLxROy6fx/mLXFTDSX4eLA==", + "HMQarkPWOUDIg5+5ja2dBQ==", + "HMWOlMmzocOIiJ7yG1YaDQ==", + "HOi+vsGAae4vhr+lJ5ATnQ==", + "HPvYV94ufwiNHEImu4OYvQ==", + "HRF3WL/ue3/QlYyu7NUTrA==", + "HRWYX2XOdsOqYzCcqkwIyw==", + "HYylUirJRqLm+dkp39fSOQ==", + "HaHTsLzx7V3G1SFknXpGxA==", + "HaIRV9SNPRTPDOSX9sK/bg==", + "HaSc7MZphCMysTy2JbTJkw==", + "Hb+pdSavvJ9lUXkSVZW8Og==", + "HbT6W1Ssd3W7ApKzrmsbcg==", + "HbXv8InyZqFT7i3VrllBgg==", + "HdB7Se47cWjPgpJN0pZuiA==", + "HdXg64DBy5WcL5fRRiUVOg==", + "HeQbUuBM9sqfXFXRBDISSw==", + "HfvsiCQN/3mT0FabCU5ygQ==", + "HgIFX42oUdRPu7sKAXhNWg==", + "HhBHt5lQauNl7EZXpsDHJA==", + "HiAgt86AyznvbI2pnLalVQ==", + "HjlPM2FQWdILUXHalIhQ5w==", + "HjyxyL0db2hGDq2ZjwOOhg==", + "HkbdaMuDTPBDnt3wAn5RpQ==", + "Hm6MG6BXbAGURVJKWRM6ZA==", + "HnVfyqgJ+1xSsN4deTXcIA==", + "HoaBBw2aPCyhh0f5GxF+/Q==", + "Hs3vUOOs2TWQdQZHs+FaQQ==", + "Hst3yfyTB7yBUinvVzYROQ==", + "HtDXgMuF8PJ1haWk88S0Ew==", + "HuDuxs2KiGqmeyY1s1PjpQ==", + "HwLSUie8bzH+pOJT3XQFyg==", + "HxEU37uBMeiR5y8q/pM42g==", + "Hy1nqC40l5ItxumkIC2LAA==", + "I+wVQA+jpPTJ6xEsAlYucg==", + "I07W2eDQwe6DVsm1zHKM8A==", + "I5qDndyelK4Njv4YrX7S6w==", + "I9KNZC1tijiG1T72C4cVqQ==", + "IA1jmtfpYkz/E2wD0+27WA==", + "IADk81pIu8NIL/+9Fi94pA==", + "IAMInfSYb76GxDlAr1dsTg==", + "ICPdBCdONUqPwD5BXU5lrw==", + "IEz72W2/W8xBx5aCobUFOQ==", + "IHhyR6+5sZXTH+/NrghIPg==", + "IHyIeMad23fSDisblwyfpA==", + "IKgNa2oPaFVGYnOsL+GC5Q==", + "INNBBin5ePwTyhPIyndHHg==", + "IPLD9nT5EEYG9ioaSIYuuA==", + "ITYL3tDwddEdWSD6J6ULaA==", + "ITZ3P47ALS0JguFms6/cDA==", + "IUZ5aGpkJ9rLgSg6oAmMlw==", + "IUwVHH6+8/0c+nOrjclOWA==", + "IWZnTJ3Hb9qw9HAK/M9gTw==", + "IYIP2UBRyWetVfYLRsi1SQ==", + "IYIbEaErHoFBn8sTT9ICIQ==", + "IbN736G1Px5bsYqE5gW1JQ==", + "IdadoCPmSgHDHzn1zyf8Jw==", + "IdmcpJXyVDajzeiGZixhSA==", + "IhHyHbHGyQS+VawxteLP0w==", + "IhpXs1TK7itQ3uTzZPRP5Q==", + "IindlAnepkazs5DssBCPhA==", + "IjmLaf3stWDAwvjzNbJpQA==", + "Ily2MKoFI1zr5LxBy93EmQ==", + "Iqszlv4R49UevjGxIPMhIA==", + "IrDuBrVu1HWm0BthAHyOLQ==", + "Is3uxoSNqoIo5I15z6Z2UQ==", + "IshzWega6zr3979khNVFQQ==", + "It+K/RCYMOfNrDZxo7lbcA==", + "IwLbkL33z+LdTjaFYh93kg==", + "IwfeA6d0cT4nDTCCRhK+pA==", + "J/PNYu4y6ZMWFFXsAhaoow==", + "J/eAtAPswMELIj8K2ai+Xg==", + "J0NauydfKsACUUEpMhQg8A==", + "J1nYqJ7tIQK1+a/3sMXI/Q==", + "J2NFyb8cXEpZyxWDthYQiA==", + "J4MC9He6oqjOWsYQh9nl3Q==", + "J8v2f6hWFu8oLuwhOeoQjA==", + "JATLdpQm//SQnkyCfI5x7Q==", + "JBkbaBiorCtFq9M9lSUdMg==", + "JC8Q+8yOJ52NvtVeyHo68w==", + "JFFeXsFsMA59iNtZey7LAA==", + "JFHutgSe1/SlcYKIbNNYwQ==", + "JFi6N1PlrpKaYECOnI7GFg==", + "JGEy6VP3sz3LHiyT2UwNHQ==", + "JGeqHRQpf4No74aCs+YTfA==", + "JGx8sTyvr4bLREIhSqpFkw==", + "JHBjKpCgSgrNNACZW1W+1w==", + "JIC8R48jGVqro6wmG2KXIw==", + "JJJkp1TpuDx5wrua2Wml7g==", + "JJbzQ/trOeqQomsKXKwUpQ==", + "JKg64m6mU7C/CkTwVn4ASg==", + "JKmZqz9cUnj6eTsWnFaB0A==", + "JKphO0UYjFqcbPr6EeBuqg==", + "JLq/DrW2f26NaRwfpDXIEA==", + "JPxEncA4IkfBDvpjHsQzig==", + "JQf9UmutPh3tAnu7FDk3nA==", + "JSr/lqDej81xqUvd/O2s7w==", + "JSyhTcHLTfzHsPrxJyiVrA==", + "JSyq2MIuObPnEgEUDyALjQ==", + "JVSLiwurnCelNBiG2nflpQ==", + "JXCYeWjFqcdSf6QwB54G+A==", + "JYJvOZ4CHktLrYJyAbdOnA==", + "JZRjdJLgZ+S0ieWVDj8IJg==", + "Ja3ECL7ClwDrWMTdcSQ6Ug==", + "JaYQXntiyznQzrTlEeZMIw==", + "Jbxl8Nw1vlHO9rtu0q/Fpg==", + "Jcxjli2tcIAjCe+5LyvqdQ==", + "Je1UESovkBa9T6wS0hevLw==", + "JgXSPXDqaS1G9NqmJXZG0A==", + "JgxNrUlL8wutG04ogKFPvw==", + "JipruVZx4ban3Zo5nNM37g==", + "Jit0X0srSNFnn8Ymi1EY+g==", + "Jj4IrSVpqQnhFrzNvylSzA==", + "Jm862vBTCYbv/V4T1t46+Q==", + "JnE6BK0vpWIhNkaeaYNUzw==", + "JoATsk/aJH0UcDchFMksWA==", + "JquDByOmaQEpFb47ZJ4+JA==", + "JrKGKAKdjfAaYeQH8Y2ZRQ==", + "Js7g8Dr6XsnGURA4UNF0Ug==", + "Jt4Eg6MJn8O4Ph/K2LeSUA==", + "Ju4YwtPw+MKzpbC0wJsZow==", + "JvXTdChcE3AqMbFYTT3/wg==", + "JyIDGL1m/w+pQDOyyeYupA==", + "JyUJEnU6hJu8x2NCnGrYFw==", + "JzW+yhrjXW1ivKu3mUXPXg==", + "K1CGbMfhlhIuS0YHLG30PQ==", + "K1RL+tLjICBvMupe7QppIQ==", + "K1RgR6HR5uDEQgZ32TAFgA==", + "K2gk9zWGd0lJFRMQ1AjQ/Q==", + "K3NBEG8jJTJbSrYSOC3FKw==", + "K4VS+DDkTdBblG93l2eNkA==", + "K4yZNVoqHjXNhrZzz2gTew==", + "K5lhaAIZkGeP5rH2ebSJFw==", + "K8PVQhEJCEH1ghwOdztjRw==", + "K9A87aMlJC8XB9LuFM913g==", + "KCJJfgLe00+tjSfP6EBcUg==", + "KGI/cXVz6v6CfL8H6akcUQ==", + "KI7tQFYW38zYHOzkKp9/lQ==", + "KO2XVYyNZadcQv8aCNn5JA==", + "KOm8PTa+ICgDrgK9QxCJZw==", + "KOmdvm+wJuZ/nT/o1+xOuw==", + "KPh6TwYpspne4KZA6NyMbw==", + "KQw25X4LnQ9is+qdqfxo0w==", + "KR401XBdgCrtVDSaXqPEiA==", + "KSorNz/PLR/YYkxaj1fuqw==", + "KSumhnbKxMXQDkZIpDSWmQ==", + "KTjwL+qswa+Bid8xLdjMTg==", + "KXuFON8tMBizNkCC48ICLA==", + "KXvdjZ3rRKn60djPTCENGA==", + "KYuUNrkTvjUWQovw9dNakA==", + "Kh/J1NpDBGoyDU+Mrnnxkg==", + "KhUT2buOXavGCpcDOcbOYg==", + "KhrIIHfqXl9zGE9aGrkRVg==", + "Kj1QI+s9261S3lTtPKd9eg==", + "KjfL7YyVqmCJGBGDFdJ0gw==", + "KjnL3x+56r3M2pDj1pPihA==", + "KkXlgPJPen6HLxbNn5llBw==", + "KkwQL0DeUM3nPFfHb2ej+A==", + "KlY5TGg0pR/57TVX+ik1KQ==", + "KmcGEE0pacQ/HDUgjlt7Pg==", + "KodYHHN62zESrXUye7M01g==", + "Koiog/hpN7ew5kgJbty34A==", + "Kt6BTG1zdeBZ3nlVk+BZKQ==", + "KuNY8qAJBce+yUIluW8AYw==", + "KujFdhhgB9q4oJfjYMSsLg==", + "KyLQxi5UP+qOiyZl0PoHNQ==", + "KzWdWPP2gH0DoMYV4ndJRg==", + "Kzs+/IZJO8v4uIv9mlyJ2Q==", + "L+N/6geuokiLPPSDXM9Qkg==", + "L2D7G0btrwxl9V4dP3XM5Q==", + "L2IeUnATZHqOPcrnW2APbA==", + "L2RofFWDO0fVgSz4D2mtdw==", + "L3Jt5dHQpWQk74IAuDOL8g==", + "L4+C6I7ausPl6JbIbmozAg==", + "LATQEY7f47i77M6p11wjWA==", + "LCj4hI520tA685Sscq6uLw==", + "LCvz/h9hbouXCmdWDPGWqg==", + "LDuBcL5r3PUuzKKZ9x6Kfw==", + "LEVYAE54618FrlXkDN01Kw==", + "LFcpCtnSnsCPD2gT/RA+Zg==", + "LGwcvetzQ3QqKjNh5vA8vw==", + "LHQETSI5zsejvDaPpsO29g==", + "LJeLdqmriyAQp+QjZGFkdQ==", + "LJtRcR70ug6UHiuqbT6NGw==", + "LKyOFgUKKGUU/PxpFYMILw==", + "LMCZqd3UoF/kHHwzTdj7Tw==", + "LMEtzh0+J27+4zORfcjITw==", + "LPYFDbTEp5nGtG6uO8epSw==", + "LQttmX92SI94+hDNVd8Gtw==", + "LSN9GmT6LUHlCAMFqpuPIA==", + "LUWxfy4lfgB5wUrqCOUisw==", + "LWWfRqgtph1XrpxF4N64TA==", + "LWd0+N3M94n81qd346LfJQ==", + "LZAKplVoNjeQgfaHqkyEJA==", + "La0gzdbDyXUq6YAXeKPuJA==", + "LawT9ZygiVtBk0XJ+KkQgQ==", + "LbPp1oL0t3K2BAlIN+l8DA==", + "LblwOqNiciHmt2NXjd89tg==", + "LcF0OqPWrcpHby8RwXz1Yg==", + "LcoJBEPTlSsQwfuoKQUxEw==", + "LhqRc9oewY4XaaXTcnXIHQ==", + "Lo1xTCEWSxVuIGEbBEkVxA==", + "LoUv/f2lcWpjftzpdivMww==", + "LpoayYsTO8WLFLCSh2kf2w==", + "Lqel4GdU0ZkfoJVXI5WC/Q==", + "LqgzKxbI6WTMz0AMIDJR5w==", + "LsmsPokAwWNCuC74MaqFCQ==", + "Lt/pVD4TFRoiikmgAxEWEw==", + "Lu02ic/E94s42A14m7NGCA==", + "LyPXOoOPMieqINtX8C9Zag==", + "LyYPOZKm8bBegMr5NTSBfg==", + "M/cQja3uIk1im9++brbBOA==", + "M0ESOGwJ4WZ4Ons1ljP0bQ==", + "M20iX2sUfw5SXaZLZYlTaA==", + "M2JMnViESVHTZaru6LDM6w==", + "M2suCoFHJ5fh9oKEpUG3xA==", + "M55eersiJuN9v61r8DoAjQ==", + "M98hjSxCwvZ27aBaJTGozQ==", + "M9oqlPb63e0kZE0zWOm+JQ==", + "MArbGuIAGnw4+fw6mZIxaw==", + "MBjMU/17AXBK0tqyARZP5w==", + "MFeXfNZy6Q9wBfZmPQy3xg==", + "MI+HSMRh8KTW+Afiaxd/Fw==", + "MJ1FuK8PXcmnBAG9meU84A==", + "MK7AqlJIGqK2+K5mCvMXRQ==", + "ML7ipnY/g8mA1PUIju1j8Q==", + "MLHt6Ak288G0RGhCVaOeqA==", + "MLlVniZ08FHAS5xe+ZKRaA==", + "MMaegl2Md9s/wOx5o9564w==", + "MN94B0r5CNAF9sl3Kccdbw==", + "MOrAbuJTyGKPC6MgYJlx5Q==", + "MQYM3BT77i35LG9HcqxY2Q==", + "MQvAr+OOfnYnr/Il/2Ubkg==", + "MUkRa/PjeWMhbCTq43g6Aw==", + "MVoxyIA+emaulH8Oks8Weg==", + "MWcV03ULc0vSt/pFPYPvFA==", + "MbI04HlTGCoc/6WDejwtaQ==", + "MdvhC1cuXqni/0mtQlSOCw==", + "MeKXnEfxeuQu9t3r/qWvcw==", + "MfkyURTBfkNZwB+wZKjP4g==", + "Mj87ajJ/yR41XwAbFzJbcA==", + "Ml3mi1lGS1IspHp3dYYClg==", + "MlKWxeEh8404vXenBLq4bw==", + "MlOOZOwcRGIkifaktEq0aQ==", + "MnStiFQAr3QlaRZ02SYGaQ==", + "Mofqu40zMRrlcGRLS42eBw==", + "MpAwWMt7bcs4eL7hCSLudQ==", + "MqqDg9Iyt4k3vYVW5F+LDw==", + "Mr5mCtC53+wwmwujOU/fWw==", + "MrbEUlTagbesBNg0OemHpw==", + "MrxR3cJaDHp0t3jQNThEyg==", + "MsCloSmTFoBpm7XWYb+ueQ==", + "Muf2Eafcf9G3U2ZvQ9OgtQ==", + "MvMbvZNKbXFe2XdN+HtnpQ==", + "N+K1ibXAOyMWdfYctNDSZQ==", + "N/HgDydvaXuJvTCBhG/KtA==", + "N2KovXW14hN/6+iWa1Yv3g==", + "N2X7KWekNN+fMmwyXgKD5w==", + "N3YDSkBUqSmrmNvZZx4a1Q==", + "N4/mQFyhDpPzmihjFJJn6w==", + "N65PqIWiQeS082D6qpfrAg==", + "N7fHwb397tuQHtBz1P80ZQ==", + "N8dXCawxSBX40fgRRSDqlQ==", + "N9nD7BGEM7LDwWIMDB+rEQ==", + "NBmB/cQfS+ipERd7j9+oVg==", + "ND2hYtAIQGMxBF7o7+u7nQ==", + "ND9l4JWcncRaSLATsq0LVw==", + "NDZWIhhixq7NT8baJUR4VQ==", + "NGApiVkDSwzO45GT57GDQw==", + "NKGY0ANVZ0gnUtzVx1pKSw==", + "NKRzJndo2uXNiNppVnqy1g==", + "NMbAjbnuK7EkVeY3CQI5VA==", + "NN/ymVQNa17JOTGr6ki3eQ==", + "NOmu8oZc6CcKLu+Wfz2YOQ==", + "NQVQfN3nIg9ipHiFh4BvfQ==", + "NRyFx6jqO/oo9ojvbYzsAg==", + "NSrzwNlB0bde3ph8k6ZQcQ==", + "NZtcY8fIpSKPso/KA6ZfzA==", + "Nc5kiwXCAyjpzt43G5RF1A==", + "NdULoUDGhIolzw1PyYKV0A==", + "NdVyHoTbBhX6Umz/9vbi0g==", + "Ndx5LDiVyyTz/Fh3oBTgvA==", + "Nf9fbRHm844KZ2sqUjNgkA==", + "NfxVYc3RNWZwzh2RmfXpiA==", + "Ng5v/B9Z10TTfsDFQ/XrXQ==", + "NhZbSq0CjDNOAIvBHBM9zA==", + "NiQ/m4DZXUbpca9aZdzWAw==", + "NiawWuMBDo0Q3P2xK/vnLQ==", + "NjeDgQ1nzH1XGRnLNqCmSg==", + "NmQrsmb8PVP05qnSulPe5Q==", + "NmWmDxwK5FpKlZbo0Rt8RA==", + "NoX8lkY+kd2GPuGjp+s0tQ==", + "NquRbPn8fFQhBrUCQeRRoQ==", + "Nr4zGo5VUrjXbI8Lr4YVWQ==", + "Nsd+DfRX6L54xs+iWeMjCQ==", + "NtwqUO3SKZE/9MXLbTJo/g==", + "NuBYjwlxadAH+vLWYRZ3bg==", + "NvkR0inSzAdetpI4SOXGhw==", + "NvurnIHin4O+wNP7MnrZ1w==", + "NxSdT2+MUkQN49pyNO2bJw==", + "NyF+4VRog7etp90B9FuEjA==", + "O/EizzJSuFY8MpusBRn7Tg==", + "O1ckWUwuhD44MswpaD6/rw==", + "O209ftgvu0vSr0UZywRFXA==", + "O538ibsrI4gkE5tfwjxjmg==", + "O5N2yd+QQggPBinQ+zIhtQ==", + "O7JiE0bbp583G6ZWRGBcfw==", + "O839JUrR+JS30/nOp428QA==", + "OChiB4BzcRE8Qxilu6TgJg==", + "OEJ40VmMDYzc2ESEMontRA==", + "OERGn45uzfDfglzFFn6JAg==", + "OFLn4wun6lq484I7f6yEwg==", + "OGpsXRHlaN8BvZftxh1e7A==", + "OHJBT2SEv5b5NxBpiAf7oQ==", + "OIwtfdq37eQ0qoXuB2j7Hw==", + "OMO4pqzfcbQ11YO4nkTXfg==", + "OONAvFS/kmH7+vPhAGTNSg==", + "OOS6wQCJsXH8CsWEidB35A==", + "OVHqwV8oQMC5KSMzd5VemA==", + "OaNpzwshdHUZMphQXa6i8w==", + "Oc3BqTF3ZBW3xE0QsnFn/A==", + "OlpA9HsF8MBh7b45WZSSlg==", + "OlwHO6Sg2zIwsCOCRu0HiQ==", + "Omi2ZB9kdR1HrVP2nueQkA==", + "Omr+zPWVucPCSfkgOzLmSQ==", + "OnmvXbyT2BYsSDJYZhLScA==", + "OpC/sL320wl5anx6AVEL+A==", + "OpL+vHwPasW30s2E1TYgpA==", + "OrqJKjRndcZ8OjE3cSQv7g==", + "Otz/PgYOEZ1CQDW54FWJIQ==", + "OwArFF1hpdBupCkanpwT+Q==", + "OwIGvTh8FPFqa4ijNkguAw==", + "Owg8qCpjZa+PmbhZew6/sw==", + "OzFRv+PzPqTNmOnvZGoo5g==", + "OzH7jTcyeM7RPVFtBdakpQ==", + "OzMR5D2LriC5yrVd5hchnA==", + "P0Pc8owrqt6spdf7FgBFSw==", + "P14k+fyz0TG9yIPdojp52w==", + "P3y5MoXrkRTSLhCdLlnc4A==", + "P430CeF2MDkuq11YdjvV8A==", + "P5WPQc5NOaK7WQiRtFabkw==", + "P5fucOJhtcRIoElFJS4ffg==", + "P5wS+xB8srW4a5KDp/JVkA==", + "P7eMlOz9YUcJO+pJy0Kpkw==", + "P8lUiLFoL100c9YSQWYqDA==", + "PAlx9+U+yQCAc5Fi0BOG0w==", + "PBULPuFXb6V3Di713n3Gug==", + "PCOGl7GIqbizAKj/sZmlwQ==", + "PD+yHtJxZJ2XEvjIPIJHsQ==", + "PF0lpolQQXlpc3qTLMBk8w==", + "PHwJ5ZAqqftZ4ypr8H1qiQ==", + "PKtXc4x4DEjM45dnmPWzyg==", + "PMCWKgog/G+GFZcIruSONw==", + "PMvG4NqJP76kMRAup6TSZA==", + "PPa7BDMpRdxJdBxkuWCxKA==", + "PTAm/jGkie7OlgVOvPKpaA==", + "PTW+fhZq/ErxHqpM0DZwHQ==", + "PXC6ZpdMH0ATis/jGW12iA==", + "PaROi5U16Tk35p0EKX5JpA==", + "ParhxI6RtLETBSwB0vwChQ==", + "PbDVq2Iw1eeM8c2o/XYdTA==", + "PbnxuVerGwHyshkumqAARg==", + "Pc+u0MAzp4lndTz4m6oQ5w==", + "PcdBtV8pfKU0YbDpsjPgwg==", + "PcoVtZrS1x1Q+6nfm4f80w==", + "PdBgXFq5mBqNxgCiqaRnkw==", + "PeJS+mXnAA6jQ0WxybRQ8w==", + "PfkWkSbAxIt1Iso0znW0+Q==", + "PggVPQL5YKqSU/1asihcrg==", + "PibGJQNw7VHPTgqeCzGUGA==", + "Po0lhBfiMaXhl+vYh1D8gA==", + "PolhKCedOsplEcaX4hQ0YQ==", + "Pp1ZMxJ8yajdbfKM4HAQxA==", + "PqLCd/pwc+q5GkL6MB0jTg==", + "Pt3i49uweYVgWze3OjkjJA==", + "Pu9pEf+Tek3J+3jmQNqrKw==", + "Pv9FWQEDLKnG/9K9EIz4Gw==", + "PwvPBc+4L73xK22S9kTrdA==", + "PxReytUUn/BbxYTFMu1r2Q==", + "PybPZhJErbRTuAafrrkb3g==", + "Q0TJZxpn3jk67L7N+YDaNA==", + "Q1pdQadt12anX1QRmU2Y/A==", + "Q3TpCE+wnmH/1h/EPWsBtQ==", + "Q4bfQslDSqU64MOQbBQEUw==", + "Q6vGRQiNwoyz7bDETGvi5g==", + "Q7Df6zGwvb4rC+EtIKfaSw==", + "Q7teXmTHAC5qBy+t7ugf0w==", + "Q8RVI/kRbKuXa8HAQD7zUA==", + "QAz7FA+jpz9GgLvwdoNTEQ==", + "QCpzCTReHxGm5lcLsgwPCA==", + "QGYFMpkv37CS2wmyp42ppg==", + "QH36wzyIhh6I56Vnx79hRA==", + "QH3lAwOYBAJ0Fd5pULAZqw==", + "QIKjir/ppRyS63BwUcHWmw==", + "QJEbr3+42P9yiAfrekKdRQ==", + "QTz21WkhpPjfK8YoBrpo+w==", + "QV0OG5bpjrjku4AzDvp9yw==", + "QVwuN66yPajcjiRnVk/V8g==", + "QWURrsEgxbJ8MWcaRmOWqw==", + "Qc+XYy2qyWJ5VVwd2PExbw==", + "Qf7JFJJuuacSzl6djUT2EQ==", + "Qg1ubGl+orphvT990e5ZPA==", + "QiozlNcQCbqXtwItWExqJQ==", + "QmSBVvdk0tqH9RAicXq2zA==", + "QmcURiMzmVeUNaYPSOtTTg==", + "QoUC9nyK1BAzoUVnBLV2zw==", + "QoqHzpHDHTwQD5UF30NruQ==", + "QozQL0DTtr+PXNKifv6l6g==", + "Qrh7OEHjp80IW+YzQwzlJg==", + "QsquNcCZL9wv7oZFqm64vQ==", + "QtD35QhE8sAccPrDnhtQmQ==", + "Qv6wWP4PpycDGxe7EZNSCw==", + "QvYZxsLdu+3nV/WhY1DsYg==", + "Qx6rVv9Xj8CBjqikWI9KFA==", + "QyyiJ5I/OZC50o89fa5EmQ==", + "R+beucURp/H5jLs4kW6wmg==", + "R/y6+JJP8rzz1KITJ4qWBw==", + "R1TCCfgltnXBvt5AiUnCtQ==", + "R2OOV18CV/YpWL1xzr/VQg==", + "R2Use39If2C0FVBP7KDerA==", + "R36O31Pj8jn0AWSuqI7X2Q==", + "R3ijnutzvK6IKV3AKHQZSA==", + "R5oOM58zdbVxFSDQnNWqeA==", + "R6Me6sSGP5xpNI8R0xGOWw==", + "R6cO8GzYfOGTIi773jtkXw==", + "R81DX/5a7DYKkS4CU+TL+w==", + "R8FxgXWKBpEVbnl41+tWEw==", + "R8ULpSNu9FcCwXZM0QedSg==", + "R906Kxp2VFVR3VD+o6Vxcw==", + "R97chlspND/sE9/HMScXjQ==", + "RAAw14BA1ws5Wu/rU7oegw==", + "RAECgYZmcF4WxcFcZ4A0Ww==", + "RBMv0IxXEO3o7MnV47Bzow==", + "RClzwwKh51rbB4ekl99EZA==", + "RDgGGxTtcPvRg/5KRRlz4w==", + "REnDNe9mGfqVGZt+GdsmjQ==", + "RHKCMAqrPjvUYt13BVcmvw==", + "RHToSGASrwEmvzjX6VPvNQ==", + "RIVYGO2smx9rmRoDVYMPXw==", + "RIZYDgXqsIdTf9o2Tp/S7g==", + "RJJqFMeiCZHdsqs72J17MQ==", + "RKVDdE1AkILTFndYWi9wFg==", + "RM5CpIiB94Sqxi462G7caA==", + "RNK9G1hfuz3ETY/RmA9+aA==", + "RNdyt6ZRGvwYG5Ws3QTuEA==", + "ROSt+NlEoiPFtpRqKtDUrQ==", + "RQOlmzHwQKFpafKPJj0D8w==", + "RQywrOLZEKw9+kG6qTzr3g==", + "RUmhye56tQu9xXs4SRJpOQ==", + "RVD3Ij6sRwwxTUDAxwELtA==", + "RWI0HfpP7643OSEZR8kxzw==", + "RYkDwwng6eeffPHxt8iD9A==", + "RZTpYKxOAH9JgF1QFGN+hw==", + "RfSwpO/ywQx4lfgeYlBr2w==", + "RgtwfY5pTolKrUGT+6Pp6g==", + "RhcqXY4OsZlVVF7ZlkTeRw==", + "RiahBXX2JbPzt8baPiP/8g==", + "RkQK9S1ezo+dFYHQP57qrw==", + "RlNPyhgYOIn28R4vKCVtYA==", + "RnOXOygwJFqrD+DlM3R5Ew==", + "RnxOYPSQdHS6fw4KkDJtrA==", + "RppDe/WGt1Ed6Vqg1+cCkQ==", + "RqYpA5AY7mKPaSxoQfI1CA==", + "RrE3B3X/SJi3CqCUlTYwaw==", + "Rrq0ak9YexLqqbSD4SSXlw==", + "Rs8deApkoosIJSfX7NXtAA==", + "RuLeQHP1wHsxhdmYMcgtrQ==", + "RvXWAFwM+mUAPW1MjPBaHA==", + "Rvchz/xjcY9uKiDAkRBMmA==", + "Rww3qkF3kWSd+AaMT0kfdw==", + "RxmdoO8ak8y/HzMSIm+yBQ==", + "Ry3zgZ6KHrpNyb7+Tt2Pkw==", + "RzeH+G3gvuK1z+nJGYqARQ==", + "S+b37XhKRm8cDwRb1gSsKQ==", + "S2MAIYeDQeJ1pl9vhtYtUg==", + "S3VQa6DH+BdlSrxT/g6B5g==", + "S47hklz3Ow+n5aY6+qsCoA==", + "S4RvORcJ3m6WhnAgV4YfYA==", + "S4rFuiKLFKZ+cL7ldiTwpg==", + "S7Vjy/gOWp0HozPP1RUOZw==", + "S8jlvuYuankCnvIvMVMzmg==", + "S9L29U2P5K8wNW+sWbiH7w==", + "SCO9nQncEcyVXGCtx30Jdg==", + "SChDh/Np1HyTPWfICfE1uA==", + "SDi5+FoP9bMyKYp+vVv1XA==", + "SEGu+cSbeeeZg4xWwsSErQ==", + "SEIZhyguLoyH7So0p1KY0A==", + "SESKbGF35rjO64gktmLTWA==", + "SElc2+YVi3afE1eG1MI7dQ==", + "SFn78uklZfMtKoz2N0xDaQ==", + "SIuKH/Qediq0TyvqUF93HQ==", + "SM7E98MyViSSS9G0Pwzwyw==", + "SNPYH4r/J9vpciGN2ybP5Q==", + "SOdpdrk2ayeyv0xWdNuy9g==", + "SPGpjEJrpflv1hF0qsFlPw==", + "SPHU6ES1WVm0Mu2LB+YjrA==", + "SSKhl2L3Mvy93DcZulADtA==", + "SUAwMWLMml8uGqagz5oqhQ==", + "SVFbcjXbV7HRg+7jUrzpwg==", + "SVLHWPCCH7GPVCF7QApPbw==", + "SVuEYfQ9FGyVMo1672n0Yg==", + "SbMjjI8/P8B9a9H2G0wHEQ==", + "Scto+9TWxj1eZgvNKo+a9A==", + "SfwnYZCKP1iUJyU1yq4eKg==", + "SiSlasZ+6U2IZYogqr2UPg==", + "Slu3z535ijcs5kzDnR7kfA==", + "SmRWEzqddY9ucGAP5jXjAg==", + "Sr9c0ReRpkDYGAiqSy683g==", + "Srl4HivgHMxMOUHyM3jvNw==", + "StDtLMlCI75g4XC59mESEQ==", + "StoXC7TBzyRViPzytAlzyQ==", + "StpQm/cQF8cT0LFzKUhC5w==", + "SusSOsWNoAerAIMBVWHtfA==", + "Swjn3YkWgj0uxbZ1Idtk+A==", + "SzCGM8ypE58FLaR1+1ccxQ==", + "Szko0IPE7RX2+mfsWczrMg==", + "T/6gSz2HwWJDFIVrmcm8Ug==", + "T1pMWdoNDpIsHF8nKuOn2A==", + "T6LA+daQqRI38iDKZTdg1A==", + "T7waQc3PvTFr0yWGKmFQdQ==", + "T9WoUJNwp8h4Yydixbx6nA==", + "TA9WjiLAFgJubLN4StPwLw==", + "TAD0Lk95CD86vbwrcRogaQ==", + "TBQpcKq2huNC5OmI2wzRQw==", + "TDrq23VUdzEU/8L5i8jRJQ==", + "TGB+FIzzKnouLh5bAiVOQg==", + "THfzE2G2NVKKfO+A2TjeFw==", + "THs1r8ZEPChSGrrhrNTlsA==", + "TI90EuS/bHq/CAlX32UFXg==", + "TIKadc6FAaRWSQUg5OATgg==", + "TIWSM78m0RprwgPGK/e0JA==", + "TLJbasOoVO435E5NE5JDcA==", + "TNyvLixb03aP2f8cDozzfA==", + "TSGL3iQYUgVg/O9SBKP9EA==", + "TSPFvkgw6uLsJh66Ou0H9w==", + "TVlHoi8J7sOZ2Ti7Dm92cQ==", + "TXab/hqNGWaSK+fXAoB2bg==", + "TYlnrwgyeZoRgOpBYneRAg==", + "TZ3ATPOFjNqFGSKY3vP2Hw==", + "TZT86wXfzFffjt0f95UF5w==", + "TafM7nTE5d+tBpRCsb8TjQ==", + "TahqPgS7kEg+y6Df0HBASw==", + "TcFinyBrUoAEcLzWdFymow==", + "TcGhAJHRr7eMwGeFgpFBhg==", + "TcyyXrSsQsnz0gJ36w4Dxw==", + "TeBGJCqSqbzvljIh9viAqA==", + "TfHvdbl2M4deg65QKBTPng==", + "TfNHjSTV8w6Pg6+FaGlxvA==", + "TgWe70YalDPyyUz6n88ujg==", + "Tk5MAqd1gyHpkYi8ErlbWg==", + "TlJizlASbPtShZhkPww4UA==", + "Tm4zk2Lmg8w4ITMI31NfTA==", + "Tmx0suRHzlUK4FdBivwOwA==", + "Tp52d1NndiC9w3crFqFm9g==", + "TrLmfgwaNATh24eSrOT+pw==", + "TrWS+reCJ0vbrDNT5HDR9w==", + "Tu6w6DtX2RJJ3Ym3o3QAWw==", + "TuaG3wRdM9BWKAxh2UmAsg==", + "Tud+AMyuFkWYYZ73yoJGpQ==", + "Tug3eh+28ttyf+U7jfpg5w==", + "U+bB5NjFIuQr/Y5UpXHwxA==", + "U+oTpcjhc0E+6UjP11OE/Q==", + "U0KmEI6e5zJkaI4YJyA5Ew==", + "U49SfOBeqQV9wzsNkboi8Q==", + "U6VQghxOXsydh3Naa5Nz4A==", + "U9kE50Wq5/EHO03c5hE4Ug==", + "UAqf4owQ+EmrE45hBcUMEw==", + "UEMwF4kwgIGxGT4jrBhMPQ==", + "UHpge5Bldt9oPGo2oxnYvQ==", + "UIXytIHyVODxlrg+eQoARA==", + "UK+R+hAoVeZ4xvsoZjdWpw==", + "UNRlg6+CYVOt68NwgufGNA==", + "UNdKik7Vy23LjjPzEdzNsg==", + "UNt7CNMtltJWq8giDciGyA==", + "UP7NXAE0uxHRXUAWPhto0w==", + "UP9mmAKzeQqGhod7NCqzhg==", + "UPYR575ASaBSZIR3aX1IgQ==", + "UPzS4LR3p/h0u69+7YemrQ==", + "UQTQk5rrs6lEb1a+nkLwfg==", + "USCvrMEm/Wqeu9oX6FrgcQ==", + "USq1iF90eUv41QBebs3bhw==", + "UTmTgvl+vGiCDQpLXyVgOg==", + "UVEZPoH9cysC+17MKHFraw==", + "UXUNYEOffgW3AdBs7zTMFA==", + "UZoibx+y1YJy/uRSa9Oa2w==", + "Ua6aO6HwM+rY4sPR19CNFA==", + "UbABE6ECnjB+9YvblE9CYw==", + "UbSFw5jtyLk5MealqJw++A==", + "Ugt8HVC/aUzyWpiHd0gCOQ==", + "UgvtdE2eBZBUCAJG/6c0og==", + "Uh1mvZNGehK1AaI4a1auKQ==", + "Uje3Ild84sN41JEg3PEHDg==", + "UjmDFO7uzjl4RZDPeMeNyg==", + "Um1ftRBycvb+363a90Osog==", + "Umd+5fTcxa3mzRFDL9Z8Ww==", + "Uo+FIhw1mfjF6/M8cE1c/Q==", + "Uo1ebgsOxc3eDRds1ah3ag==", + "UreSZCIdDgloih8KLeX7gg==", + "UtLYUlQJ02oKcjNR3l+ktg==", + "Uudn69Kcv2CGz2FbfJSSEA==", + "UvC1WADanMrhT+gPp/yVqA==", + "Uw6Iw+TP9ZdZGm2b/DAmkg==", + "UwqBVd4Wfias4ElOjk2BzQ==", + "Uy4QI8D2y1bq/HDNItCtAw==", + "UymZUnEEQWVnLDdRemv+Tw==", + "UzPPFSXgeV7KW4CN5GIQXA==", + "V+QzdKh5gxTPp2yPC9ZNEg==", + "V/xG5QFyx1pihimKmAo8ZA==", + "V1fvtnJ0L3sluj9nI5KzRw==", + "V2P75JFB4Se9h7TCUMfeNA==", + "V5HEaY3v9agOhsbYOAZgJA==", + "V5HKdaTHjA8IzvHNd9C51g==", + "V6CRKrKezPwsRdbm0DJ2Yg==", + "V6zyoX6MERIybGhhULnZiw==", + "V7eji28JSg3vTi30BCS7gw==", + "V8m51xgUgywRoV6BGKUrgg==", + "V8q+xz4ljszLZMrOMOngug==", + "V9G1we3DOIQGKXjjPqIppQ==", + "V9vkAanK+Pkc4FGAokJsTA==", + "VAg/aU5nl72O+cdNuPRO4g==", + "VCL3xfPVCL5RjihQM59fgg==", + "VE4sLM5bKlLdk85sslxiLQ==", + "VGRCSrgGTkBNb8sve0fYnQ==", + "VH70dN82yPCRctmAHMfCig==", + "VI8pgqBZeGWNaxkuqQVe7g==", + "VIC7inSiqzM6v9VqtXDyCw==", + "VIkS30v268x+M1GCcq/A8A==", + "VJt2kPVBLEBpGpgvuv1oUw==", + "VK95g27ws2C6J2h/7rC2qA==", + "VOB+9Bcfu8aHKGdNO0iMRw==", + "VOvrzqiZ1EHw+ZzzTWtpsw==", + "VPa7DG6v7KnzMvtJPb88LQ==", + "VPqyIomYm7HbK5biVDvlpw==", + "VQIpquUqmeyt/q6OgxzduQ==", + "VRnx+kd6VdxChwsfbo1oeQ==", + "VUDsc9RMS1fSM43c+Jo9dQ==", + "VWNDBOtjiiI4uVNntOlu/A==", + "VWb8U4jF/Ic0+wpoXi/y/g==", + "VWy9lB5t4fNCp4O/4n8S4w==", + "VX+cVXV8p9i5EBTMoiQOQQ==", + "VXu4ARjq7DS2IR/gT24Pfw==", + "VZX1FnyC8NS2k3W+RGQm4g==", + "VaJc9vtYlqJbRPGb5Tf0ow==", + "VbCoGr8apEcN7xfdaVwVXw==", + "VbHoWmtiiPdABvkbt+3XKQ==", + "Vg2E5qEDfC+QxZTZDCu9yQ==", + "VhYGC8KYe5Up+UJ2OTLKUw==", + "Vik8tGNxO0xfdV0pFmmFDw==", + "ViweSJuNWbx5Lc49ETEs/A==", + "VjclDY8HN4fSpB263jsEiQ==", + "VllbOAjeW3Dpbj5lp2OSmA==", + "VoPth5hDHhkQcrQTxHXbuw==", + "VpmBstwR7qPVqPgKYQTA3g==", + "VsXEBIaMkVftkxt1kIh7TA==", + "Vu0E+IJXBnc25x4n41kQig==", + "VzQ1NwNv9btxUzxwVqvHQg==", + "VznvTPAAwAev+yhl9oZT0w==", + "W+M4BcYNmjj7xAximDGWsA==", + "W/0s1x3Qm+wN8DhROk6FrQ==", + "W/5ThNLu43uT1O+fg0Fzwg==", + "W04GeDh+Tk/I1S85KlozRA==", + "W2x0SBzSIsTRgyWUCOZ/lg==", + "W4CfeVp9mXgk04flryL7iA==", + "W4utAK3ws0zjiba/3i91YA==", + "W5now3RWSzzMDAxsHSl++Q==", + "W8bATujVUT80v2XGJTKXDg==", + "W8y32OLHihfeV0XFw7LmOg==", + "WADmxH7R6B4LR+W6HqQQ6A==", + "WBu0gJmmjVdVbjDmQOkU6w==", + "WGKFTWJac8uehn3N59yHJw==", + "WHutPin+uUEqtrA7L8878A==", + "WKehT4nGF2T7aKuzABDMlA==", + "WLsh3UF4WXdHwgnbKEwRlQ==", + "WLwpjgr9KzevuogoHZaVUw==", + "WN7lFJfw4lSnTCcbmt5nsg==", + "WNfDNaWUOqABQ6c6kR+eyw==", + "WQMffxULFKJ+bun6NrCURA==", + "WQznrwqvMhUlM3CzmbhAOQ==", + "WRjYdKdtnd1G9e/vFXCt0g==", + "WRoJMO0BCJyn5V6qnpUi4Q==", + "WTr3q/gDkmB4Zyj7Ly20+w==", + "WVhfn2yJZ43qCTu0TVWJwA==", + "WWN44lbUnEdHmxSfMCZc6w==", + "WY7mCUGvpXrC8gkBB46euw==", + "WbAdlac/PhYUq7J2+n5f+w==", + "Wd0dOs7eIMqW5wnILTQBtg==", + "WdCWezJU4JK43EOZ9YHVdg==", + "Wf2olJCYZRGTTZxZoBePuQ==", + "WjDqf1LyFyhdd8qkwWk+MA==", + "WkSJpxBa45XJRWWZFee7hw==", + "Wn+Vj4eiWx0WPUHr3nFbyA==", + "WnHK5ZQDR6Da5cGODXeo0A==", + "WrJMOuXSLKKzgmIDALkyNw==", + "WtT0QAERZSiIt2SFDiAizg==", + "WwraoO97OTalvavjUsqhxQ==", + "Wx9jh/teM0LJHrvTScssyQ==", + "WyCFB4+6lVtlzu3ExHAGbQ==", + "WzjvUJ4jZAEK7sBqw+m07A==", + "X/Gha4Ajjm/GStp/tv+Jvw==", + "X1PaCfEDScclLtOTiF5JUw==", + "X2Tawm2Cra6H7WtXi1Z4Qw==", + "X2YfnPXgF2VHVX95ZcBaxQ==", + "X4hrgqMIcApsjA9qOWBoCw==", + "X4kdXUuhcUqMSduqhfLpxA==", + "X4o0OkTz0ec70mzgwRfltA==", + "X6Ln4si8G5aKar52ZH/FEQ==", + "X6ulLp4noBgefQTsbuIbYQ==", + "X9QAaNjgiOeAWSphrGtyVw==", + "XA2hUgq3GVPpxtRYiqnclg==", + "XAq/C+XyR6m3uzzLlMWO5Q==", + "XEwOJG24eaEtAuBWtMxhwg==", + "XF/yncdoT4ruPeXCxEhl9Q==", + "XGAXhUFjORwKmAq9gGEcRg==", + "XHHEg/8KZioW/4/wgSEkbQ==", + "XHjrTLXkm/bBY/BewmJcCQ==", + "XJihma9zSRrXLC+T+VcFDA==", + "XLq/nWX8lQqjxsK9jlCqUg==", + "XOG1PYgqoG8gVLIbVLTQgg==", + "XSb71ae0v+yDxNF5HJXGbQ==", + "XTCcsVfEvqxnjc0K5PLcyw==", + "XV13yK0QypJXmgI+dj4KYw==", + "XV5MYe0Q7YMtoBD6/iMdSw==", + "XVVy3e6dTnO3HpgD6BtwQw==", + "XXFr0WUuGsH5nXPas7hR3Q==", + "Xconi1dtldH90Wou9swggw==", + "XddlSluOH6VkR7spFIFmdQ==", + "XdkxmYYooeDKzy7PXVigBQ==", + "XePy/hhnQwHXFeXUQQ55Vg==", + "XfBOCJwi2dezYzLe316ivw==", + "XfY+QUriCAA1+3QAsswdgg==", + "XgPHx2+ULpm14IOZU2lrDg==", + "XjjrIpsmATV/lyln4tPb+g==", + "Xo8ZjXOIoXlBjFCGdlPuZw==", + "XpGXh76RDgXC4qnTCsnNHA==", + "XqFSbgvgZn0CpaZoZiRauQ==", + "XqTK/2QuGWj50tGmiDxysA==", + "XqUO7ULEYhDOuT/I2J8BOA==", + "XqW7UBTobbV4lt1yfh0LZw==", + "XrFDomoH2qFjQ2jJ2yp9lA==", + "XsF7R12agx/KkRWl0TyXRA==", + "Xv0mNYedaBc57RrcbHr9OA==", + "XwKWd03sAz8MmvJEuN08xA==", + "Y1Nm3omeWX2MXaCjDDYnWQ==", + "Y1flEyZZAYxauMo4cmtJ1w==", + "Y26jxXvl79RcffH8O8b9Ew==", + "Y5KKN7t/v9JSxG/m1GMPSA==", + "Y5XR8Igvau/h+c1pRgKayg==", + "Y5iDQySR2c3MK7RPMCgSrw==", + "Y78dviyBS3Jq9zoRD5sZtQ==", + "Y7OofF9eUvp7qlpgdrzvkg==", + "Y7XpxIwsGK3Lm/7jX/rRmg==", + "Y7iDCWYrO1coopM3RZWIPg==", + "YA+zdEC+yEgFWRIgS1Eiqw==", + "YA0kMTJ82PYuLA4pkn4rfw==", + "YHM6NNHjmodv+G0mRLK7kw==", + "YK+q7uJObkQZvOwQ9hplMg==", + "YLz+HA6qIneP+4naavq44Q==", + "YNqIHCmBp/EbCgaPKJ7phw==", + "YPgMthbpcBN2CMkugV60hQ==", + "YVlRQHQglkbj3J2nHiP/Hw==", + "YXHQ3JI9+oca8pc/jMH6mA==", + "YZ39RIXpeLAhyMgmW2vfkQ==", + "YZt6HwCvdI5DRQqndA/hBQ==", + "YaUKOTyByjUvp1XaoLiW5Q==", + "YfbfE3WyYOW7083Y8sGfwQ==", + "YgVpC5d5V6K/BpOD663yQA==", + "YhLEPsi/TNyeUJw69SPYzQ==", + "Yig+Wh18VIqdsmwtwfoUQw==", + "Yjm5tSq1ejZn3aWqqysNvA==", + "YmaksRzoU+OwlpiEaBDYaQ==", + "YmjZJyNfHN5FaTL/HAm8ww==", + "YodhkayN5wsgPZEYN7/KNA==", + "YrEP9z2WPQ8l7TY1qWncDA==", + "YtZ8CYfnIpMd2FFA5fJ+1Q==", + "Yw4ztKv6yqxK9U1L0noFXg==", + "Yy2pPhITTmkEwoudXizHqQ==", + "YzTV0esAxBFVls3e0qRsnA==", + "Z+bsbVP91KrJvxrujBLrrQ==", + "Z0sjccxzKylgEiPCFBqPSA==", + "Z2MkqmpQXdlctCTCUDPyzw==", + "Z2rwGmVEMCY6nCfHO3qOzw==", + "Z5B+uOmPZbpbFWHpI9WhPw==", + "Z8T1b9RsUWf59D06MUrXCQ==", + "Z9bDWIgcq6XwMoU2ECDR5Q==", + "ZAQHWU6RMg4IadOxuaukyw==", + "ZCdad3AwhVArttapWFwT/Q==", + "ZH5Es/4lJ+D5KEkF1BVSGg==", + "ZIZx4MehWTVXPN9cVQBmyA==", + "ZItMIn1vhGqAlpDHclg0Ig==", + "ZJY+hujfd58mTKTdsmHoQQ==", + "ZJc7GV0Yb6MrXkpDVIuc8g==", + "ZKXxq9yr7NGBOHidht34uQ==", + "ZKeTDCboOgCptrjSfgu0xw==", + "ZKvox7BaQg4/p5jIX69Umw==", + "ZNrjP1fLdQpGykFXoLBNPw==", + "ZQ0ZnTsZKWxbRj7Tilh24Q==", + "ZQSDYgpsimK+lYGdXBWE/w==", + "ZRWyfXyXqAaOEjkzWl949Q==", + "ZRnR6i+5WKMRfs3BDRBCJg==", + "ZSmN8mmI9lDEHkJqBBg0Nw==", + "ZV8mEgJweIYk0/l0BFKetA==", + "ZVnErH1Si4u51QoT0OT7pA==", + "ZWXfE3uGU91WpPMGyknmqw==", + "ZXeMG5eqQpZO/SGKC4WQkA==", + "ZYW30FfgwHmW6nAbUGmwzA==", + "ZZImGypBWwYOAW43xDRWCQ==", + "ZaPsR9X77SNt7dLjMJUh8A==", + "ZbLVNTQSVZQWTNgC4ZGfQg==", + "ZcuIvc8fDI+2uF0I0uLiVA==", + "ZfRlID+pC1Rr4IY14jolMw==", + "ZgdpqFrVGiaHkh9o3rDszg==", + "ZgjifTVKmxOieco81gnccQ==", + "ZiJ/kJ9GneF3TIEm08lfvQ==", + "ZlBNHAiYsfaEEiPQ1z+rCA==", + "ZlOAnCLV1PkR0kb3E+Nfuw==", + "ZmVpw1TUVuT13Zw/MNI5hQ==", + "ZmblZauRqO5tGysY3/0kDw==", + "ZoNSxARrRiKZF5Wvpg7bew==", + "Zqd6+81TwYuiIgLrToFOTQ==", + "ZqjnqxZE/BjOUY0CMdVl0g==", + "ZqkmoGB0p5uT5J6XBGh7Tw==", + "ZrCezGLz38xKmzAom6yCTQ==", + "ZrCnZB/U/vcqEtI1cSvnww==", + "ZtWvgitOSRDWq7LAKYYd4Q==", + "ZtmnX24AwYAXHb2ZDC6MeQ==", + "ZuayB6IpbeITokKGVi9R5w==", + "ZvvxwDd0I6MsYd7aobjLUA==", + "ZyDh3vCQWzS5DI1zSasXWA==", + "ZybIEGf1Rn/26vlHmuMxhw==", + "ZydKlOpn2ySBW0G3uAqwuw==", + "ZygAjaN62XhW5smlLkks+Q==", + "Zyo0fzewcqXiKe2mAwKx5g==", + "ZyoaR1cMiKAsElmYZqKjLA==", + "Zz/5VMbw1TqwazReplvsEg==", + "ZzT5b0dYQXkQHTXySpWEaA==", + "ZzduJxTnXLD9EPKMn1LI4Q==", + "a/Y6IAVFv0ykRs9WD+ming==", + "a1aL8zQ+ie3YPogE3hyFFg==", + "a4EYNljinYTx9vb1VvUA6A==", + "a4rPqbDWiMivVzaRxvAj7g==", + "a5gZ5uuRrXEAjgaoh7PXAg==", + "a6IszND1m+6w+W+CvseC7g==", + "a6vem8n6WmRZAalDrHNP0g==", + "a7Pv1SOWYnkhIUC22dhdDA==", + "aD4QvtMlr8Lk/zZgZ6zIMg==", + "aEnHUfn7UE/Euh6jsMuZ7g==", + "aFJuE/s+Kbge4ppn+wulkA==", + "aIPde9CtyZrhbHLK740bfw==", + "aJFbBhYtMbTyMFBFIz/dTA==", + "aK9nybtiIBUvxgs1iQFgsw==", + "aLY2pCT0WfFO5EJyinLpPg==", + "aLh1XEUrfR9W82gzusKcOg==", + "aMa1yVA71/w6Uf1Szc9rMA==", + "aMmrAzoRWLOMPHhBuxczKg==", + "aN5x46Gw1VihRalwCt1CGg==", + "aOeJZUIZM9YWjIEokFPnzQ==", + "aRpdnrOyu5mWB1P5YMbvOA==", + "aRrcmH+Ud3mF1vEXcpEm4w==", + "aTWiWjyeSDVY/q8y9xc2zg==", + "aWZRql2IUPVe9hS3dxgVfQ==", + "aXqiibI6BpW3qilV6izHaQ==", + "aXrbsro7KLV8s4I4NMi4Eg==", + "aXs9qTEXLTkN956ch3pnOA==", + "aY6B28XdPnuYnbOy9uSP8A==", + "adJAjAFyR2ne1puEgRiH+g==", + "adT+OjEB2kqpeYi4kQ6FPg==", + "afMd/Hr3rYz/l7a3CfdDjg==", + "ahAbmGJZvUOXrcK6OydNGQ==", + "alJtvTAD7dH/zss/Ek1DMQ==", + "alqHQBz8V446EdzuVfeY5Q==", + "anyANMnNkUqr3JuPJz5Qzw==", + "apWEPWUvMC24Y+2vTSLXoA==", + "aqcOby9QyEbizPsgO3g0yw==", + "ash1r2J6B0PUxJe8P0otVQ==", + "asouSfUjJa8yfMG7BBe+fA==", + "auvG6kWMnhCMi7c7e9eHrw==", + "avFTp3rS6z5zxQUZQuaBHQ==", + "avZp5K7zJvRvJvpLSldNAw==", + "aw4CzX8pYbPVMuNrGCEcWg==", + "axEl7xXt/bwlvxKhI7hx4g==", + "ayBGGPEy++biljvGcwIjXA==", + "aySnrShOW4/xRSzl/dtSKQ==", + "ays5/F7JANIgPHN0vp2dqQ==", + "b06KGv5zDYsTxyTbQ9/eyA==", + "b0vZfEyuTja2JYMa20Rtbg==", + "b16O4LF7sVqB7aLU2f3F1A==", + "b3BQG9/9qDNC/bNSTBY/sQ==", + "b3q8kjHJPj9DWrz3yNgwjQ==", + "b4BoZmzVErvuynxirLxn0w==", + "b4aFwwcWMXsSdgS1AdFOXA==", + "b53qqLnrTBthRXmmnuXWvw==", + "b6rrRA0W247O+FfvDHbVCQ==", + "b85nxzs8xiHxaqezuDVWvg==", + "b8BZV1NfBdLi70ir4vYvZg==", + "bA2kaTpeXflTElTnQRp6GQ==", + "bBEndaOStXBpAK79FrgHaw==", + "bG+P+p34t/IJ1ubRiWg6IA==", + "bGGUhiG9SqJMHQWitXTcYQ==", + "bIk7Fa6SW7X18hfDjTKowg==", + "bJ1cZW7KsXmoLw0BcoppJg==", + "bJgsuw29cO2WozqsGZxl7w==", + "bK045TkBlz+/3+6n6Qwvrg==", + "bL2FuwsPT7a7oserJQnPcw==", + "bLEntCrCHFy9pg3T3gbBzg==", + "bLd38ZNkVeuhf0joEAxnBQ==", + "bLsStF0DDebpO+xulqGNtg==", + "bMWFvjM8eVezU1ZXKmdgqw==", + "bMb1ia0rElr2ZpZVhva0Jw==", + "bNDKcFu8T5Y6OoLSV+o/Sw==", + "bNq/hj0Cjt4lkLQeVxDVdQ==", + "bO55S58bqDiRWXSAIUGJKw==", + "bPRX2zl+K1S0iWAWUn1DZw==", + "bQ7J5mebp38rfP/fuqQOsg==", + "bQKkL+/KUCsAXlwwIH0N3w==", + "bTNRjJm+FfSQVfd56nNNqQ==", + "bUF0JIfS4uKd3JZj2xotLQ==", + "bUxQBaqKyvlSHcuRL9whjg==", + "bV9r7j2kNJpDCEM5E2339Q==", + "bWwtTFlhO3xEh/pdw0uWaQ==", + "bb/U8UynPHwczew/hxLQxw==", + "bbBsi6tXMVWyq3SDVTIXUg==", + "beSrliUu0BOadCWmx+yZyA==", + "bfUD03N2PRDT+MZ+WFVtow==", + "bhVbgJ4Do4v56D9mBuR/EA==", + "birqO8GOwGEI97zYaHyAuw==", + "bjLZ7ot/X/vWSVx4EYwMCg==", + "bkRdUHAksJZGzE1gugizYQ==", + "blygTgAHZJ3NzyAT33Bfww==", + "bs2QG8yYWxPzhtyMqO6u3A==", + "bsHIShcLS134C+dTxFQHyA==", + "bvbMJZMHScwjJALxEyGIyg==", + "bvyB6OEwhwCIfJ6KRhjnRw==", + "bz294kSG4egZnH2dJ8HwEg==", + "bzVeU2qM9zHuzf7cVIsSZw==", + "bzXXzQGZs8ustv0K4leklA==", + "c1wbFbN7AdUERO/xVPJlgw==", + "c3WVxyC5ZFtzGeQlH5Gw+w==", + "c5Tc7rTFXNJqYyc0ppW+Iw==", + "c5q/8n7Oeffv3B1snHM/lA==", + "c5ymZKqx/td1MiS2ERiz9A==", + "c6Yhwy/q3j7skXq52l36Ww==", + "cBBOQn7ZjxDku0CUrxq2ng==", + "cFFE2R4GztNoftYkqalqUQ==", + "cHSj5dpQ04h/WyefjABfmQ==", + "cHkOsVd80Rgwepeweq4S1g==", + "cLR0Ry4/N5swqga1R6QDMw==", + "cMo6l1EQESx1rIo+R4Vogg==", + "cNsC9bH30eM1EZS6IdEdtQ==", + "cSHSg9xJz/3F6kc+hKXkwg==", + "cT3PwwS6ALZA/na9NjtdzA==", + "cTvDd8okNUx0RCMer6O8sw==", + "cUyqCa7Oue934riyC17F8g==", + "cVhdRFuZaW/09CYPmtNv5g==", + "cWUg7AfqhiiEmBIu+ryImA==", + "cWdlhVZD7NWHUGte24tMjg==", + "cXpfd6Io6Glj2/QzrDMCvA==", + "ca+kx+kf7JuZ3pfYKDwFlg==", + "caepyBOAFu0MxbcXrGf6TA==", + "catI+QUNk3uJ+mUBY3bY8Q==", + "cbBXgB1WQ/i8Xul0bYY2fg==", + "ccK42Lm8Tsv73YMVZRwL6A==", + "cchuqe+CWCJpoakjHLvUfA==", + "ccmy4GVuX967KaQyycmO0w==", + "ccy3Ke2k4+evIw0agHlh3w==", + "cdWUm6uLNzR/knuj2x75eA==", + "cffrYrBX3UQhfX1TbAF+GQ==", + "cfh5VZFmIqJH/bKboDvtlA==", + "cgSEbLqqvDsNUyeA3ryJ6Q==", + "chwv4+xbEAa93PHg8q9zgQ==", + "ck86G8HsbXflyrK7MBntLg==", + "ckugAisBNX18eQz+EnEjjw==", + "cl4t9FXabQg7tbh1g7a0OA==", + "coGEgMVs2b314qrXMjNumQ==", + "cszpMdGbsbe6BygqMlnC9Q==", + "ctJYJegZhG42i+vnPFWAWw==", + "cu4ZluwohhfIYLkWp72pqA==", + "cuQslgfqD2VOMhAdnApHrA==", + "cvMJ714elj/HUh89a9lzOQ==", + "cvOg7N4DmTM+ok1NBLyBiQ==", + "cvZT1pvNbIL8TWg+SoTZdA==", + "cvrGmub2LoJ+FaM5HTPt9A==", + "cw1gBLtxH/m4H7dSM7yvFg==", + "cwBNvZc0u4bGABo88YUsVQ==", + "cxpZ4bloGv734LBf4NpVhA==", + "cxqHS4UbPolcYUwMMzgoOA==", + "czBWiYsQtNFrksWwoQxlOw==", + "d+ctfXU0j07rpRRzb5/HDA==", + "d/Wd3Ma1xYyoMByPQnA9Cw==", + "d0NBFiwGlQNclKObRtGVMQ==", + "d0VAZLbLcDUgLgIfT1GmVQ==", + "d0qvm3bl38rRCpYdWqolCQ==", + "d13Rj3NJdcat0K/kxlHLFw==", + "dAq8/1JSQf1f4QPLUitp0g==", + "dCDaYYrgASXPMGFRV0RCGg==", + "dChBe9QR29ObPFu/9PusLg==", + "dFSavcNwGd8OaLUdWq3sng==", + "dFetwmFw+D6bPMAZodUMZQ==", + "dG98w8MynOoX7aWmkvt+jg==", + "dGjcKAOGBd4gIjJq7fL+qQ==", + "dGrf9SWJ13+eWS6BtmKCNw==", + "dJHKDkfMFJeoULg7U4wwDQ==", + "dK2DU3t1ns+DWDwfBvH3SQ==", + "dL6n/JsK+Iq6UTbQuo/GOw==", + "dM9up4vKQV5LeX82j//1jQ==", + "dMRx4Mf6LrN64tiJuyWmDw==", + "dNTU+/2DdZyGGTdc+3KMhQ==", + "dNq2InSVDGnYXjkxPNPRxA==", + "dOS+mVCy3rFX9FvpkTxGXA==", + "dRFCIbVu0Y8XbjG5i+UFCQ==", + "dTMoNd6DDr1Tu8tuZWLudw==", + "dUx1REyXKiDFAABooqrKEA==", + "dVh/XMTUIx1nYN4q1iH1bA==", + "dXDPnL1ggEoBqR13aaW9HA==", + "dZg5w8rFETMp9SgW7m0gfg==", + "dZgMquvZmfLqP4EcFaWCiA==", + "daBhAvmE9shDgmciDAC5eg==", + "dhTevyxTYAuKbdLWhG47Kw==", + "dihDsG7+6aocG6M9BWrCzQ==", + "dmAfbd9F0OJHRAhNMEkRsA==", + "dml2gqLPsKpbIZ93zTXwCQ==", + "dnvatwSEcl73ROwcZ4bbIQ==", + "dpSTNOCPFHN5yGoMpl1EUA==", + "dqVw2q2nhCvTcW82MT7z0g==", + "drfODfDI6GyMW7hzkmzQvA==", + "dsueq9eygFXILDC7ZpamuA==", + "dtnE401dC0zRWU0S/QOTAg==", + "duRFqmvqF93uf/vWn8aOmg==", + "dxWv00FN/2Cgmgq9U3NVDQ==", + "e/nWuo5YalCAFKsoJmFyFA==", + "e2xLFVavnZIUUtxJx+qa1g==", + "e369ZIQjxMZJtopA//G55Q==", + "e4B3HmWjW+6hQzcOLru6Xg==", + "e5KCqQ/1GAyVMRNgQpYf6g==", + "e5l9ZiNWXglpw6nVCtO8JQ==", + "e5txnNRcGs2a9+mBFcF1Qg==", + "e9GqAEnk8XI5ix6kJuieNQ==", + "eAOEgF5N80A/oDVnlZYRAw==", + "eBapvE+hdyFTsZ0y5yrahg==", + "eC/RcoCVQBlXdE9WtcgXIw==", + "eCy/T+a8kXggn1L8SQwgvA==", + "eDWsx4isnr2xPveBOGc7Hw==", + "eDcyiPaB954q5cPXcuxAQw==", + "eFimq+LuHi42byKnBeqnZQ==", + "eFkXKRd2dwu/KWI5ZFpEzw==", + "eJDUejE/Ez/7kV+S74PDYg==", + "eJFIQh/TR7JriMzYiTw4Sg==", + "eJLrGwPRa6NgWiOrw1pA7w==", + "eJlcN+gJnqAnctbWSIO9uA==", + "eKQCVzLuzoCLcB4im8147A==", + "eLYKLr4labZeLiRrDJ9mnA==", + "ePlsM/iOMme2jEUYwi15ng==", + "eQ45Mvf5in9xKrP6/qjYbg==", + "eRwaYiog2DdlGQyaltCMJg==", + "eS/vTdSlMUnpmnl1PbHjyw==", + "eTMPXa60OTGjSPmvR4IgGw==", + "eV+RwWPiGEB+76bqvw+hbA==", + "eWgLAqJOU+fdn8raHb9HCw==", + "eXFOya6x5inTdGwJx/xtUQ==", + "eYAQWuWZX2346VMCD6s7/A==", + "eYE9No9sN5kUZ5ePEyS3+Q==", + "eddhS+FkXxiUnbPoCd5JJw==", + "edlXkskLx287vOBZ9+gVYg==", + "ehfPlu6YctzzpQmFiQDxGA==", + "ehwc2vvwNUAI7MxU4MWQZw==", + "ejfikwrSPMqEHjZAk3DMkA==", + "emVLJVzha7ui5OFHPJzeRQ==", + "enj9VEzLbmeOyYugTmdGfQ==", + "epY+dsm5EMoXnZCnO4WSHw==", + "es/L9iW8wsyLeC5S4Q8t+g==", + "eshD40tvOA6bXb0Fs/cH3A==", + "etRjRvfL/IwceY/IJ1tgzQ==", + "euxzbIq4vfGYoY3s1QmLcw==", + "evaWFoxZNQcRszIRnxqB+A==", + "ewPT4dM12nDWEDoRfiZZnA==", + "ewe/P3pJLYu/kMb5tpvVog==", + "ezsm4aFd6+DO9FUxz0A8Pg==", + "f/BjtP5fmFw2dRHgocbFlg==", + "f07bdNVAe9x+cAMdF1bByQ==", + "f09F7+1LRolRL5nZTcfKGA==", + "f0H/AFSx2KLZi9kVx5BAZg==", + "f1+fHgR5rDPsCZOzqrHM7Q==", + "f1Gs++Iilgq9GHukcnBG3w==", + "f1h+Vp+xmdZsZIziHrB2+g==", + "f5Xo7F1uaiM760Qbt978iw==", + "f6Ye5F0Lkn34uLVDCzogFQ==", + "f6iLrMpxKhFxIlfRsFAuew==", + "f9ywiGXsz+PuEsLTV3zIbQ==", + "fAKFfwlCOyhtdBK6yNnsNg==", + "fDOUzPTU2ndpbH0vgkgrJQ==", + "fFvXa1dbMoOOoWZdHxPGjw==", + "fHL+fHtDxhALZFb9W/uHuw==", + "fHNpW230mNib08aB7IM3XQ==", + "fKalNdhsyxTt1w08bv9fJA==", + "fM5uYpkvJFArnYiQ3MrQnA==", + "fO0+6TsjL+45p9mSsMRiIg==", + "fOARCnIg/foF/6tm7m9+3w==", + "fQS0jnQMnHBn7+JZWkiE/g==", + "fS471/rN4K2m10mUwGFuLg==", + "fSANOaHD0Koaqg7AoieY9A==", + "fU32wmMeD44UsFSqFY0wBA==", + "fU5ZZ1bIVsV+eXxOpGWo/Q==", + "fUAy3f9bAglLvZWvkO2Lug==", + "fVCRaPsTCKEVLkoF4y3zEw==", + "fW3QZyq5UixIA1mP6eWgqQ==", + "fX4G68hFL7DmEmjbWlCBJQ==", + "fY9VATklOvceDfHZDDk57A==", + "fZrj3wGQSt8RXv0ykJROcQ==", + "fbTm027Ms0/tEzbGnKZMDA==", + "fdqt93OrpG13KAJ5cASvkg==", + "fgXfRuqFfAu8qxbTi4bmhA==", + "fgdUFvQPb5h+Rqz8pzLsmw==", + "fhcbn9xE/6zobqQ2niSBgA==", + "fiv0DJivQeqUkrzDNlluRw==", + "fmC+85h5WBuk8fDEUWPjtQ==", + "fo3JL+2kPgDWfP+CCrFlFw==", + "foPAmiABJ3IXBoed2EgQXA==", + "foXSDEUwMhfHWJSmSejsQg==", + "fpXijBOM3Ai1RkmHven5Ww==", + "fsW2DaKYTCC7gswCT+ByQQ==", + "fsoXIbq0T0nmSpW8b+bj+g==", + "fsrX00onlGvfsuiCc35pGg==", + "ftsf2qztw3NC78ep/CZXWQ==", + "fv/PW8oexJYWf5De30fdLQ==", + "fvm0IQfnbfZFETg9v3z/Fg==", + "fxg/vQq9WPpmQsqQ4RFYaA==", + "fy54Milpa7KZH/zgrDmMXQ==", + "fzkmVWKhJsxyCwiqB/ULnQ==", + "g/z9yk94XaeBRFj4hqPzdw==", + "g0GbRp2hFVIdc7ct7Ky7ag==", + "g0aTR8aJ0uVy3YvGYu5xrw==", + "g0kHTNRI7x/lAsr92EEppw==", + "g0lWrzEYMntVIahC7i0O2g==", + "g1ELwsk6hQ+RAY1BH640Pg==", + "g2nh2xENCFOpHZfdEXnoQA==", + "g5EzTJ0KA4sO3+Opss3LMg==", + "g6udffWh7qUnSIo1Ldn3eA==", + "g6zSo8BvLuKqdmBFM1ejLA==", + "g8TcogVxHpw7uhgNFt5VCQ==", + "gAoV4BZYdW1Wm712YXOhWQ==", + "gB8wkuIzvuDAIhDtNT1gyA==", + "gBgJF0PiGEfcUnXF0RO7/w==", + "gC7gUwGumN7GNlWwfIOjJQ==", + "gDLjxT7vm07arF4SRX5/Vg==", + "gDxqUdxxeXDYhJk9zcrNyA==", + "gEHGeR2F82OgBeAlnYhRSw==", + "gFEnTI8os2BfRGqx9p5x8w==", + "gGLz3Ss+amU7y6JF09jq7A==", + "gICaI06E9scnisonpvqCsA==", + "gK7dhke5ChQzlYc/bcIkcg==", + "gR0sgItXIH8hE4FVs9Q07w==", + "gR3B8usSEb0NLos51BmJQg==", + "gTB2zM3RPm27mUQRXc/YRg==", + "gTnsH3IzALFscTZ1JkA9pw==", + "gU3gu8Y5CYVPqHrZmLYHbQ==", + "gUNP5w7ANJm257qjFxSJrA==", + "gW0oKhtQQ7BxozxUWw5XvQ==", + "gXlb7bbRqHXusTE5deolGA==", + "gYGQBLo5TdMyXks0LsZhsQ==", + "gYgCu/qUpXWryubJauuPNw==", + "gYnznEt9r97haD/j2Cko7g==", + "gYvdNJCDDQmNhtJ6NKSuTA==", + "gZNJ1Qq6OcnwXqc+jXzMLQ==", + "gZWTFt5CuLqMz6OhWL+hqQ==", + "gaEtlJtD6ZjF5Ftx0IFt0A==", + "gf1Ypna/Tt+TZ08Y+GcvGg==", + "gfhkPuMvjoC3CGcnOvki3Q==", + "gfnbviaVhKvv1UvlRGznww==", + "ggIfX1J4dX3xQoHnHUI7VA==", + "gglLMohmJDPRGMY1XKndjQ==", + "ghp8sWGKWw20S/z1tbTxFg==", + "ginkFyNVMwkZLE49AbfqfA==", + "gkrg0NR0iCaL7edq0vtewA==", + "glnqaRfwm6NxivtB2nySzw==", + "gnAIpoCyl3mQytLFgBEgGA==", + "gnez1VrH+UHT8C/SB9qGdA==", + "gnkadeCgjdmLdlu/AjBZJg==", + "goSgZ8N5UbT5NMnW3PjIlQ==", + "gqehq46BhFX2YLknuMv02w==", + "gsC/mWD8KFblxB0JxNuqJw==", + "gvvyX5ATi4q9NhnwxRxC8w==", + "gwyVIrTk5o0YMKQq4lpJ+Q==", + "gxwbqZDHLbQVqXjaq42BCg==", + "h+KRDKIvyVUBmRjv1LcCyg==", + "h0MH5NGFfChgmRJ3E/R3HQ==", + "h13Xuonj+0dD1xH86IhSyQ==", + "h1NNwMy0RjQmLloSw1hvdg==", + "h2B0ty0GobQhDnFqmKOpKQ==", + "h2cnQQF2/R3Mq2hWdDdrTg==", + "h3vYYI9yhpSZV2MQMJtwFQ==", + "h5HsEsObPuPFqREfynVblw==", + "h7Fc+eT/GuC8iWI+YTD0UQ==", + "hCzsi1yDv9ja5/o7t94j9Q==", + "hDGa2yLwNvgBd/v6mxmQaQ==", + "hDILjSpTLqJpiSSSGu445A==", + "hIABph+vhtSF5kkZQtOCTA==", + "hIJA+1QGuKEj+3ijniyBSQ==", + "hIjgi20+km+Ks23NJ4VQ6Q==", + "hJ8leLNuJ6DK5V8scnDaZQ==", + "hJSP7CostefBkJrwVEjKHA==", + "hK8KhTFcR06onlIJjTji/Q==", + "hKOsXOBoFTl/K4xE+RNHDA==", + "hN9bmMHfmnVBVr+7Ibd2Ng==", + "hNHqznsrIVRSQdII6crkww==", + "hP7dSa8lLn9KTE/Z0s4GVQ==", + "hPnPQOhz4QKhZi02KD6C+A==", + "hRxbdeniAVFgKUgB9Q3Y+g==", + "hSNZWNKUtDtMo6otkXA/DA==", + "hSkY45CeB6Ilvh0Io4W6cg==", + "hUWqqG1QwYgGC5uXJpCvJw==", + "hW9DJA1YCxHmVUAF7rhSmQ==", + "hWoxz5HhE50oYBNRoPp1JQ==", + "hY82j+sUQQRpCi6CCGea5A==", + "hZlX6qOfwxW5SPfqtRqaMw==", + "hdzol5dk//Q6tCm4+OndIA==", + "hf9HFxWRNX2ucH8FLS7ytA==", + "hfcH5Az2M7rp+EjtVpPwsg==", + "hiYg+aVzdBUDCG0CXz9kCw==", + "hkOBNoHbno2iNR7t3/d4vg==", + "hlMumZ7RJFpILuKs09ABtw==", + "hlu7os0KtAkpBTBV6D2jyQ==", + "hlvtFGW8r0PkbUAYXEM+Hw==", + "hnCUnoxofUiqQvrxl73M8w==", + "hq35Fjgvrcx6I9e6egWS4w==", + "hqeSvwu8eqA072iidlJBAw==", + "htDbVu1xGhCRd8qoMlBoMg==", + "htNVAogFakQkTX6GHoCVXg==", + "hv5GrLEIjPb4bGOi8RSO0w==", + "hvsZ5JmVevK1zclFYmxHaw==", + "hy303iin+Wm7JA6MeelwiQ==", + "i2sSvrTh/RdLJX0uKhbrew==", + "i42XumprV/aDT5R0HcmfIQ==", + "i6ZYpFwsyWyMJNgqUMSV1A==", + "i6r+mZfyhZyqlYv56o0H+w==", + "i8XXN7jcrmhnrOVDV8a2Hw==", + "i9IRqAqKjBTppsxtPB7rdw==", + "iANKiuMqWzrHSk9nbPe3bQ==", + "iCF+GWw9/YGQXsOOPAnPHQ==", + "iCnm5fPmSmxsIzuRK6osrA==", + "iFtadcw8v6betKka9yaJfg==", + "iGI9uqMoBBAjPszpxjZBWQ==", + "iGuY4VxcotHvMFXuXum7KA==", + "iGykaF+h4p46HhrWqL8Ffg==", + "iIWxFdolLcnXqIjPMg+5kQ==", + "iIm8c9uDotr87Aij+4vnMw==", + "iJ2nT8w8LuK11IXYqBK+YA==", + "iK0dWKHjVVexuXvMWJV9pg==", + "iPwX3SbbG9ez9HoHsrHbKw==", + "iQ304I1hmLZktA1d1cuOJA==", + "iS9wumBV5ktCTefFzKYfkA==", + "iSeH0JFSGK73F470Rhtesw==", + "iUsUCB0mfRsE9KPEQctIzw==", + "iVDd2Zk7vwmEh97LkOONpQ==", + "iWNlSnwrtCmVF89B+DZqOQ==", + "ibsb1ncaLZXAYgGkMO7tjQ==", + "ieEAgvK9LsWh2t6DsQOpWA==", + "ifZM0gBm9g9L09YlL+vXBg==", + "ifuJCv9ZA84Vz1FYAPsyEA==", + "ilBBNK/IV69xKTShvI94fQ==", + "imZ+mwiT22sW2M9alcUFfg==", + "inrUwXyKikpOW0y2Kl1wGw==", + "ionqS0piAOY2LeSReAz4zg==", + "ipPPjxpXHS1tcykXmrHPMQ==", + "irnD9K8bsT+up/JUrxPw6A==", + "iruDC5MeywV4yA8o1tw/KQ==", + "isep9d+Q7DEUf0W7CJJYzw==", + "itPtn+JaO4i7wz2wOPOmDQ==", + "iu5csar0IQQBOTgw5OvJwQ==", + "iujlt9fXcUXEYc+T2s5UjA==", + "iwKBOGDTFzV4aXgDGfyUkw==", + "izeyFvXOumNgVyLrbKW45g==", + "j+8/VARfbQSYhHzj0KPurQ==", + "j+lDhAnWAyso+1N8cm85hQ==", + "j4FBMnNfdBwx0VsDeTvhFg==", + "j8nMH8mK/0Aae7ZkqyPgdg==", + "j8to4gtSIRYpCogv2TESuQ==", + "jCgdKXsBCgf7giUKnr6paQ==", + "jEdanvXKyZdZJG6mj/3FWw==", + "jEqP0dyHKHiUjZ9dNNGTlQ==", + "jGHMJqbj6X1NdTDyWmXYAQ==", + "jHOoSl3ldFYr9YErEBnD3w==", + "jKJn4czwUl/6wtZklcMsSg==", + "jLI3XpVfjJ6IzrwOc4g9Pw==", + "jLkmUZ6fV56GfhC0nkh4GA==", + "jMZKSMP2THqwpWqJNJRWdw==", + "jNJQ6otieHBYIXA9LjXprg==", + "jNcMS2zX1iSZN9uYnb2EIg==", + "jOPdd330tB6+7C29a9wn0Q==", + "jQVlDU+HjZ2OHSDBidxX5A==", + "jQjyjWCEo9nWFjP4O8lehw==", + "jS0JuioLGAVaHdo/96JFoQ==", + "jTg9Y6EfpON4CRFOq0QovA==", + "jTmPbq+wh30+yJ/dRXk1cA==", + "jV/D2B11NLXZRH77sG9lBw==", + "jWsC7kdp2YmIZpfXGUimiA==", + "jZMDIu95ITTjaUX0pk4V5g==", + "jd6IpPJwOJW1otHKtKZ5Gw==", + "jdRzkUJrWxrqoyNH9paHfQ==", + "jdVMQqApseHH3fd91NFhxg==", + "jfegbZSZWkDoPulFomVntA==", + "jgNijyoj2JrQNSlUv4gk4A==", + "ji+1YHlRvzevs3q5Uw1gfA==", + "ji306HRiq965zb8EZD2uig==", + "jiV+b/1EFMnHG6J0hHpzBg==", + "jjNMPXbmpFNsCpWY0cv3eg==", + "jkUpkLoIXuu7aSH8ZghIAQ==", + "joDXdLpXvRjOqkRiYaD/Sw==", + "jon1y9yMEGfiIBjsDeeJdA==", + "jp5Em/0Ml4Txr1ptTUQjpg==", + "jpNUgFnanr9Sxvj2xbBXZw==", + "jpjpNjL1IKzJdGqWujhxCw==", + "jqPQ0aOuvOJte/ghI1RVng==", + "jrRH0aTUYCOpPLZwzwPRfQ==", + "jrfRznO0nAz6tZM1mHOKIA==", + "jt9Ocr9D8EwGRgrXVz//aQ==", + "jx7rpxbm1NaUMcE2ktg5sA==", + "jz7QlwxCIzysP39Cgro8jg==", + "k+IBS52XdOe5/hLp28ufnA==", + "k/Aou2Jmyh8Bu3k8/+ndsQ==", + "k/OVIllJvW6BefaLEPq7DA==", + "k/pBSWE2BvUsvJhA9Zl5uw==", + "k0XIjxp2vFG7sTrKcfAihA==", + "k1DPiH6NkOFXP/r3N12GyA==", + "k2KP9oPMnHmFlZO6u6tgyw==", + "k6OmSlaSZ5CB0i7SD9LczQ==", + "k8eZxqwxiN/ievXdLSEL/w==", + "kBAB2PSjXwqoQOXNrv80AA==", + "kFrRjz7Cf2KvLtz9X6oD+w==", + "kGeXrHEN6o7h5qJYcThCPw==", + "kHcBZXoxnFJ+GMwBZ/xhfQ==", + "kIGxCUxSlNgsKZ45Al1lWw==", + "kJdY3XEdJS/hyHdR+IN0GA==", + "kMUdiwM7WR8KGOucLK4Brw==", + "kNGIV3+jQmJlZDTXy1pnyA==", + "kRnBEH6ILR5GNSmjHYOclw==", + "kSUectNPXpXNg+tIveTFRw==", + "kTCHqcb3Cos51o8cL+MXcg==", + "kUhyc3G8Zvx8+q5q5nVEhw==", + "kUudvRfA33uJDzHIShQd3Q==", + "kWPUUi7x9kKKa6nJ+FDR5Q==", + "kZ/mZZg9YSDmk2rCGChYAg==", + "kZ0D191c/uv4YMG15yVLDw==", + "kZkmDatUOdIqs7GzH3nI1A==", + "ka7pMp8eSiv92WgAsz2vdA==", + "kcJ1acgBv6FtUhV8KuWoow==", + "kgKWQJJQKLUuD2VYKIKvxA==", + "kggaIvN2tlbZdZRI8S5Apw==", + "kgyUtd8MFe0tuuxDEUZA9w==", + "kh51WUI5TRnKhur6ZEpRTQ==", + "kj5WqpRCjWAfjM7ULMcuPQ==", + "kjWYVC7Eok2w2YT4rrI+IA==", + "kkbX+a00dfiTgbMI+aJpMg==", + "kly/2kE4/7ffbO34WTgoGg==", + "knYKU74onR6NkGVjQLezZg==", + "kq26VyDyJTH/eM6QvS2cMw==", + "kr8tw1+3NxoPExnAtTmfxg==", + "ksOFI9C7IrDNk4OP6SpPgw==", + "kuWGANwzNRpG4XmY7KjjNg==", + "kvAaIJb+aRAfKK104dxFAA==", + "kwlAQhR2jPMmfLTAwcmoxw==", + "kydoXVaNcx1peR5g6i588g==", + "kzGNkWh3fz27cZer4BspUQ==", + "kzTl7WH/JXsX1fqgnuTOgw==", + "kzXsrxWRnWhkA82LsLRYog==", + "kzYddqiMsY3EYrpxve2/CQ==", + "l+x2QhxG8wb5AQbcRxXlmA==", + "l0E0U/CJsyCVSTsXW4Fp+w==", + "l2NppPcweAtmA1V2CNdk2Q==", + "l2ZB9TvT68rn8AAN4MdxWw==", + "l2mAbuFF3QBIUILDODiUHQ==", + "l4ddTxbTCW5UmZW+KRmx6A==", + "l5f3I6osM9oxLRAwnUnc5A==", + "l6QHU5JsJExNoOnqxBPVbw==", + "l6Ssc04/CnsqUua9ELu2iQ==", + "l8/KMItWaW3n4g1Yot/rcQ==", + "lC5EumoIcctvxYqwELqIqw==", + "lFUq6PGk9dBRtUuiEW7Cug==", + "lHN2dn2cUKJ8ocVL3vEhUQ==", + "lJFPmPWcDzDp5B2S8Ad8AA==", + "lK2xe+OuPutp4os0ZAZx5w==", + "lM/EhwTsbivA7MDecaVTPw==", + "lMaO8Yf+6YNowGyhDkPhQA==", + "lMjip5hbCjkD9JQjuhewDg==", + "lNF8PvUIN02NattcGi5u4g==", + "lON3WM0uMJ30F8poBMvAjQ==", + "lOPJhHqCtMRFZfWMX/vFZQ==", + "lTE6u9G/RzvmbuAzq2J2/Q==", + "lV70RNlE++04G1KFB3BMXA==", + "lY+tivtsfvU0LJzBQ6itYQ==", + "lacCCRiWdquNm4YRO7FoKA==", + "leDlMcM+B1mDE8k5SWtUeg==", + "lf1fwA0YoWUZaEybE+LyMQ==", + "lfOLLyZNbsWQgHRhicr4ag==", + "lffapwUUgaQOIqLz2QPbAg==", + "lhAOM81Ej6YZYBu45pQYgg==", + "lizovLQxu6L9sbafNQuShQ==", + "lkl6XkrTMUpXi46dPxTPxg==", + "lkzFdvtBx5bV6xZO0cxK7g==", + "ll2M0QQzBsj5OFi02fv3Yg==", + "llOvGOUDVfX68jKnAlvVRA==", + "llujnWE17U8MIHmx4SbrSA==", + "lqhgbgEqROAdfzEnJ17eXA==", + "lsBTMnse2BgPS6wvPbe7JA==", + "luO1R8dUM9gy1E2lojRQoA==", + "luR/kvHLwA6tSdLeTM4TzA==", + "lwYQm2ynA3ik2gE1m11IEg==", + "lyfqic/AbEJbCiw+wA01FA==", + "lz+SeifYXxamOLs1FsFmSQ==", + "lzUQ1o7JAbdJYpmEqi6KnQ==", + "m+eh+ZqS74w2q0vejBkjaw==", + "m/Lp4U75AQyk9c8cX14HJg==", + "m06wctjNc3o7iyBHDMZs2w==", + "m3XYojKO+I6PXlVRUQBC3w==", + "m416yrrAlv+YPClGvGh+qQ==", + "m5JIUETVXcRza4VL4xlJbg==", + "m6get5wjq5j1i5abnpXuZQ==", + "m6srF+pMehggHB1tdoxlPg==", + "m9iuy4UtsjmyPzy6FTTZvw==", + "mAiD16zf+rCc7Qzxjd5buA==", + "mAzsVkijuqihhmhNTTz65g==", + "mDXHuOmI4ayjy2kLSHku1Q==", + "mI0eT4Rlr7QerMIngcu/ng==", + "mMLhjdWNnZ8zts9q+a2v3g==", + "mMfn8OaKBxtetweulho+xQ==", + "mNlYGAOPc6KIMW8ITyBzIg==", + "mNv2Q67zePjk/jbQuvkAFA==", + "mPk1IsU5DmDFA/Ym5+1ojw==", + "mPwCyD0yrIDonVi+fhXyEQ==", + "mS99D+CXhwyfVt8xJ+dJZA==", + "mSJF9dJnxZ15lTC6ilbJ2A==", + "mSstwJq7IkJ0JBJ5T8xDKg==", + "mTAqtg6oi0iytHQCaSVUsA==", + "mTLBkP+yGHsdk5g7zLjVUw==", + "mU4CqbAwpwqegxJaOz9ofQ==", + "mUek9NkXm8HiVhQ6YXiyzA==", + "mVT74Eht+gAowINoMKV7IQ==", + "mW6TCje9Zg2Ep7nzmDjSYQ==", + "mXBfDUt/sBW5OUZs2sihvw==", + "mXPtbPaoNAAlGmUMmJEWBQ==", + "mXZ4JeBwT2WJQL4a/Tm4jQ==", + "mXycPfF5zOvcj1p4hnikWw==", + "mc45FSMtzdw2PTcEBwHWPw==", + "md6zNd7ZBn3qArYqQz7/fw==", + "me61ST+JrXM5k3/a11gRAA==", + "meHzY9dIF7llDpFQo1gyMg==", + "miiOqnhtef1ODjFzMHnxjA==", + "mjFBVRJ7TgnJx+Q74xllPg==", + "mjQS8CpyGnsZIDOIEdYUxg==", + "mk1CKDah7EzDJEdhL22B7w==", + "mmRob7iyTkTLDu8ObmTPow==", + "mnalaO6xJucSiZ0+99r3Cg==", + "mpOtwBvle+nyY6lUBwTemw==", + "mpWNaUH9kn4WY26DWNAh3Q==", + "mr1qjhliRfl87wPOrJbFQg==", + "mrinv7KooPQPrLCNTRWCFg==", + "mrxlFD3FBqpSZr1kuuwxGg==", + "msstzxq++XO0AqNTmA7Bmg==", + "mxug34EekabLz0JynutfBg==", + "myzvc+2MfxGD9uuvZYdnqQ==", + "n+xYzfKmMoB3lWkdZ+D3rg==", + "n1M2dgFPpmaICP+JwxHUug==", + "n1ixvP7SfwYT3L2iWpJg6A==", + "n5GA+pA9mO/f4RN9NL9lNg==", + "n6QVaozMGniCO0PCwGQZ6w==", + "n7Bns42aTungqxKkRfQ5OQ==", + "n7KL1Kv027TSxBVwzt9qeA==", + "n7h9v2N1gOcvMuBEf8uThw==", + "nDAsSla+9XfAlQSPsXtzPA==", + "nE72uQToQFVLOzcu/nMjww==", + "nFBXCPeiwxK9mLXPScXzTA==", + "nFPDZGZowr3XXLmDVpo7hg==", + "nGzPc0kI/EduVjiK7bzM6Q==", + "nHTsDl0xeQPC5zNRnoa0Rw==", + "nHUpYmfV59fe3RWaXhPs3Q==", + "nL4iEd3b5v4Y9fHWDs+Lrw==", + "nMuMtK/Zkb3Xr34oFuX/Lg==", + "nNaGqigseHw30DaAhjBU3g==", + "nOiwBFnXxCBfPCHYITgqNg==", + "nR3ACzeVF5YcLX6Gj6AGyQ==", + "nULSbtw2dXbfVjZh33pDiA==", + "nUgYO7/oVNSX8fJqP2dbdg==", + "nVDxVhaa2o38gd1XJgE3aw==", + "nW3zZshjZEoM8KVJoVfnuQ==", + "nY/H7vThZ+dDxoPRyql+Cg==", + "neQoa8pvETr07blVMN3pgA==", + "nf8x+F03kOpMhsCSUWEhVg==", + "ng1Q0A7ljho3TUWWYl46sw==", + "nhAnHuCGXcYlqzOxrrEe1g==", + "nkbLVLvh3ClKED97+nH+7Q==", + "nkedTagkmf6YE4tEY+0fKw==", + "nknBKPgb7US42v8A0fTl/w==", + "nmD7fEU4u7/4+W/pkC4/0Q==", + "nqpKfidczdgrNaAyPi7BOQ==", + "nqtQI1bSM7DCO9P1jGV97Q==", + "nsnX3tKkN1elr18E31tXDw==", + "nvLEpj6ZZF3LWH3wUB6lKg==", + "nvUKoKfC6j8fz3gEDQrc/w==", + "nvmBgp0YlUrdZ05INsEE8Q==", + "nwtCsN1xEYaHvEOPzBv+qQ==", + "nx/U4Tode5ILux4DSR+QMg==", + "nxDGRpePV3H4NChn4eLwag==", + "nyaekSYTKzfSeSfPrB114Q==", + "nykEOLL/o7h0cs0yvdeT2g==", + "o+areESiXgSO0Lby56cBeg==", + "o+nYS4TqJc6XOiuUzEpC3A==", + "o/Y4U6rWfsUCXJ72p5CUGw==", + "o1uhaQg5/zfne84BFAINUQ==", + "o1zeXHJEKevURAAbUE/Vog==", + "o5XVEpdP4OXH0NEO4Yfc/A==", + "o64LDtKq/Fulf1PkVfFcyg==", + "o7y4zQXQAryST2cak4gVbw==", + "o9tdzmIu+3J/EYU4YWyTkA==", + "oAHVGBSJ2cf4dVnb/KEYmw==", + "oDca3JEdRb4vONT9GUUsaQ==", + "oFNMOKbQXcydxnp8fUNOHw==", + "oFanDWdePmmZN0xqwpUukA==", + "oGH7SMLI2/qjd9Vnhi3s0A==", + "oIU19xAvLJwQSZzIH577aA==", + "oIWwTbkVS5DDL47mY9/1KQ==", + "oKt57TPe4PogmsGssc3Cbg==", + "oLWWIn/2AbKRHnddr2og9g==", + "oMJLQTH1wW7LvOV0KRx/dw==", + "oNOI17POQCAkDwj6lJsYOA==", + "oONlXCW4aAqGczQ/bUllBw==", + "oPcxgoismve6+jXyIKK6AQ==", + "oPlhC4ebXdkIDazeMSn1fQ==", + "oQjugfjraFziga1BcwRLRA==", + "oR8rvIZoeoaZ/ufpo0htfQ==", + "oSnrpW4UmmVXtUGWqLq+tQ==", + "oUqO4HrBvkpSL781qAC9+w==", + "oVlG+0rjrg2tdFImxIeVBA==", + "oad5SwflzN0vfNcyEyF4EA==", + "obW3kzv2KBvuckU7F+tfjA==", + "ocRh5LR1ZIN9Johnht8fhQ==", + "ocpLRASvTgqfkY20YlVFHQ==", + "ocvA1/NbyxM0hanwwY6EiA==", + "odGhKtO4bDW5R8SYiI5yCg==", + "ogcuGHUZJkmv+vCz567a2g==", + "ohK6EftXOqBzIMI+5XnESw==", + "ojZY7Gi2QJXE/fp6Wy31iA==", + "ojf6uL85EuEYgLvHoGhUrw==", + "ojugpLIfzflgU2lonfdGxA==", + "ol9xhVTG9e1wNo50JdZbOA==", + "olTSlmirL9MFhKORiOKYkQ==", + "omAjyj1l6gyQAlBGfdxJTw==", + "onFcHOO1c3pDdfCb5N4WkQ==", + "oqlkgrYe9aCOwHXddxuyag==", + "oxoZP897lgMg/KLcZAtkAg==", + "oyYtf08AkWLR52bXm5+sKw==", + "ozVqYsmUueKifb4lDyVyrg==", + "p+bx+/WQWALXEBCTnIMr4w==", + "p/48hurJ1kh2FFPpyChzJg==", + "p/7qM5+Lwzw1/lIPY91YxQ==", + "p0eNK7zJd7D/HEGaVOrtrQ==", + "p2JPOX8yDQ0agG+tUyyT/g==", + "p3V7NfveB6cNxFW7+XQNeQ==", + "p48i7AfSSAyTdJSyHvOONw==", + "p73gSu4d+4T/ZNNkIv9Nlw==", + "p8W1LgFuW6JSOKjHkx3+aA==", + "pCQmlnn3BxhsV2GwqjRhXg==", + "pFKzcRHSUBqSMtkEJvrR1Q==", + "pGQEWJ38hb/ZYy2P1+FIuw==", + "pHo1O5zrCHCiLvopP2xaWw==", + "pHozgRyMiEmyzThtJnY4MQ==", + "pKaTI+TfcV3p/sxbd2e7YQ==", + "pT1raq2fChffFSIBX3fRiA==", + "pUfWmRXo70yGkUD/x5oIvA==", + "pVG1hL96/+hQ+58rJJy6/A==", + "pVgjGg4TeTNhKimyOu3AAw==", + "pW4gDKtVLj48gNz6V17QdA==", + "pZfn6IiG+V28fN8E2hawDQ==", + "pa8nkpAAzDKUldWjIvYMYg==", + "pcoBh5ic7baSD4TZWb3BSw==", + "pdPwUHauXOowaq9hpL2yFw==", + "pdaY6kZ8+QqkMOInvvACNA==", + "peMW+rpwmXrSwplVuB/gTA==", + "pfGcaa49SM3S6yJIPk/EJQ==", + "plXHHzA8X9QGwWzlJxhLRw==", + "pnJnBzAJlO4j3IRqcfmhkQ==", + "prCOYlboBnzmLEBG/OeVrQ==", + "prOsOG0adI4o+oz50moipw==", + "pulldyBt2sw6QDvTrCh6zw==", + "pv/m2mA/RJiEQu2Qyfv9RA==", + "pvXHwJ3dwf9GDzfDD9JI3g==", + "pw1jplCdTC+b0ThX0FXOjw==", + "pxuSWn1u+bHtRjyh2Z8veA==", + "pyrUqiZ98gVXxlXQNXv5fA==", + "pzC8Y0Vj9MPBy3YXR32z6w==", + "q/siBRjx6wNu+OTvpFKDwA==", + "q4z6A4l3nhX3smTmXr+Sig==", + "q5g3c8tnQTW2EjNfb2sukw==", + "q6LG0VzO1oxiogAAU63hyg==", + "q7m/EtZySBjZNBjQ5m1hKw==", + "q8YF9G2jqydAxSqwyyys5Q==", + "qA0sTaeNPNIiQbjIe1bOgQ==", + "qCPfJTR8ecTw6u6b1yHibA==", + "qE/h/Z+6buZWf+cmPdhxog==", + "qIFpKKwUmztsBpJgMaVvSg==", + "qIUJPanWmGzTD1XxvHp+6w==", + "qNOSm15bdkIDSc/iUr+UTQ==", + "qNyy6Fc0b8oOMWqqaliZ/w==", + "qO4HlyHMK5ygX+6HbwQe8w==", + "qOEIUWtGm5vx/+fg4tuazg==", + "qP1cCE4zsKGTPhjbcpczMw==", + "qQQwJ/aF87BbnLu3okXxaw==", + "qYHdgFAXhF/XcW4lxqfvWQ==", + "qYuo5vY8V3tZx41Kh9/4Dw==", + "qZ2q5j2gH3O56xqxkNhlIA==", + "qaTdVEeZ6S8NMOxfm+wOMA==", + "qcpeZWUlPllQYZU6mHVwUw==", + "qenHZKKlTUiEFv6goKM/Mw==", + "qkvEep4vvXhc2ZJ6R449Mg==", + "qngzBJbiTB4fivrdnE5gOg==", + "qnkFUlJ8QT322JuCI3LQgg==", + "qnsBdl050y9cUaWxbCczRw==", + "qnzWszsyJhYtx8wkMN6b1g==", + "qoK2keBg3hdbn7Q24kkVXg==", + "qpFJZqzkklby+u1UT3c1iA==", + "qt5CsMts2aD4lw/4Q6bHYQ==", + "qxALQrqHoDq9d91nU0DckA==", + "qyRmvxh8p4j4f+61c10ZFQ==", + "r/b5px/UImGNjT/X5sYjuA==", + "r0QffVKB9OD9yGsOtqzlhA==", + "r0hAwlS0mPZVfCSB+2G6uQ==", + "r1VGXWeqGeGbfKjigaAS+Q==", + "r2f2MyT+ww1g9uEBzdYI1w==", + "r36kVMpF+9J+sfI3GeGqow==", + "r3lQAYOYhwlLnDWQIunKqg==", + "r95wJtP5rsTExKMS7QhHcw==", + "rBt6L/KLT7eybxKt5wtFdg==", + "rCxoo4TP/+fupXMuIM0sDA==", + "rHagXw+CkF3uEWPWDKXvog==", + "rIMXaCaozDvrdpvpWvyZOQ==", + "rJ9qVn8/2nOxexWzqIHlcQ==", + "rJCuanCy51ydVD4nInf9IQ==", + "rKAQxu80Q8g1EEhW5Wh8tg==", + "rKb3TBM4EPx/RErFOFVCnQ==", + "rLZII1R6EGus+tYCiUtm6g==", + "rM/BOovNgnvebKMxZQdk7g==", + "rMm9bHK69h0fcMkMdGgeeA==", + "rOYeIcB+Rg5V6JG2k4zS2w==", + "rSvhrHyIlnIBlfNJqemEbw==", + "rTwJggSxTbwIYdp07ly0LA==", + "rUp5Mfc57+A8Q29SPcvH/Q==", + "rWliqgfZ3/uCRBOZ9sMmdA==", + "rXGWY/Gq+ZEsmvBHUfFMmQ==", + "rXSbbRABEf4Ymtda45w8Fw==", + "rXfWkabSPN+23Ei1bdxfmQ==", + "rXtGpN17Onx8LnccJnXwJQ==", + "rZKD8oJnIj5fSNGiccfcvA==", + "raKMXnnX6PFFsbloDqyVzQ==", + "raYifKqev8pASjjuV+UTKQ==", + "rcY4Ot40678ByCfqvGOGdg==", + "rdeftHE7gwAT67wwhCmkYQ==", + "rfPTskbnoh3hRJH6ZAzQRg==", + "rgcXxjx3pDLotH7TTfAoZw==", + "rh7bzsTQ1UZjG7amysr0Gg==", + "rhgtLQh0F9bRA6IllM7AGw==", + "ri4AOITPdB1YHyXV+5S51g==", + "rkeLYwMZ1/pW2EmIibALfA==", + "rlXt6zKE7DswUl0oWGOQUQ==", + "rqHKB91H3qVuQAm+Ym5cUA==", + "rqucO37p86LpzehR/asCSQ==", + "rs2QrN4qzAHCHhkcrAvIfA==", + "rtJdfki8fG6CB36CADp0QA==", + "rtd6mqFgGe98mqO0pFGbSw==", + "rueNryrchijjmWaA3kljYg==", + "rvE64KQGkVkbl07y7JwBqw==", + "rwplpbNJz0ADUHTmzAj15Q==", + "rwtF86ZAbWyKI6kLn4+KBw==", + "rxfACPLtKXbYua18l3WlUw==", + "rzj6mjHCcMEouL66083BAg==", + "s+eHg5K9zZ2Jozu5Oya9ZQ==", + "s/BZAhh1cTV3JCDUQsV8mA==", + "s2AKVTwrY65/SWqQxDGJQg==", + "s5+78jS4hQYrFtxqTW3g1Q==", + "s5RUHVRNAoKMuPR/Jkfc2Q==", + "s7iW1M6gkAMp+D/3jHY58w==", + "s8NpalwgPdHPla7Zi9FJ3w==", + "sBpytpE38xz0zYeT+0qc2A==", + "sC11Rf/mau3FG5SnON4+vQ==", + "sCLMrLjEUQ6P1L8tz90Kxg==", + "sEeblUmISi1HK4omrWuPTA==", + "sGLPmr568+SalaQr8SE/PA==", + "sLJrshdEANp0qk2xOUtTnQ==", + "sLdxIKap0ZfC3GpUk3gjog==", + "sNmW2b2Ud7dZi3qOF8O8EQ==", + "sQAxqWXeiu/Su0pnnXgI9A==", + "sQskMBELEq86o1SJGQqfzg==", + "sQzCwNDlRsSH7iB9cTbBcg==", + "sS6QcitMPdvUBLiMXkWQkw==", + "sWLcS+m4aWk31BiBF+vfJQ==", + "sXlFMSTBFnq0STHj6cS/8w==", + "sa2DECaqYH1z1/AFhpHi+g==", + "saEpnDGBSZWqeXSJm34eOA==", + "scCQPl0em2Zmv/RQYar60g==", + "sfIClgTMtZo9CM9MHaoqhQ==", + "sfowXUMdN2mCoBVrUzulZg==", + "sfte/o9vVNyida/yLvqADA==", + "siHwJx6EgeB1gBT9z/vTyw==", + "skrQRB9xbOsiSA19YgAdIQ==", + "snGTzo540cCqgBjxrfNpKw==", + "soBA65OmZdfBGJkBmY/4Iw==", + "spHVvA/pc7nF9Q4ON020+w==", + "spJI3xFUlpCDqzg0XCxopA==", + "sr3UXbMg5zzkRduFx/as7g==", + "sw+bmpzqsM4gEQtnqocQLQ==", + "swJhrPwllq5JORWiP5EkDA==", + "swsVVsPi/5aPFBGP+jmPIw==", + "syeBfQBUmkXNWCZ1GV8xSA==", + "t+bYn9UqrzKiuxAYGF7RLA==", + "t0WN8TwMLgi8UVEImoFXKg==", + "t2EkpUsLOEOsrnep0nZSmA==", + "t2vWMIh2BvfDSQaz5T1TZw==", + "t3Txxjq43e/CtQmfQTKwWg==", + "t5U+VMsTtlWAAWSW+00SfQ==", + "t5wh9JGSkQO78QoQoEqvXA==", + "t7HaNlXL16fVwjgSXmeOAQ==", + "t8pjhdyNJirkvYgWIO/eKg==", + "tBQDfy48FnIOZI04rxfdcA==", + "tFMJRXfWE9g78O1uBUxeqQ==", + "tFmWYH82I3zb+ymk5dhepA==", + "tG+rpfJBXlyGXxTmkceiKA==", + "tHDbi43e6k6uBgO0hA+Uiw==", + "tIqwBotg052wGBL65DZ+yA==", + "tJt6VDdAPEemBUvnoc4viA==", + "tOdlnsE3L3XCBDJRmb/OqA==", + "tOkYq1BZY152/7IJ6ZYKUg==", + "tU31r8zla146sqczdKXufg==", + "tVhXk9Ff3wAg56FbdNtcFg==", + "tVvWdA+JqH0HR2OlNVRoag==", + "tVw8U1AsslIFmQs4H1xshg==", + "tX8X8KoxUQ8atFSCxgwE1Q==", + "tXVb5f90k9l3e1oK2NGXog==", + "tXuu7YpZOuMLTv87NjKerA==", + "tY916jrSySzrL+YTcVmYKQ==", + "tYeIZjIm0tVEsYxH1iIiUQ==", + "tb5+2dmYALJibez1W4zXgA==", + "td7nDgTDmKPSODRusMcupw==", + "tdgI9v7cqJsgCAeW1Fii1A==", + "tdiTXKrkqxstDasT0D5BPA==", + "tejpAZp7y32SO2+o4OGvwQ==", + "tfgO55QqUyayjDfQh+Zo1Q==", + "tj2rWvF2Fl+XIccctj8Mhw==", + "tnUtJ/DQX9WaVJyTgemsUA==", + "tq5xUJt8GtjDIh1b48SthQ==", + "tr+U/vt+MIGXPRQYYWJfRg==", + "trjM81KANPZrg9iSThWx6Q==", + "tsiqwelcBAMU/HpLGBtMGw==", + "twPn6wTGqI0aR//0wP3xtA==", + "twjiDKJM7528oIu/el4Zbg==", + "tzV7ixFH37ze4zuLILTlfA==", + "u/QxrP1NOM/bOJlJlsi/jQ==", + "u2WQlcMxOACy6VbJXK4FwA==", + "u5cUPxM6/spLIV8VidPrAA==", + "uC2lzm7HaMAoczJO6Z/IhQ==", + "uChFnF0oCwARhAOz/d47eA==", + "uESeJe/nYrHCq4RQbrNpGA==", + "uExgqZkkJnZj252l5dKAGg==", + "uIkVijg7RPi/1j7c18G1qA==", + "uJZGw3IY2nCcdVeWW1geNQ==", + "uMq8cDVWFD+tpn8aeP8Pqg==", + "uNWFZlP7DA96sf+LWiAhtQ==", + "uNzpptKjihEfKRo5A1nWmw==", + "uO+uK1DntCxVRr1KttfUIw==", + "uOHrw37yF9oLLVd16nUpeg==", + "uOkMpYy/7DYYoethJdixfQ==", + "uPdjKJIGzN7pbGZDZdCGaA==", + "uPi8TsGY3vQsMVo/nsbgVQ==", + "uPm+cF4Jq08S5pQhYFjU8A==", + "uPnL9tboMZo0Kl2fe24CmA==", + "uQs79rbD/wEakMUxqMI48A==", + "uSIiF1r9F18avZczmlEuMQ==", + "uT6WRh5UpVdeABssoP2VTg==", + "uTA0XbiH3fTeVV7u5z0b3w==", + "uTHBqApdKOAgdwX3cjrCYQ==", + "uU1TX5DoDg6EcFKgFcn0GA==", + "uXuPA/2KJbb7ZX+NymN3dw==", + "uXvr6vi5kazZ9BCg2PWPJA==", + "uZ2gUA74/7Q33tI2TcGQlg==", + "ucLMWnNDSqE4NOCGWvcGWw==", + "udU65VtsvJspYmamiOsgXw==", + "ueODvMv/f9ZD8O0aIHn4sg==", + "ugY8rTtJkN4CXWMVcRZiZw==", + "uhT12XY79CtbwhcSfAmAXQ==", + "ulLuTZqhEDkX0EJ3xwRP9A==", + "ulpDxLeQnIRPnq6oaah2AA==", + "up2MVDi9ve+s83/nwNtZ7Q==", + "uqe3rFveJ2JIkcZQ3ZMXHQ==", + "uqp92lAqjec8UQYfyjaEZw==", + "ur9JDCVNwzSH4q4ngDlHNQ==", + "uu+ncs63SdQIvG6z4r7Q3Q==", + "uuiJ+yB7JLDh2ulthM0mjg==", + "uvKYnKE01D5r7kR9UQyo5A==", + "uvzmRcvgepW6mZbMfYgcNw==", + "uwA6N5LptSXqIBkTO0Jd7Q==", + "uwGivY3/C9WK+dirRPJZ4A==", + "uzEgwx1iAXAvWPKSVwYSeQ==", + "uzkNhmo2d08tv5AmnyqkoQ==", + "v/PshI6JjkL9nojLlMNfhg==", + "v0Bvws1WYVoEgDt8xmVKew==", + "v1AWe5qb5y3vSKFb7ADeEw==", + "v4xIYrfPGILEbD/LwVDDzA==", + "v6jZicMNM3ysm3U5xu0HoQ==", + "v7BrkRmK0FfWSHunTRHQFQ==", + "vCekQ2nOQKiN/q8Be/qwZg==", + "vFFzkWgGyw6OPADONtEojQ==", + "vFox1d3llOeBeCUZGvTy0A==", + "vFtC0B2oe1gck28JOM1dyg==", + "vGKknndb4j6VTV8DxeT4fQ==", + "vHGjRRSlZHJIliCwIkCAmQ==", + "vHVXsAMQqc0qp7HA5Q+YkA==", + "vHmQUl4WHXs1E/Shh+TeyA==", + "vIORTYSHFIXk5E2NyIvWcQ==", + "vMuaLvAntJB5o7lmt/kVXA==", + "vOJ55zFdgPPauPyFYBf01w==", + "vRgkZZGVN7YZrlml0vxrKA==", + "vSKsa0JhLCe9QFZKkcj58Q==", + "vTAmgfq3GxL4+ubXpzwk5w==", + "vUC0HlTTHj6qNHwfviDtAw==", + "vUE8Iw3NyWXURpXyoNJdaw==", + "vWn9OPnrJgfPavg4D6T/HQ==", + "vX7RIhatQeXAMr1+OjzhZw==", + "vZtL0yWpSIA+9v8i23bZSg==", + "vb6Agwzk4JG0Nn7qRPPFMQ==", + "vbyiKeDCQ4q9dDRI1Q0Ong==", + "vg3jozLXEmAnmJwdfcEN0g==", + "vhdFtKVH4bVatb4n8KzeXw==", + "vjrSYGUpeKOtJ2cNgLFg2g==", + "vljJciS+uuIvL7XXm5688g==", + "vmqfGJE6r4yDahtU/HLrxw==", + "vnOJ3e9Zd4wPx8PX7QgZzQ==", + "voO3krg4sdy4Iu+MZEr8+g==", + "vqYHQ3MnHrAIAr1QHwfIag==", + "vsRNZx4thFFFPneubKq1Fw==", + "vvEH5A39TTe1AOC11rRCLA==", + "vvh9vAIrXjIwLVkuJb5oDQ==", + "vwno3vugCvt6ooT3CD4qIQ==", + "w+jzM0I5DRzoUiLS/9QIMQ==", + "w0PKdssv+Zc5J/BbphoxpA==", + "w1zN28mSrI/gqHsgs4ME3A==", + "w3G+qXXqqKi8F5s+qvkBUg==", + "w5N/aHbtOIKzcvG3GlMjGA==", + "wDiGoFEfIVEDyyc4VpwhWQ==", + "wEJDulZafLuXCvcqBYioFQ==", + "wHA+D5cObfV3kGORCdEknw==", + "wI7JrSPQwYHpv2lRsQu9nQ==", + "wIfvvLKC61gOpsddUFjVog==", + "wJ4uCrl4DPg70ltw1dZO3w==", + "wJKFMqh6MGctWfasjHrPEg==", + "wJpepvmtQQ3sz3tVFDnFqw==", + "wK6Srd83eLigZ11Q20XGrg==", + "wM8tnXO4PDlLVHspZFcjYw==", + "wMOE/pEKVIklE75xjt6b6w==", + "wMum67lfk5E1ohUObJgrOg==", + "wMyJLQJdmrC2TSeFkIuSvQ==", + "wOc4TbwQGUwOC1B3BEZ4OQ==", + "wOhbpTzmFla8R0kI9OiHaA==", + "wPhJcp7U7IVX83szbIOOxQ==", + "wQKL8Ga6JQkpZ7yymDkC3w==", + "wR2Gxb07nkaPcZHlEjr8iA==", + "wRqaDZVHHurp5whOQ1kDbQ==", + "wTO49YX/ePHMWtcoxUAHpw==", + "wUYhs4j3W9nIywu1HIv2JA==", + "wVfSZYjMjbTsD2gaSbwuqQ==", + "wX2URK6eDDHeEOF3cgPgHA==", + "wX70jKLKJApHnhyK0r6t3A==", + "wajwXfWz2J+O+NVaj6j2UQ==", + "wc+8ohFWgOF4VlSYiZIGwQ==", + "wdRyYjaM11VmqkkxV/5bsA==", + "wfwuxn+Vja1DNwiDwL2pcQ==", + "wgH1GlUxWi6/yLLFzE76uQ==", + "who8uUamlHWHXnBf7dwy4A==", + "wlWxtQDJ+siGhN2fJn3qtw==", + "wnfYUctNK+UPwefX5y4/Rw==", + "wpZqFkKafFpLcykN2IISqg==", + "wqUJ1Gq1Yz2cXFkbcCmzHQ==", + "wqWqe0KRjZlUIrGgEOG9Mg==", + "wrewZ0hoHODf7qmoGcOd7g==", + "wsp+vmW8sEqXYVURd/gjHA==", + "wt+qDLU38kzNU75ZYi3Hbw==", + "wtyAZIfhomcHe9dLbYoSvA==", + "wux5Y8AipBnc5tJapTzgEQ==", + "wv4NC9CIpwuGf/nOQYe/oA==", + "wxkb8evGEaGf/rg/1XUWiA==", + "wy/Z8505o4sVovk4UuBp1A==", + "wyqmQGB6vgRVrYtmB2vB7w==", + "wyx5mnUMgP5wjykjAfTO7w==", + "x+8rwkqKCv0juoT5m1A4eg==", + "x/BIDm6TKMhqu/gtb3kGyw==", + "x/MpsQvziUpW40nNUHDS5Q==", + "x0eIHCvQLd2jdDaXwSWTYQ==", + "x1A74vg/hwwjAx6GrkU8zw==", + "x2NpqNnqRihktNzpxmepkQ==", + "x2nSgcTjA3oGgI8mMgiqjw==", + "x5lyMArsv1MuJmEFlWCnNw==", + "x5zMDuW66467ofgL3spLUQ==", + "x6M66krXSi0EhppwmDmsxA==", + "x6lNRGgJcRxgKTlzhc1WPg==", + "x8kRVzohTdhkryvYeMvkMw==", + "x9TIZ9Ua++3BX+MpjgTuWA==", + "x9VwDdFPp/rJ+SF16ooWYg==", + "xAAipGfHTGTjp9Qk1MR8RQ==", + "xJi0T+psHOXMivSOVpMWeQ==", + "xLm/bJBonpTs0PwsF0DvRg==", + "xMIHeno2qj3V8q9H1xezeg==", + "xNilc7UOu1kyP0+nK5MrLw==", + "xPe76nHyHmald6kmMQsKdg==", + "xQpYjaAmrQudWgsdu24J0A==", + "xTizUioizbMQxD0T6fy/EQ==", + "xUXEE7OBBCudsQnuj5ycOA==", + "xWYecfzAtXT9WyQ8NYY/hw==", + "xX6atcCApI08oVLjjLteLg==", + "xYD8jrCDmuQna+p1ebnKDQ==", + "xbBxUP9JyY0wDgHDipBHeg==", + "xdCCdP8SNBOK3IsX6PiPQA==", + "xdmY+qyoxxuRZa9kuNpDEg==", + "xfYZ6qhWNBqqJ0PdWRjOwA==", + "xfjBQk3CrNjhufdPIhr91A==", + "xiFlcSfa/gnPiO+LwbixcQ==", + "xiyRfVG0EfBA+rCk+tgWRQ==", + "xjA21QjNdThLW3VV7SCnrg==", + "xjTMO2mvtpvwQrounD4e8g==", + "xktOghh1S9nIX6fXWnT+Ug==", + "xmGgK3W5y+oCd0K2u8XjZQ==", + "xmsYnsJq78/f9xuKuQ2pBQ==", + "xoPSM86Se+1hHX0y3hhdkw==", + "xs8J3cesq7lDhP/dNltqOw==", + "xsCZVhCk2qJmOqvUjK3Y8Q==", + "xsf0m31Am0W9eLhopAkfnA==", + "xukOAM0QVsA72qEy0yku9A==", + "xvipmmwKdYt4eoKvvRnjEg==", + "xweGAZf+Yb3TtwR/sGmGIA==", + "xzGzN5Hhbh0m/KezjNvXbQ==", + "y+1I05LDAYJ09tKMs3zW6g==", + "y+cl1/Knb9MZPz8nBB0M+w==", + "y/e3HSdg7T19FanRpJ7+7Q==", + "y1J+o6DC2sETFsySgpDZyA==", + "y2JOIoIiT9cV1VxplZPraQ==", + "y2Tn2gmhKs5WKc01ce74rg==", + "y4/HohCJxtt+cT7nLJB08w==", + "y4Y4mSSTw/WrIdRpktc5Hw==", + "y4iBxAMn/KzMmaWShdYiIw==", + "y4mfEDerrhaqApDdhP5vjA==", + "y7yS9x3yshVhMpDbQtfYOQ==", + "yCu+DVU/ceMTOZ5h/7wQTg==", + "yD3Dd4ToRrl53k/2NSCJiw==", + "yDrAd1ot38soBk7zKdnT8A==", + "yKLLiqzxfrCsr6+Rm6kx1Q==", + "yKrsKX4/1B1C0TyvciNz5w==", + "yL1DwlIIREPuyuCFULi0uw==", + "yLAhLNezvqVHmN1SfMRrPw==", + "yOE90OHQdyOfrAgwDvn2gA==", + "yPIeWcW8+3HjDagegrN8bw==", + "yQCLV9IoPyXEOaj3IdFMWw==", + "yQmNZnp/JZywbBiZs3gecA==", + "yS/yMnJDHW0iaOsbj4oPTg==", + "yTVJKBn72RjakMBXDoBKHg==", + "yTgN5xFIdz1MzFS6xMl5uQ==", + "yU3N0HMSP5etuHPNrVkZtg==", + "yV3IbbTWAbHMhMGVvgb/ZQ==", + "yYBIS9PZbKo7Gram7IXWPA==", + "yYVW07lOZHdgtX42xJONIA==", + "yYmnM/WOgi+48Rw7foGyXA==", + "yYp4iuI5f/y/l1AEJxYolQ==", + "ybpTgPr3SjJ12Rj5lC/IMA==", + "ycjv4XkS5O7zcF3sqq9MwQ==", + "yctId8ltkl3+xqi9bj+RqA==", + "ydVj2odhergi+2zGUwK4/A==", + "yf06Slv9l3IZEjVqvxP2aA==", + "yfAaL0MMtSXPQ37pBdmHxQ==", + "yhI5jHlfFJxu4eV5VJO2zQ==", + "yhRi5M9Etuu9HSu4d24i3w==", + "yhexr/OFKfZl0o3lS70e4w==", + "ylA6sU7Kaf9fMNIx1+sIlw==", + "ymtA8EMPMgmMcimWZZ0A1Q==", + "ynaj4XjU27b7XbqPyxI8Ig==", + "yqQPU4jT9XvRABZgNQXjgg==", + "yqtj8GfLaUHYv/BsdjxIVw==", + "ysRQ+7Aq7eVLOp88KnFVMA==", + "ytDXLDBqWiU1w3sTurYmaw==", + "yteeQr3ub2lDXgLziZV+DQ==", + "yxCyBXqGWA735JEyljDP7Q==", + "z+1oDVy8GJ5u/UDF+bIQdA==", + "z/e5M2lE9qh3bzB97jZCKA==", + "z0BU//aSjYHAkGGk3ZSGNg==", + "z20AAnvj7WsfJeOu3vemlA==", + "z3L2BNjQOMOfTVBUxcpnRA==", + "z4Bft++f72QeDh4PWGr/sw==", + "z4oKy2wKH+sbNSgGjbdHGw==", + "z5DveTu377UW8IHnsiUGZg==", + "z920R8eahJPiTsifrPYdxA==", + "z9cd+Qj+ueX34Zf3997MNQ==", + "zCRZgVsHbQZcVMHd9pGD3A==", + "zCpibjrZOA3FQ4lYt0WoVA==", + "zDSQ3NJuUGkVOlvVCATRwA==", + "zDUZCzQesFjO1JI3PwDjfg==", + "zEzWZ6l7EKoVUxvk/l78Mw==", + "zJ7ScHNxr2leCDNNcuDApA==", + "zNLlWGW/aKBhUwQZ4DZWoQ==", + "zVupSPz7cD0v/mD/eUIIjg==", + "zZtYkKU50PPEj6qSbO5/Sw==", + "za4rzveYVMFe3Gw531DQJQ==", + "zaqyy3GaJ7cp8qDoLJWcTw==", + "zbjXhZaeyMfdTb2zxvmRMg==", + "zeELfk015D5krExLKRUYtg==", + "zeHF6fdeqcOId3fRUGscRw==", + "zgEyxj/sCs63O98sZS94Yw==", + "zi04Yc01ZheuFAQc59E45A==", + "zirOtGUXeRL22ezfotZfQg==", + "zm+z+OOyHhljV2TjA3U9zw==", + "zrZWcqQsUE3ocWE0fG+SOA==", + "ztULoqHvCOE6qV7ocqa4/w==", + "zwQ/3MzTJ9rfBmrANIh14w==", + "zwY6tCjjya/bgrYaCncaag==", + "zxsSqovedB3HT99jVblCnQ==", + "zyA9f5J7mw5InjhcfeumAQ==", +]); diff --git a/browser/components/newtab/lib/HighlightsFeed.sys.mjs b/browser/components/newtab/lib/HighlightsFeed.sys.mjs new file mode 100644 index 0000000000..c603b886da --- /dev/null +++ b/browser/components/newtab/lib/HighlightsFeed.sys.mjs @@ -0,0 +1,322 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { actionTypes as at } from "resource://activity-stream/common/Actions.sys.mjs"; + +import { shortURL } from "resource://activity-stream/lib/ShortURL.sys.mjs"; +import { + TOP_SITES_DEFAULT_ROWS, + TOP_SITES_MAX_SITES_PER_ROW, +} from "resource://activity-stream/common/Reducers.sys.mjs"; +import { Dedupe } from "resource://activity-stream/common/Dedupe.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + DownloadsManager: "resource://activity-stream/lib/DownloadsManager.sys.mjs", + FilterAdult: "resource://activity-stream/lib/FilterAdult.sys.mjs", + LinksCache: "resource://activity-stream/lib/LinksCache.sys.mjs", + NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", + PageThumbs: "resource://gre/modules/PageThumbs.sys.mjs", + Screenshots: "resource://activity-stream/lib/Screenshots.sys.mjs", + SectionsManager: "resource://activity-stream/lib/SectionsManager.sys.mjs", +}); + +const HIGHLIGHTS_MAX_LENGTH = 16; + +export const MANY_EXTRA_LENGTH = + HIGHLIGHTS_MAX_LENGTH * 5 + + TOP_SITES_DEFAULT_ROWS * TOP_SITES_MAX_SITES_PER_ROW; + +export const SECTION_ID = "highlights"; +export const SYNC_BOOKMARKS_FINISHED_EVENT = "weave:engine:sync:applied"; +export const BOOKMARKS_RESTORE_SUCCESS_EVENT = "bookmarks-restore-success"; +export const BOOKMARKS_RESTORE_FAILED_EVENT = "bookmarks-restore-failed"; +const RECENT_DOWNLOAD_THRESHOLD = 36 * 60 * 60 * 1000; + +export class HighlightsFeed { + constructor() { + this.dedupe = new Dedupe(this._dedupeKey); + this.linksCache = new lazy.LinksCache( + lazy.NewTabUtils.activityStreamLinks, + "getHighlights", + ["image"] + ); + lazy.PageThumbs.addExpirationFilter(this); + this.downloadsManager = new lazy.DownloadsManager(); + } + + _dedupeKey(site) { + // Treat bookmarks, pocket, and downloaded items as un-dedupable, otherwise show one of a url + return ( + site && + (site.pocket_id || site.type === "bookmark" || site.type === "download" + ? {} + : site.url) + ); + } + + init() { + Services.obs.addObserver(this, SYNC_BOOKMARKS_FINISHED_EVENT); + Services.obs.addObserver(this, BOOKMARKS_RESTORE_SUCCESS_EVENT); + Services.obs.addObserver(this, BOOKMARKS_RESTORE_FAILED_EVENT); + lazy.SectionsManager.onceInitialized(this.postInit.bind(this)); + } + + postInit() { + lazy.SectionsManager.enableSection(SECTION_ID, true /* isStartup */); + this.fetchHighlights({ broadcast: true, isStartup: true }); + this.downloadsManager.init(this.store); + } + + uninit() { + lazy.SectionsManager.disableSection(SECTION_ID); + lazy.PageThumbs.removeExpirationFilter(this); + Services.obs.removeObserver(this, SYNC_BOOKMARKS_FINISHED_EVENT); + Services.obs.removeObserver(this, BOOKMARKS_RESTORE_SUCCESS_EVENT); + Services.obs.removeObserver(this, BOOKMARKS_RESTORE_FAILED_EVENT); + } + + observe(subject, topic, data) { + // When we receive a notification that a sync has happened for bookmarks, + // or Places finished importing or restoring bookmarks, refresh highlights + const manyBookmarksChanged = + (topic === SYNC_BOOKMARKS_FINISHED_EVENT && data === "bookmarks") || + topic === BOOKMARKS_RESTORE_SUCCESS_EVENT || + topic === BOOKMARKS_RESTORE_FAILED_EVENT; + if (manyBookmarksChanged) { + this.fetchHighlights({ broadcast: true }); + } + } + + filterForThumbnailExpiration(callback) { + const state = this.store + .getState() + .Sections.find(section => section.id === SECTION_ID); + + callback( + state && state.initialized + ? state.rows.reduce((acc, site) => { + // Screenshots call in `fetchImage` will search for preview_image_url or + // fallback to URL, so we prevent both from being expired. + acc.push(site.url); + if (site.preview_image_url) { + acc.push(site.preview_image_url); + } + return acc; + }, []) + : [] + ); + } + + /** + * Chronologically sort highlights of all types except 'visited'. Then just append + * the rest at the end of highlights. + * @param {Array} pages The full list of links to order. + * @return {Array} A sorted array of highlights + */ + _orderHighlights(pages) { + const splitHighlights = { chronologicalCandidates: [], visited: [] }; + for (let page of pages) { + if (page.type === "history") { + splitHighlights.visited.push(page); + } else { + splitHighlights.chronologicalCandidates.push(page); + } + } + + return splitHighlights.chronologicalCandidates + .sort((a, b) => a.date_added < b.date_added) + .concat(splitHighlights.visited); + } + + /** + * Refresh the highlights data for content. + * @param {bool} options.broadcast Should the update be broadcasted. + */ + async fetchHighlights(options = {}) { + // If TopSites are enabled we need them for deduping, so wait for + // TOP_SITES_UPDATED. We also need the section to be registered to update + // state, so wait for postInit triggered by lazy.SectionsManager initializing. + if ( + (!this.store.getState().TopSites.initialized && + this.store.getState().Prefs.values["feeds.system.topsites"] && + this.store.getState().Prefs.values["feeds.topsites"]) || + !this.store.getState().Sections.length + ) { + return; + } + + // We broadcast when we want to force an update, so get fresh links + if (options.broadcast) { + this.linksCache.expire(); + } + + // Request more than the expected length to allow for items being removed by + // deduping against Top Sites or multiple history from the same domain, etc. + const manyPages = await this.linksCache.request({ + numItems: MANY_EXTRA_LENGTH, + excludeBookmarks: + !this.store.getState().Prefs.values[ + "section.highlights.includeBookmarks" + ], + excludeHistory: + !this.store.getState().Prefs.values[ + "section.highlights.includeVisited" + ], + excludePocket: + !this.store.getState().Prefs.values["section.highlights.includePocket"], + }); + + if ( + this.store.getState().Prefs.values["section.highlights.includeDownloads"] + ) { + // We only want 1 download that is less than 36 hours old, and the file currently exists + let results = await this.downloadsManager.getDownloads( + RECENT_DOWNLOAD_THRESHOLD, + { numItems: 1, onlySucceeded: true, onlyExists: true } + ); + if (results.length) { + // We only want 1 download, the most recent one + manyPages.push({ + ...results[0], + type: "download", + }); + } + } + + const orderedPages = this._orderHighlights(manyPages); + + // Remove adult highlights if we need to + const checkedAdult = lazy.FilterAdult.filter(orderedPages); + + // Remove any Highlights that are in Top Sites already + const [, deduped] = this.dedupe.group( + this.store.getState().TopSites.rows, + checkedAdult + ); + + // Keep all "bookmark"s and at most one (most recent) "history" per host + const highlights = []; + const hosts = new Set(); + for (const page of deduped) { + const hostname = shortURL(page); + // Skip this history page if we already something from the same host + if (page.type === "history" && hosts.has(hostname)) { + continue; + } + + // If we already have the image for the card, use that immediately. Else + // asynchronously fetch the image. NEVER fetch a screenshot for downloads + if (!page.image && page.type !== "download") { + this.fetchImage(page, options.isStartup); + } + + // Adjust the type for 'history' items that are also 'bookmarked' when we + // want to include bookmarks + if ( + page.type === "history" && + page.bookmarkGuid && + this.store.getState().Prefs.values[ + "section.highlights.includeBookmarks" + ] + ) { + page.type = "bookmark"; + } + + // We want the page, so update various fields for UI + Object.assign(page, { + hasImage: page.type !== "download", // Downloads do not have an image - all else types fall back to a screenshot + hostname, + type: page.type, + pocket_id: page.pocket_id, + }); + + // Add the "bookmark", "pocket", or not-skipped "history" + highlights.push(page); + hosts.add(hostname); + + // Remove internal properties that might be updated after dispatch + delete page.__sharedCache; + + // Skip the rest if we have enough items + if (highlights.length === HIGHLIGHTS_MAX_LENGTH) { + break; + } + } + + const { initialized } = this.store + .getState() + .Sections.find(section => section.id === SECTION_ID); + // Broadcast when required or if it is the first update. + const shouldBroadcast = options.broadcast || !initialized; + + lazy.SectionsManager.updateSection( + SECTION_ID, + { rows: highlights }, + shouldBroadcast, + options.isStartup + ); + } + + /** + * Fetch an image for a given highlight and update the card with it. If no + * image is available then fallback to fetching a screenshot. + */ + fetchImage(page, isStartup = false) { + // Request a screenshot if we don't already have one pending + const { preview_image_url: imageUrl, url } = page; + return lazy.Screenshots.maybeCacheScreenshot( + page, + imageUrl || url, + "image", + image => { + lazy.SectionsManager.updateSectionCard( + SECTION_ID, + url, + { image }, + true, + isStartup + ); + } + ); + } + + onAction(action) { + // Relay the downloads actions to DownloadsManager - it is a child of HighlightsFeed + this.downloadsManager.onAction(action); + switch (action.type) { + case at.INIT: + this.init(); + break; + case at.SYSTEM_TICK: + case at.TOP_SITES_UPDATED: + this.fetchHighlights({ + broadcast: false, + isStartup: !!action.meta?.isStartup, + }); + break; + case at.PREF_CHANGED: + // Update existing pages when the user changes what should be shown + if (action.data.name.startsWith("section.highlights.include")) { + this.fetchHighlights({ broadcast: true }); + } + break; + case at.PLACES_HISTORY_CLEARED: + case at.PLACES_LINK_BLOCKED: + case at.DOWNLOAD_CHANGED: + case at.POCKET_LINK_DELETED_OR_ARCHIVED: + this.fetchHighlights({ broadcast: true }); + break; + case at.PLACES_LINKS_CHANGED: + case at.PLACES_SAVED_TO_POCKET: + this.linksCache.expire(); + this.fetchHighlights({ broadcast: false }); + break; + case at.UNINIT: + this.uninit(); + break; + } + } +} diff --git a/browser/components/newtab/lib/LinksCache.sys.mjs b/browser/components/newtab/lib/LinksCache.sys.mjs new file mode 100644 index 0000000000..0dfb89e74e --- /dev/null +++ b/browser/components/newtab/lib/LinksCache.sys.mjs @@ -0,0 +1,133 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This should be slightly less than SYSTEM_TICK_INTERVAL as timer +// comparisons are too exact while the async/await functionality will make the +// last recorded time a little bit later. This causes the comparasion to skip +// updates. +// It should be 10% less than SYSTEM_TICK to update at least once every 5 mins. +// https://github.com/mozilla/activity-stream/pull/3695#discussion_r144678214 +const EXPIRATION_TIME = 4.5 * 60 * 1000; // 4.5 minutes + +/** + * Cache link results from a provided object property and refresh after some + * amount of time has passed. Allows for migrating data from previously cached + * links to the new links with the same url. + */ +export class LinksCache { + /** + * Create a links cache for a given object property. + * + * @param {object} linkObject Object containing the link property + * @param {string} linkProperty Name of property on object to access + * @param {array} properties Optional properties list to migrate to new links. + * @param {function} shouldRefresh Optional callback receiving the old and new + * options to refresh even when not expired. + */ + constructor( + linkObject, + linkProperty, + properties = [], + shouldRefresh = () => {} + ) { + this.clear(); + + // Allow getting links from both methods and array properties + this.linkGetter = options => { + const ret = linkObject[linkProperty]; + return typeof ret === "function" ? ret.call(linkObject, options) : ret; + }; + + // Always migrate the shared cache data in addition to any custom properties + this.migrateProperties = ["__sharedCache", ...properties]; + this.shouldRefresh = shouldRefresh; + } + + /** + * Clear the cached data. + */ + clear() { + this.cache = Promise.resolve([]); + this.lastOptions = {}; + this.expire(); + } + + /** + * Force the next request to update the cache. + */ + expire() { + delete this.lastUpdate; + } + + /** + * Request data and update the cache if necessary. + * + * @param {object} options Optional data to pass to the underlying method. + * @returns {promise(array)} Links array with objects that can be modified. + */ + async request(options = {}) { + // Update the cache if the data has been expired + const now = Date.now(); + if ( + this.lastUpdate === undefined || + now > this.lastUpdate + EXPIRATION_TIME || + // Allow custom rules around refreshing based on options + this.shouldRefresh(this.lastOptions, options) + ) { + // Update request state early so concurrent requests can refer to it + this.lastOptions = options; + this.lastUpdate = now; + + // Save a promise before awaits, so other requests wait for correct data + // eslint-disable-next-line no-async-promise-executor + this.cache = new Promise(async (resolve, reject) => { + try { + // Allow fast lookup of old links by url that might need to migrate + const toMigrate = new Map(); + for (const oldLink of await this.cache) { + if (oldLink) { + toMigrate.set(oldLink.url, oldLink); + } + } + + // Update the cache with migrated links without modifying source objects + resolve( + (await this.linkGetter(options)).map(link => { + // Keep original array hole positions + if (!link) { + return link; + } + + // Migrate data to the new link copy if we have an old link + const newLink = Object.assign({}, link); + const oldLink = toMigrate.get(newLink.url); + if (oldLink) { + for (const property of this.migrateProperties) { + const oldValue = oldLink[property]; + if (oldValue !== undefined) { + newLink[property] = oldValue; + } + } + } else { + // Share data among link copies and new links from future requests + newLink.__sharedCache = {}; + } + // Provide a helper to update the cached link + newLink.__sharedCache.updateLink = (property, value) => { + newLink[property] = value; + }; + + return newLink; + }) + ); + } catch (error) { + reject(error); + } + }); + } + + // Provide a shallow copy of the cached link objects for callers to modify + return (await this.cache).map(link => link && Object.assign({}, link)); + } +} diff --git a/browser/components/newtab/lib/NewTabInit.sys.mjs b/browser/components/newtab/lib/NewTabInit.sys.mjs new file mode 100644 index 0000000000..db30e009ec --- /dev/null +++ b/browser/components/newtab/lib/NewTabInit.sys.mjs @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { + actionCreators as ac, + actionTypes as at, +} from "resource://activity-stream/common/Actions.sys.mjs"; + +/** + * NewTabInit - A placeholder for now. This will send a copy of the state to all + * newly opened tabs. + */ +export class NewTabInit { + constructor() { + this._repliedEarlyTabs = new Map(); + } + + reply(target) { + // Skip this reply if we already replied to an early tab + if (this._repliedEarlyTabs.get(target)) { + return; + } + + const action = { + type: at.NEW_TAB_INITIAL_STATE, + data: this.store.getState(), + }; + this.store.dispatch(ac.AlsoToOneContent(action, target)); + + // Remember that this early tab has already gotten a rehydration response in + // case it thought we lost its initial REQUEST and asked again + if (this._repliedEarlyTabs.has(target)) { + this._repliedEarlyTabs.set(target, true); + } + } + + onAction(action) { + switch (action.type) { + case at.NEW_TAB_STATE_REQUEST: + this.reply(action.meta.fromTarget); + break; + case at.NEW_TAB_INIT: + // Initialize data for early tabs that might REQUEST twice + if (action.data.simulated) { + this._repliedEarlyTabs.set(action.data.portID, false); + } + break; + case at.NEW_TAB_UNLOAD: + // Clean up for any tab (no-op if not an early tab) + this._repliedEarlyTabs.delete(action.meta.fromTarget); + break; + } + } +} diff --git a/browser/components/newtab/lib/PersistentCache.sys.mjs b/browser/components/newtab/lib/PersistentCache.sys.mjs new file mode 100644 index 0000000000..1db9ca102e --- /dev/null +++ b/browser/components/newtab/lib/PersistentCache.sys.mjs @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * A file (disk) based persistent cache of a JSON serializable object. + */ +export class PersistentCache { + /** + * Create a cache object based on a name. + * + * @param {string} name Name of the cache. It will be used to create the filename. + * @param {boolean} preload (optional). Whether the cache should be preloaded from file. Defaults to false. + */ + constructor(name, preload = false) { + this.name = name; + this._filename = `activity-stream.${name}.json`; + if (preload) { + this._load(); + } + } + + /** + * Set a value to be cached with the specified key. + * + * @param {string} key The cache key. + * @param {object} value The data to be cached. + */ + async set(key, value) { + const data = await this._load(); + data[key] = value; + await this._persist(data); + } + + /** + * Get a value from the cache. + * + * @param {string} key (optional) The cache key. If not provided, we return the full cache. + * @returns {object} The cached data. + */ + async get(key) { + const data = await this._load(); + return key ? data[key] : data; + } + + /** + * Load the cache into memory if it isn't already. + */ + _load() { + return ( + this._cache || + // eslint-disable-next-line no-async-promise-executor + (this._cache = new Promise(async (resolve, reject) => { + let filepath; + try { + filepath = PathUtils.join(PathUtils.localProfileDir, this._filename); + } catch (error) { + reject(error); + return; + } + + let data = {}; + try { + data = await IOUtils.readJSON(filepath); + } catch (error) { + if ( + // isInstance() is not available in node unit test. It should be safe to use instanceof as it's directly from IOUtils. + // eslint-disable-next-line mozilla/use-isInstance + !(error instanceof DOMException) || + error.name !== "NotFoundError" + ) { + console.error(`Failed to parse ${this._filename}:`, error.message); + } + } + + resolve(data); + })) + ); + } + + /** + * Persist the cache to file. + */ + async _persist(data) { + const filepath = PathUtils.join(PathUtils.localProfileDir, this._filename); + await IOUtils.writeJSON(filepath, data, { + tmpPath: `${filepath}.tmp`, + }); + } +} diff --git a/browser/components/newtab/lib/PersonalityProvider/NaiveBayesTextTagger.mjs b/browser/components/newtab/lib/PersonalityProvider/NaiveBayesTextTagger.mjs new file mode 100644 index 0000000000..d5930e3147 --- /dev/null +++ b/browser/components/newtab/lib/PersonalityProvider/NaiveBayesTextTagger.mjs @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export class NaiveBayesTextTagger { + constructor(model, toksToTfIdfVector) { + this.model = model; + this.toksToTfIdfVector = toksToTfIdfVector; + } + + /** + * Determines if the tokenized text belongs to class according to binary naive Bayes + * classifier. Returns an object containing the class label ("label"), and + * the log probability ("logProb") that the text belongs to that class. If + * the positive class is more likely, then "label" is the positive class + * label. If the negative class is matched, then "label" is set to null. + */ + tagTokens(tokens) { + let fv = this.toksToTfIdfVector(tokens, this.model.vocab_idfs); + + let bestLogProb = null; + let bestClassId = -1; + let bestClassLabel = null; + let logSumExp = 0.0; // will be P(x). Used to create a proper probability + for (let classId = 0; classId < this.model.classes.length; classId++) { + let classModel = this.model.classes[classId]; + let classLogProb = classModel.log_prior; + + // dot fv with the class model + for (let pair of Object.values(fv)) { + let [termId, tfidf] = pair; + classLogProb += tfidf * classModel.feature_log_probs[termId]; + } + + if (bestLogProb === null || classLogProb > bestLogProb) { + bestLogProb = classLogProb; + bestClassId = classId; + } + logSumExp += Math.exp(classLogProb); + } + + // now normalize the probability by dividing by P(x) + logSumExp = Math.log(logSumExp); + bestLogProb -= logSumExp; + if (bestClassId === this.model.positive_class_id) { + bestClassLabel = this.model.positive_class_label; + } else { + bestClassLabel = null; + } + + let confident = + bestClassId === this.model.positive_class_id && + bestLogProb > this.model.positive_class_threshold_log_prob; + return { + label: bestClassLabel, + logProb: bestLogProb, + confident, + }; + } +} diff --git a/browser/components/newtab/lib/PersonalityProvider/NmfTextTagger.mjs b/browser/components/newtab/lib/PersonalityProvider/NmfTextTagger.mjs new file mode 100644 index 0000000000..5c77152d8d --- /dev/null +++ b/browser/components/newtab/lib/PersonalityProvider/NmfTextTagger.mjs @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +export class NmfTextTagger { + constructor(model, toksToTfIdfVector) { + this.model = model; + this.toksToTfIdfVector = toksToTfIdfVector; + } + + /** + * A multiclass classifier that scores tokenized text against several classes through + * inference of a nonnegative matrix factorization of TF-IDF vectors and + * class labels. Returns a map of class labels as string keys to scores. + * (Higher is more confident.) All classes get scored, so it is up to + * consumer of this data determine what classes are most valuable. + */ + tagTokens(tokens) { + let fv = this.toksToTfIdfVector(tokens, this.model.vocab_idfs); + let fve = Object.values(fv); + + // normalize by the sum of the vector + let sum = 0.0; + for (let pair of fve) { + // eslint-disable-next-line prefer-destructuring + sum += pair[1]; + } + for (let i = 0; i < fve.length; i++) { + // eslint-disable-next-line prefer-destructuring + fve[i][1] /= sum; + } + + // dot the document with each topic vector so that we can transform it into + // the latent space + let toksInLatentSpace = []; + for (let topicVect of this.model.topic_word) { + let fvDotTwv = 0; + // dot fv with each topic word vector + for (let pair of fve) { + let [termId, tfidf] = pair; + fvDotTwv += tfidf * topicVect[termId]; + } + toksInLatentSpace.push(fvDotTwv); + } + + // now project toksInLatentSpace back into class space + let predictions = {}; + Object.keys(this.model.document_topic).forEach(topic => { + let score = 0; + for (let i = 0; i < toksInLatentSpace.length; i++) { + score += toksInLatentSpace[i] * this.model.document_topic[topic][i]; + } + predictions[topic] = score; + }); + + return predictions; + } +} diff --git a/browser/components/newtab/lib/PersonalityProvider/PersonalityProvider.sys.mjs b/browser/components/newtab/lib/PersonalityProvider/PersonalityProvider.sys.mjs new file mode 100644 index 0000000000..406a0fa200 --- /dev/null +++ b/browser/components/newtab/lib/PersonalityProvider/PersonalityProvider.sys.mjs @@ -0,0 +1,277 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + BasePromiseWorker: "resource://gre/modules/PromiseWorker.sys.mjs", + NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", + RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", + Utils: "resource://services-settings/Utils.sys.mjs", +}); + +const RECIPE_NAME = "personality-provider-recipe"; +const MODELS_NAME = "personality-provider-models"; + +export class PersonalityProvider { + constructor(modelKeys) { + this.modelKeys = modelKeys; + this.onSync = this.onSync.bind(this); + this.setup(); + } + + setScores(scores) { + this.scores = scores || {}; + this.interestConfig = this.scores.interestConfig; + this.interestVector = this.scores.interestVector; + } + + get personalityProviderWorker() { + if (this._personalityProviderWorker) { + return this._personalityProviderWorker; + } + + this._personalityProviderWorker = new lazy.BasePromiseWorker( + "resource://activity-stream/lib/PersonalityProvider/PersonalityProvider.worker.mjs", + { type: "module" } + ); + + return this._personalityProviderWorker; + } + + get baseAttachmentsURL() { + // Returning a promise, so we can have an async getter. + return this._getBaseAttachmentsURL(); + } + + async _getBaseAttachmentsURL() { + if (this._baseAttachmentsURL) { + return this._baseAttachmentsURL; + } + const server = lazy.Utils.SERVER_URL; + const serverInfo = await ( + await fetch(`${server}/`, { + credentials: "omit", + }) + ).json(); + const { + capabilities: { + attachments: { base_url }, + }, + } = serverInfo; + this._baseAttachmentsURL = base_url; + return this._baseAttachmentsURL; + } + + setup() { + this.setupSyncAttachment(RECIPE_NAME); + this.setupSyncAttachment(MODELS_NAME); + } + + teardown() { + this.teardownSyncAttachment(RECIPE_NAME); + this.teardownSyncAttachment(MODELS_NAME); + if (this._personalityProviderWorker) { + this._personalityProviderWorker.terminate(); + } + } + + setupSyncAttachment(collection) { + lazy.RemoteSettings(collection).on("sync", this.onSync); + } + + teardownSyncAttachment(collection) { + lazy.RemoteSettings(collection).off("sync", this.onSync); + } + + onSync(event) { + this.personalityProviderWorker.post("onSync", [event]); + } + + /** + * Gets contents of the attachment if it already exists on file, + * and if not attempts to download it. + */ + getAttachment(record) { + return this.personalityProviderWorker.post("getAttachment", [record]); + } + + /** + * Returns a Recipe from remote settings to be consumed by a RecipeExecutor. + * A Recipe is a set of instructions on how to processes a RecipeExecutor. + */ + async getRecipe() { + if (!this.recipes || !this.recipes.length) { + const result = await lazy.RemoteSettings(RECIPE_NAME).get(); + this.recipes = await Promise.all( + result.map(async record => ({ + ...(await this.getAttachment(record)), + recordKey: record.key, + })) + ); + } + return this.recipes[0]; + } + + /** + * Grabs a slice of browse history for building a interest vector + */ + async fetchHistory(columns, beginTimeSecs, endTimeSecs) { + let sql = `SELECT url, title, visit_count, frecency, last_visit_date, description + FROM moz_places + WHERE last_visit_date >= ${beginTimeSecs * 1000000} + AND last_visit_date < ${endTimeSecs * 1000000}`; + columns.forEach(requiredColumn => { + sql += ` AND IFNULL(${requiredColumn}, '') <> ''`; + }); + sql += " LIMIT 30000"; + + const { activityStreamProvider } = lazy.NewTabUtils; + const history = await activityStreamProvider.executePlacesQuery(sql, { + columns, + params: {}, + }); + + return history; + } + + /** + * Handles setup and metrics of history fetch. + */ + async getHistory() { + let endTimeSecs = new Date().getTime() / 1000; + let beginTimeSecs = endTimeSecs - this.interestConfig.history_limit_secs; + if ( + !this.interestConfig || + !this.interestConfig.history_required_fields || + !this.interestConfig.history_required_fields.length + ) { + return []; + } + let history = await this.fetchHistory( + this.interestConfig.history_required_fields, + beginTimeSecs, + endTimeSecs + ); + + return history; + } + + async setBaseAttachmentsURL() { + await this.personalityProviderWorker.post("setBaseAttachmentsURL", [ + await this.baseAttachmentsURL, + ]); + } + + async setInterestConfig() { + this.interestConfig = this.interestConfig || (await this.getRecipe()); + await this.personalityProviderWorker.post("setInterestConfig", [ + this.interestConfig, + ]); + } + + async setInterestVector() { + await this.personalityProviderWorker.post("setInterestVector", [ + this.interestVector, + ]); + } + + async fetchModels() { + const models = await lazy.RemoteSettings(MODELS_NAME).get(); + return this.personalityProviderWorker.post("fetchModels", [models]); + } + + async generateTaggers() { + await this.personalityProviderWorker.post("generateTaggers", [ + this.modelKeys, + ]); + } + + async generateRecipeExecutor() { + await this.personalityProviderWorker.post("generateRecipeExecutor"); + } + + async createInterestVector() { + const history = await this.getHistory(); + + const interestVectorResult = await this.personalityProviderWorker.post( + "createInterestVector", + [history] + ); + + return interestVectorResult; + } + + async init(callback) { + await this.setBaseAttachmentsURL(); + await this.setInterestConfig(); + if (!this.interestConfig) { + return; + } + + // We always generate a recipe executor, no cache used here. + // This is because the result of this is an object with + // functions (taggers) so storing it in cache is not possible. + // Thus we cannot use it to rehydrate anything. + const fetchModelsResult = await this.fetchModels(); + // If this fails, log an error and return. + if (!fetchModelsResult.ok) { + return; + } + await this.generateTaggers(); + await this.generateRecipeExecutor(); + + // If we don't have a cached vector, create a new one. + if (!this.interestVector) { + const interestVectorResult = await this.createInterestVector(); + // If that failed, log an error and return. + if (!interestVectorResult.ok) { + return; + } + this.interestVector = interestVectorResult.interestVector; + } + + // This happens outside the createInterestVector call above, + // because create can be skipped if rehydrating from cache. + // In that case, the interest vector is provided and not created, so we just set it. + await this.setInterestVector(); + + this.initialized = true; + if (callback) { + callback(); + } + } + + async calculateItemRelevanceScore(pocketItem) { + if (!this.initialized) { + return pocketItem.item_score || 1; + } + const itemRelevanceScore = await this.personalityProviderWorker.post( + "calculateItemRelevanceScore", + [pocketItem] + ); + if (!itemRelevanceScore) { + return -1; + } + const { scorableItem, rankingVector } = itemRelevanceScore; + // Put the results on the item for debugging purposes. + pocketItem.scorableItem = scorableItem; + pocketItem.rankingVector = rankingVector; + return rankingVector.score; + } + + /** + * Returns an object holding the personalization scores of this provider instance. + */ + getScores() { + return { + // We cannot return taggers here. + // What we return here goes into persistent cache, and taggers have functions on it. + // If we attempted to save taggers into persistent cache, it would store it to disk, + // and the next time we load it, it would start thowing function is not defined. + interestConfig: this.interestConfig, + interestVector: this.interestVector, + }; + } +} diff --git a/browser/components/newtab/lib/PersonalityProvider/PersonalityProvider.worker.mjs b/browser/components/newtab/lib/PersonalityProvider/PersonalityProvider.worker.mjs new file mode 100644 index 0000000000..49797f4f2b --- /dev/null +++ b/browser/components/newtab/lib/PersonalityProvider/PersonalityProvider.worker.mjs @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { PersonalityProviderWorker } from "resource://activity-stream/lib/PersonalityProvider/PersonalityProviderWorkerClass.mjs"; + +import { PromiseWorker } from "resource://gre/modules/workers/PromiseWorker.mjs"; + +const personalityProviderWorker = new PersonalityProviderWorker(); + +// This is boiler plate worker stuff that connects it to the main thread PromiseWorker. +const worker = new PromiseWorker.AbstractWorker(); +worker.dispatch = function (method, args = []) { + return personalityProviderWorker[method](...args); +}; +worker.postMessage = function (message, ...transfers) { + self.postMessage(message, ...transfers); +}; +worker.close = function () { + self.close(); +}; + +self.addEventListener("message", msg => worker.handleMessage(msg)); +self.addEventListener("unhandledrejection", function (error) { + throw error.reason; +}); diff --git a/browser/components/newtab/lib/PersonalityProvider/PersonalityProviderWorkerClass.mjs b/browser/components/newtab/lib/PersonalityProvider/PersonalityProviderWorkerClass.mjs new file mode 100644 index 0000000000..372c061dd9 --- /dev/null +++ b/browser/components/newtab/lib/PersonalityProvider/PersonalityProviderWorkerClass.mjs @@ -0,0 +1,306 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { + tokenize, + toksToTfIdfVector, +} from "resource://activity-stream/lib/PersonalityProvider/Tokenize.mjs"; +import { NaiveBayesTextTagger } from "resource://activity-stream/lib/PersonalityProvider/NaiveBayesTextTagger.mjs"; +import { NmfTextTagger } from "resource://activity-stream/lib/PersonalityProvider/NmfTextTagger.mjs"; +import { RecipeExecutor } from "resource://activity-stream/lib/PersonalityProvider/RecipeExecutor.mjs"; + +// A helper function to create a hash out of a file. +async function _getFileHash(filepath) { + const data = await IOUtils.read(filepath); + // File is an instance of Uint8Array + const digest = await crypto.subtle.digest("SHA-256", data); + const uint8 = new Uint8Array(digest); + // return the two-digit hexadecimal code for a byte + const toHex = b => b.toString(16).padStart(2, "0"); + return Array.from(uint8, toHex).join(""); +} + +/** + * V2 provider builds and ranks an interest profile (also called an “interest vector”) off the browse history. + * This allows Firefox to classify pages into topics, by examining the text found on the page. + * It does this by looking at the history text content, title, and description. + */ +export class PersonalityProviderWorker { + async getPersonalityProviderDir() { + const personalityProviderDir = PathUtils.join( + await PathUtils.getLocalProfileDir(), + "personality-provider" + ); + + // Cache this so we don't need to await again. + this.getPersonalityProviderDir = () => + Promise.resolve(personalityProviderDir); + return personalityProviderDir; + } + + setBaseAttachmentsURL(url) { + this.baseAttachmentsURL = url; + } + + setInterestConfig(interestConfig) { + this.interestConfig = interestConfig; + } + + setInterestVector(interestVector) { + this.interestVector = interestVector; + } + + onSync(event) { + const { + data: { created, updated, deleted }, + } = event; + // Remove every removed attachment. + const toRemove = deleted.concat(updated.map(u => u.old)); + toRemove.forEach(record => this.deleteAttachment(record)); + + // Download every new/updated attachment. + const toDownload = created.concat(updated.map(u => u.new)); + // maybeDownloadAttachment is async but we don't care inside onSync. + toDownload.forEach(record => this.maybeDownloadAttachment(record)); + } + + /** + * Attempts to download the attachment, but only if it doesn't already exist. + */ + async maybeDownloadAttachment(record, retries = 3) { + const { + attachment: { filename, hash, size }, + } = record; + await IOUtils.makeDirectory(await this.getPersonalityProviderDir()); + const localFilePath = PathUtils.join( + await this.getPersonalityProviderDir(), + filename + ); + + let retry = 0; + while ( + retry++ < retries && + // exists is an issue for perf because I might not need to call it. + (!(await IOUtils.exists(localFilePath)) || + (await IOUtils.stat(localFilePath)).size !== size || + (await _getFileHash(localFilePath)) !== hash) + ) { + await this._downloadAttachment(record); + } + } + + /** + * Downloads the attachment to disk assuming the dir already exists + * and any existing files matching the filename are clobbered. + */ + async _downloadAttachment(record) { + const { + attachment: { location: loc, filename }, + } = record; + const remoteFilePath = this.baseAttachmentsURL + loc; + const localFilePath = PathUtils.join( + await this.getPersonalityProviderDir(), + filename + ); + + const xhr = new XMLHttpRequest(); + // Set false here for a synchronous request, because we're in a worker. + xhr.open("GET", remoteFilePath, false); + xhr.setRequestHeader("Accept-Encoding", "gzip"); + xhr.responseType = "arraybuffer"; + xhr.withCredentials = false; + xhr.send(null); + + if (xhr.status !== 200) { + console.error(`Failed to fetch ${remoteFilePath}: ${xhr.statusText}`); + return; + } + + const buffer = xhr.response; + const bytes = new Uint8Array(buffer); + + await IOUtils.write(localFilePath, bytes, { + tmpPath: `${localFilePath}.tmp`, + }); + } + + async deleteAttachment(record) { + const { + attachment: { filename }, + } = record; + await IOUtils.makeDirectory(await this.getPersonalityProviderDir()); + const path = PathUtils.join( + await this.getPersonalityProviderDir(), + filename + ); + + await IOUtils.remove(path, { ignoreAbsent: true }); + // Cleanup the directory if it is empty, do nothing if it is not empty. + try { + await IOUtils.remove(await this.getPersonalityProviderDir(), { + ignoreAbsent: true, + }); + } catch (e) { + // This is likely because the directory is not empty, so we don't care. + } + } + + /** + * Gets contents of the attachment if it already exists on file, + * and if not attempts to download it. + */ + async getAttachment(record) { + const { + attachment: { filename }, + } = record; + const filepath = PathUtils.join( + await this.getPersonalityProviderDir(), + filename + ); + + try { + await this.maybeDownloadAttachment(record); + return await IOUtils.readJSON(filepath); + } catch (error) { + console.error(`Failed to load ${filepath}: ${error.message}`); + } + return {}; + } + + async fetchModels(models) { + this.models = await Promise.all( + models.map(async record => ({ + ...(await this.getAttachment(record)), + recordKey: record.key, + })) + ); + if (!this.models.length) { + return { + ok: false, + }; + } + return { + ok: true, + }; + } + + generateTaggers(modelKeys) { + if (!this.taggers) { + let nbTaggers = []; + let nmfTaggers = {}; + + for (let model of this.models) { + if (!modelKeys.includes(model.recordKey)) { + continue; + } + if (model.model_type === "nb") { + nbTaggers.push(new NaiveBayesTextTagger(model, toksToTfIdfVector)); + } else if (model.model_type === "nmf") { + nmfTaggers[model.parent_tag] = new NmfTextTagger( + model, + toksToTfIdfVector + ); + } + } + this.taggers = { nbTaggers, nmfTaggers }; + } + } + + /** + * Sets and generates a Recipe Executor. + * A Recipe Executor is a set of actions that can be consumed by a Recipe. + * The Recipe determines the order and specifics of which the actions are called. + */ + generateRecipeExecutor() { + const recipeExecutor = new RecipeExecutor( + this.taggers.nbTaggers, + this.taggers.nmfTaggers, + tokenize + ); + this.recipeExecutor = recipeExecutor; + } + + /** + * Examines the user's browse history and returns an interest vector that + * describes the topics the user frequently browses. + */ + createInterestVector(historyObj) { + let interestVector = {}; + + for (let historyRec of historyObj) { + let ivItem = this.recipeExecutor.executeRecipe( + historyRec, + this.interestConfig.history_item_builder + ); + if (ivItem === null) { + continue; + } + interestVector = this.recipeExecutor.executeCombinerRecipe( + interestVector, + ivItem, + this.interestConfig.interest_combiner + ); + if (interestVector === null) { + return null; + } + } + + const finalResult = this.recipeExecutor.executeRecipe( + interestVector, + this.interestConfig.interest_finalizer + ); + + return { + ok: true, + interestVector: finalResult, + }; + } + + /** + * Calculates a score of a Pocket item when compared to the user's interest + * vector. Returns the score. Higher scores are better. Assumes this.interestVector + * is populated. + */ + calculateItemRelevanceScore(pocketItem) { + const { personalization_models } = pocketItem; + let scorableItem; + + // If the server provides some models, we can just use them, + // and skip generating them. + if (personalization_models && Object.keys(personalization_models).length) { + scorableItem = { + id: pocketItem.id, + item_tags: personalization_models, + item_score: pocketItem.item_score, + item_sort_id: 1, + }; + } else { + scorableItem = this.recipeExecutor.executeRecipe( + pocketItem, + this.interestConfig.item_to_rank_builder + ); + if (scorableItem === null) { + return null; + } + } + + // We're doing a deep copy on an object. + let rankingVector = JSON.parse(JSON.stringify(this.interestVector)); + + Object.keys(scorableItem).forEach(key => { + rankingVector[key] = scorableItem[key]; + }); + + rankingVector = this.recipeExecutor.executeRecipe( + rankingVector, + this.interestConfig.item_ranker + ); + + if (rankingVector === null) { + return null; + } + + return { scorableItem, rankingVector }; + } +} diff --git a/browser/components/newtab/lib/PersonalityProvider/RecipeExecutor.mjs b/browser/components/newtab/lib/PersonalityProvider/RecipeExecutor.mjs new file mode 100644 index 0000000000..4f420c0812 --- /dev/null +++ b/browser/components/newtab/lib/PersonalityProvider/RecipeExecutor.mjs @@ -0,0 +1,1119 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * RecipeExecutor is the core feature engineering pipeline for the in-browser + * personalization work. These pipelines are called "recipes". A recipe is an + * array of objects that define a "step" in the recipe. A step is simply an + * object with a field "function" that specifies what is being done in the step + * along with other fields that are semantically defined for that step. + * + * There are two types of recipes "builder" recipes and "combiner" recipes. Builder + * recipes mutate an object until it matches some set of critera. Combiner + * recipes take two objects, (a "left" and a "right"), and specify the steps + * to merge the right object into the left object. + * + * A short nonsense example recipe is: + * [ {"function": "get_url_domain", "path_length": 1, "field": "url", "dest": "url_domain"}, + * {"function": "nb_tag", "fields": ["title", "description"]}, + * {"function": "conditionally_nmf_tag", "fields": ["title", "description"]} ] + * + * Recipes are sandboxed by the fact that the step functions must be explicitly + * allowed. Functions allowed for builder recipes are specifed in the + * RecipeExecutor.ITEM_BUILDER_REGISTRY, while combiner functions are allowed + * in RecipeExecutor.ITEM_COMBINER_REGISTRY . + */ +export class RecipeExecutor { + constructor(nbTaggers, nmfTaggers, tokenize) { + this.ITEM_BUILDER_REGISTRY = { + nb_tag: this.naiveBayesTag, + conditionally_nmf_tag: this.conditionallyNmfTag, + accept_item_by_field_value: this.acceptItemByFieldValue, + tokenize_url: this.tokenizeUrl, + get_url_domain: this.getUrlDomain, + tokenize_field: this.tokenizeField, + copy_value: this.copyValue, + keep_top_k: this.keepTopK, + scalar_multiply: this.scalarMultiply, + elementwise_multiply: this.elementwiseMultiply, + vector_multiply: this.vectorMultiply, + scalar_add: this.scalarAdd, + vector_add: this.vectorAdd, + make_boolean: this.makeBoolean, + allow_fields: this.allowFields, + filter_by_value: this.filterByValue, + l2_normalize: this.l2Normalize, + prob_normalize: this.probNormalize, + set_default: this.setDefault, + lookup_value: this.lookupValue, + copy_to_map: this.copyToMap, + scalar_multiply_tag: this.scalarMultiplyTag, + apply_softmax_tags: this.applySoftmaxTags, + }; + this.ITEM_COMBINER_REGISTRY = { + combiner_add: this.combinerAdd, + combiner_max: this.combinerMax, + combiner_collect_values: this.combinerCollectValues, + }; + this.nbTaggers = nbTaggers; + this.nmfTaggers = nmfTaggers; + this.tokenize = tokenize; + } + + /** + * Determines the type of a field. Valid types are: + * string + * number + * array + * map (strings to anything) + */ + _typeOf(data) { + let t = typeof data; + if (t === "object") { + if (data === null) { + return "null"; + } + if (Array.isArray(data)) { + return "array"; + } + return "map"; + } + return t; + } + + /** + * Returns a scalar, either because it was a constant, or by + * looking it up from the item. Allows for a default value if the lookup + * fails. + */ + _lookupScalar(item, k, dfault) { + if (this._typeOf(k) === "number") { + return k; + } else if ( + this._typeOf(k) === "string" && + k in item && + this._typeOf(item[k]) === "number" + ) { + return item[k]; + } + return dfault; + } + + /** + * Simply appends all the strings from a set fields together. If the field + * is a list, then the cells of the list are append. + */ + _assembleText(item, fields) { + let textArr = []; + for (let field of fields) { + if (field in item) { + let type = this._typeOf(item[field]); + if (type === "string") { + textArr.push(item[field]); + } else if (type === "array") { + for (let ele of item[field]) { + textArr.push(String(ele)); + } + } else { + textArr.push(String(item[field])); + } + } + } + return textArr.join(" "); + } + + /** + * Runs the naive bayes text taggers over a set of text fields. Stores the + * results in new fields: + * nb_tags: a map of text strings to probabilites + * nb_tokens: the tokenized text that was tagged + * + * Config: + * fields: an array containing a list of fields to concatenate and tag + */ + naiveBayesTag(item, config) { + let text = this._assembleText(item, config.fields); + let tokens = this.tokenize(text); + let tags = {}; + let extended_tags = {}; + + for (let nbTagger of this.nbTaggers) { + let result = nbTagger.tagTokens(tokens); + if (result.label !== null && result.confident) { + extended_tags[result.label] = result; + tags[result.label] = Math.exp(result.logProb); + } + } + item.nb_tags = tags; + item.nb_tags_extended = extended_tags; + item.nb_tokens = tokens; + return item; + } + + /** + * Selectively runs NMF text taggers depending on which tags were found + * by the naive bayes taggers. Writes the results in into new fields: + * nmf_tags_parent_weights: map of pareent tags to probabilites of those parent tags + * nmf_tags: map of strings to maps of strings to probabilities + * nmf_tags_parent map of child tags to parent tags + * + * Config: + * Not configurable + */ + conditionallyNmfTag(item, config) { + let nestedNmfTags = {}; + let parentTags = {}; + let parentWeights = {}; + + if (!("nb_tags" in item) || !("nb_tokens" in item)) { + return null; + } + + Object.keys(item.nb_tags).forEach(parentTag => { + let nmfTagger = this.nmfTaggers[parentTag]; + if (nmfTagger !== undefined) { + nestedNmfTags[parentTag] = {}; + parentWeights[parentTag] = item.nb_tags[parentTag]; + let nmfTags = nmfTagger.tagTokens(item.nb_tokens); + Object.keys(nmfTags).forEach(nmfTag => { + nestedNmfTags[parentTag][nmfTag] = nmfTags[nmfTag]; + parentTags[nmfTag] = parentTag; + }); + } + }); + + item.nmf_tags = nestedNmfTags; + item.nmf_tags_parent = parentTags; + item.nmf_tags_parent_weights = parentWeights; + + return item; + } + + /** + * Checks a field's value against another value (either from another field + * or a constant). If the test passes, then the item is emitted, otherwise + * the pipeline is aborted. + * + * Config: + * field Field to read the value to test. Left side of operator. + * op one of ==, !=, <, <=, >, >= + * rhsValue Constant value to compare against. Right side of operator. + * rhsField Field to read value to compare against. Right side of operator. + * + * NOTE: rhsValue takes precidence over rhsField. + */ + acceptItemByFieldValue(item, config) { + if (!(config.field in item)) { + return null; + } + let rhs = null; + if ("rhsValue" in config) { + rhs = config.rhsValue; + } else if ("rhsField" in config && config.rhsField in item) { + rhs = item[config.rhsField]; + } + if (rhs === null) { + return null; + } + + if ( + // eslint-disable-next-line eqeqeq + (config.op === "==" && item[config.field] == rhs) || + // eslint-disable-next-line eqeqeq + (config.op === "!=" && item[config.field] != rhs) || + (config.op === "<" && item[config.field] < rhs) || + (config.op === "<=" && item[config.field] <= rhs) || + (config.op === ">" && item[config.field] > rhs) || + (config.op === ">=" && item[config.field] >= rhs) + ) { + return item; + } + + return null; + } + + /** + * Splits a URL into text-like tokens. + * + * Config: + * field Field containing a URL + * dest Field to write the tokens to as an array of strings + * + * NOTE: Any initial 'www' on the hostname is removed. + */ + tokenizeUrl(item, config) { + if (!(config.field in item)) { + return null; + } + + let url = new URL(item[config.field]); + let domain = url.hostname; + if (domain.startsWith("www.")) { + domain = domain.substring(4); + } + let toks = this.tokenize(domain); + let pathToks = this.tokenize( + decodeURIComponent(url.pathname.replace(/\+/g, " ")) + ); + for (let tok of pathToks) { + toks.push(tok); + } + for (let pair of url.searchParams.entries()) { + let k = this.tokenize(decodeURIComponent(pair[0].replace(/\+/g, " "))); + for (let tok of k) { + toks.push(tok); + } + if (pair[1] !== null && pair[1] !== "") { + let v = this.tokenize(decodeURIComponent(pair[1].replace(/\+/g, " "))); + for (let tok of v) { + toks.push(tok); + } + } + } + item[config.dest] = toks; + + return item; + } + + /** + * Gets the hostname (minus any initial "www." along with the left most + * directories on the path. + * + * Config: + * field Field containing the URL + * dest Field to write the array of strings to + * path_length OPTIONAL (DEFAULT: 0) Number of leftmost subdirectories to include + */ + getUrlDomain(item, config) { + if (!(config.field in item)) { + return null; + } + + let url = new URL(item[config.field]); + let domain = url.hostname.toLocaleLowerCase(); + if (domain.startsWith("www.")) { + domain = domain.substring(4); + } + item[config.dest] = domain; + let pathLength = 0; + if ("path_length" in config) { + pathLength = config.path_length; + } + if (pathLength > 0) { + item[config.dest] += url.pathname + .toLocaleLowerCase() + .split("/") + .slice(0, pathLength + 1) + .join("/"); + } + + return item; + } + + /** + * Splits a field into tokens. + * Config: + * field Field containing a string to tokenize + * dest Field to write the array of strings to + */ + tokenizeField(item, config) { + if (!(config.field in item)) { + return null; + } + + item[config.dest] = this.tokenize(item[config.field]); + + return item; + } + + /** + * Deep copy from one field to another. + * Config: + * src Field to read from + * dest Field to write to + */ + copyValue(item, config) { + if (!(config.src in item)) { + return null; + } + + item[config.dest] = JSON.parse(JSON.stringify(item[config.src])); + + return item; + } + + /** + * Converts a field containing a map of strings to a map of strings + * to numbers, to a map of strings to numbers containing at most k elements. + * This operation is performed by first, promoting all the subkeys up one + * level, and then taking the top (or bottom) k values. + * + * Config: + * field Points to a map of strings to a map of strings to numbers + * k Maximum number of items to keep + * descending OPTIONAL (DEFAULT: True) Sorts score in descending order + * (i.e. keeps maximum) + */ + keepTopK(item, config) { + if (!(config.field in item)) { + return null; + } + let k = this._lookupScalar(item, config.k, 1048576); + let descending = !("descending" in config) || config.descending !== false; + + // we can't sort by the values in the map, so we have to convert this + // to an array, and then sort. + let sortable = []; + Object.keys(item[config.field]).forEach(outerKey => { + let innerType = this._typeOf(item[config.field][outerKey]); + if (innerType === "map") { + Object.keys(item[config.field][outerKey]).forEach(innerKey => { + sortable.push({ + key: innerKey, + value: item[config.field][outerKey][innerKey], + }); + }); + } else { + sortable.push({ key: outerKey, value: item[config.field][outerKey] }); + } + }); + + sortable.sort((a, b) => { + if (descending) { + return b.value - a.value; + } + return a.value - b.value; + }); + + // now take the top k + let newMap = {}; + let i = 0; + for (let pair of sortable) { + if (i >= k) { + break; + } + newMap[pair.key] = pair.value; + i++; + } + item[config.field] = newMap; + + return item; + } + + /** + * Scalar multiplies a vector by some constant + * + * Config: + * field Points to: + * a map of strings to numbers + * an array of numbers + * a number + * k Either a number, or a string. If it's a number then This + * is the scalar value to multiply by. If it's a string, + * the value in the pointed to field is used. + * default OPTIONAL (DEFAULT: 0), If k is a string, and no numeric + * value is found, then use this value. + */ + scalarMultiply(item, config) { + if (!(config.field in item)) { + return null; + } + let k = this._lookupScalar(item, config.k, config.dfault); + + let fieldType = this._typeOf(item[config.field]); + if (fieldType === "number") { + item[config.field] *= k; + } else if (fieldType === "array") { + for (let i = 0; i < item[config.field].length; i++) { + item[config.field][i] *= k; + } + } else if (fieldType === "map") { + Object.keys(item[config.field]).forEach(key => { + item[config.field][key] *= k; + }); + } else { + return null; + } + + return item; + } + + /** + * Elementwise multiplies either two maps or two arrays together, storing + * the result in left. If left and right are of the same type, results in an + * error. + * + * Maps are special case. For maps the left must be a nested map such as: + * { k1: { k11: 1, k12: 2}, k2: { k21: 3, k22: 4 } } and right needs to be + * simple map such as: { k1: 5, k2: 6} . The operation is then to mulitply + * every value of every right key, to every value every subkey where the + * parent keys match. Using the previous examples, the result would be: + * { k1: { k11: 5, k12: 10 }, k2: { k21: 18, k22: 24 } } . + * + * Config: + * left + * right + */ + elementwiseMultiply(item, config) { + if (!(config.left in item) || !(config.right in item)) { + return null; + } + let leftType = this._typeOf(item[config.left]); + if (leftType !== this._typeOf(item[config.right])) { + return null; + } + if (leftType === "array") { + if (item[config.left].length !== item[config.right].length) { + return null; + } + for (let i = 0; i < item[config.left].length; i++) { + item[config.left][i] *= item[config.right][i]; + } + } else if (leftType === "map") { + Object.keys(item[config.left]).forEach(outerKey => { + let r = 0.0; + if (outerKey in item[config.right]) { + r = item[config.right][outerKey]; + } + Object.keys(item[config.left][outerKey]).forEach(innerKey => { + item[config.left][outerKey][innerKey] *= r; + }); + }); + } else if (leftType === "number") { + item[config.left] *= item[config.right]; + } else { + return null; + } + + return item; + } + + /** + * Vector multiplies (i.e. dot products) two vectors and stores the result in + * third field. Both vectors must either by maps, or arrays of numbers with + * the same length. + * + * Config: + * left A field pointing to either a map of strings to numbers, + * or an array of numbers + * right A field pointing to either a map of strings to numbers, + * or an array of numbers + * dest The field to store the dot product. + */ + vectorMultiply(item, config) { + if (!(config.left in item) || !(config.right in item)) { + return null; + } + + let leftType = this._typeOf(item[config.left]); + if (leftType !== this._typeOf(item[config.right])) { + return null; + } + + let destVal = 0.0; + if (leftType === "array") { + if (item[config.left].length !== item[config.right].length) { + return null; + } + for (let i = 0; i < item[config.left].length; i++) { + destVal += item[config.left][i] * item[config.right][i]; + } + } else if (leftType === "map") { + Object.keys(item[config.left]).forEach(key => { + if (key in item[config.right]) { + destVal += item[config.left][key] * item[config.right][key]; + } + }); + } else { + return null; + } + + item[config.dest] = destVal; + return item; + } + + /** + * Adds a constant value to all elements in the field. Mathematically, + * this is the same as taking a 1-vector, scalar multiplying it by k, + * and then vector adding it to a field. + * + * Config: + * field A field pointing to either a map of strings to numbers, + * or an array of numbers + * k Either a number, or a string. If it's a number then This + * is the scalar value to multiply by. If it's a string, + * the value in the pointed to field is used. + * default OPTIONAL (DEFAULT: 0), If k is a string, and no numeric + * value is found, then use this value. + */ + scalarAdd(item, config) { + let k = this._lookupScalar(item, config.k, config.dfault); + if (!(config.field in item)) { + return null; + } + + let fieldType = this._typeOf(item[config.field]); + if (fieldType === "array") { + for (let i = 0; i < item[config.field].length; i++) { + item[config.field][i] += k; + } + } else if (fieldType === "map") { + Object.keys(item[config.field]).forEach(key => { + item[config.field][key] += k; + }); + } else if (fieldType === "number") { + item[config.field] += k; + } else { + return null; + } + + return item; + } + + /** + * Adds two vectors together and stores the result in left. + * + * Config: + * left A field pointing to either a map of strings to numbers, + * or an array of numbers + * right A field pointing to either a map of strings to numbers, + * or an array of numbers + */ + vectorAdd(item, config) { + if (!(config.left in item)) { + return this.copyValue(item, { src: config.right, dest: config.left }); + } + if (!(config.right in item)) { + return null; + } + + let leftType = this._typeOf(item[config.left]); + if (leftType !== this._typeOf(item[config.right])) { + return null; + } + if (leftType === "array") { + if (item[config.left].length !== item[config.right].length) { + return null; + } + for (let i = 0; i < item[config.left].length; i++) { + item[config.left][i] += item[config.right][i]; + } + return item; + } else if (leftType === "map") { + Object.keys(item[config.right]).forEach(key => { + let v = 0; + if (key in item[config.left]) { + v = item[config.left][key]; + } + item[config.left][key] = v + item[config.right][key]; + }); + return item; + } + + return null; + } + + /** + * Converts a vector from real values to boolean integers. (i.e. either 1/0 + * or 1/-1). + * + * Config: + * field Field containing either a map of strings to numbers or + * an array of numbers to convert. + * threshold OPTIONAL (DEFAULT: 0) Values above this will be replaced + * with 1.0. Those below will be converted to 0. + * keep_negative OPTIONAL (DEFAULT: False) If true, values below the + * threshold will be converted to -1 instead of 0. + */ + makeBoolean(item, config) { + if (!(config.field in item)) { + return null; + } + let threshold = this._lookupScalar(item, config.threshold, 0.0); + let type = this._typeOf(item[config.field]); + if (type === "array") { + for (let i = 0; i < item[config.field].length; i++) { + if (item[config.field][i] > threshold) { + item[config.field][i] = 1.0; + } else if (config.keep_negative) { + item[config.field][i] = -1.0; + } else { + item[config.field][i] = 0.0; + } + } + } else if (type === "map") { + Object.keys(item[config.field]).forEach(key => { + let value = item[config.field][key]; + if (value > threshold) { + item[config.field][key] = 1.0; + } else if (config.keep_negative) { + item[config.field][key] = -1.0; + } else { + item[config.field][key] = 0.0; + } + }); + } else if (type === "number") { + let value = item[config.field]; + if (value > threshold) { + item[config.field] = 1.0; + } else if (config.keep_negative) { + item[config.field] = -1.0; + } else { + item[config.field] = 0.0; + } + } else { + return null; + } + + return item; + } + + /** + * Removes all keys from the item except for the ones specified. + * + * fields An array of strings indicating the fields to keep + */ + allowFields(item, config) { + let newItem = {}; + for (let ele of config.fields) { + if (ele in item) { + newItem[ele] = item[ele]; + } + } + return newItem; + } + + /** + * Removes all keys whose value does not exceed some threshold. + * + * Config: + * field Points to a map of strings to numbers + * threshold Values must exceed this value, otherwise they are removed. + */ + filterByValue(item, config) { + if (!(config.field in item)) { + return null; + } + let threshold = this._lookupScalar(item, config.threshold, 0.0); + let filtered = {}; + Object.keys(item[config.field]).forEach(key => { + let value = item[config.field][key]; + if (value > threshold) { + filtered[key] = value; + } + }); + item[config.field] = filtered; + + return item; + } + + /** + * Rewrites a field so that its values are now L2 normed. + * + * Config: + * field Points to a map of strings to numbers, or an array of numbers + */ + l2Normalize(item, config) { + if (!(config.field in item)) { + return null; + } + let data = item[config.field]; + let type = this._typeOf(data); + if (type === "array") { + let norm = 0.0; + for (let datum of data) { + norm += datum * datum; + } + norm = Math.sqrt(norm); + if (norm !== 0) { + for (let i = 0; i < data.length; i++) { + data[i] /= norm; + } + } + } else if (type === "map") { + let norm = 0.0; + Object.keys(data).forEach(key => { + norm += data[key] * data[key]; + }); + norm = Math.sqrt(norm); + if (norm !== 0) { + Object.keys(data).forEach(key => { + data[key] /= norm; + }); + } + } else { + return null; + } + + item[config.field] = data; + + return item; + } + + /** + * Rewrites a field so that all of its values sum to 1.0 + * + * Config: + * field Points to a map of strings to numbers, or an array of numbers + */ + probNormalize(item, config) { + if (!(config.field in item)) { + return null; + } + let data = item[config.field]; + let type = this._typeOf(data); + if (type === "array") { + let norm = 0.0; + for (let datum of data) { + norm += datum; + } + if (norm !== 0) { + for (let i = 0; i < data.length; i++) { + data[i] /= norm; + } + } + } else if (type === "map") { + let norm = 0.0; + Object.keys(item[config.field]).forEach(key => { + norm += item[config.field][key]; + }); + if (norm !== 0) { + Object.keys(item[config.field]).forEach(key => { + item[config.field][key] /= norm; + }); + } + } else { + return null; + } + + return item; + } + + /** + * Stores a value, if it is not already present + * + * Config: + * field field to write to if it is missing + * value value to store in that field + */ + setDefault(item, config) { + let val = this._lookupScalar(item, config.value, config.value); + if (!(config.field in item)) { + item[config.field] = val; + } + + return item; + } + + /** + * Selctively promotes an value from an inner map up to the outer map + * + * Config: + * haystack Points to a map of strings to values + * needle Key inside the map we should promote up + * dest Where we should write the value of haystack[needle] + */ + lookupValue(item, config) { + if (config.haystack in item && config.needle in item[config.haystack]) { + item[config.dest] = item[config.haystack][config.needle]; + } + + return item; + } + + /** + * Demotes a field into a map + * + * Config: + * src Field to copy + * dest_map Points to a map + * dest_key Key inside dest_map to copy src to + */ + copyToMap(item, config) { + if (config.src in item) { + if (!(config.dest_map in item)) { + item[config.dest_map] = {}; + } + item[config.dest_map][config.dest_key] = item[config.src]; + } + + return item; + } + + /** + * Config: + * field Points to a string to number map + * k Scalar to multiply the values by + * log_scale Boolean, if true, then the values will be transformed + * by a logrithm prior to multiplications + */ + scalarMultiplyTag(item, config) { + let EPSILON = 0.000001; + if (!(config.field in item)) { + return null; + } + let k = this._lookupScalar(item, config.k, 1); + let type = this._typeOf(item[config.field]); + if (type === "map") { + Object.keys(item[config.field]).forEach(parentKey => { + Object.keys(item[config.field][parentKey]).forEach(key => { + let v = item[config.field][parentKey][key]; + if (config.log_scale) { + v = Math.log(v + EPSILON); + } + item[config.field][parentKey][key] = v * k; + }); + }); + } else { + return null; + } + + return item; + } + + /** + * Independently applies softmax across all subtags. + * + * Config: + * field Points to a map of strings with values being another map of strings + */ + applySoftmaxTags(item, config) { + let type = this._typeOf(item[config.field]); + if (type !== "map") { + return null; + } + + let abort = false; + let softmaxSum = {}; + Object.keys(item[config.field]).forEach(tag => { + if (this._typeOf(item[config.field][tag]) !== "map") { + abort = true; + return; + } + if (abort) { + return; + } + softmaxSum[tag] = 0; + Object.keys(item[config.field][tag]).forEach(subtag => { + if (this._typeOf(item[config.field][tag][subtag]) !== "number") { + abort = true; + return; + } + let score = item[config.field][tag][subtag]; + softmaxSum[tag] += Math.exp(score); + }); + }); + if (abort) { + return null; + } + + Object.keys(item[config.field]).forEach(tag => { + Object.keys(item[config.field][tag]).forEach(subtag => { + item[config.field][tag][subtag] = + Math.exp(item[config.field][tag][subtag]) / softmaxSum[tag]; + }); + }); + + return item; + } + + /** + * Vector adds a field and stores the result in left. + * + * Config: + * field The field to vector add + */ + combinerAdd(left, right, config) { + if (!(config.field in right)) { + return left; + } + let type = this._typeOf(right[config.field]); + if (!(config.field in left)) { + if (type === "map") { + left[config.field] = {}; + } else if (type === "array") { + left[config.field] = []; + } else if (type === "number") { + left[config.field] = 0; + } else { + return null; + } + } + if (type !== this._typeOf(left[config.field])) { + return null; + } + if (type === "map") { + Object.keys(right[config.field]).forEach(key => { + if (!(key in left[config.field])) { + left[config.field][key] = 0; + } + left[config.field][key] += right[config.field][key]; + }); + } else if (type === "array") { + for (let i = 0; i < right[config.field].length; i++) { + if (i < left[config.field].length) { + left[config.field][i] += right[config.field][i]; + } else { + left[config.field].push(right[config.field][i]); + } + } + } else if (type === "number") { + left[config.field] += right[config.field]; + } else { + return null; + } + + return left; + } + + /** + * Stores the maximum value of the field in left. + * + * Config: + * field The field to vector add + */ + combinerMax(left, right, config) { + if (!(config.field in right)) { + return left; + } + let type = this._typeOf(right[config.field]); + if (!(config.field in left)) { + if (type === "map") { + left[config.field] = {}; + } else if (type === "array") { + left[config.field] = []; + } else if (type === "number") { + left[config.field] = 0; + } else { + return null; + } + } + if (type !== this._typeOf(left[config.field])) { + return null; + } + if (type === "map") { + Object.keys(right[config.field]).forEach(key => { + if ( + !(key in left[config.field]) || + right[config.field][key] > left[config.field][key] + ) { + left[config.field][key] = right[config.field][key]; + } + }); + } else if (type === "array") { + for (let i = 0; i < right[config.field].length; i++) { + if (i < left[config.field].length) { + if (left[config.field][i] < right[config.field][i]) { + left[config.field][i] = right[config.field][i]; + } + } else { + left[config.field].push(right[config.field][i]); + } + } + } else if (type === "number") { + if (left[config.field] < right[config.field]) { + left[config.field] = right[config.field]; + } + } else { + return null; + } + + return left; + } + + /** + * Associates a value in right with another value in right. This association + * is then stored in a map in left. + * + * For example: If a sequence of rights is: + * { 'tags': {}, 'url_domain': 'maseratiusa.com/maserati', 'time': 41 } + * { 'tags': {}, 'url_domain': 'mbusa.com/mercedes', 'time': 21 } + * { 'tags': {}, 'url_domain': 'maseratiusa.com/maserati', 'time': 34 } + * + * Then assuming a 'sum' operation, left can build a map that would look like: + * { + * 'maseratiusa.com/maserati': 75, + * 'mbusa.com/mercedes': 21, + * } + * + * Fields: + * left_field field in the left to store / update the map + * right_key_field Field in the right to use as a key + * right_value_field Field in the right to use as a value + * operation One of "sum", "max", "overwrite", "count" + */ + combinerCollectValues(left, right, config) { + let op; + if (config.operation === "sum") { + op = (a, b) => a + b; + } else if (config.operation === "max") { + op = (a, b) => (a > b ? a : b); + } else if (config.operation === "overwrite") { + op = (a, b) => b; + } else if (config.operation === "count") { + op = (a, b) => a + 1; + } else { + return null; + } + if (!(config.left_field in left)) { + left[config.left_field] = {}; + } + if ( + !(config.right_key_field in right) || + !(config.right_value_field in right) + ) { + return left; + } + + let key = right[config.right_key_field]; + let rightValue = right[config.right_value_field]; + let leftValue = 0.0; + if (key in left[config.left_field]) { + leftValue = left[config.left_field][key]; + } + + left[config.left_field][key] = op(leftValue, rightValue); + + return left; + } + + /** + * Executes a recipe. Returns an object on success, or null on failure. + */ + executeRecipe(item, recipe) { + let newItem = item; + if (recipe) { + for (let step of recipe) { + let op = this.ITEM_BUILDER_REGISTRY[step.function]; + if (op === undefined) { + return null; + } + newItem = op.call(this, newItem, step); + if (newItem === null) { + break; + } + } + } + return newItem; + } + + /** + * Executes a recipe. Returns an object on success, or null on failure. + */ + executeCombinerRecipe(item1, item2, recipe) { + let newItem1 = item1; + for (let step of recipe) { + let op = this.ITEM_COMBINER_REGISTRY[step.function]; + if (op === undefined) { + return null; + } + newItem1 = op.call(this, newItem1, item2, step); + if (newItem1 === null) { + break; + } + } + + return newItem1; + } +} diff --git a/browser/components/newtab/lib/PersonalityProvider/Tokenize.mjs b/browser/components/newtab/lib/PersonalityProvider/Tokenize.mjs new file mode 100644 index 0000000000..740b2fc541 --- /dev/null +++ b/browser/components/newtab/lib/PersonalityProvider/Tokenize.mjs @@ -0,0 +1,83 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Unicode specifies certain mnemonics for code pages and character classes. +// They call them "character properties" https://en.wikipedia.org/wiki/Unicode_character_property . +// These mnemonics are have been adopted by many regular expression libraries, +// however the standard Javascript regexp system doesn't support unicode +// character properties, so we have to define these ourself. +// +// Each of these sections contains the characters values / ranges for specific +// character property: Whitespace, Symbol (S), Punctuation (P), Number (N), +// Mark (M), and Letter (L). +const UNICODE_SPACE = + "\x20\xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000"; +const UNICODE_SYMBOL = + "\\x24\\x2B\x3C-\x3E\\x5E\x60\\x7C\x7E\xA2-\xA6\xA8\xA9\xAC\xAE-\xB1\xB4\xB8\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u02FF\u0375\u0384\u0385\u03F6\u0482\u058D-\u058F\u0606-\u0608\u060B\u060E\u060F\u06DE\u06E9\u06FD\u06FE\u07F6\u09F2\u09F3\u09FA\u09FB\u0AF1\u0B70\u0BF3-\u0BFA\u0C7F\u0D4F\u0D79\u0E3F\u0F01-\u0F03\u0F13\u0F15-\u0F17\u0F1A-\u0F1F\u0F34\u0F36\u0F38\u0FBE-\u0FC5\u0FC7-\u0FCC\u0FCE\u0FCF\u0FD5-\u0FD8\u109E\u109F\u1390-\u1399\u17DB\u1940\u19DE-\u19FF\u1B61-\u1B6A\u1B74-\u1B7C\u1FBD\u1FBF-\u1FC1\u1FCD-\u1FCF\u1FDD-\u1FDF\u1FED-\u1FEF\u1FFD\u1FFE\u2044\u2052\u207A-\u207C\u208A-\u208C\u20A0-\u20BE\u2100\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F\u218A\u218B\u2190-\u2307\u230C-\u2328\u232B-\u23FE\u2400-\u2426\u2440-\u244A\u249C-\u24E9\u2500-\u2767\u2794-\u27C4\u27C7-\u27E5\u27F0-\u2982\u2999-\u29D7\u29DC-\u29FB\u29FE-\u2B73\u2B76-\u2B95\u2B98-\u2BB9\u2BBD-\u2BC8\u2BCA-\u2BD1\u2BEC-\u2BEF\u2CE5-\u2CEA\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFB\u3004\u3012\u3013\u3020\u3036\u3037\u303E\u303F\u309B\u309C\u3190\u3191\u3196-\u319F\u31C0-\u31E3\u3200-\u321E\u322A-\u3247\u3250\u3260-\u327F\u328A-\u32B0\u32C0-\u32FE\u3300-\u33FF\u4DC0-\u4DFF\uA490-\uA4C6\uA700-\uA716\uA720\uA721\uA789\uA78A\uA828-\uA82B\uA836-\uA839\uAA77-\uAA79\uAB5B\uFB29\uFBB2-\uFBC1\uFDFC\uFDFD\uFE62\uFE64-\uFE66\uFE69\uFF04\uFF0B\uFF1C-\uFF1E\uFF3E\uFF40\uFF5C\uFF5E\uFFE0-\uFFE6\uFFE8-\uFFEE\uFFFC\uFFFD"; +const UNICODE_PUNCT = + "\x21-\x23\x25-\\x2A\x2C-\x2F\x3A\x3B\\x3F\x40\\x5B-\\x5D\x5F\\x7B\x7D\xA1\xA7\xAB\xB6\xB7\xBB\xBF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u0AF0\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166D\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E44\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65"; + +const UNICODE_NUMBER = + "0-9\xB2\xB3\xB9\xBC-\xBE\u0660-\u0669\u06F0-\u06F9\u07C0-\u07C9\u0966-\u096F\u09E6-\u09EF\u09F4-\u09F9\u0A66-\u0A6F\u0AE6-\u0AEF\u0B66-\u0B6F\u0B72-\u0B77\u0BE6-\u0BF2\u0C66-\u0C6F\u0C78-\u0C7E\u0CE6-\u0CEF\u0D58-\u0D5E\u0D66-\u0D78\u0DE6-\u0DEF\u0E50-\u0E59\u0ED0-\u0ED9\u0F20-\u0F33\u1040-\u1049\u1090-\u1099\u1369-\u137C\u16EE-\u16F0\u17E0-\u17E9\u17F0-\u17F9\u1810-\u1819\u1946-\u194F\u19D0-\u19DA\u1A80-\u1A89\u1A90-\u1A99\u1B50-\u1B59\u1BB0-\u1BB9\u1C40-\u1C49\u1C50-\u1C59\u2070\u2074-\u2079\u2080-\u2089\u2150-\u2182\u2185-\u2189\u2460-\u249B\u24EA-\u24FF\u2776-\u2793\u2CFD\u3007\u3021-\u3029\u3038-\u303A\u3192-\u3195\u3220-\u3229\u3248-\u324F\u3251-\u325F\u3280-\u3289\u32B1-\u32BF\uA620-\uA629\uA6E6-\uA6EF\uA830-\uA835\uA8D0-\uA8D9\uA900-\uA909\uA9D0-\uA9D9\uA9F0-\uA9F9\uAA50-\uAA59\uABF0-\uABF9\uFF10-\uFF19"; +const UNICODE_MARK = + "\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D4-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C03\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D01-\u0D03\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF8\u1CF9\u1DC0-\u1DF5\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F"; +const UNICODE_LETTER = + "A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC"; + +const REGEXP_SPLITS = new RegExp( + `[${UNICODE_SPACE}${UNICODE_SYMBOL}${UNICODE_PUNCT}]+` +); +// Match all token characters, so okay for regex to split multiple code points +// eslint-disable-next-line no-misleading-character-class +const REGEXP_ALPHANUMS = new RegExp( + `^[${UNICODE_NUMBER}${UNICODE_MARK}${UNICODE_LETTER}]+$` +); + +/** + * Downcases the text, and splits it into consecutive alphanumeric characters. + * This is locale aware, and so will not strip accents. This uses "word + * breaks", and os is not appropriate for languages without them + * (e.g. Chinese). + */ +export function tokenize(text) { + return text + .toLocaleLowerCase() + .split(REGEXP_SPLITS) + .filter(tok => tok.match(REGEXP_ALPHANUMS)); +} + +/** + * Converts a sequence of tokens into an L2 normed TF-IDF. Any terms that are + * not preindexed (i.e. does have a computed inverse document frequency) will + * be dropped. + */ +export function toksToTfIdfVector(tokens, vocab_idfs) { + let tfidfs = {}; + + // calcualte the term frequencies + for (let tok of tokens) { + if (!(tok in vocab_idfs)) { + continue; + } + if (!(tok in tfidfs)) { + tfidfs[tok] = [vocab_idfs[tok][0], 1]; + } else { + tfidfs[tok][1]++; + } + } + + // now multiply by the log inverse document frequencies, then take + // the L2 norm of this. + let l2Norm = 0.0; + Object.keys(tfidfs).forEach(tok => { + tfidfs[tok][1] *= vocab_idfs[tok][1]; + l2Norm += tfidfs[tok][1] * tfidfs[tok][1]; + }); + l2Norm = Math.sqrt(l2Norm); + Object.keys(tfidfs).forEach(tok => { + tfidfs[tok][1] /= l2Norm; + }); + + return tfidfs; +} diff --git a/browser/components/newtab/lib/PlacesFeed.sys.mjs b/browser/components/newtab/lib/PlacesFeed.sys.mjs new file mode 100644 index 0000000000..70011412f8 --- /dev/null +++ b/browser/components/newtab/lib/PlacesFeed.sys.mjs @@ -0,0 +1,572 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { + actionCreators as ac, + actionTypes as at, + actionUtils as au, +} from "resource://activity-stream/common/Actions.sys.mjs"; + +import { shortURL } from "resource://activity-stream/lib/ShortURL.sys.mjs"; + +// We use importESModule here instead of static import so that +// the Karma test environment won't choke on this module. This +// is because the Karma test environment already stubs out +// AboutNewTab, and overrides importESModule to be a no-op (which +// can't be done for a static import statement). + +// eslint-disable-next-line mozilla/use-static-import +const { AboutNewTab } = ChromeUtils.importESModule( + "resource:///modules/AboutNewTab.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", + NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", + PartnerLinkAttribution: "resource:///modules/PartnerLinkAttribution.sys.mjs", + pktApi: "chrome://pocket/content/pktApi.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", +}); + +const LINK_BLOCKED_EVENT = "newtab-linkBlocked"; +const PLACES_LINKS_CHANGED_DELAY_TIME = 1000; // time in ms to delay timer for places links changed events + +// The pref to store the blocked sponsors of the sponsored Top Sites. +// The value of this pref is an array (JSON serialized) of hostnames of the +// blocked sponsors. +const TOP_SITES_BLOCKED_SPONSORS_PREF = "browser.topsites.blockedSponsors"; + +/** + * PlacesObserver - observes events from PlacesUtils.observers + */ +class PlacesObserver { + constructor(dispatch) { + this.dispatch = dispatch; + this.QueryInterface = ChromeUtils.generateQI(["nsISupportsWeakReference"]); + this.handlePlacesEvent = this.handlePlacesEvent.bind(this); + } + + handlePlacesEvent(events) { + const removedPages = []; + const removedBookmarks = []; + + for (const { + itemType, + source, + dateAdded, + guid, + title, + url, + isRemovedFromStore, + isTagging, + type, + } of events) { + switch (type) { + case "history-cleared": + this.dispatch({ type: at.PLACES_HISTORY_CLEARED }); + break; + case "page-removed": + if (isRemovedFromStore) { + removedPages.push(url); + } + break; + case "bookmark-added": + // Skips items that are not bookmarks (like folders), about:* pages or + // default bookmarks, added when the profile is created. + if ( + isTagging || + itemType !== lazy.PlacesUtils.bookmarks.TYPE_BOOKMARK || + source === lazy.PlacesUtils.bookmarks.SOURCES.IMPORT || + source === lazy.PlacesUtils.bookmarks.SOURCES.RESTORE || + source === lazy.PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP || + source === lazy.PlacesUtils.bookmarks.SOURCES.SYNC || + (!url.startsWith("http://") && !url.startsWith("https://")) + ) { + return; + } + + this.dispatch({ type: at.PLACES_LINKS_CHANGED }); + this.dispatch({ + type: at.PLACES_BOOKMARK_ADDED, + data: { + bookmarkGuid: guid, + bookmarkTitle: title, + dateAdded: dateAdded * 1000, + url, + }, + }); + break; + case "bookmark-removed": + if ( + isTagging || + (itemType === lazy.PlacesUtils.bookmarks.TYPE_BOOKMARK && + source !== lazy.PlacesUtils.bookmarks.SOURCES.IMPORT && + source !== lazy.PlacesUtils.bookmarks.SOURCES.RESTORE && + source !== + lazy.PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP && + source !== lazy.PlacesUtils.bookmarks.SOURCES.SYNC) + ) { + removedBookmarks.push(url); + } + break; + } + } + + if (removedPages.length || removedBookmarks.length) { + this.dispatch({ type: at.PLACES_LINKS_CHANGED }); + } + + if (removedPages.length) { + this.dispatch({ + type: at.PLACES_LINKS_DELETED, + data: { urls: removedPages }, + }); + } + + if (removedBookmarks.length) { + this.dispatch({ + type: at.PLACES_BOOKMARKS_REMOVED, + data: { urls: removedBookmarks }, + }); + } + } +} + +export class PlacesFeed { + constructor() { + this.placesChangedTimer = null; + this.customDispatch = this.customDispatch.bind(this); + this.placesObserver = new PlacesObserver(this.customDispatch); + } + + addObservers() { + lazy.PlacesUtils.observers.addListener( + ["bookmark-added", "bookmark-removed", "history-cleared", "page-removed"], + this.placesObserver.handlePlacesEvent + ); + + Services.obs.addObserver(this, LINK_BLOCKED_EVENT); + } + + /** + * setTimeout - A custom function that creates an nsITimer that can be cancelled + * + * @param {func} callback A function to be executed after the timer expires + * @param {int} delay The time (in ms) the timer should wait before the function is executed + */ + setTimeout(callback, delay) { + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback(callback, delay, Ci.nsITimer.TYPE_ONE_SHOT); + return timer; + } + + customDispatch(action) { + // If we are changing many links at once, delay this action and only dispatch + // one action at the end + if (action.type === at.PLACES_LINKS_CHANGED) { + if (this.placesChangedTimer) { + this.placesChangedTimer.delay = PLACES_LINKS_CHANGED_DELAY_TIME; + } else { + this.placesChangedTimer = this.setTimeout(() => { + this.placesChangedTimer = null; + this.store.dispatch(ac.OnlyToMain(action)); + }, PLACES_LINKS_CHANGED_DELAY_TIME); + } + } else { + // To avoid blocking Places notifications on expensive work, run it at the + // next tick of the events loop. + Services.tm.dispatchToMainThread(() => + this.store.dispatch(ac.BroadcastToContent(action)) + ); + } + } + + removeObservers() { + if (this.placesChangedTimer) { + this.placesChangedTimer.cancel(); + this.placesChangedTimer = null; + } + lazy.PlacesUtils.observers.removeListener( + ["bookmark-added", "bookmark-removed", "history-cleared", "page-removed"], + this.placesObserver.handlePlacesEvent + ); + Services.obs.removeObserver(this, LINK_BLOCKED_EVENT); + } + + /** + * observe - An observer for the LINK_BLOCKED_EVENT. + * Called when a link is blocked. + * Links can be blocked outside of newtab, + * which is why we need to listen to this + * on such a generic level. + * + * @param {null} subject + * @param {str} topic The name of the event + * @param {str} value The data associated with the event + */ + observe(subject, topic, value) { + if (topic === LINK_BLOCKED_EVENT) { + this.store.dispatch( + ac.BroadcastToContent({ + type: at.PLACES_LINK_BLOCKED, + data: { url: value }, + }) + ); + } + } + + /** + * Open a link in a desired destination defaulting to action's event. + */ + openLink(action, where = "", isPrivate = false) { + const params = { + private: isPrivate, + targetBrowser: action._target.browser, + forceForeground: false, // This ensure we maintain user preference for how to open new tabs. + globalHistoryOptions: { + triggeringSponsoredURL: action.data.sponsored_tile_id + ? action.data.url + : undefined, + }, + }; + + // Always include the referrer (even for http links) if we have one + const { event, referrer, typedBonus } = action.data; + if (referrer) { + const ReferrerInfo = Components.Constructor( + "@mozilla.org/referrer-info;1", + "nsIReferrerInfo", + "init" + ); + params.referrerInfo = new ReferrerInfo( + Ci.nsIReferrerInfo.UNSAFE_URL, + true, + Services.io.newURI(referrer) + ); + } + + // Pocket gives us a special reader URL to open their stories in + const urlToOpen = + action.data.type === "pocket" ? action.data.open_url : action.data.url; + + try { + let uri = Services.io.newURI(urlToOpen); + if (!["http", "https"].includes(uri.scheme)) { + throw new Error( + `Can't open link using ${uri.scheme} protocol from the new tab page.` + ); + } + } catch (e) { + console.error(e); + return; + } + + // Mark the page as typed for frecency bonus before opening the link + if (typedBonus) { + lazy.PlacesUtils.history.markPageAsTyped(Services.io.newURI(urlToOpen)); + } + + const win = action._target.browser.ownerGlobal; + win.openTrustedLinkIn( + urlToOpen, + where || win.whereToOpenLink(event), + params + ); + + // If there's an original URL e.g. using the unprocessed %YYYYMMDDHH% tag, + // add a visit for that so it may become a frecent top site. + if (action.data.original_url) { + lazy.PlacesUtils.history.insert({ + url: action.data.original_url, + visits: [{ transition: lazy.PlacesUtils.history.TRANSITION_TYPED }], + }); + } + } + + async saveToPocket(site, browser) { + const sendToPocket = + lazy.NimbusFeatures.pocketNewtab.getVariable("sendToPocket"); + // An experiment to send the user directly to Pocket's signup page. + if (sendToPocket && !lazy.pktApi.isUserLoggedIn()) { + const pocketNewtabExperiment = lazy.ExperimentAPI.getExperiment({ + featureId: "pocketNewtab", + }); + const pocketSiteHost = Services.prefs.getStringPref( + "extensions.pocket.site" + ); // getpocket.com + let utmSource = "firefox_newtab_save_button"; + // We want to know if the user is in a Pocket newtab related experiment. + let utmCampaign = pocketNewtabExperiment?.slug; + let utmContent = pocketNewtabExperiment?.branch?.slug; + + const url = new URL(`https://${pocketSiteHost}/signup`); + url.searchParams.append("utm_source", utmSource); + if (utmCampaign && utmContent) { + url.searchParams.append("utm_campaign", utmCampaign); + url.searchParams.append("utm_content", utmContent); + } + + const win = browser.ownerGlobal; + win.openTrustedLinkIn(url.href, "tab"); + return; + } + + const { url, title } = site; + try { + let data = await lazy.NewTabUtils.activityStreamLinks.addPocketEntry( + url, + title, + browser + ); + if (data) { + this.store.dispatch( + ac.BroadcastToContent({ + type: at.PLACES_SAVED_TO_POCKET, + data: { + url, + open_url: data.item.open_url, + title, + pocket_id: data.item.item_id, + }, + }) + ); + } + } catch (err) { + console.error(err); + } + } + + /** + * Deletes an item from a user's saved to Pocket feed + * @param {int} itemID + * The unique ID given by Pocket for that item; used to look the item up when deleting + */ + async deleteFromPocket(itemID) { + try { + await lazy.NewTabUtils.activityStreamLinks.deletePocketEntry(itemID); + this.store.dispatch({ type: at.POCKET_LINK_DELETED_OR_ARCHIVED }); + } catch (err) { + console.error(err); + } + } + + /** + * Archives an item from a user's saved to Pocket feed + * @param {int} itemID + * The unique ID given by Pocket for that item; used to look the item up when archiving + */ + async archiveFromPocket(itemID) { + try { + await lazy.NewTabUtils.activityStreamLinks.archivePocketEntry(itemID); + this.store.dispatch({ type: at.POCKET_LINK_DELETED_OR_ARCHIVED }); + } catch (err) { + console.error(err); + } + } + + /** + * Sends an attribution request for Top Sites interactions. + * @param {object} data + * Attribution paramters from a Top Site. + */ + makeAttributionRequest(data) { + let args = Object.assign( + { + campaignID: Services.prefs.getStringPref( + "browser.partnerlink.campaign.topsites" + ), + }, + data + ); + lazy.PartnerLinkAttribution.makeRequest(args); + } + + async fillSearchTopSiteTerm({ _target, data }) { + const searchEngine = await Services.search.getEngineByAlias(data.label); + _target.browser.ownerGlobal.gURLBar.search(data.label, { + searchEngine, + searchModeEntry: "topsites_newtab", + }); + } + + _getDefaultSearchEngine(isPrivateWindow) { + return Services.search[ + isPrivateWindow ? "defaultPrivateEngine" : "defaultEngine" + ]; + } + + handoffSearchToAwesomebar(action) { + const { _target, data, meta } = action; + const searchEngine = this._getDefaultSearchEngine( + lazy.PrivateBrowsingUtils.isBrowserPrivate(_target.browser) + ); + const urlBar = _target.browser.ownerGlobal.gURLBar; + let isFirstChange = true; + + const newtabSession = AboutNewTab.activityStream.store.feeds + .get("feeds.telemetry") + ?.sessions.get(au.getPortIdOfSender(action)); + if (!data || !data.text) { + urlBar.setHiddenFocus(); + } else { + urlBar.handoff(data.text, searchEngine, newtabSession?.session_id); + isFirstChange = false; + } + + const checkFirstChange = () => { + // Check if this is the first change since we hidden focused. If it is, + // remove hidden focus styles, prepend the search alias and hide the + // in-content search. + if (isFirstChange) { + isFirstChange = false; + urlBar.removeHiddenFocus(true); + urlBar.handoff("", searchEngine, newtabSession?.session_id); + this.store.dispatch( + ac.OnlyToOneContent({ type: at.DISABLE_SEARCH }, meta.fromTarget) + ); + urlBar.removeEventListener("compositionstart", checkFirstChange); + urlBar.removeEventListener("paste", checkFirstChange); + } + }; + + const onKeydown = ev => { + // Check if the keydown will cause a value change. + if (ev.key.length === 1 && !ev.altKey && !ev.ctrlKey && !ev.metaKey) { + checkFirstChange(); + } + // If the Esc button is pressed, we are done. Show in-content search and cleanup. + if (ev.key === "Escape") { + onDone(); // eslint-disable-line no-use-before-define + } + }; + + const onDone = ev => { + // We are done. Show in-content search again and cleanup. + this.store.dispatch( + ac.OnlyToOneContent({ type: at.SHOW_SEARCH }, meta.fromTarget) + ); + + const forceSuppressFocusBorder = ev?.type === "mousedown"; + urlBar.removeHiddenFocus(forceSuppressFocusBorder); + + urlBar.removeEventListener("keydown", onKeydown); + urlBar.removeEventListener("mousedown", onDone); + urlBar.removeEventListener("blur", onDone); + urlBar.removeEventListener("compositionstart", checkFirstChange); + urlBar.removeEventListener("paste", checkFirstChange); + }; + + urlBar.addEventListener("keydown", onKeydown); + urlBar.addEventListener("mousedown", onDone); + urlBar.addEventListener("blur", onDone); + urlBar.addEventListener("compositionstart", checkFirstChange); + urlBar.addEventListener("paste", checkFirstChange); + } + + /** + * Add the hostnames of the given urls to the Top Sites sponsor blocklist. + * + * @param {array} urls + * An array of the objects structured as `{ url }` + */ + addToBlockedTopSitesSponsors(urls) { + const blockedPref = JSON.parse( + Services.prefs.getStringPref(TOP_SITES_BLOCKED_SPONSORS_PREF, "[]") + ); + const merged = new Set([...blockedPref, ...urls.map(url => shortURL(url))]); + + Services.prefs.setStringPref( + TOP_SITES_BLOCKED_SPONSORS_PREF, + JSON.stringify([...merged]) + ); + } + + onAction(action) { + switch (action.type) { + case at.INIT: + // Briefly avoid loading services for observing for better startup timing + Services.tm.dispatchToMainThread(() => this.addObservers()); + break; + case at.UNINIT: + this.removeObservers(); + break; + case at.ABOUT_SPONSORED_TOP_SITES: { + const url = `${Services.urlFormatter.formatURLPref( + "app.support.baseURL" + )}sponsor-privacy`; + const win = action._target.browser.ownerGlobal; + win.openTrustedLinkIn(url, "tab"); + break; + } + case at.BLOCK_URL: { + if (action.data) { + let sponsoredTopSites = []; + action.data.forEach(site => { + const { url, pocket_id, isSponsoredTopSite } = site; + lazy.NewTabUtils.activityStreamLinks.blockURL({ url, pocket_id }); + if (isSponsoredTopSite) { + sponsoredTopSites.push({ url }); + } + }); + if (sponsoredTopSites.length) { + this.addToBlockedTopSitesSponsors(sponsoredTopSites); + } + } + break; + } + case at.BOOKMARK_URL: + lazy.NewTabUtils.activityStreamLinks.addBookmark( + action.data, + action._target.browser.ownerGlobal + ); + break; + case at.DELETE_BOOKMARK_BY_ID: + lazy.NewTabUtils.activityStreamLinks.deleteBookmark(action.data); + break; + case at.DELETE_HISTORY_URL: { + const { url, forceBlock, pocket_id } = action.data; + lazy.NewTabUtils.activityStreamLinks.deleteHistoryEntry(url); + if (forceBlock) { + lazy.NewTabUtils.activityStreamLinks.blockURL({ url, pocket_id }); + } + break; + } + case at.OPEN_NEW_WINDOW: + this.openLink(action, "window"); + break; + case at.OPEN_PRIVATE_WINDOW: + this.openLink(action, "window", true); + break; + case at.SAVE_TO_POCKET: + this.saveToPocket(action.data.site, action._target.browser); + break; + case at.DELETE_FROM_POCKET: + this.deleteFromPocket(action.data.pocket_id); + break; + case at.ARCHIVE_FROM_POCKET: + this.archiveFromPocket(action.data.pocket_id); + break; + case at.FILL_SEARCH_TERM: + this.fillSearchTopSiteTerm(action); + break; + case at.HANDOFF_SEARCH_TO_AWESOMEBAR: + this.handoffSearchToAwesomebar(action); + break; + case at.OPEN_LINK: { + this.openLink(action); + break; + } + case at.PARTNER_LINK_ATTRIBUTION: + this.makeAttributionRequest(action.data); + break; + } + } +} + +// Exported for testing only +PlacesFeed.PlacesObserver = PlacesObserver; diff --git a/browser/components/newtab/lib/PrefsFeed.sys.mjs b/browser/components/newtab/lib/PrefsFeed.sys.mjs new file mode 100644 index 0000000000..1c6f9b0d45 --- /dev/null +++ b/browser/components/newtab/lib/PrefsFeed.sys.mjs @@ -0,0 +1,273 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { + actionCreators as ac, + actionTypes as at, +} from "resource://activity-stream/common/Actions.sys.mjs"; +import { Prefs } from "resource://activity-stream/lib/ActivityStreamPrefs.sys.mjs"; + +// We use importESModule here instead of static import so that +// the Karma test environment won't choke on this module. This +// is because the Karma test environment already stubs out +// AppConstants, and overrides importESModule to be a no-op (which +// can't be done for a static import statement). + +// eslint-disable-next-line mozilla/use-static-import +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", + Region: "resource://gre/modules/Region.sys.mjs", +}); + +export class PrefsFeed { + constructor(prefMap) { + this._prefMap = prefMap; + this._prefs = new Prefs(); + this.onExperimentUpdated = this.onExperimentUpdated.bind(this); + this.onPocketExperimentUpdated = this.onPocketExperimentUpdated.bind(this); + } + + onPrefChanged(name, value) { + const prefItem = this._prefMap.get(name); + if (prefItem) { + let action = "BroadcastToContent"; + if (prefItem.skipBroadcast) { + action = "OnlyToMain"; + if (prefItem.alsoToPreloaded) { + action = "AlsoToPreloaded"; + } + } + + this.store.dispatch( + ac[action]({ + type: at.PREF_CHANGED, + data: { name, value }, + }) + ); + } + } + + _setStringPref(values, key, defaultValue) { + this._setPref(values, key, defaultValue, Services.prefs.getStringPref); + } + + _setBoolPref(values, key, defaultValue) { + this._setPref(values, key, defaultValue, Services.prefs.getBoolPref); + } + + _setIntPref(values, key, defaultValue) { + this._setPref(values, key, defaultValue, Services.prefs.getIntPref); + } + + _setPref(values, key, defaultValue, getPrefFunction) { + let value = getPrefFunction( + `browser.newtabpage.activity-stream.${key}`, + defaultValue + ); + values[key] = value; + this._prefMap.set(key, { value }); + } + + /** + * Handler for when experiment data updates. + */ + onExperimentUpdated(event, reason) { + const value = lazy.NimbusFeatures.newtab.getAllVariables() || {}; + this.store.dispatch( + ac.BroadcastToContent({ + type: at.PREF_CHANGED, + data: { + name: "featureConfig", + value, + }, + }) + ); + } + + /** + * Handler for Pocket specific experiment data updates. + */ + onPocketExperimentUpdated(event, reason) { + const value = lazy.NimbusFeatures.pocketNewtab.getAllVariables() || {}; + // Loaded experiments are set up inside init() + if ( + reason !== "feature-experiment-loaded" && + reason !== "feature-rollout-loaded" + ) { + this.store.dispatch( + ac.BroadcastToContent({ + type: at.PREF_CHANGED, + data: { + name: "pocketConfig", + value, + }, + }) + ); + } + } + + init() { + this._prefs.observeBranch(this); + lazy.NimbusFeatures.newtab.onUpdate(this.onExperimentUpdated); + lazy.NimbusFeatures.pocketNewtab.onUpdate(this.onPocketExperimentUpdated); + + this._storage = this.store.dbStorage.getDbTable("sectionPrefs"); + + // Get the initial value of each activity stream pref + const values = {}; + for (const name of this._prefMap.keys()) { + values[name] = this._prefs.get(name); + } + + // These are not prefs, but are needed to determine stuff in content that can only be + // computed in main process + values.isPrivateBrowsingEnabled = lazy.PrivateBrowsingUtils.enabled; + values.platform = AppConstants.platform; + + // Save the geo pref if we have it + if (lazy.Region.home) { + values.region = lazy.Region.home; + this.geo = values.region; + } else if (this.geo !== "") { + // Watch for geo changes and use a dummy value for now + Services.obs.addObserver(this, lazy.Region.REGION_TOPIC); + this.geo = ""; + } + + // Get the firefox accounts url for links and to send firstrun metrics to. + values.fxa_endpoint = Services.prefs.getStringPref( + "browser.newtabpage.activity-stream.fxaccounts.endpoint", + "https://accounts.firefox.com" + ); + + // Get the firefox update channel with values as default, nightly, beta or release + values.appUpdateChannel = Services.prefs.getStringPref( + "app.update.channel", + "" + ); + + // Read the pref for search shortcuts top sites experiment from firefox.js and store it + // in our internal list of prefs to watch + let searchTopSiteExperimentPrefValue = Services.prefs.getBoolPref( + "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts" + ); + values["improvesearch.topSiteSearchShortcuts"] = + searchTopSiteExperimentPrefValue; + this._prefMap.set("improvesearch.topSiteSearchShortcuts", { + value: searchTopSiteExperimentPrefValue, + }); + + values.mayHaveSponsoredTopSites = Services.prefs.getBoolPref( + "browser.topsites.useRemoteSetting" + ); + + // Read the pref for search hand-off from firefox.js and store it + // in our internal list of prefs to watch + let handoffToAwesomebarPrefValue = Services.prefs.getBoolPref( + "browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar" + ); + values["improvesearch.handoffToAwesomebar"] = handoffToAwesomebarPrefValue; + this._prefMap.set("improvesearch.handoffToAwesomebar", { + value: handoffToAwesomebarPrefValue, + }); + + // Add experiment values and default values + values.featureConfig = lazy.NimbusFeatures.newtab.getAllVariables() || {}; + values.pocketConfig = + lazy.NimbusFeatures.pocketNewtab.getAllVariables() || {}; + this._setBoolPref(values, "logowordmark.alwaysVisible", false); + this._setBoolPref(values, "feeds.section.topstories", false); + this._setBoolPref(values, "discoverystream.enabled", false); + this._setBoolPref( + values, + "discoverystream.sponsored-collections.enabled", + false + ); + this._setBoolPref(values, "discoverystream.isCollectionDismissible", false); + this._setBoolPref(values, "discoverystream.hardcoded-basic-layout", false); + this._setBoolPref(values, "discoverystream.personalization.enabled", false); + this._setBoolPref(values, "discoverystream.personalization.override"); + this._setStringPref( + values, + "discoverystream.personalization.modelKeys", + "" + ); + this._setStringPref(values, "discoverystream.spocs-endpoint", ""); + this._setStringPref(values, "discoverystream.spocs-endpoint-query", ""); + this._setStringPref(values, "newNewtabExperience.colors", ""); + + // Set the initial state of all prefs in redux + this.store.dispatch( + ac.BroadcastToContent({ + type: at.PREFS_INITIAL_VALUES, + data: values, + meta: { + isStartup: true, + }, + }) + ); + } + + uninit() { + this.removeListeners(); + } + + removeListeners() { + this._prefs.ignoreBranch(this); + lazy.NimbusFeatures.newtab.offUpdate(this.onExperimentUpdated); + lazy.NimbusFeatures.pocketNewtab.offUpdate(this.onPocketExperimentUpdated); + if (this.geo === "") { + Services.obs.removeObserver(this, lazy.Region.REGION_TOPIC); + } + } + + async _setIndexedDBPref(id, value) { + const name = id === "topsites" ? id : `feeds.section.${id}`; + try { + await this._storage.set(name, value); + } catch (e) { + console.error("Could not set section preferences."); + } + } + + observe(subject, topic, data) { + switch (topic) { + case lazy.Region.REGION_TOPIC: + this.store.dispatch( + ac.BroadcastToContent({ + type: at.PREF_CHANGED, + data: { name: "region", value: lazy.Region.home }, + }) + ); + break; + } + } + + onAction(action) { + switch (action.type) { + case at.INIT: + this.init(); + break; + case at.UNINIT: + this.uninit(); + break; + case at.CLEAR_PREF: + Services.prefs.clearUserPref(this._prefs._branchStr + action.data.name); + break; + case at.SET_PREF: + this._prefs.set(action.data.name, action.data.value); + break; + case at.UPDATE_SECTION_PREFS: + this._setIndexedDBPref(action.data.id, action.data.value); + break; + } + } +} diff --git a/browser/components/newtab/lib/RecommendationProvider.sys.mjs b/browser/components/newtab/lib/RecommendationProvider.sys.mjs new file mode 100644 index 0000000000..03e976544f --- /dev/null +++ b/browser/components/newtab/lib/RecommendationProvider.sys.mjs @@ -0,0 +1,291 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + PersistentCache: "resource://activity-stream/lib/PersistentCache.sys.mjs", + PersonalityProvider: + "resource://activity-stream/lib/PersonalityProvider/PersonalityProvider.sys.mjs", +}); + +import { + actionTypes as at, + actionCreators as ac, +} from "resource://activity-stream/common/Actions.sys.mjs"; + +const CACHE_KEY = "personalization"; +const PREF_PERSONALIZATION_MODEL_KEYS = + "discoverystream.personalization.modelKeys"; +const PREF_USER_TOPSTORIES = "feeds.section.topstories"; +const PREF_SYSTEM_TOPSTORIES = "feeds.system.topstories"; +const PREF_PERSONALIZATION = "discoverystream.personalization.enabled"; +const MIN_PERSONALIZATION_UPDATE_TIME = 12 * 60 * 60 * 1000; // 12 hours +const PREF_PERSONALIZATION_OVERRIDE = + "discoverystream.personalization.override"; + +// The main purpose of this class is to handle interactions with the recommendation provider. +// A recommendation provider scores a list of stories, currently this is a personality provider. +// So all calls to the provider, anything involved with the setup of the provider, +// accessing prefs for the provider, or updaing devtools with provider state, is contained in here. +export class RecommendationProvider { + constructor() { + // Persistent cache for remote endpoint data. + this.cache = new lazy.PersistentCache(CACHE_KEY, true); + } + + async setProvider(isStartup = false, scores) { + // A provider is already set. This can happen when new stories come in + // and we need to update their scores. + // We can use the existing one, a fresh one is created after startup. + // Using the existing one might be a bit out of date, + // but it's fine for now. We can rely on restarts for updates. + // See bug 1629931 for improvements to this. + if (!this.provider) { + this.provider = new lazy.PersonalityProvider(this.modelKeys); + this.provider.setScores(scores); + } + + if (this.provider && this.provider.init) { + await this.provider.init(); + this.store.dispatch( + ac.BroadcastToContent({ + type: at.DISCOVERY_STREAM_PERSONALIZATION_INIT, + meta: { + isStartup, + }, + }) + ); + } + } + + async enable(isStartup) { + await this.loadPersonalizationScoresCache(isStartup); + Services.obs.addObserver(this, "idle-daily"); + this.loaded = true; + this.store.dispatch( + ac.BroadcastToContent({ + type: at.DISCOVERY_STREAM_PERSONALIZATION_UPDATED, + meta: { + isStartup, + }, + }) + ); + } + + get showStories() { + // Combine user-set stories opt-out with Mozilla-set config + return ( + this.store.getState().Prefs.values[PREF_SYSTEM_TOPSTORIES] && + this.store.getState().Prefs.values[PREF_USER_TOPSTORIES] + ); + } + + get personalized() { + // If stories are not displayed, no point in trying to personalize them. + if (!this.showStories) { + return false; + } + const spocsPersonalized = + this.store.getState().Prefs.values?.pocketConfig?.spocsPersonalized; + const recsPersonalized = + this.store.getState().Prefs.values?.pocketConfig?.recsPersonalized; + const personalization = + this.store.getState().Prefs.values[PREF_PERSONALIZATION]; + + // There is a server sent flag to keep personalization on. + // If the server stops sending this, we turn personalization off, + // until the server starts returning the signal. + const overrideState = + this.store.getState().Prefs.values[PREF_PERSONALIZATION_OVERRIDE]; + + return ( + personalization && + !overrideState && + (spocsPersonalized || recsPersonalized) + ); + } + + get modelKeys() { + if (!this._modelKeys) { + this._modelKeys = + this.store.getState().Prefs.values[PREF_PERSONALIZATION_MODEL_KEYS]; + } + + return this._modelKeys; + } + + /* + * This creates a new recommendationProvider using fresh data, + * It's run on a last updated timer. This is the opposite of loadPersonalizationScoresCache. + * This is also much slower so we only trigger this in the background on idle-daily. + * It causes new profiles to pick up personalization slowly because the first time + * a new profile is run you don't have any old cache to use, so it needs to wait for the first + * idle-daily. Older profiles can rely on cache during the idle-daily gap. Idle-daily is + * usually run once every 24 hours. + */ + async updatePersonalizationScores() { + if ( + !this.personalized || + Date.now() - this.personalizationLastUpdated < + MIN_PERSONALIZATION_UPDATE_TIME + ) { + return; + } + + await this.setProvider(); + + const personalization = { scores: this.provider.getScores() }; + this.personalizationLastUpdated = Date.now(); + + this.store.dispatch( + ac.BroadcastToContent({ + type: at.DISCOVERY_STREAM_PERSONALIZATION_LAST_UPDATED, + data: { + lastUpdated: this.personalizationLastUpdated, + }, + }) + ); + personalization._timestamp = this.personalizationLastUpdated; + this.cache.set("personalization", personalization); + } + + /* + * This just re hydrates the provider from cache. + * We can call this on startup because it's generally fast. + * It reports to devtools the last time the data in the cache was updated. + */ + async loadPersonalizationScoresCache(isStartup = false) { + const cachedData = (await this.cache.get()) || {}; + const { personalization } = cachedData; + + if (this.personalized && personalization?.scores) { + await this.setProvider(isStartup, personalization.scores); + + this.personalizationLastUpdated = personalization._timestamp; + + this.store.dispatch( + ac.BroadcastToContent({ + type: at.DISCOVERY_STREAM_PERSONALIZATION_LAST_UPDATED, + data: { + lastUpdated: this.personalizationLastUpdated, + }, + meta: { + isStartup, + }, + }) + ); + } + } + + // This turns personalization on/off if the server sends the override command. + // The server sends a true signal to keep personalization on. So a malfunctioning + // server would more likely mistakenly turn off personalization, and not turn it on. + // This is safer, because the override is for cases where personalization is causing issues. + // So having it mistakenly go off is safe, but it mistakenly going on could be bad. + personalizationOverride(overrideCommand) { + // Are we currently in an override state. + // This is useful to know if we want to do a cleanup. + const overrideState = + this.store.getState().Prefs.values[PREF_PERSONALIZATION_OVERRIDE]; + + // Is this profile currently set to be personalized. + const personalization = + this.store.getState().Prefs.values[PREF_PERSONALIZATION]; + + // If we have an override command, profile is currently personalized, + // and is not currently being overridden, we can set the override pref. + if (overrideCommand && personalization && !overrideState) { + this.store.dispatch(ac.SetPref(PREF_PERSONALIZATION_OVERRIDE, true)); + } + + // This is if we need to revert an override and do cleanup. + // We do this if we are in an override state, + // but not currently receiving the override signal. + if (!overrideCommand && overrideState) { + this.store.dispatch({ + type: at.CLEAR_PREF, + data: { name: PREF_PERSONALIZATION_OVERRIDE }, + }); + } + } + + async calculateItemRelevanceScore(item) { + if (this.provider) { + const scoreResult = await this.provider.calculateItemRelevanceScore(item); + if (scoreResult === 0 || scoreResult) { + item.score = scoreResult; + } + } + } + + teardown() { + if (this.provider && this.provider.teardown) { + // This removes any in memory listeners if available. + this.provider.teardown(); + } + if (this.loaded) { + Services.obs.removeObserver(this, "idle-daily"); + } + this.loaded = false; + } + + async resetState() { + this._modelKeys = null; + this.personalizationLastUpdated = null; + this.provider = null; + await this.cache.set("personalization", {}); + this.store.dispatch( + ac.OnlyToMain({ + type: at.DISCOVERY_STREAM_PERSONALIZATION_RESET, + }) + ); + } + + async observe(subject, topic, data) { + switch (topic) { + case "idle-daily": + await this.updatePersonalizationScores(); + this.store.dispatch( + ac.BroadcastToContent({ + type: at.DISCOVERY_STREAM_PERSONALIZATION_UPDATED, + }) + ); + break; + } + } + + async onAction(action) { + switch (action.type) { + case at.INIT: + await this.enable(true /* isStartup */); + break; + case at.DISCOVERY_STREAM_CONFIG_CHANGE: + this.teardown(); + await this.resetState(); + await this.enable(); + break; + case at.DISCOVERY_STREAM_DEV_IDLE_DAILY: + Services.obs.notifyObservers(null, "idle-daily"); + break; + case at.PREF_CHANGED: + switch (action.data.name) { + case PREF_PERSONALIZATION_MODEL_KEYS: + this.store.dispatch( + ac.BroadcastToContent({ + type: at.DISCOVERY_STREAM_CONFIG_RESET, + }) + ); + break; + } + break; + case at.DISCOVERY_STREAM_PERSONALIZATION_TOGGLE: + let enabled = this.store.getState().Prefs.values[PREF_PERSONALIZATION]; + this.store.dispatch(ac.SetPref(PREF_PERSONALIZATION, !enabled)); + break; + case at.DISCOVERY_STREAM_PERSONALIZATION_OVERRIDE: + this.personalizationOverride(action.data.override); + break; + } + } +} diff --git a/browser/components/newtab/lib/Screenshots.sys.mjs b/browser/components/newtab/lib/Screenshots.sys.mjs new file mode 100644 index 0000000000..e5423bd52f --- /dev/null +++ b/browser/components/newtab/lib/Screenshots.sys.mjs @@ -0,0 +1,140 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// We use importESModule here instead of static import so that +// the Karma test environment won't choke on this module. This +// is because the Karma test environment already stubs out +// XPCOMUtils, and overrides importESModule to be a no-op (which +// can't be done for a static import statement). + +// eslint-disable-next-line mozilla/use-static-import +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + BackgroundPageThumbs: "resource://gre/modules/BackgroundPageThumbs.sys.mjs", + PageThumbs: "resource://gre/modules/PageThumbs.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", +}); + +const GREY_10 = "#F9F9FA"; + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "gPrivilegedAboutProcessEnabled", + "browser.tabs.remote.separatePrivilegedContentProcess", + false +); + +export const Screenshots = { + /** + * Get a screenshot / thumbnail for a url. Either returns the disk cached + * image or initiates a background request for the url. + * + * @param url {string} The url to get a thumbnail + * @return {Promise} Resolves a custom object or null if failed + */ + async getScreenshotForURL(url) { + try { + await lazy.BackgroundPageThumbs.captureIfMissing(url, { + backgroundColor: GREY_10, + }); + + // The privileged about content process is able to use the moz-page-thumb + // protocol, so if it's enabled, send that down. + if (lazy.gPrivilegedAboutProcessEnabled) { + return lazy.PageThumbs.getThumbnailURL(url); + } + + // Otherwise, for normal content processes, we fallback to using + // Blob URIs for the screenshots. + const imgPath = lazy.PageThumbs.getThumbnailPath(url); + + const filePathResponse = await fetch(`file://${imgPath}`); + const fileContents = await filePathResponse.blob(); + + // Check if the file is empty, which indicates there isn't actually a + // thumbnail, so callers can show a failure state. + if (fileContents.size === 0) { + return null; + } + + return { path: imgPath, data: fileContents }; + } catch (err) { + console.error(`getScreenshot(${url}) failed:`, err); + } + + // We must have failed to get the screenshot, so persist the failure by + // storing an empty file. Future calls will then skip requesting and return + // failure, so do the same thing here. The empty file should not expire with + // the usual filtering process to avoid repeated background requests, which + // can cause unwanted high CPU, network and memory usage - Bug 1384094 + try { + await lazy.PageThumbs._store(url, url, null, true); + } catch (err) { + // Probably failed to create the empty file, but not much more we can do. + } + return null; + }, + + /** + * Checks if all the open windows are private browsing windows. If so, we do not + * want to collect screenshots. If there exists at least 1 non-private window, + * we are ok to collect screenshots. + */ + _shouldGetScreenshots() { + for (let win of Services.wm.getEnumerator("navigator:browser")) { + if (!lazy.PrivateBrowsingUtils.isWindowPrivate(win)) { + // As soon as we encounter 1 non-private window, screenshots are fair game. + return true; + } + } + return false; + }, + + /** + * Conditionally get a screenshot for a link if there's no existing pending + * screenshot. Updates the cached link's desired property with the result. + * + * @param link {object} Link object to update + * @param url {string} Url to get a screenshot of + * @param property {string} Name of property on object to set + @ @param onScreenshot {function} Callback for when the screenshot loads + */ + async maybeCacheScreenshot(link, url, property, onScreenshot) { + // If there are only private windows open, do not collect screenshots + if (!this._shouldGetScreenshots()) { + return; + } + // __sharedCache may not exist yet for links from default top sites that + // don't have a default tippy top icon. + if (!link.__sharedCache) { + link.__sharedCache = { + updateLink(prop, val) { + link[prop] = val; + }, + }; + } + const cache = link.__sharedCache; + // Nothing to do if we already have a pending screenshot or + // if a previous request failed and returned null. + if (cache.fetchingScreenshot || link[property] !== undefined) { + return; + } + + // Save the promise to the cache so other links get it immediately + cache.fetchingScreenshot = this.getScreenshotForURL(url); + + // Clean up now that we got the screenshot + const screenshot = await cache.fetchingScreenshot; + delete cache.fetchingScreenshot; + + // Update the cache for future links and call back for existing content + cache.updateLink(property, screenshot); + onScreenshot(screenshot); + }, +}; diff --git a/browser/components/newtab/lib/SearchShortcuts.sys.mjs b/browser/components/newtab/lib/SearchShortcuts.sys.mjs new file mode 100644 index 0000000000..1448f87ca4 --- /dev/null +++ b/browser/components/newtab/lib/SearchShortcuts.sys.mjs @@ -0,0 +1,73 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// List of sites we match against Topsites in order to identify sites +// that should be converted to search Topsites +export const SEARCH_SHORTCUTS = [ + { keyword: "@amazon", shortURL: "amazon", url: "https://amazon.com" }, + { keyword: "@\u767E\u5EA6", shortURL: "baidu", url: "https://baidu.com" }, + { keyword: "@google", shortURL: "google", url: "https://google.com" }, + { + keyword: "@\u044F\u043D\u0434\u0435\u043A\u0441", + shortURL: "yandex", + url: "https://yandex.com", + }, +]; + +// These can be added via the editor but will not be added organically +export const CUSTOM_SEARCH_SHORTCUTS = [ + ...SEARCH_SHORTCUTS, + { keyword: "@bing", shortURL: "bing", url: "https://bing.com" }, + { + keyword: "@duckduckgo", + shortURL: "duckduckgo", + url: "https://duckduckgo.com", + }, + { keyword: "@ebay", shortURL: "ebay", url: "https://ebay.com" }, + { keyword: "@twitter", shortURL: "twitter", url: "https://twitter.com" }, + { + keyword: "@wikipedia", + shortURL: "wikipedia", + url: "https://wikipedia.org", + }, +]; + +// Note: you must add the activity stream branch to the beginning of this if using outside activity stream +export const SEARCH_SHORTCUTS_EXPERIMENT = + "improvesearch.topSiteSearchShortcuts"; + +export const SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF = + "improvesearch.topSiteSearchShortcuts.searchEngines"; + +export const SEARCH_SHORTCUTS_HAVE_PINNED_PREF = + "improvesearch.topSiteSearchShortcuts.havePinned"; + +export function getSearchProvider(candidateShortURL) { + return ( + SEARCH_SHORTCUTS.filter(match => candidateShortURL === match.shortURL)[0] || + null + ); +} + +// Get the search form URL for a given search keyword. This allows us to pick +// different tippytop icons for the different variants. Sush as yandex.com vs. yandex.ru. +// See more details in bug 1643523. +export async function getSearchFormURL(keyword) { + const engine = await Services.search.getEngineByAlias(keyword); + return engine?.wrappedJSObject._searchForm; +} + +// Check topsite against predefined list of valid search engines +// https://searchfox.org/mozilla-central/rev/ca869724246f4230b272ed1c8b9944596e80d920/toolkit/components/search/nsSearchService.js#939 +export async function checkHasSearchEngine(keyword) { + try { + return !!(await Services.search.getAppProvidedEngines()).find( + e => e.aliases.includes(keyword) && !e.hidden + ); + } catch { + // When the search service has not successfully initialized, + // there will be no search engines ready. + return false; + } +} diff --git a/browser/components/newtab/lib/SectionsManager.sys.mjs b/browser/components/newtab/lib/SectionsManager.sys.mjs new file mode 100644 index 0000000000..96bba0c9ea --- /dev/null +++ b/browser/components/newtab/lib/SectionsManager.sys.mjs @@ -0,0 +1,715 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// We use importESModule here instead of static import so that +// the Karma test environment won't choke on this module. This +// is because the Karma test environment already stubs out +// EventEmitter, and overrides importESModule to be a no-op (which +// can't be done for a static import statement). + +// eslint-disable-next-line mozilla/use-static-import +const { EventEmitter } = ChromeUtils.importESModule( + "resource://gre/modules/EventEmitter.sys.mjs" +); +import { + actionCreators as ac, + actionTypes as at, +} from "resource://activity-stream/common/Actions.sys.mjs"; +import { getDefaultOptions } from "resource://activity-stream/lib/ActivityStreamStorage.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", +}); + +/* + * Generators for built in sections, keyed by the pref name for their feed. + * Built in sections may depend on options stored as serialised JSON in the pref + * `${feed_pref_name}.options`. + */ + +const BUILT_IN_SECTIONS = ({ newtab, pocketNewtab }) => ({ + "feeds.section.topstories": options => ({ + id: "topstories", + pref: { + titleString: { + id: "home-prefs-recommended-by-header-generic", + }, + descString: { + id: "home-prefs-recommended-by-description-generic", + }, + nestedPrefs: [ + ...(Services.prefs.getBoolPref( + "browser.newtabpage.activity-stream.system.showSponsored", + true + ) + ? [ + { + name: "showSponsored", + titleString: + "home-prefs-recommended-by-option-sponsored-stories", + icon: "icon-info", + eventSource: "POCKET_SPOCS", + }, + ] + : []), + ...(pocketNewtab.recentSavesEnabled + ? [ + { + name: "showRecentSaves", + titleString: "home-prefs-recommended-by-option-recent-saves", + icon: "icon-info", + eventSource: "POCKET_RECENT_SAVES", + }, + ] + : []), + ], + learnMore: { + link: { + href: "https://getpocket.com/firefox/new_tab_learn_more", + id: "home-prefs-recommended-by-learn-more", + }, + }, + }, + shouldHidePref: options.hidden, + eventSource: "TOP_STORIES", + icon: options.provider_icon, + title: { + id: "newtab-section-header-stories", + }, + learnMore: { + link: { + href: "https://getpocket.com/firefox/new_tab_learn_more", + message: { id: "newtab-pocket-learn-more" }, + }, + }, + compactCards: false, + rowsPref: "section.topstories.rows", + maxRows: 4, + availableLinkMenuOptions: [ + "CheckBookmarkOrArchive", + "CheckSavedToPocket", + "Separator", + "OpenInNewWindow", + "OpenInPrivateWindow", + "Separator", + "BlockUrl", + ], + emptyState: { + message: { + id: "newtab-empty-section-topstories-generic", + }, + icon: "check", + }, + shouldSendImpressionStats: true, + dedupeFrom: ["highlights"], + }), + "feeds.section.highlights": options => ({ + id: "highlights", + pref: { + titleString: { + id: "home-prefs-recent-activity-header", + }, + descString: { + id: "home-prefs-recent-activity-description", + }, + nestedPrefs: [ + { + name: "section.highlights.includeVisited", + titleString: "home-prefs-highlights-option-visited-pages", + }, + { + name: "section.highlights.includeBookmarks", + titleString: "home-prefs-highlights-options-bookmarks", + }, + { + name: "section.highlights.includeDownloads", + titleString: "home-prefs-highlights-option-most-recent-download", + }, + { + name: "section.highlights.includePocket", + titleString: "home-prefs-highlights-option-saved-to-pocket", + hidden: !Services.prefs.getBoolPref( + "extensions.pocket.enabled", + true + ), + }, + ], + }, + shouldHidePref: false, + eventSource: "HIGHLIGHTS", + icon: "chrome://global/skin/icons/highlights.svg", + title: { + id: "newtab-section-header-recent-activity", + }, + compactCards: true, + rowsPref: "section.highlights.rows", + maxRows: 4, + emptyState: { + message: { id: "newtab-empty-section-highlights" }, + icon: "chrome://global/skin/icons/highlights.svg", + }, + shouldSendImpressionStats: false, + }), +}); + +export const SectionsManager = { + ACTIONS_TO_PROXY: ["WEBEXT_CLICK", "WEBEXT_DISMISS"], + CONTEXT_MENU_PREFS: { CheckSavedToPocket: "extensions.pocket.enabled" }, + CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES: { + history: [ + "CheckBookmark", + "CheckSavedToPocket", + "Separator", + "OpenInNewWindow", + "OpenInPrivateWindow", + "Separator", + "BlockUrl", + "DeleteUrl", + ], + bookmark: [ + "CheckBookmark", + "CheckSavedToPocket", + "Separator", + "OpenInNewWindow", + "OpenInPrivateWindow", + "Separator", + "BlockUrl", + "DeleteUrl", + ], + pocket: [ + "ArchiveFromPocket", + "CheckSavedToPocket", + "Separator", + "OpenInNewWindow", + "OpenInPrivateWindow", + "Separator", + "BlockUrl", + ], + download: [ + "OpenFile", + "ShowFile", + "Separator", + "GoToDownloadPage", + "CopyDownloadLink", + "Separator", + "RemoveDownload", + "BlockUrl", + ], + }, + initialized: false, + sections: new Map(), + async init(prefs = {}, storage) { + this._storage = storage; + const featureConfig = { + newtab: lazy.NimbusFeatures.newtab.getAllVariables() || {}, + pocketNewtab: lazy.NimbusFeatures.pocketNewtab.getAllVariables() || {}, + }; + + for (const feedPrefName of Object.keys(BUILT_IN_SECTIONS(featureConfig))) { + const optionsPrefName = `${feedPrefName}.options`; + await this.addBuiltInSection(feedPrefName, prefs[optionsPrefName]); + + this._dedupeConfiguration = []; + this.sections.forEach(section => { + if (section.dedupeFrom) { + this._dedupeConfiguration.push({ + id: section.id, + dedupeFrom: section.dedupeFrom, + }); + } + }); + } + + Object.keys(this.CONTEXT_MENU_PREFS).forEach(k => + Services.prefs.addObserver(this.CONTEXT_MENU_PREFS[k], this) + ); + + this.initialized = true; + this.emit(this.INIT); + }, + observe(subject, topic, data) { + switch (topic) { + case "nsPref:changed": + for (const pref of Object.keys(this.CONTEXT_MENU_PREFS)) { + if (data === this.CONTEXT_MENU_PREFS[pref]) { + this.updateSections(); + } + } + break; + } + }, + updateSectionPrefs(id, collapsed) { + const section = this.sections.get(id); + if (!section) { + return; + } + + const updatedSection = Object.assign({}, section, { + pref: Object.assign({}, section.pref, collapsed), + }); + this.updateSection(id, updatedSection, true); + }, + async addBuiltInSection(feedPrefName, optionsPrefValue = "{}") { + let options; + let storedPrefs; + const featureConfig = { + newtab: lazy.NimbusFeatures.newtab.getAllVariables() || {}, + pocketNewtab: lazy.NimbusFeatures.pocketNewtab.getAllVariables() || {}, + }; + try { + options = JSON.parse(optionsPrefValue); + } catch (e) { + options = {}; + console.error(`Problem parsing options pref for ${feedPrefName}`); + } + try { + storedPrefs = (await this._storage.get(feedPrefName)) || {}; + } catch (e) { + storedPrefs = {}; + console.error(`Problem getting stored prefs for ${feedPrefName}`); + } + const defaultSection = + BUILT_IN_SECTIONS(featureConfig)[feedPrefName](options); + const section = Object.assign({}, defaultSection, { + pref: Object.assign( + {}, + defaultSection.pref, + getDefaultOptions(storedPrefs) + ), + }); + section.pref.feed = feedPrefName; + this.addSection(section.id, Object.assign(section, { options })); + }, + addSection(id, options) { + this.updateLinkMenuOptions(options, id); + this.sections.set(id, options); + this.emit(this.ADD_SECTION, id, options); + }, + removeSection(id) { + this.emit(this.REMOVE_SECTION, id); + this.sections.delete(id); + }, + enableSection(id, isStartup = false) { + this.updateSection(id, { enabled: true }, true, isStartup); + this.emit(this.ENABLE_SECTION, id); + }, + disableSection(id) { + this.updateSection( + id, + { enabled: false, rows: [], initialized: false }, + true + ); + this.emit(this.DISABLE_SECTION, id); + }, + updateSections() { + this.sections.forEach((section, id) => + this.updateSection(id, section, true) + ); + }, + updateSection(id, options, shouldBroadcast, isStartup = false) { + this.updateLinkMenuOptions(options, id); + if (this.sections.has(id)) { + const optionsWithDedupe = Object.assign({}, options, { + dedupeConfigurations: this._dedupeConfiguration, + }); + this.sections.set(id, Object.assign(this.sections.get(id), options)); + this.emit( + this.UPDATE_SECTION, + id, + optionsWithDedupe, + shouldBroadcast, + isStartup + ); + } + }, + + /** + * Save metadata to places db and add a visit for that URL. + */ + updateBookmarkMetadata({ url }) { + this.sections.forEach((section, id) => { + if (id === "highlights") { + // Skip Highlights cards, we already have that metadata. + return; + } + if (section.rows) { + section.rows.forEach(card => { + if ( + card.url === url && + card.description && + card.title && + card.image + ) { + lazy.PlacesUtils.history.update({ + url: card.url, + title: card.title, + description: card.description, + previewImageURL: card.image, + }); + // Highlights query skips bookmarks with no visits. + lazy.PlacesUtils.history.insert({ + url, + title: card.title, + visits: [{}], + }); + } + }); + } + }); + }, + + /** + * Sets the section's context menu options. These are all available context menu + * options minus the ones that are tied to a pref (see CONTEXT_MENU_PREFS) set + * to false. + * + * @param options section options + * @param id section ID + */ + updateLinkMenuOptions(options, id) { + if (options.availableLinkMenuOptions) { + options.contextMenuOptions = options.availableLinkMenuOptions.filter( + o => + !this.CONTEXT_MENU_PREFS[o] || + Services.prefs.getBoolPref(this.CONTEXT_MENU_PREFS[o]) + ); + } + + // Once we have rows, we can give each card it's own context menu based on it's type. + // We only want to do this for highlights because those have different data types. + // All other sections (built by the web extension API) will have the same context menu per section + if (options.rows && id === "highlights") { + this._addCardTypeLinkMenuOptions(options.rows); + } + }, + + /** + * Sets each card in highlights' context menu options based on the card's type. + * (See types.js for a list of types) + * + * @param rows section rows containing a type for each card + */ + _addCardTypeLinkMenuOptions(rows) { + for (let card of rows) { + if (!this.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES[card.type]) { + console.error( + `No context menu for highlight type ${card.type} is configured` + ); + } else { + card.contextMenuOptions = + this.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES[card.type]; + + // Remove any options that shouldn't be there based on CONTEXT_MENU_PREFS. + // For example: If the Pocket extension is disabled, we should remove the CheckSavedToPocket option + // for each card that has it + card.contextMenuOptions = card.contextMenuOptions.filter( + o => + !this.CONTEXT_MENU_PREFS[o] || + Services.prefs.getBoolPref(this.CONTEXT_MENU_PREFS[o]) + ); + } + } + }, + + /** + * Update a specific section card by its url. This allows an action to be + * broadcast to all existing pages to update a specific card without having to + * also force-update the rest of the section's cards and state on those pages. + * + * @param id The id of the section with the card to be updated + * @param url The url of the card to update + * @param options The options to update for the card + * @param shouldBroadcast Whether or not to broadcast the update + * @param isStartup If this update is during startup. + */ + updateSectionCard(id, url, options, shouldBroadcast, isStartup = false) { + if (this.sections.has(id)) { + const card = this.sections.get(id).rows.find(elem => elem.url === url); + if (card) { + Object.assign(card, options); + } + this.emit( + this.UPDATE_SECTION_CARD, + id, + url, + options, + shouldBroadcast, + isStartup + ); + } + }, + removeSectionCard(sectionId, url) { + if (!this.sections.has(sectionId)) { + return; + } + const rows = this.sections + .get(sectionId) + .rows.filter(row => row.url !== url); + this.updateSection(sectionId, { rows }, true); + }, + onceInitialized(callback) { + if (this.initialized) { + callback(); + } else { + this.once(this.INIT, callback); + } + }, + uninit() { + Object.keys(this.CONTEXT_MENU_PREFS).forEach(k => + Services.prefs.removeObserver(this.CONTEXT_MENU_PREFS[k], this) + ); + SectionsManager.initialized = false; + }, +}; + +for (const action of [ + "ACTION_DISPATCHED", + "ADD_SECTION", + "REMOVE_SECTION", + "ENABLE_SECTION", + "DISABLE_SECTION", + "UPDATE_SECTION", + "UPDATE_SECTION_CARD", + "INIT", + "UNINIT", +]) { + SectionsManager[action] = action; +} + +EventEmitter.decorate(SectionsManager); + +export class SectionsFeed { + constructor() { + this.init = this.init.bind(this); + this.onAddSection = this.onAddSection.bind(this); + this.onRemoveSection = this.onRemoveSection.bind(this); + this.onUpdateSection = this.onUpdateSection.bind(this); + this.onUpdateSectionCard = this.onUpdateSectionCard.bind(this); + } + + init() { + SectionsManager.on(SectionsManager.ADD_SECTION, this.onAddSection); + SectionsManager.on(SectionsManager.REMOVE_SECTION, this.onRemoveSection); + SectionsManager.on(SectionsManager.UPDATE_SECTION, this.onUpdateSection); + SectionsManager.on( + SectionsManager.UPDATE_SECTION_CARD, + this.onUpdateSectionCard + ); + // Catch any sections that have already been added + SectionsManager.sections.forEach((section, id) => + this.onAddSection( + SectionsManager.ADD_SECTION, + id, + section, + true /* isStartup */ + ) + ); + } + + uninit() { + SectionsManager.uninit(); + SectionsManager.emit(SectionsManager.UNINIT); + SectionsManager.off(SectionsManager.ADD_SECTION, this.onAddSection); + SectionsManager.off(SectionsManager.REMOVE_SECTION, this.onRemoveSection); + SectionsManager.off(SectionsManager.UPDATE_SECTION, this.onUpdateSection); + SectionsManager.off( + SectionsManager.UPDATE_SECTION_CARD, + this.onUpdateSectionCard + ); + } + + onAddSection(event, id, options, isStartup = false) { + if (options) { + this.store.dispatch( + ac.BroadcastToContent({ + type: at.SECTION_REGISTER, + data: Object.assign({ id }, options), + meta: { + isStartup, + }, + }) + ); + + // Make sure the section is in sectionOrder pref. Otherwise, prepend it. + const orderedSections = this.orderedSectionIds; + if (!orderedSections.includes(id)) { + orderedSections.unshift(id); + this.store.dispatch( + ac.SetPref("sectionOrder", orderedSections.join(",")) + ); + } + } + } + + onRemoveSection(event, id) { + this.store.dispatch( + ac.BroadcastToContent({ type: at.SECTION_DEREGISTER, data: id }) + ); + } + + onUpdateSection( + event, + id, + options, + shouldBroadcast = false, + isStartup = false + ) { + if (options) { + const action = { + type: at.SECTION_UPDATE, + data: Object.assign(options, { id }), + meta: { + isStartup, + }, + }; + this.store.dispatch( + shouldBroadcast + ? ac.BroadcastToContent(action) + : ac.AlsoToPreloaded(action) + ); + } + } + + onUpdateSectionCard( + event, + id, + url, + options, + shouldBroadcast = false, + isStartup = false + ) { + if (options) { + const action = { + type: at.SECTION_UPDATE_CARD, + data: { id, url, options }, + meta: { + isStartup, + }, + }; + this.store.dispatch( + shouldBroadcast + ? ac.BroadcastToContent(action) + : ac.AlsoToPreloaded(action) + ); + } + } + + get orderedSectionIds() { + return this.store.getState().Prefs.values.sectionOrder.split(","); + } + + get enabledSectionIds() { + let sections = this.store + .getState() + .Sections.filter(section => section.enabled) + .map(s => s.id); + // Top Sites is a special case. Append if the feed is enabled. + if (this.store.getState().Prefs.values["feeds.topsites"]) { + sections.push("topsites"); + } + return sections; + } + + moveSection(id, direction) { + const orderedSections = this.orderedSectionIds; + const enabledSections = this.enabledSectionIds; + let index = orderedSections.indexOf(id); + orderedSections.splice(index, 1); + if (direction > 0) { + // "Move Down" + while (index < orderedSections.length) { + // If the section at the index is enabled/visible, insert moved section after. + // Otherwise, move on to the next spot and check it. + if (enabledSections.includes(orderedSections[index++])) { + break; + } + } + } else { + // "Move Up" + while (index > 0) { + // If the section at the previous index is enabled/visible, insert moved section there. + // Otherwise, move on to the previous spot and check it. + index--; + if (enabledSections.includes(orderedSections[index])) { + break; + } + } + } + + orderedSections.splice(index, 0, id); + this.store.dispatch(ac.SetPref("sectionOrder", orderedSections.join(","))); + } + + async onAction(action) { + switch (action.type) { + case at.INIT: + SectionsManager.onceInitialized(this.init); + break; + // Wait for pref values, as some sections have options stored in prefs + case at.PREFS_INITIAL_VALUES: + SectionsManager.init( + action.data, + this.store.dbStorage.getDbTable("sectionPrefs") + ); + break; + case at.PREF_CHANGED: { + if (action.data) { + const matched = action.data.name.match( + /^(feeds.section.(\S+)).options$/i + ); + if (matched) { + await SectionsManager.addBuiltInSection( + matched[1], + action.data.value + ); + this.store.dispatch({ + type: at.SECTION_OPTIONS_CHANGED, + data: matched[2], + }); + } + } + break; + } + case at.UPDATE_SECTION_PREFS: + SectionsManager.updateSectionPrefs(action.data.id, action.data.value); + break; + case at.PLACES_BOOKMARK_ADDED: + SectionsManager.updateBookmarkMetadata(action.data); + break; + case at.WEBEXT_DISMISS: + if (action.data) { + SectionsManager.removeSectionCard( + action.data.source, + action.data.url + ); + } + break; + case at.SECTION_DISABLE: + SectionsManager.disableSection(action.data); + break; + case at.SECTION_ENABLE: + SectionsManager.enableSection(action.data); + break; + case at.SECTION_MOVE: + this.moveSection(action.data.id, action.data.direction); + break; + case at.UNINIT: + this.uninit(); + break; + } + if ( + SectionsManager.ACTIONS_TO_PROXY.includes(action.type) && + SectionsManager.sections.size > 0 + ) { + SectionsManager.emit( + SectionsManager.ACTION_DISPATCHED, + action.type, + action.data + ); + } + } +} diff --git a/browser/components/newtab/lib/ShortURL.sys.mjs b/browser/components/newtab/lib/ShortURL.sys.mjs new file mode 100644 index 0000000000..6ee34c20dd --- /dev/null +++ b/browser/components/newtab/lib/ShortURL.sys.mjs @@ -0,0 +1,88 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// We use importESModule here instead of static import so that +// the Karma test environment won't choke on this module. This +// is because the Karma test environment already stubs out +// XPCOMUtils, and overrides importESModule to be a no-op (which +// can't be done for a static import statement). + +// eslint-disable-next-line mozilla/use-static-import +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "IDNService", + "@mozilla.org/network/idn-service;1", + "nsIIDNService" +); + +/** + * Properly convert internationalized domain names. + * @param {string} host Domain hostname. + * @returns {string} Hostname suitable to be displayed. + */ +function handleIDNHost(hostname) { + try { + return lazy.IDNService.convertToDisplayIDN(hostname, {}); + } catch (e) { + // If something goes wrong (e.g. host is an IP address) just fail back + // to the full domain. + return hostname; + } +} + +/** + * Get the effective top level domain of a host. + * @param {string} host The host to be analyzed. + * @return {str} The suffix or empty string if there's no suffix. + */ +export function getETLD(host) { + try { + return Services.eTLD.getPublicSuffixFromHost(host); + } catch (err) { + return ""; + } +} + +/** + * shortURL - Creates a short version of a link's url, used for display purposes + * e.g. {url: http://www.foosite.com} => "foosite" + * + * @param {obj} link A link object + * {str} link.url (required)- The url of the link + * @return {str} A short url + */ +export function shortURL({ url }) { + if (!url) { + return ""; + } + + // Make sure we have a valid / parseable url + let parsed; + try { + parsed = new URL(url); + } catch (ex) { + // Not entirely sure what we have, but just give it back + return url; + } + + // Clean up the url (lowercase hostname via URL and remove www.) + const hostname = parsed.hostname.replace(/^www\./i, ""); + + // Remove the eTLD (e.g., com, net) and the preceding period from the hostname + const eTLD = getETLD(hostname); + const eTLDExtra = eTLD.length ? -(eTLD.length + 1) : Infinity; + + // Ideally get the short eTLD-less host but fall back to longer url parts + return ( + handleIDNHost(hostname.slice(0, eTLDExtra) || hostname) || + parsed.pathname || + parsed.href + ); +} diff --git a/browser/components/newtab/lib/SiteClassifier.sys.mjs b/browser/components/newtab/lib/SiteClassifier.sys.mjs new file mode 100644 index 0000000000..64c7309bf5 --- /dev/null +++ b/browser/components/newtab/lib/SiteClassifier.sys.mjs @@ -0,0 +1,103 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// We use importESModule here instead of static import so that +// the Karma test environment won't choke on this module. This +// is because the Karma test environment already stubs out +// RemoteSettings, and overrides importESModule to be a no-op (which +// can't be done for a static import statement). + +// eslint-disable-next-line mozilla/use-static-import +const { RemoteSettings } = ChromeUtils.importESModule( + "resource://services-settings/remote-settings.sys.mjs" +); + +// Returns whether the passed in params match the criteria. +// To match, they must contain all the params specified in criteria and the values +// must match if a value is provided in criteria. +function _hasParams(criteria, params) { + for (let param of criteria) { + const val = params.get(param.key); + if ( + val === null || + (param.value && param.value !== val) || + (param.prefix && !val.startsWith(param.prefix)) + ) { + return false; + } + } + return true; +} + +/** + * classifySite + * Classifies a given URL into a category based on classification data from RemoteSettings. + * The data from remote settings can match a category by one of the following: + * - match the exact URL + * - match the hostname or second level domain (sld) + * - match query parameter(s), and optionally their values or prefixes + * - match both (hostname or sld) and query parameter(s) + * + * The data looks like: + * [{ + * "type": "hostname-and-params-match", + * "criteria": [ + * { + * "url": "https://matchurl.com", + * "hostname": "matchhostname.com", + * "sld": "secondleveldomain", + * "params": [ + * { + * "key": "matchparam", + * "value": "matchvalue", + * "prefix": "matchpPrefix", + * }, + * ], + * }, + * ], + * "weight": 300, + * },...] + */ +export async function classifySite(url, RS = RemoteSettings) { + let category = "other"; + let parsedURL; + + // Try to parse the url. + for (let _url of [url, `https://${url}`]) { + try { + parsedURL = new URL(_url); + break; + } catch (e) {} + } + + if (parsedURL) { + // If we parsed successfully, find a match. + const hostname = parsedURL.hostname.replace(/^www\./i, ""); + const params = parsedURL.searchParams; + // NOTE: there will be an initial/default local copy of the data in m-c. + // Therefore, this should never return an empty list []. + const siteTypes = await RS("sites-classification").get(); + const sortedSiteTypes = siteTypes.sort( + (x, y) => (y.weight || 0) - (x.weight || 0) + ); + for (let type of sortedSiteTypes) { + for (let criteria of type.criteria) { + if (criteria.url && criteria.url !== url) { + continue; + } + if (criteria.hostname && criteria.hostname !== hostname) { + continue; + } + if (criteria.sld && criteria.sld !== hostname.split(".")[0]) { + continue; + } + if (criteria.params && !_hasParams(criteria.params, params)) { + continue; + } + return type.type; + } + } + } + return category; +} diff --git a/browser/components/newtab/lib/Store.sys.mjs b/browser/components/newtab/lib/Store.sys.mjs new file mode 100644 index 0000000000..3a4fdfa98d --- /dev/null +++ b/browser/components/newtab/lib/Store.sys.mjs @@ -0,0 +1,188 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { ActivityStreamMessageChannel } from "resource://activity-stream/lib/ActivityStreamMessageChannel.sys.mjs"; +import { ActivityStreamStorage } from "resource://activity-stream/lib/ActivityStreamStorage.sys.mjs"; +import { Prefs } from "resource://activity-stream/lib/ActivityStreamPrefs.sys.mjs"; +import { reducers } from "resource://activity-stream/common/Reducers.sys.mjs"; +import { redux } from "resource://activity-stream/vendor/Redux.sys.mjs"; + +/** + * Store - This has a similar structure to a redux store, but includes some extra + * functionality to allow for routing of actions between the Main processes + * and child processes via a ActivityStreamMessageChannel. + * It also accepts an array of "Feeds" on inititalization, which + * can listen for any action that is dispatched through the store. + */ +export class Store { + /** + * constructor - The redux store and message manager are created here, + * but no listeners are added until "init" is called. + */ + constructor() { + this._middleware = this._middleware.bind(this); + // Bind each redux method so we can call it directly from the Store. E.g., + // store.dispatch() will call store._store.dispatch(); + for (const method of ["dispatch", "getState", "subscribe"]) { + this[method] = (...args) => this._store[method](...args); + } + this.feeds = new Map(); + this._prefs = new Prefs(); + this._messageChannel = new ActivityStreamMessageChannel({ + dispatch: this.dispatch, + }); + this._store = redux.createStore( + redux.combineReducers(reducers), + redux.applyMiddleware(this._middleware, this._messageChannel.middleware) + ); + this.storage = null; + } + + /** + * _middleware - This is redux middleware consumed by redux.createStore. + * it calls each feed's .onAction method, if one + * is defined. + */ + _middleware() { + return next => action => { + next(action); + for (const store of this.feeds.values()) { + if (store.onAction) { + store.onAction(action); + } + } + }; + } + + /** + * initFeed - Initializes a feed by calling its constructor function + * + * @param {string} feedName The name of a feed, as defined in the object + * passed to Store.init + * @param {Action} initAction An optional action to initialize the feed + */ + initFeed(feedName, initAction) { + const feed = this._feedFactories.get(feedName)(); + feed.store = this; + this.feeds.set(feedName, feed); + if (initAction && feed.onAction) { + feed.onAction(initAction); + } + } + + /** + * uninitFeed - Removes a feed and calls its uninit function if defined + * + * @param {string} feedName The name of a feed, as defined in the object + * passed to Store.init + * @param {Action} uninitAction An optional action to uninitialize the feed + */ + uninitFeed(feedName, uninitAction) { + const feed = this.feeds.get(feedName); + if (!feed) { + return; + } + if (uninitAction && feed.onAction) { + feed.onAction(uninitAction); + } + this.feeds.delete(feedName); + } + + /** + * onPrefChanged - Listener for handling feed changes. + */ + onPrefChanged(name, value) { + if (this._feedFactories.has(name)) { + if (value) { + this.initFeed(name, this._initAction); + } else { + this.uninitFeed(name, this._uninitAction); + } + } + } + + /** + * init - Initializes the ActivityStreamMessageChannel channel, and adds feeds. + * + * Note that it intentionally initializes the TelemetryFeed first so that the + * addon is able to report the init errors from other feeds. + * + * @param {Map} feedFactories A Map of feeds with the name of the pref for + * the feed as the key and a function that + * constructs an instance of the feed. + * @param {Action} initAction An optional action that will be dispatched + * to feeds when they're created. + * @param {Action} uninitAction An optional action for when feeds uninit. + */ + async init(feedFactories, initAction, uninitAction) { + this._feedFactories = feedFactories; + this._initAction = initAction; + this._uninitAction = uninitAction; + + const telemetryKey = "feeds.telemetry"; + if (feedFactories.has(telemetryKey) && this._prefs.get(telemetryKey)) { + this.initFeed(telemetryKey); + } + + await this._initIndexedDB(telemetryKey); + + for (const pref of feedFactories.keys()) { + if (pref !== telemetryKey && this._prefs.get(pref)) { + this.initFeed(pref); + } + } + + this._prefs.observeBranch(this); + + // Dispatch an initial action after all enabled feeds are ready + if (initAction) { + this.dispatch(initAction); + } + + // Dispatch NEW_TAB_INIT/NEW_TAB_LOAD events after INIT event. + this._messageChannel.simulateMessagesForExistingTabs(); + } + + async _initIndexedDB(telemetryKey) { + // "snippets" is the name of one storage space, but these days it is used + // not for snippet-related data (snippets were removed in bug 1715158), + // but storage for impression or session data for all ASRouter messages. + // + // We keep the name "snippets" to avoid having to do an IndexedDB database + // migration. + this.dbStorage = new ActivityStreamStorage({ + storeNames: ["sectionPrefs", "snippets"], + }); + // Accessing the db causes the object stores to be created / migrated. + // This needs to happen before other instances try to access the db, which + // would update only a subset of the stores to the latest version. + try { + await this.dbStorage.db; // eslint-disable-line no-unused-expressions + } catch (e) { + this.dbStorage.telemetry = null; + } + } + + /** + * uninit - Uninitalizes each feed, clears them, and destroys the message + * manager channel. + * + * @return {type} description + */ + uninit() { + if (this._uninitAction) { + this.dispatch(this._uninitAction); + } + this._prefs.ignoreBranch(this); + this.feeds.clear(); + this._feedFactories = null; + } + + /** + * getMessageChannel - Used by the AboutNewTabParent actor to get the message channel. + */ + getMessageChannel() { + return this._messageChannel; + } +} diff --git a/browser/components/newtab/lib/SystemTickFeed.sys.mjs b/browser/components/newtab/lib/SystemTickFeed.sys.mjs new file mode 100644 index 0000000000..d87860fab2 --- /dev/null +++ b/browser/components/newtab/lib/SystemTickFeed.sys.mjs @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { actionTypes as at } from "resource://activity-stream/common/Actions.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + clearInterval: "resource://gre/modules/Timer.sys.mjs", + setInterval: "resource://gre/modules/Timer.sys.mjs", +}); + +// Frequency at which SYSTEM_TICK events are fired +export const SYSTEM_TICK_INTERVAL = 5 * 60 * 1000; + +export class SystemTickFeed { + init() { + this._idleService = Cc["@mozilla.org/widget/useridleservice;1"].getService( + Ci.nsIUserIdleService + ); + this._hasObserver = false; + this.setTimer(); + } + + setTimer() { + this.intervalId = lazy.setInterval(() => { + if (this._idleService.idleTime > SYSTEM_TICK_INTERVAL) { + this.cancelTimer(); + Services.obs.addObserver(this, "user-interaction-active"); + this._hasObserver = true; + return; + } + this.dispatchTick(); + }, SYSTEM_TICK_INTERVAL); + } + + cancelTimer() { + lazy.clearInterval(this.intervalId); + this.intervalId = null; + } + + observe() { + this.dispatchTick(); + Services.obs.removeObserver(this, "user-interaction-active"); + this._hasObserver = false; + this.setTimer(); + } + + dispatchTick() { + ChromeUtils.idleDispatch(() => + this.store.dispatch({ type: at.SYSTEM_TICK }) + ); + } + + onAction(action) { + switch (action.type) { + case at.INIT: + this.init(); + break; + case at.UNINIT: + this.cancelTimer(); + if (this._hasObserver) { + Services.obs.removeObserver(this, "user-interaction-active"); + this._hasObserver = false; + } + break; + } + } +} diff --git a/browser/components/newtab/lib/TelemetryFeed.sys.mjs b/browser/components/newtab/lib/TelemetryFeed.sys.mjs new file mode 100644 index 0000000000..99bed168a8 --- /dev/null +++ b/browser/components/newtab/lib/TelemetryFeed.sys.mjs @@ -0,0 +1,1122 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// We use importESModule here instead of static import so that +// the Karma test environment won't choke on these module. This +// is because the Karma test environment already stubs out +// XPCOMUtils, and overrides importESModule to be a no-op (which +// can't be done for a static import statement). MESSAGE_TYPES_HASH / msg +// isn't something that the tests for this module seem to rely on in the +// Karma environment, but if that ever becomes the case, we should import +// those into unit-entry like we do for the ASRouter tests. + +// eslint-disable-next-line mozilla/use-static-import +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +// eslint-disable-next-line mozilla/use-static-import +const { MESSAGE_TYPE_HASH: msg } = ChromeUtils.importESModule( + "resource:///modules/asrouter/ActorConstants.sys.mjs" +); + +import { + actionTypes as at, + actionUtils as au, +} from "resource://activity-stream/common/Actions.sys.mjs"; +import { Prefs } from "resource://activity-stream/lib/ActivityStreamPrefs.sys.mjs"; +import { classifySite } from "resource://activity-stream/lib/SiteClassifier.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs", + AboutWelcomeTelemetry: + "resource:///modules/aboutwelcome/AboutWelcomeTelemetry.sys.mjs", + ClientID: "resource://gre/modules/ClientID.sys.mjs", + ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", + ExtensionSettingsStore: + "resource://gre/modules/ExtensionSettingsStore.sys.mjs", + HomePage: "resource:///modules/HomePage.sys.mjs", + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", + TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs", + TelemetrySession: "resource://gre/modules/TelemetrySession.sys.mjs", + UTEventReporting: "resource://activity-stream/lib/UTEventReporting.sys.mjs", + UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs", + pktApi: "chrome://pocket/content/pktApi.sys.mjs", +}); +ChromeUtils.defineLazyGetter( + lazy, + "Telemetry", + () => new lazy.AboutWelcomeTelemetry() +); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "handoffToAwesomebarPrefValue", + "browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar", + false, + (preference, previousValue, new_value) => + Glean.newtabHandoffPreference.enabled.set(new_value) +); + +// This is a mapping table between the user preferences and its encoding code +export const USER_PREFS_ENCODING = { + showSearch: 1 << 0, + "feeds.topsites": 1 << 1, + "feeds.section.topstories": 1 << 2, + "feeds.section.highlights": 1 << 3, + showSponsored: 1 << 5, + "asrouter.userprefs.cfr.addons": 1 << 6, + "asrouter.userprefs.cfr.features": 1 << 7, + showSponsoredTopSites: 1 << 8, +}; + +export const PREF_IMPRESSION_ID = "impressionId"; +export const TELEMETRY_PREF = "telemetry"; +export const EVENTS_TELEMETRY_PREF = "telemetry.ut.events"; + +// Used as the missing value for timestamps in the session ping +const TIMESTAMP_MISSING_VALUE = -1; + +// Page filter for onboarding telemetry, any value other than these will +// be set as "other" +const ONBOARDING_ALLOWED_PAGE_VALUES = [ + "about:welcome", + "about:home", + "about:newtab", +]; + +ChromeUtils.defineLazyGetter( + lazy, + "browserSessionId", + () => lazy.TelemetrySession.getMetadata("").sessionId +); + +// The scalar category for TopSites of Contextual Services +const SCALAR_CATEGORY_TOPSITES = "contextual.services.topsites"; +// `contextId` is a unique identifier used by Contextual Services +const CONTEXT_ID_PREF = "browser.contextual-services.contextId"; +ChromeUtils.defineLazyGetter(lazy, "contextId", () => { + let _contextId = Services.prefs.getStringPref(CONTEXT_ID_PREF, null); + if (!_contextId) { + _contextId = String(Services.uuid.generateUUID()); + Services.prefs.setStringPref(CONTEXT_ID_PREF, _contextId); + } + return _contextId; +}); + +const ACTIVITY_STREAM_PREF_BRANCH = "browser.newtabpage.activity-stream."; +const NEWTAB_PING_PREFS = { + showSearch: Glean.newtabSearch.enabled, + "feeds.topsites": Glean.topsites.enabled, + showSponsoredTopSites: Glean.topsites.sponsoredEnabled, + "feeds.section.topstories": Glean.pocket.enabled, + showSponsored: Glean.pocket.sponsoredStoriesEnabled, + topSitesRows: Glean.topsites.rows, +}; +const TOP_SITES_BLOCKED_SPONSORS_PREF = "browser.topsites.blockedSponsors"; + +export class TelemetryFeed { + constructor() { + this.sessions = new Map(); + this._prefs = new Prefs(); + this._impressionId = this.getOrCreateImpressionId(); + this._aboutHomeSeen = false; + this._classifySite = classifySite; + this._browserOpenNewtabStart = null; + } + + get telemetryEnabled() { + return this._prefs.get(TELEMETRY_PREF); + } + + get eventTelemetryEnabled() { + return this._prefs.get(EVENTS_TELEMETRY_PREF); + } + + get telemetryClientId() { + Object.defineProperty(this, "telemetryClientId", { + value: lazy.ClientID.getClientID(), + }); + return this.telemetryClientId; + } + + get processStartTs() { + let startupInfo = Services.startup.getStartupInfo(); + let processStartTs = startupInfo.process.getTime(); + + Object.defineProperty(this, "processStartTs", { + value: processStartTs, + }); + return this.processStartTs; + } + + init() { + this._beginObservingNewtabPingPrefs(); + Services.obs.addObserver( + this.browserOpenNewtabStart, + "browser-open-newtab-start" + ); + // Set two scalars for the "deletion-request" ping (See bug 1602064 and 1729474) + Services.telemetry.scalarSet( + "deletion.request.impression_id", + this._impressionId + ); + Services.telemetry.scalarSet("deletion.request.context_id", lazy.contextId); + Glean.newtab.locale.set(Services.locale.appLocaleAsBCP47); + Glean.newtabHandoffPreference.enabled.set( + lazy.handoffToAwesomebarPrefValue + ); + } + + getOrCreateImpressionId() { + let impressionId = this._prefs.get(PREF_IMPRESSION_ID); + if (!impressionId) { + impressionId = String(Services.uuid.generateUUID()); + this._prefs.set(PREF_IMPRESSION_ID, impressionId); + } + return impressionId; + } + + browserOpenNewtabStart() { + let now = Cu.now(); + this._browserOpenNewtabStart = Math.round(this.processStartTs + now); + + ChromeUtils.addProfilerMarker( + "UserTiming", + now, + "browser-open-newtab-start" + ); + } + + setLoadTriggerInfo(port) { + // XXX note that there is a race condition here; we're assuming that no + // other tab will be interleaving calls to browserOpenNewtabStart and + // when at.NEW_TAB_INIT gets triggered by RemotePages and calls this + // method. For manually created windows, it's hard to imagine us hitting + // this race condition. + // + // However, for session restore, where multiple windows with multiple tabs + // might be restored much closer together in time, it's somewhat less hard, + // though it should still be pretty rare. + // + // The fix to this would be making all of the load-trigger notifications + // return some data with their notifications, and somehow propagate that + // data through closures into the tab itself so that we could match them + // + // As of this writing (very early days of system add-on perf telemetry), + // the hypothesis is that hitting this race should be so rare that makes + // more sense to live with the slight data inaccuracy that it would + // introduce, rather than doing the correct but complicated thing. It may + // well be worth reexamining this hypothesis after we have more experience + // with the data. + + let data_to_save; + try { + if (!this._browserOpenNewtabStart) { + throw new Error("No browser-open-newtab-start recorded."); + } + data_to_save = { + load_trigger_ts: this._browserOpenNewtabStart, + load_trigger_type: "menu_plus_or_keyboard", + }; + } catch (e) { + // if no mark was returned, we have nothing to save + return; + } + this.saveSessionPerfData(port, data_to_save); + } + + /** + * Lazily initialize UTEventReporting to send pings + */ + get utEvents() { + Object.defineProperty(this, "utEvents", { + value: new lazy.UTEventReporting(), + }); + return this.utEvents; + } + + /** + * Get encoded user preferences, multiple prefs will be combined via bitwise OR operator + */ + get userPreferences() { + let prefs = 0; + + for (const pref of Object.keys(USER_PREFS_ENCODING)) { + if (this._prefs.get(pref)) { + prefs |= USER_PREFS_ENCODING[pref]; + } + } + return prefs; + } + + /** + * Check if it is in the CFR experiment cohort by querying against the + * experiment manager of Messaging System + * + * @return {bool} + */ + get isInCFRCohort() { + const experimentData = lazy.ExperimentAPI.getExperimentMetaData({ + featureId: "cfr", + }); + if (experimentData && experimentData.slug) { + return true; + } + + return false; + } + + /** + * addSession - Start tracking a new session + * + * @param {string} id the portID of the open session + * @param {string} the URL being loaded for this session (optional) + * @return {obj} Session object + */ + addSession(id, url) { + // XXX refactor to use setLoadTriggerInfo or saveSessionPerfData + + // "unexpected" will be overwritten when appropriate + let load_trigger_type = "unexpected"; + let load_trigger_ts; + + if (!this._aboutHomeSeen && url === "about:home") { + this._aboutHomeSeen = true; + + // XXX note that this will be incorrectly set in the following cases: + // session_restore following by clicking on the toolbar button, + // or someone who has changed their default home page preference to + // something else and later clicks the toolbar. It will also be + // incorrectly unset if someone changes their "Home Page" preference to + // about:newtab. + // + // That said, the ratio of these mistakes to correct cases should + // be very small, and these issues should follow away as we implement + // the remaining load_trigger_type values for about:home in issue 3556. + // + // XXX file a bug to implement remaining about:home cases so this + // problem will go away and link to it here. + load_trigger_type = "first_window_opened"; + + // The real perceived trigger of first_window_opened is the OS-level + // clicking of the icon. We express this by using the process start + // absolute timestamp. + load_trigger_ts = this.processStartTs; + } + + const session = { + session_id: String(Services.uuid.generateUUID()), + // "unknown" will be overwritten when appropriate + page: url ? url : "unknown", + perf: { + load_trigger_type, + is_preloaded: false, + }, + }; + + if (load_trigger_ts) { + session.perf.load_trigger_ts = load_trigger_ts; + } + + this.sessions.set(id, session); + return session; + } + + /** + * endSession - Stop tracking a session + * + * @param {string} portID the portID of the session that just closed + */ + endSession(portID) { + const session = this.sessions.get(portID); + + if (!session) { + // It's possible the tab was never visible – in which case, there was no user session. + return; + } + + Glean.newtab.closed.record({ newtab_visit_id: session.session_id }); + if ( + this.telemetryEnabled && + (lazy.NimbusFeatures.glean.getVariable("newtabPingEnabled") ?? true) + ) { + GleanPings.newtab.submit("newtab_session_end"); + } + + if (session.perf.visibility_event_rcvd_ts) { + let absNow = this.processStartTs + Cu.now(); + session.session_duration = Math.round( + absNow - session.perf.visibility_event_rcvd_ts + ); + + // Rounding all timestamps in perf to ease the data processing on the backend. + // NB: use `TIMESTAMP_MISSING_VALUE` if the value is missing. + session.perf.visibility_event_rcvd_ts = Math.round( + session.perf.visibility_event_rcvd_ts + ); + session.perf.load_trigger_ts = Math.round( + session.perf.load_trigger_ts || TIMESTAMP_MISSING_VALUE + ); + session.perf.topsites_first_painted_ts = Math.round( + session.perf.topsites_first_painted_ts || TIMESTAMP_MISSING_VALUE + ); + } else { + // This session was never shown (i.e. the hidden preloaded newtab), there was no user session either. + this.sessions.delete(portID); + return; + } + + let sessionEndEvent = this.createSessionEndEvent(session); + this.sendUTEvent(sessionEndEvent, this.utEvents.sendSessionEndEvent); + this.sessions.delete(portID); + } + + /** + * handleNewTabInit - Handle NEW_TAB_INIT, which creates a new session and sets the a flag + * for session.perf based on whether or not this new tab is preloaded + * + * @param {obj} action the Action object + */ + handleNewTabInit(action) { + const session = this.addSession( + au.getPortIdOfSender(action), + action.data.url + ); + session.perf.is_preloaded = + action.data.browser.getAttribute("preloadedState") === "preloaded"; + } + + /** + * createPing - Create a ping with common properties + * + * @param {string} id The portID of the session, if a session is relevant (optional) + * @return {obj} A telemetry ping + */ + createPing(portID) { + const ping = { + addon_version: Services.appinfo.appBuildID, + locale: Services.locale.appLocaleAsBCP47, + user_prefs: this.userPreferences, + }; + + // If the ping is part of a user session, add session-related info + if (portID) { + const session = this.sessions.get(portID) || this.addSession(portID); + Object.assign(ping, { session_id: session.session_id }); + + if (session.page) { + Object.assign(ping, { page: session.page }); + } + } + return ping; + } + + createUserEvent(action) { + return Object.assign( + this.createPing(au.getPortIdOfSender(action)), + action.data, + { action: "activity_stream_user_event" } + ); + } + + createSessionEndEvent(session) { + return Object.assign(this.createPing(), { + session_id: session.session_id, + page: session.page, + session_duration: session.session_duration, + action: "activity_stream_session", + perf: session.perf, + profile_creation_date: + lazy.TelemetryEnvironment.currentEnvironment.profile.resetDate || + lazy.TelemetryEnvironment.currentEnvironment.profile.creationDate, + }); + } + + /** + * Create a ping for AS router event. The client_id is set to "n/a" by default, + * different component can override this by its own telemetry collection policy. + */ + async createASRouterEvent(action) { + let event = { + ...action.data, + addon_version: Services.appinfo.appBuildID, + locale: Services.locale.appLocaleAsBCP47, + }; + const session = this.sessions.get(au.getPortIdOfSender(action)); + if (event.event_context && typeof event.event_context === "object") { + event.event_context = JSON.stringify(event.event_context); + } + switch (event.action) { + case "cfr_user_event": + event = await this.applyCFRPolicy(event); + break; + case "badge_user_event": + case "whats-new-panel_user_event": + event = await this.applyWhatsNewPolicy(event); + break; + case "infobar_user_event": + event = await this.applyInfoBarPolicy(event); + break; + case "spotlight_user_event": + event = await this.applySpotlightPolicy(event); + break; + case "toast_notification_user_event": + event = await this.applyToastNotificationPolicy(event); + break; + case "moments_user_event": + event = await this.applyMomentsPolicy(event); + break; + case "onboarding_user_event": + event = await this.applyOnboardingPolicy(event, session); + break; + case "asrouter_undesired_event": + event = this.applyUndesiredEventPolicy(event); + break; + default: + event = { ping: event }; + break; + } + return event; + } + + /** + * Per Bug 1484035, CFR metrics comply with following policies: + * 1). In release, it collects impression_id and bucket_id + * 2). In prerelease, it collects client_id and message_id + * 3). In shield experiments conducted in release, it collects client_id and message_id + * 4). In Private Browsing windows, unless in experiment, collects impression_id and bucket_id + */ + async applyCFRPolicy(ping) { + if ( + (lazy.UpdateUtils.getUpdateChannel(true) === "release" || + ping.is_private) && + !this.isInCFRCohort + ) { + ping.message_id = "n/a"; + ping.impression_id = this._impressionId; + } else { + ping.client_id = await this.telemetryClientId; + } + delete ping.action; + delete ping.is_private; + return { ping, pingType: "cfr" }; + } + + /** + * Per Bug 1482134, all the metrics for What's New panel use client_id in + * all the release channels + */ + async applyWhatsNewPolicy(ping) { + ping.client_id = await this.telemetryClientId; + ping.browser_session_id = lazy.browserSessionId; + // Attach page info to `event_context` if there is a session associated with this ping + delete ping.action; + return { ping, pingType: "whats-new-panel" }; + } + + async applyInfoBarPolicy(ping) { + ping.client_id = await this.telemetryClientId; + ping.browser_session_id = lazy.browserSessionId; + delete ping.action; + return { ping, pingType: "infobar" }; + } + + async applySpotlightPolicy(ping) { + ping.client_id = await this.telemetryClientId; + ping.browser_session_id = lazy.browserSessionId; + delete ping.action; + return { ping, pingType: "spotlight" }; + } + + async applyToastNotificationPolicy(ping) { + ping.client_id = await this.telemetryClientId; + ping.browser_session_id = lazy.browserSessionId; + delete ping.action; + return { ping, pingType: "toast_notification" }; + } + + /** + * Per Bug 1484035, Moments metrics comply with following policies: + * 1). In release, it collects impression_id, and treats bucket_id as message_id + * 2). In prerelease, it collects client_id and message_id + * 3). In shield experiments conducted in release, it collects client_id and message_id + */ + async applyMomentsPolicy(ping) { + if ( + lazy.UpdateUtils.getUpdateChannel(true) === "release" && + !this.isInCFRCohort + ) { + ping.message_id = "n/a"; + ping.impression_id = this._impressionId; + } else { + ping.client_id = await this.telemetryClientId; + } + delete ping.action; + return { ping, pingType: "moments" }; + } + + /** + * Per Bug 1482134, all the metrics for Onboarding in AS router use client_id in + * all the release channels + */ + async applyOnboardingPolicy(ping, session) { + ping.client_id = await this.telemetryClientId; + ping.browser_session_id = lazy.browserSessionId; + // Attach page info to `event_context` if there is a session associated with this ping + if (ping.action === "onboarding_user_event" && session && session.page) { + let event_context; + + try { + event_context = ping.event_context + ? JSON.parse(ping.event_context) + : {}; + } catch (e) { + // If `ping.event_context` is not a JSON serialized string, then we create a `value` + // key for it + event_context = { value: ping.event_context }; + } + + if (ONBOARDING_ALLOWED_PAGE_VALUES.includes(session.page)) { + event_context.page = session.page; + } else { + console.error(`Invalid 'page' for Onboarding event: ${session.page}`); + } + ping.event_context = JSON.stringify(event_context); + } + delete ping.action; + return { ping, pingType: "onboarding" }; + } + + applyUndesiredEventPolicy(ping) { + ping.impression_id = this._impressionId; + delete ping.action; + return { ping, pingType: "undesired-events" }; + } + + sendUTEvent(event_object, eventFunction) { + if (this.telemetryEnabled && this.eventTelemetryEnabled) { + eventFunction(event_object); + } + } + + handleTopSitesSponsoredImpressionStats(action) { + const { data } = action; + const { + type, + position, + source, + advertiser: advertiser_name, + tile_id, + } = data; + // Legacy telemetry expects 1-based tile positions. + const legacyTelemetryPosition = position + 1; + + let pingType; + + const session = this.sessions.get(au.getPortIdOfSender(action)); + if (type === "impression") { + pingType = "topsites-impression"; + Services.telemetry.keyedScalarAdd( + `${SCALAR_CATEGORY_TOPSITES}.impression`, + `${source}_${legacyTelemetryPosition}`, + 1 + ); + if (session) { + Glean.topsites.impression.record({ + advertiser_name, + tile_id, + newtab_visit_id: session.session_id, + is_sponsored: true, + position, + }); + } + } else if (type === "click") { + pingType = "topsites-click"; + Services.telemetry.keyedScalarAdd( + `${SCALAR_CATEGORY_TOPSITES}.click`, + `${source}_${legacyTelemetryPosition}`, + 1 + ); + if (session) { + Glean.topsites.click.record({ + advertiser_name, + tile_id, + newtab_visit_id: session.session_id, + is_sponsored: true, + position, + }); + } + } else { + console.error("Unknown ping type for sponsored TopSites impression"); + return; + } + + Glean.topSites.pingType.set(pingType); + Glean.topSites.position.set(legacyTelemetryPosition); + Glean.topSites.source.set(source); + Glean.topSites.tileId.set(tile_id); + if (data.reporting_url) { + Glean.topSites.reportingUrl.set(data.reporting_url); + } + Glean.topSites.advertiser.set(advertiser_name); + Glean.topSites.contextId.set(lazy.contextId); + GleanPings.topSites.submit(); + } + + handleTopSitesOrganicImpressionStats(action) { + const session = this.sessions.get(au.getPortIdOfSender(action)); + if (!session) { + return; + } + + switch (action.data?.type) { + case "impression": + Glean.topsites.impression.record({ + newtab_visit_id: session.session_id, + is_sponsored: false, + position: action.data.position, + }); + break; + + case "click": + Glean.topsites.click.record({ + newtab_visit_id: session.session_id, + is_sponsored: false, + position: action.data.position, + }); + break; + + default: + break; + } + } + + handleUserEvent(action) { + let userEvent = this.createUserEvent(action); + this.sendUTEvent(userEvent, this.utEvents.sendUserEvent); + } + + handleDiscoveryStreamUserEvent(action) { + const pocket_logged_in_status = lazy.pktApi.isUserLoggedIn(); + Glean.pocket.isSignedIn.set(pocket_logged_in_status); + this.handleUserEvent({ + ...action, + data: { + ...(action.data || {}), + value: { + ...(action.data?.value || {}), + pocket_logged_in_status, + }, + }, + }); + const session = this.sessions.get(au.getPortIdOfSender(action)); + switch (action.data?.event) { + case "CLICK": + const { card_type, topic, recommendation_id, tile_id, shim } = + action.data.value ?? {}; + if ( + action.data.source === "POPULAR_TOPICS" || + card_type === "topics_widget" + ) { + Glean.pocket.topicClick.record({ + newtab_visit_id: session.session_id, + topic, + }); + } else if (["spoc", "organic"].includes(card_type)) { + Glean.pocket.click.record({ + newtab_visit_id: session.session_id, + is_sponsored: card_type === "spoc", + position: action.data.action_position, + recommendation_id, + tile_id, + }); + if (shim) { + Glean.pocket.shim.set(shim); + GleanPings.spoc.submit("click"); + } + } + break; + case "SAVE_TO_POCKET": + Glean.pocket.save.record({ + newtab_visit_id: session.session_id, + is_sponsored: action.data.value?.card_type === "spoc", + position: action.data.action_position, + recommendation_id: action.data.value?.recommendation_id, + tile_id: action.data.value?.tile_id, + }); + if (action.data.value?.shim) { + Glean.pocket.shim.set(action.data.value.shim); + GleanPings.spoc.submit("save"); + } + break; + } + } + + async handleASRouterUserEvent(action) { + const { ping, pingType } = await this.createASRouterEvent(action); + if (!pingType) { + console.error("Unknown ping type for ASRouter telemetry"); + return; + } + + // Now that the action has become a ping, we can echo it to Glean. + if (this.telemetryEnabled) { + lazy.Telemetry.submitGleanPingForPing({ ...ping, pingType }); + } + } + + /** + * This function is used by ActivityStreamStorage to report errors + * trying to access IndexedDB. + */ + SendASRouterUndesiredEvent(data) { + this.handleASRouterUserEvent({ + data: { ...data, action: "asrouter_undesired_event" }, + }); + } + + async sendPageTakeoverData() { + if (this.telemetryEnabled) { + const value = {}; + let homeAffected = false; + let newtabCategory = "disabled"; + let homePageCategory = "disabled"; + + // Check whether or not about:home and about:newtab are set to a custom URL. + // If so, classify them. + if (Services.prefs.getBoolPref("browser.newtabpage.enabled")) { + newtabCategory = "enabled"; + if ( + lazy.AboutNewTab.newTabURLOverridden && + !lazy.AboutNewTab.newTabURL.startsWith("moz-extension://") + ) { + value.newtab_url_category = await this._classifySite( + lazy.AboutNewTab.newTabURL + ); + newtabCategory = value.newtab_url_category; + } + } + // Check if the newtab page setting is controlled by an extension. + await lazy.ExtensionSettingsStore.initialize(); + const newtabExtensionInfo = lazy.ExtensionSettingsStore.getSetting( + "url_overrides", + "newTabURL" + ); + if (newtabExtensionInfo && newtabExtensionInfo.id) { + value.newtab_extension_id = newtabExtensionInfo.id; + newtabCategory = "extension"; + } + + const homePageURL = lazy.HomePage.get(); + if ( + !["about:home", "about:blank"].includes(homePageURL) && + !homePageURL.startsWith("moz-extension://") + ) { + value.home_url_category = await this._classifySite(homePageURL); + homeAffected = true; + homePageCategory = value.home_url_category; + } + const homeExtensionInfo = lazy.ExtensionSettingsStore.getSetting( + "prefs", + "homepage_override" + ); + if (homeExtensionInfo && homeExtensionInfo.id) { + value.home_extension_id = homeExtensionInfo.id; + homeAffected = true; + homePageCategory = "extension"; + } + if (!homeAffected && !lazy.HomePage.overridden) { + homePageCategory = "enabled"; + } + + Glean.newtab.newtabCategory.set(newtabCategory); + Glean.newtab.homepageCategory.set(homePageCategory); + if (lazy.NimbusFeatures.glean.getVariable("newtabPingEnabled") ?? true) { + GleanPings.newtab.submit("component_init"); + } + } + } + + onAction(action) { + switch (action.type) { + case at.INIT: + this.init(); + this.sendPageTakeoverData(); + break; + case at.NEW_TAB_INIT: + this.handleNewTabInit(action); + break; + case at.NEW_TAB_UNLOAD: + this.endSession(au.getPortIdOfSender(action)); + break; + case at.SAVE_SESSION_PERF_DATA: + this.saveSessionPerfData(au.getPortIdOfSender(action), action.data); + break; + case at.DISCOVERY_STREAM_IMPRESSION_STATS: + this.handleDiscoveryStreamImpressionStats( + au.getPortIdOfSender(action), + action.data + ); + break; + case at.DISCOVERY_STREAM_USER_EVENT: + this.handleDiscoveryStreamUserEvent(action); + break; + case at.TELEMETRY_USER_EVENT: + this.handleUserEvent(action); + break; + // The next few action types come from ASRouter, which doesn't use + // Actions from Actions.jsm, but uses these other custom strings. + case msg.TOOLBAR_BADGE_TELEMETRY: + // Intentional fall-through + case msg.TOOLBAR_PANEL_TELEMETRY: + // Intentional fall-through + case msg.MOMENTS_PAGE_TELEMETRY: + // Intentional fall-through + case msg.DOORHANGER_TELEMETRY: + // Intentional fall-through + case msg.INFOBAR_TELEMETRY: + // Intentional fall-through + case msg.SPOTLIGHT_TELEMETRY: + // Intentional fall-through + case msg.TOAST_NOTIFICATION_TELEMETRY: + // Intentional fall-through + case at.AS_ROUTER_TELEMETRY_USER_EVENT: + this.handleASRouterUserEvent(action); + break; + case at.TOP_SITES_SPONSORED_IMPRESSION_STATS: + this.handleTopSitesSponsoredImpressionStats(action); + break; + case at.TOP_SITES_ORGANIC_IMPRESSION_STATS: + this.handleTopSitesOrganicImpressionStats(action); + break; + case at.UNINIT: + this.uninit(); + break; + case at.ABOUT_SPONSORED_TOP_SITES: + this.handleAboutSponsoredTopSites(action); + break; + case at.BLOCK_URL: + this.handleBlockUrl(action); + break; + } + } + + handleBlockUrl(action) { + const session = this.sessions.get(au.getPortIdOfSender(action)); + // TODO: Do we want to not send this unless there's a newtab_visit_id? + if (!session) { + return; + } + + // Despite the action name, this is actually a bulk dismiss action: + // it can be applied to multiple topsites simultaneously. + const { data } = action; + for (const datum of data) { + if (datum.is_pocket_card) { + // There is no instrumentation for Pocket dismissals (yet). + continue; + } + const { position, advertiser_name, tile_id, isSponsoredTopSite } = datum; + Glean.topsites.dismiss.record({ + advertiser_name, + tile_id, + newtab_visit_id: session.session_id, + is_sponsored: !!isSponsoredTopSite, + position, + }); + } + } + + handleAboutSponsoredTopSites(action) { + const session = this.sessions.get(au.getPortIdOfSender(action)); + const { data } = action; + const { position, advertiser_name, tile_id } = data; + + if (session) { + Glean.topsites.showPrivacyClick.record({ + advertiser_name, + tile_id, + newtab_visit_id: session.session_id, + position, + }); + } + } + + /** + * Handle impression stats actions from Discovery Stream. + * + * @param {String} port The session port with which this is associated + * @param {Object} data The impression data structured as {source: "SOURCE", tiles: [{id: 123}]} + * + */ + handleDiscoveryStreamImpressionStats(port, data) { + let session = this.sessions.get(port); + + if (!session) { + throw new Error("Session does not exist."); + } + + const { tiles } = data; + tiles.forEach(tile => { + Glean.pocket.impression.record({ + newtab_visit_id: session.session_id, + is_sponsored: tile.type === "spoc", + position: tile.pos, + recommendation_id: tile.recommendation_id, + tile_id: tile.id, + }); + if (tile.shim) { + Glean.pocket.shim.set(tile.shim); + GleanPings.spoc.submit("impression"); + } + }); + } + + /** + * Take all enumerable members of the data object and merge them into + * the session.perf object for the given port, so that it is sent to the + * server when the session ends. All members of the data object should + * be valid values of the perf object, as defined in pings.js and the + * data*.md documentation. + * + * @note Any existing keys with the same names already in the + * session perf object will be overwritten by values passed in here. + * + * @param {String} port The session with which this is associated + * @param {Object} data The perf data to be + */ + saveSessionPerfData(port, data) { + // XXX should use try/catch and send a bad state indicator if this + // get blows up. + let session = this.sessions.get(port); + + // XXX Partial workaround for #3118; avoids the worst incorrect associations + // of times with browsers, by associating the load trigger with the + // visibility event as the user is most likely associating the trigger to + // the tab just shown. This helps avoid associating with a preloaded + // browser as those don't get the event until shown. Better fix for more + // cases forthcoming. + // + // XXX the about:home check (and the corresponding test) should go away + // once the load_trigger stuff in addSession is refactored into + // setLoadTriggerInfo. + // + if (data.visibility_event_rcvd_ts && session.page !== "about:home") { + this.setLoadTriggerInfo(port); + } + + let timestamp = data.topsites_first_painted_ts; + + if ( + timestamp && + session.page === "about:home" && + !lazy.HomePage.overridden && + Services.prefs.getIntPref("browser.startup.page") === 1 + ) { + lazy.AboutNewTab.maybeRecordTopsitesPainted(timestamp); + } + + Object.assign(session.perf, data); + + if (data.visibility_event_rcvd_ts && !session.newtabOpened) { + session.newtabOpened = true; + const source = ONBOARDING_ALLOWED_PAGE_VALUES.includes(session.page) + ? session.page + : "other"; + Glean.newtab.opened.record({ + newtab_visit_id: session.session_id, + source, + }); + } + } + + _beginObservingNewtabPingPrefs() { + Services.prefs.addObserver(ACTIVITY_STREAM_PREF_BRANCH, this); + + for (const pref of Object.keys(NEWTAB_PING_PREFS)) { + const fullPrefName = ACTIVITY_STREAM_PREF_BRANCH + pref; + this._setNewtabPrefMetrics(fullPrefName, false); + } + Glean.pocket.isSignedIn.set(lazy.pktApi.isUserLoggedIn()); + + Services.prefs.addObserver(TOP_SITES_BLOCKED_SPONSORS_PREF, this); + this._setBlockedSponsorsMetrics(); + } + + _stopObservingNewtabPingPrefs() { + Services.prefs.removeObserver(ACTIVITY_STREAM_PREF_BRANCH, this); + Services.prefs.removeObserver(TOP_SITES_BLOCKED_SPONSORS_PREF, this); + } + + observe(subject, topic, data) { + if (data === TOP_SITES_BLOCKED_SPONSORS_PREF) { + this._setBlockedSponsorsMetrics(); + } else { + this._setNewtabPrefMetrics(data, true); + } + } + + _setNewtabPrefMetrics(fullPrefName, isChanged) { + const pref = fullPrefName.slice(ACTIVITY_STREAM_PREF_BRANCH.length); + if (!Object.hasOwn(NEWTAB_PING_PREFS, pref)) { + return; + } + const metric = NEWTAB_PING_PREFS[pref]; + switch (Services.prefs.getPrefType(fullPrefName)) { + case Services.prefs.PREF_BOOL: + metric.set(Services.prefs.getBoolPref(fullPrefName)); + break; + + case Services.prefs.PREF_INT: + metric.set(Services.prefs.getIntPref(fullPrefName)); + break; + } + if (isChanged) { + switch (fullPrefName) { + case `${ACTIVITY_STREAM_PREF_BRANCH}feeds.topsites`: + case `${ACTIVITY_STREAM_PREF_BRANCH}showSponsoredTopSites`: + Glean.topsites.prefChanged.record({ + pref_name: fullPrefName, + new_value: Services.prefs.getBoolPref(fullPrefName), + }); + break; + } + } + } + + _setBlockedSponsorsMetrics() { + let blocklist; + try { + blocklist = JSON.parse( + Services.prefs.getStringPref(TOP_SITES_BLOCKED_SPONSORS_PREF, "[]") + ); + } catch (e) {} + if (blocklist) { + Glean.newtab.blockedSponsors.set(blocklist); + } + } + + uninit() { + this._stopObservingNewtabPingPrefs(); + + try { + Services.obs.removeObserver( + this.browserOpenNewtabStart, + "browser-open-newtab-start" + ); + } catch (e) { + // Operation can fail when uninit is called before + // init has finished setting up the observer + } + + // Only uninit if the getter has initialized it + if (Object.prototype.hasOwnProperty.call(this, "utEvents")) { + this.utEvents.uninit(); + } + + // TODO: Send any unfinished sessions + } +} diff --git a/browser/components/newtab/lib/TippyTopProvider.sys.mjs b/browser/components/newtab/lib/TippyTopProvider.sys.mjs new file mode 100644 index 0000000000..8f32516119 --- /dev/null +++ b/browser/components/newtab/lib/TippyTopProvider.sys.mjs @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const TIPPYTOP_PATH = "chrome://activity-stream/content/data/content/tippytop/"; +const TIPPYTOP_JSON_PATH = + "chrome://activity-stream/content/data/content/tippytop/top_sites.json"; + +/* + * Get a domain from a url optionally stripping subdomains. + */ +export function getDomain(url, strip = "www.") { + let domain = ""; + try { + domain = new URL(url).hostname; + } catch (ex) {} + if (strip === "*") { + try { + domain = Services.eTLD.getBaseDomainFromHost(domain); + } catch (ex) {} + } else if (domain.startsWith(strip)) { + domain = domain.slice(strip.length); + } + return domain; +} + +export class TippyTopProvider { + constructor() { + this._sitesByDomain = new Map(); + this.initialized = false; + } + + async init() { + // Load the Tippy Top sites from the json manifest. + try { + for (const site of await ( + await fetch(TIPPYTOP_JSON_PATH, { + credentials: "omit", + }) + ).json()) { + for (const domain of site.domains) { + this._sitesByDomain.set(domain, site); + } + } + this.initialized = true; + } catch (error) { + console.error("Failed to load tippy top manifest."); + } + } + + processSite(site, strip) { + const tippyTop = this._sitesByDomain.get(getDomain(site.url, strip)); + if (tippyTop) { + site.tippyTopIcon = TIPPYTOP_PATH + tippyTop.image_url; + site.smallFavicon = TIPPYTOP_PATH + tippyTop.favicon_url; + site.backgroundColor = tippyTop.background_color; + } + return site; + } +} diff --git a/browser/components/newtab/lib/TopSitesFeed.sys.mjs b/browser/components/newtab/lib/TopSitesFeed.sys.mjs new file mode 100644 index 0000000000..db21411fdd --- /dev/null +++ b/browser/components/newtab/lib/TopSitesFeed.sys.mjs @@ -0,0 +1,2007 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { + actionCreators as ac, + actionTypes as at, +} from "resource://activity-stream/common/Actions.sys.mjs"; +import { TippyTopProvider } from "resource://activity-stream/lib/TippyTopProvider.sys.mjs"; +import { + insertPinned, + TOP_SITES_MAX_SITES_PER_ROW, +} from "resource://activity-stream/common/Reducers.sys.mjs"; +import { Dedupe } from "resource://activity-stream/common/Dedupe.sys.mjs"; +import { shortURL } from "resource://activity-stream/lib/ShortURL.sys.mjs"; +import { getDefaultOptions } from "resource://activity-stream/lib/ActivityStreamStorage.sys.mjs"; + +import { + CUSTOM_SEARCH_SHORTCUTS, + SEARCH_SHORTCUTS_EXPERIMENT, + SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF, + SEARCH_SHORTCUTS_HAVE_PINNED_PREF, + checkHasSearchEngine, + getSearchProvider, + getSearchFormURL, +} from "resource://activity-stream/lib/SearchShortcuts.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + FilterAdult: "resource://activity-stream/lib/FilterAdult.sys.mjs", + LinksCache: "resource://activity-stream/lib/LinksCache.sys.mjs", + NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", + PageThumbs: "resource://gre/modules/PageThumbs.sys.mjs", + Region: "resource://gre/modules/Region.sys.mjs", + RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", + Sampling: "resource://gre/modules/components-utils/Sampling.sys.mjs", + Screenshots: "resource://activity-stream/lib/Screenshots.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "log", () => { + const { Logger } = ChromeUtils.importESModule( + "resource://messaging-system/lib/Logger.sys.mjs" + ); + return new Logger("TopSitesFeed"); +}); + +// `contextId` is a unique identifier used by Contextual Services +const CONTEXT_ID_PREF = "browser.contextual-services.contextId"; +ChromeUtils.defineLazyGetter(lazy, "contextId", () => { + let _contextId = Services.prefs.getStringPref(CONTEXT_ID_PREF, null); + if (!_contextId) { + _contextId = String(Services.uuid.generateUUID()); + Services.prefs.setStringPref(CONTEXT_ID_PREF, _contextId); + } + return _contextId; +}); + +const DEFAULT_SITES_PREF = "default.sites"; +const SHOWN_ON_NEWTAB_PREF = "feeds.topsites"; +export const DEFAULT_TOP_SITES = []; +const FRECENCY_THRESHOLD = 100 + 1; // 1 visit (skip first-run/one-time pages) +const MIN_FAVICON_SIZE = 96; +const CACHED_LINK_PROPS_TO_MIGRATE = ["screenshot", "customScreenshot"]; +const PINNED_FAVICON_PROPS_TO_MIGRATE = [ + "favicon", + "faviconRef", + "faviconSize", +]; +const SECTION_ID = "topsites"; +const ROWS_PREF = "topSitesRows"; +const SHOW_SPONSORED_PREF = "showSponsoredTopSites"; +// The default total number of sponsored top sites to fetch from Contile +// and Pocket. +const MAX_NUM_SPONSORED = 2; +// Nimbus variable for the total number of sponsored top sites including +// both Contile and Pocket sources. +// The default will be `MAX_NUM_SPONSORED` if this variable is unspecified. +const NIMBUS_VARIABLE_MAX_SPONSORED = "topSitesMaxSponsored"; +// Nimbus variable to allow more than two sponsored tiles from Contile to be +//considered for Top Sites. +const NIMBUS_VARIABLE_ADDITIONAL_TILES = + "topSitesUseAdditionalTilesFromContile"; +// Nimbus variable to enable the SOV feature for sponsored tiles. +const NIMBUS_VARIABLE_CONTILE_SOV_ENABLED = "topSitesContileSovEnabled"; +// Nimbu variable for the total number of sponsor topsite that come from Contile +// The default will be `CONTILE_MAX_NUM_SPONSORED` if variable is unspecified. +const NIMBUS_VARIABLE_CONTILE_MAX_NUM_SPONSORED = "topSitesContileMaxSponsored"; + +// Search experiment stuff +const FILTER_DEFAULT_SEARCH_PREF = "improvesearch.noDefaultSearchTile"; +const SEARCH_FILTERS = [ + "google", + "search.yahoo", + "yahoo", + "bing", + "ask", + "duckduckgo", +]; + +const REMOTE_SETTING_DEFAULTS_PREF = "browser.topsites.useRemoteSetting"; +const DEFAULT_SITES_OVERRIDE_PREF = + "browser.newtabpage.activity-stream.default.sites"; +const DEFAULT_SITES_EXPERIMENTS_PREF_BRANCH = "browser.topsites.experiment."; + +// Mozilla Tiles Service (Contile) prefs +// Nimbus variable for the Contile integration. It falls back to the pref: +// `browser.topsites.contile.enabled`. +const NIMBUS_VARIABLE_CONTILE_ENABLED = "topSitesContileEnabled"; +const NIMBUS_VARIABLE_CONTILE_POSITIONS = "contileTopsitesPositions"; +const CONTILE_ENDPOINT_PREF = "browser.topsites.contile.endpoint"; +const CONTILE_UPDATE_INTERVAL = 15 * 60 * 1000; // 15 minutes +// The maximum number of sponsored top sites to fetch from Contile. +const CONTILE_MAX_NUM_SPONSORED = 2; +const TOP_SITES_BLOCKED_SPONSORS_PREF = "browser.topsites.blockedSponsors"; +const CONTILE_CACHE_PREF = "browser.topsites.contile.cachedTiles"; +const CONTILE_CACHE_VALID_FOR_PREF = "browser.topsites.contile.cacheValidFor"; +const CONTILE_CACHE_LAST_FETCH_PREF = "browser.topsites.contile.lastFetch"; + +// Partners of sponsored tiles. +const SPONSORED_TILE_PARTNER_AMP = "amp"; +const SPONSORED_TILE_PARTNER_MOZ_SALES = "moz-sales"; +const SPONSORED_TILE_PARTNERS = new Set([ + SPONSORED_TILE_PARTNER_AMP, + SPONSORED_TILE_PARTNER_MOZ_SALES, +]); + +const DISPLAY_FAIL_REASON_OVERSOLD = "oversold"; +const DISPLAY_FAIL_REASON_DISMISSED = "dismissed"; +const DISPLAY_FAIL_REASON_UNRESOLVED = "unresolved"; + +function getShortURLForCurrentSearch() { + const url = shortURL({ url: Services.search.defaultEngine.searchForm }); + return url; +} + +class TopSitesTelemetry { + constructor() { + this.allSponsoredTiles = {}; + this.sponsoredTilesConfigured = 0; + } + + _tileProviderForTiles(tiles) { + // Assumption: the list of tiles is from a single provider + return tiles && tiles.length ? this._tileProvider(tiles[0]) : null; + } + + _tileProvider(tile) { + return tile.partner || SPONSORED_TILE_PARTNER_AMP; + } + + _buildPropertyKey(tile) { + let provider = this._tileProvider(tile); + return provider + shortURL(tile); + } + + // Returns an array of strings indicating the property name (based on the + // provider and brand) of tiles that have been filtered e.g. ["moz-salesbrand1"] + // currentTiles: The list of tiles remaining and may be displayed in new tab. + // this.allSponsoredTiles: The original list of tiles set via setTiles prior to any filtering + // The returned list indicated the difference between these two lists (excluding any previously filtered tiles). + _getFilteredTiles(currentTiles) { + let notPreviouslyFilteredTiles = Object.assign( + {}, + ...Object.entries(this.allSponsoredTiles) + .filter( + ([k, v]) => + v.display_fail_reason === null || + v.display_fail_reason === undefined + ) + .map(([k, v]) => ({ [k]: v })) + ); + + // Get the property names of the newly filtered list. + let remainingTiles = currentTiles.map(el => { + return this._buildPropertyKey(el); + }); + + // Get the property names of the tiles that were filtered. + let tilesToUpdate = Object.keys(notPreviouslyFilteredTiles).filter( + element => !remainingTiles.includes(element) + ); + return tilesToUpdate; + } + + setSponsoredTilesConfigured() { + const maxSponsored = + lazy.NimbusFeatures.pocketNewtab.getVariable( + NIMBUS_VARIABLE_MAX_SPONSORED + ) ?? MAX_NUM_SPONSORED; + + this.sponsoredTilesConfigured = maxSponsored; + Glean.topsites.sponsoredTilesConfigured.set(maxSponsored); + } + + clearTilesForProvider(provider) { + Object.entries(this.allSponsoredTiles) + .filter(([k, v]) => k.startsWith(provider)) + .map(([k, v]) => delete this.allSponsoredTiles[k]); + } + + _getAdvertiser(tile) { + let label = tile.label || null; + let title = tile.title || null; + + return label ?? title ?? shortURL(tile); + } + + setTiles(tiles) { + // Assumption: the list of tiles is from a single provider, + // should be called once per tile source. + if (tiles && tiles.length) { + let tile_provider = this._tileProviderForTiles(tiles); + this.clearTilesForProvider(tile_provider); + + for (let sponsoredTile of tiles) { + this.allSponsoredTiles[this._buildPropertyKey(sponsoredTile)] = { + advertiser: this._getAdvertiser(sponsoredTile).toLowerCase(), + provider: tile_provider, + display_position: null, + display_fail_reason: null, + }; + } + } + } + + _setDisplayFailReason(filteredTiles, reason) { + for (let tile of filteredTiles) { + if (tile in this.allSponsoredTiles) { + let tileToUpdate = this.allSponsoredTiles[tile]; + tileToUpdate.display_position = null; + tileToUpdate.display_fail_reason = reason; + } + } + } + + determineFilteredTilesAndSetToOversold(nonOversoldTiles) { + let filteredTiles = this._getFilteredTiles(nonOversoldTiles); + this._setDisplayFailReason(filteredTiles, DISPLAY_FAIL_REASON_OVERSOLD); + } + + determineFilteredTilesAndSetToDismissed(nonDismissedTiles) { + let filteredTiles = this._getFilteredTiles(nonDismissedTiles); + this._setDisplayFailReason(filteredTiles, DISPLAY_FAIL_REASON_DISMISSED); + } + + _setTilePositions(currentTiles) { + // This function performs many loops over a small dataset. The size of + // dataset is limited by the number of sponsored tiles displayed on + // the newtab instance. + if (this.allSponsoredTiles) { + let tilePositionsAssigned = []; + // processing the currentTiles parameter, assigns a position to the + // corresponding property in this.allSponsoredTiles + currentTiles.forEach(item => { + let tile = this.allSponsoredTiles[this._buildPropertyKey(item)]; + if ( + tile && + (tile.display_fail_reason === undefined || + tile.display_fail_reason === null) + ) { + tile.display_position = item.sponsored_position; + // Track assigned tile slots. + tilePositionsAssigned.push(item.sponsored_position); + } + }); + + // Need to check if any objects in this.allSponsoredTiles do not + // have either a display_fail_reason or a display_position set. + // This can happen if the tiles list was updated before the + // metric is written to Glean. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1877197 + let tilesMissingPosition = []; + Object.keys(this.allSponsoredTiles).forEach(property => { + let tile = this.allSponsoredTiles[property]; + if (!tile.display_fail_reason && !tile.display_position) { + tilesMissingPosition.push(property); + } + }); + + if (tilesMissingPosition.length) { + // Determine if any available slots exist based on max number of tiles + // and the list of tiles already used and assign to a tile with missing + // value. + for (let i = 1; i <= this.sponsoredTilesConfigured; i++) { + if (!tilePositionsAssigned.includes(i)) { + let tileProperty = tilesMissingPosition.shift(); + this.allSponsoredTiles[tileProperty].display_position = i; + } + } + } + + // At this point we might still have a few unresolved states. These + // rows will be tagged with a display_fail_reason `unresolved`. + this._detectErrorConditionAndSetUnresolved(); + } + } + + // Checks the data for inconsistent state and updates the display_fail_reason + _detectErrorConditionAndSetUnresolved() { + Object.keys(this.allSponsoredTiles).forEach(property => { + let tile = this.allSponsoredTiles[property]; + if ( + (!tile.display_fail_reason && !tile.display_position) || + (tile.display_fail_reason && tile.display_position) + ) { + tile.display_position = null; + tile.display_fail_reason = DISPLAY_FAIL_REASON_UNRESOLVED; + } + }); + } + + finalizeNewtabPingFields(currentTiles) { + this._setTilePositions(currentTiles); + Glean.topsites.sponsoredTilesReceived.set( + JSON.stringify({ + sponsoredTilesReceived: Object.values(this.allSponsoredTiles), + }) + ); + } +} + +export class ContileIntegration { + constructor(topSitesFeed) { + this._topSitesFeed = topSitesFeed; + this._lastPeriodicUpdate = 0; + this._sites = []; + // The Share-of-Voice object managed by Shepherd and sent via Contile. + this._sov = null; + } + + get sites() { + return this._sites; + } + + get sov() { + return this._sov; + } + + periodicUpdate() { + let now = Date.now(); + if (now - this._lastPeriodicUpdate >= CONTILE_UPDATE_INTERVAL) { + this._lastPeriodicUpdate = now; + this.refresh(); + } + } + + async refresh() { + let updateDefaultSites = await this._fetchSites(); + await this._topSitesFeed.allocatePositions(); + if (updateDefaultSites) { + this._topSitesFeed._readDefaults(); + } + } + + /** + * Clear Contile Cache Prefs. + */ + _resetContileCachePrefs() { + Services.prefs.clearUserPref(CONTILE_CACHE_PREF); + Services.prefs.clearUserPref(CONTILE_CACHE_LAST_FETCH_PREF); + Services.prefs.clearUserPref(CONTILE_CACHE_VALID_FOR_PREF); + } + + /** + * Filter the tiles whose sponsor is on the Top Sites sponsor blocklist. + * + * @param {array} tiles + * An array of the tile objects + */ + _filterBlockedSponsors(tiles) { + const blocklist = JSON.parse( + Services.prefs.getStringPref(TOP_SITES_BLOCKED_SPONSORS_PREF, "[]") + ); + return tiles.filter(tile => !blocklist.includes(shortURL(tile))); + } + + /** + * Calculate the time Contile response is valid for based on cache-control header + * + * @param {string} cacheHeader + * string value of the Contile resposne cache-control header + */ + _extractCacheValidFor(cacheHeader) { + if (!cacheHeader) { + lazy.log.warn("Contile response cache control header is empty"); + return 0; + } + const [, staleIfError] = cacheHeader.match(/stale-if-error=\s*([0-9]+)/i); + const [, maxAge] = cacheHeader.match(/max-age=\s*([0-9]+)/i); + const validFor = + Number.parseInt(staleIfError, 10) + Number.parseInt(maxAge, 10); + return isNaN(validFor) ? 0 : validFor; + } + + /** + * Load Tiles from Contile Cache Prefs + */ + _loadTilesFromCache() { + lazy.log.info("Contile client is trying to load tiles from local cache."); + const now = Math.round(Date.now() / 1000); + const lastFetch = Services.prefs.getIntPref( + CONTILE_CACHE_LAST_FETCH_PREF, + 0 + ); + const validFor = Services.prefs.getIntPref(CONTILE_CACHE_VALID_FOR_PREF, 0); + this._topSitesFeed._telemetryUtility.setSponsoredTilesConfigured(); + if (now <= lastFetch + validFor) { + try { + let cachedTiles = JSON.parse( + Services.prefs.getStringPref(CONTILE_CACHE_PREF) + ); + this._topSitesFeed._telemetryUtility.setTiles(cachedTiles); + cachedTiles = this._filterBlockedSponsors(cachedTiles); + this._topSitesFeed._telemetryUtility.determineFilteredTilesAndSetToDismissed( + cachedTiles + ); + this._sites = cachedTiles; + lazy.log.info("Local cache loaded."); + return true; + } catch (error) { + lazy.log.warn(`Failed to load tiles from local cache: ${error}.`); + return false; + } + } + + return false; + } + + /** + * Determine number of Tiles to get from Contile + */ + _getMaxNumFromContile() { + return ( + lazy.NimbusFeatures.pocketNewtab.getVariable( + NIMBUS_VARIABLE_CONTILE_MAX_NUM_SPONSORED + ) ?? CONTILE_MAX_NUM_SPONSORED + ); + } + + async _fetchSites() { + if ( + !lazy.NimbusFeatures.newtab.getVariable( + NIMBUS_VARIABLE_CONTILE_ENABLED + ) || + !this._topSitesFeed.store.getState().Prefs.values[SHOW_SPONSORED_PREF] + ) { + if (this._sites.length) { + this._sites = []; + return true; + } + return false; + } + try { + let url = Services.prefs.getStringPref(CONTILE_ENDPOINT_PREF); + const response = await this._topSitesFeed.fetch(url, { + credentials: "omit", + }); + if (!response.ok) { + lazy.log.warn( + `Contile endpoint returned unexpected status: ${response.status}` + ); + if (response.status === 304 || response.status >= 500) { + return this._loadTilesFromCache(); + } + } + + const lastFetch = Math.round(Date.now() / 1000); + Services.prefs.setIntPref(CONTILE_CACHE_LAST_FETCH_PREF, lastFetch); + this._topSitesFeed._telemetryUtility.setSponsoredTilesConfigured(); + + // Contile returns 204 indicating there is no content at the moment. + // If this happens, it will clear `this._sites` reset the cached tiles + // to an empty array. + if (response.status === 204) { + this._topSitesFeed._telemetryUtility.clearTilesForProvider( + SPONSORED_TILE_PARTNER_AMP + ); + if (this._sites.length) { + this._sites = []; + Services.prefs.setStringPref( + CONTILE_CACHE_PREF, + JSON.stringify(this._sites) + ); + return true; + } + return false; + } + const body = await response.json(); + + if (body?.sov) { + this._sov = JSON.parse(atob(body.sov)); + } + if (body?.tiles && Array.isArray(body.tiles)) { + const useAdditionalTiles = lazy.NimbusFeatures.newtab.getVariable( + NIMBUS_VARIABLE_ADDITIONAL_TILES + ); + + const maxNumFromContile = this._getMaxNumFromContile(); + + let { tiles } = body; + this._topSitesFeed._telemetryUtility.setTiles(tiles); + if ( + useAdditionalTiles !== undefined && + !useAdditionalTiles && + tiles.length > maxNumFromContile + ) { + tiles.length = maxNumFromContile; + this._topSitesFeed._telemetryUtility.determineFilteredTilesAndSetToOversold( + tiles + ); + } + tiles = this._filterBlockedSponsors(tiles); + this._topSitesFeed._telemetryUtility.determineFilteredTilesAndSetToDismissed( + tiles + ); + if (tiles.length > maxNumFromContile) { + lazy.log.info("Remove unused links from Contile"); + tiles.length = maxNumFromContile; + this._topSitesFeed._telemetryUtility.determineFilteredTilesAndSetToOversold( + tiles + ); + } + this._sites = tiles; + Services.prefs.setStringPref( + CONTILE_CACHE_PREF, + JSON.stringify(this._sites) + ); + Services.prefs.setIntPref( + CONTILE_CACHE_VALID_FOR_PREF, + this._extractCacheValidFor( + response.headers.get("cache-control") || + response.headers.get("Cache-Control") + ) + ); + + return true; + } + } catch (error) { + lazy.log.warn( + `Failed to fetch data from Contile server: ${error.message}` + ); + return this._loadTilesFromCache(); + } + return false; + } +} + +export class TopSitesFeed { + constructor() { + this._telemetryUtility = new TopSitesTelemetry(); + this._contile = new ContileIntegration(this); + this._tippyTopProvider = new TippyTopProvider(); + ChromeUtils.defineLazyGetter( + this, + "_currentSearchHostname", + getShortURLForCurrentSearch + ); + this.dedupe = new Dedupe(this._dedupeKey); + this.frecentCache = new lazy.LinksCache( + lazy.NewTabUtils.activityStreamLinks, + "getTopSites", + CACHED_LINK_PROPS_TO_MIGRATE, + (oldOptions, newOptions) => + // Refresh if no old options or requesting more items + !(oldOptions.numItems >= newOptions.numItems) + ); + this.pinnedCache = new lazy.LinksCache( + lazy.NewTabUtils.pinnedLinks, + "links", + [...CACHED_LINK_PROPS_TO_MIGRATE, ...PINNED_FAVICON_PROPS_TO_MIGRATE] + ); + lazy.PageThumbs.addExpirationFilter(this); + this._nimbusChangeListener = this._nimbusChangeListener.bind(this); + } + + _nimbusChangeListener(event, reason) { + // The Nimbus API current doesn't specify the changed variable(s) in the + // listener callback, so we have to refresh unconditionally on every change + // of the `newtab` feature. It should be a manageable overhead given the + // current update cadence (6 hours) of Nimbus. + // + // Skip the experiment and rollout loading reasons since this feature has + // `isEarlyStartup` enabled, the feature variables are already available + // before the experiment or rollout loads. + if ( + !["feature-experiment-loaded", "feature-rollout-loaded"].includes(reason) + ) { + this._contile.refresh(); + } + } + + init() { + // If the feed was previously disabled PREFS_INITIAL_VALUES was never received + this._readDefaults({ isStartup: true }); + this._storage = this.store.dbStorage.getDbTable("sectionPrefs"); + this._contile.refresh(); + Services.obs.addObserver(this, "browser-search-engine-modified"); + Services.obs.addObserver(this, "browser-region-updated"); + Services.prefs.addObserver(REMOTE_SETTING_DEFAULTS_PREF, this); + Services.prefs.addObserver(DEFAULT_SITES_OVERRIDE_PREF, this); + Services.prefs.addObserver(DEFAULT_SITES_EXPERIMENTS_PREF_BRANCH, this); + lazy.NimbusFeatures.newtab.onUpdate(this._nimbusChangeListener); + } + + uninit() { + lazy.PageThumbs.removeExpirationFilter(this); + Services.obs.removeObserver(this, "browser-search-engine-modified"); + Services.obs.removeObserver(this, "browser-region-updated"); + Services.prefs.removeObserver(REMOTE_SETTING_DEFAULTS_PREF, this); + Services.prefs.removeObserver(DEFAULT_SITES_OVERRIDE_PREF, this); + Services.prefs.removeObserver(DEFAULT_SITES_EXPERIMENTS_PREF_BRANCH, this); + lazy.NimbusFeatures.newtab.offUpdate(this._nimbusChangeListener); + } + + observe(subj, topic, data) { + switch (topic) { + case "browser-search-engine-modified": + // We should update the current top sites if the search engine has been changed since + // the search engine that gets filtered out of top sites has changed. + // We also need to drop search shortcuts when their engine gets removed / hidden. + if ( + data === "engine-default" && + this.store.getState().Prefs.values[FILTER_DEFAULT_SEARCH_PREF] + ) { + delete this._currentSearchHostname; + this._currentSearchHostname = getShortURLForCurrentSearch(); + } + this.refresh({ broadcast: true }); + break; + case "browser-region-updated": + this._readDefaults(); + break; + case "nsPref:changed": + if ( + data === REMOTE_SETTING_DEFAULTS_PREF || + data === DEFAULT_SITES_OVERRIDE_PREF || + data.startsWith(DEFAULT_SITES_EXPERIMENTS_PREF_BRANCH) + ) { + this._readDefaults(); + } + break; + } + } + + _dedupeKey(site) { + return site && site.hostname; + } + + /** + * _readDefaults - sets DEFAULT_TOP_SITES + */ + async _readDefaults({ isStartup = false } = {}) { + this._useRemoteSetting = false; + + if (!Services.prefs.getBoolPref(REMOTE_SETTING_DEFAULTS_PREF)) { + this.refreshDefaults( + this.store.getState().Prefs.values[DEFAULT_SITES_PREF], + { isStartup } + ); + return; + } + + // Try using default top sites from enterprise policies or tests. The pref + // is locked when set via enterprise policy. Tests have no default sites + // unless they set them via this pref. + if ( + Services.prefs.prefIsLocked(DEFAULT_SITES_OVERRIDE_PREF) || + Cu.isInAutomation + ) { + let sites = Services.prefs.getStringPref(DEFAULT_SITES_OVERRIDE_PREF, ""); + this.refreshDefaults(sites, { isStartup }); + return; + } + + // Clear out the array of any previous defaults. + DEFAULT_TOP_SITES.length = 0; + + // Read defaults from contile. + const contileEnabled = lazy.NimbusFeatures.newtab.getVariable( + NIMBUS_VARIABLE_CONTILE_ENABLED + ); + + // Keep the number of positions in the array in sync with CONTILE_MAX_NUM_SPONSORED. + // sponsored_position is a 1-based index, and contilePositions is a 0-based index, + // so we need to add 1 to each of these. + // Also currently this does not work with SOV. + let contilePositions = lazy.NimbusFeatures.pocketNewtab + .getVariable(NIMBUS_VARIABLE_CONTILE_POSITIONS) + ?.split(",") + .map(item => parseInt(item, 10) + 1) + .filter(item => !Number.isNaN(item)); + if (!contilePositions || contilePositions.length === 0) { + contilePositions = [1, 2]; + } + + let hasContileTiles = false; + if (contileEnabled) { + let contilePositionIndex = 0; + // We need to loop through potential spocs and set their positions. + // If we run out of spocs or positions, we stop. + // First, we need to know which array is shortest. This is our exit condition. + const minLength = Math.min( + contilePositions.length, + this._contile.sites.length + ); + // Loop until we run out of spocs or positions. + for (let i = 0; i < minLength; i++) { + let site = this._contile.sites[i]; + let hostname = shortURL(site); + let link = { + isDefault: true, + url: site.url, + hostname, + sendAttributionRequest: false, + label: site.name, + show_sponsored_label: hostname !== "yandex", + sponsored_position: contilePositions[contilePositionIndex++], + sponsored_click_url: site.click_url, + sponsored_impression_url: site.impression_url, + sponsored_tile_id: site.id, + partner: SPONSORED_TILE_PARTNER_AMP, + }; + if (site.image_url && site.image_size >= MIN_FAVICON_SIZE) { + // Only use the image from Contile if it's hi-res, otherwise, fallback + // to the built-in favicons. + link.favicon = site.image_url; + link.faviconSize = site.image_size; + } + DEFAULT_TOP_SITES.push(link); + } + hasContileTiles = contilePositionIndex > 0; + //This is to catch where we receive 3 tiles but reduce to 2 early in the filtering, before blocked list applied. + this._telemetryUtility.determineFilteredTilesAndSetToOversold( + DEFAULT_TOP_SITES + ); + } + + // Read defaults from remote settings. + this._useRemoteSetting = true; + let remoteSettingData = await this._getRemoteConfig(); + + const sponsoredBlocklist = JSON.parse( + Services.prefs.getStringPref(TOP_SITES_BLOCKED_SPONSORS_PREF, "[]") + ); + + for (let siteData of remoteSettingData) { + let hostname = shortURL(siteData); + // Drop default sites when Contile already provided a sponsored one with + // the same host name. + if ( + contileEnabled && + DEFAULT_TOP_SITES.findIndex(site => site.hostname === hostname) > -1 + ) { + continue; + } + // Also drop those sponsored sites that were blocked by the user before + // with the same hostname. + if ( + siteData.sponsored_position && + sponsoredBlocklist.includes(hostname) + ) { + continue; + } + let link = { + isDefault: true, + url: siteData.url, + hostname, + sendAttributionRequest: !!siteData.send_attribution_request, + }; + if (siteData.url_urlbar_override) { + link.url_urlbar = siteData.url_urlbar_override; + } + if (siteData.title) { + link.label = siteData.title; + } + if (siteData.search_shortcut) { + link = await this.topSiteToSearchTopSite(link); + } else if (siteData.sponsored_position) { + if (contileEnabled && hasContileTiles) { + continue; + } + const { + sponsored_position, + sponsored_tile_id, + sponsored_impression_url, + sponsored_click_url, + } = siteData; + link = { + sponsored_position, + sponsored_tile_id, + sponsored_impression_url, + sponsored_click_url, + show_sponsored_label: link.hostname !== "yandex", + ...link, + }; + } + DEFAULT_TOP_SITES.push(link); + } + + this.refresh({ broadcast: true, isStartup }); + } + + refreshDefaults(sites, { isStartup = false } = {}) { + // Clear out the array of any previous defaults + DEFAULT_TOP_SITES.length = 0; + + // Add default sites if any based on the pref + if (sites) { + for (const url of sites.split(",")) { + const site = { + isDefault: true, + url, + }; + site.hostname = shortURL(site); + DEFAULT_TOP_SITES.push(site); + } + } + + this.refresh({ broadcast: true, isStartup }); + } + + async _getRemoteConfig(firstTime = true) { + if (!this._remoteConfig) { + this._remoteConfig = await lazy.RemoteSettings("top-sites"); + this._remoteConfig.on("sync", () => { + this._readDefaults(); + }); + } + + let result = []; + let failed = false; + try { + result = await this._remoteConfig.get(); + } catch (ex) { + console.error(ex); + failed = true; + } + if (!result.length) { + console.error("Received empty top sites configuration!"); + failed = true; + } + // If we failed, or the result is empty, try loading from the local dump. + if (firstTime && failed) { + await this._remoteConfig.db.clear(); + // Now call this again. + return this._getRemoteConfig(false); + } + + // Sort sites based on the "order" attribute. + result.sort((a, b) => a.order - b.order); + + result = result.filter(topsite => { + // Filter by region. + if (topsite.exclude_regions?.includes(lazy.Region.home)) { + return false; + } + if ( + topsite.include_regions?.length && + !topsite.include_regions.includes(lazy.Region.home) + ) { + return false; + } + + // Filter by locale. + if (topsite.exclude_locales?.includes(Services.locale.appLocaleAsBCP47)) { + return false; + } + if ( + topsite.include_locales?.length && + !topsite.include_locales.includes(Services.locale.appLocaleAsBCP47) + ) { + return false; + } + + // Filter by experiment. + // Exclude this top site if any of the specified experiments are running. + if ( + topsite.exclude_experiments?.some(experimentID => + Services.prefs.getBoolPref( + DEFAULT_SITES_EXPERIMENTS_PREF_BRANCH + experimentID, + false + ) + ) + ) { + return false; + } + // Exclude this top site if none of the specified experiments are running. + if ( + topsite.include_experiments?.length && + topsite.include_experiments.every( + experimentID => + !Services.prefs.getBoolPref( + DEFAULT_SITES_EXPERIMENTS_PREF_BRANCH + experimentID, + false + ) + ) + ) { + return false; + } + + return true; + }); + + return result; + } + + filterForThumbnailExpiration(callback) { + const { rows } = this.store.getState().TopSites; + callback( + rows.reduce((acc, site) => { + acc.push(site.url); + if (site.customScreenshotURL) { + acc.push(site.customScreenshotURL); + } + return acc; + }, []) + ); + } + + /** + * shouldFilterSearchTile - is default filtering enabled and does a given hostname match the user's default search engine? + * + * @param {string} hostname a top site hostname, such as "amazon" or "foo" + * @returns {bool} + */ + shouldFilterSearchTile(hostname) { + if ( + this.store.getState().Prefs.values[FILTER_DEFAULT_SEARCH_PREF] && + (SEARCH_FILTERS.includes(hostname) || + hostname === this._currentSearchHostname) + ) { + return true; + } + return false; + } + + /** + * _maybeInsertSearchShortcuts - if the search shortcuts experiment is running, + * insert search shortcuts if needed + * @param {Array} plainPinnedSites (from the pinnedSitesCache) + * @returns {Boolean} Did we insert any search shortcuts? + */ + async _maybeInsertSearchShortcuts(plainPinnedSites) { + // Only insert shortcuts if the experiment is running + if (this.store.getState().Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT]) { + // We don't want to insert shortcuts we've previously inserted + const prevInsertedShortcuts = this.store + .getState() + .Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF].split(",") + .filter(s => s); // Filter out empty strings + const newInsertedShortcuts = []; + + let shouldPin = this._useRemoteSetting + ? DEFAULT_TOP_SITES.filter(s => s.searchTopSite).map(s => s.hostname) + : this.store + .getState() + .Prefs.values[SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF].split(","); + shouldPin = shouldPin + .map(getSearchProvider) + .filter(s => s && s.shortURL !== this._currentSearchHostname); + + // If we've previously inserted all search shortcuts return early + if ( + shouldPin.every(shortcut => + prevInsertedShortcuts.includes(shortcut.shortURL) + ) + ) { + return false; + } + + const numberOfSlots = + this.store.getState().Prefs.values[ROWS_PREF] * + TOP_SITES_MAX_SITES_PER_ROW; + + // The plainPinnedSites array is populated with pinned sites at their + // respective indices, and null everywhere else, but is not always the + // right length + const emptySlots = Math.max(numberOfSlots - plainPinnedSites.length, 0); + const pinnedSites = [...plainPinnedSites].concat( + Array(emptySlots).fill(null) + ); + + const tryToInsertSearchShortcut = async shortcut => { + const nextAvailable = pinnedSites.indexOf(null); + // Only add a search shortcut if the site isn't already pinned, we + // haven't previously inserted it, there's space to pin it, and the + // search engine is available in Firefox + if ( + !pinnedSites.find(s => s && shortURL(s) === shortcut.shortURL) && + !prevInsertedShortcuts.includes(shortcut.shortURL) && + nextAvailable > -1 && + (await checkHasSearchEngine(shortcut.keyword)) + ) { + const site = await this.topSiteToSearchTopSite({ url: shortcut.url }); + this._pinSiteAt(site, nextAvailable); + pinnedSites[nextAvailable] = site; + newInsertedShortcuts.push(shortcut.shortURL); + } + }; + + for (let shortcut of shouldPin) { + await tryToInsertSearchShortcut(shortcut); + } + + if (newInsertedShortcuts.length) { + this.store.dispatch( + ac.SetPref( + SEARCH_SHORTCUTS_HAVE_PINNED_PREF, + prevInsertedShortcuts.concat(newInsertedShortcuts).join(",") + ) + ); + return true; + } + } + + return false; + } + + /** + * This thin wrapper around global.fetch makes it easier for us to write + * automated tests that simulate responses from this fetch. + */ + fetch(...args) { + return fetch(...args); + } + + /** + * Fetch topsites spocs from the DiscoveryStream feed. + * + * @returns {Array} An array of sponsored tile objects. + */ + fetchDiscoveryStreamSpocs() { + let sponsored = []; + const { DiscoveryStream } = this.store.getState(); + if (DiscoveryStream) { + const discoveryStreamSpocs = + DiscoveryStream.spocs.data["sponsored-topsites"]?.items || []; + // Find the first component of a type and remove it from layout + const findSponsoredTopsitesPositions = name => { + for (const row of DiscoveryStream.layout) { + for (const component of row.components) { + if (component.placement?.name === name) { + return component.spocs.positions; + } + } + } + return null; + }; + + // Get positions from layout for now. This could be improved if we store position data in state. + const discoveryStreamSpocPositions = + findSponsoredTopsitesPositions("sponsored-topsites"); + + if (discoveryStreamSpocPositions?.length) { + function reformatImageURL(url, width, height) { + // Change the image URL to request a size tailored for the parent container width + // Also: force JPEG, quality 60, no upscaling, no EXIF data + // Uses Thumbor: https://thumbor.readthedocs.io/en/latest/usage.html + // For now we wrap this in single quotes because this is being used in a url() css rule, and otherwise would cause a parsing error. + return `'https://img-getpocket.cdn.mozilla.net/${width}x${height}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/${encodeURIComponent( + url + )}'`; + } + + // We need to loop through potential spocs and set their positions. + // If we run out of spocs or positions, we stop. + // First, we need to know which array is shortest. This is our exit condition. + const minLength = Math.min( + discoveryStreamSpocPositions.length, + discoveryStreamSpocs.length + ); + // Loop until we run out of spocs or positions. + for (let i = 0; i < minLength; i++) { + const positionIndex = discoveryStreamSpocPositions[i].index; + const spoc = discoveryStreamSpocs[i]; + const link = { + favicon: reformatImageURL(spoc.raw_image_src, 96, 96), + faviconSize: 96, + type: "SPOC", + label: spoc.title || spoc.sponsor, + title: spoc.title || spoc.sponsor, + url: spoc.url, + flightId: spoc.flight_id, + id: spoc.id, + guid: spoc.id, + shim: spoc.shim, + // For now we are assuming position based on intended position. + // Actual position can shift based on other content. + // We send the intended position in the ping. + pos: positionIndex, + // Set this so that SPOC topsites won't be shown in the URL bar. + // See Bug 1822027. Note that `sponsored_position` is 1-based. + sponsored_position: positionIndex + 1, + // This is used for topsites deduping. + hostname: shortURL({ url: spoc.url }), + partner: SPONSORED_TILE_PARTNER_MOZ_SALES, + }; + sponsored.push(link); + } + } + } + return sponsored; + } + + // eslint-disable-next-line max-statements + async getLinksWithDefaults(isStartup = false) { + const prefValues = this.store.getState().Prefs.values; + const numItems = prefValues[ROWS_PREF] * TOP_SITES_MAX_SITES_PER_ROW; + const searchShortcutsExperiment = prefValues[SEARCH_SHORTCUTS_EXPERIMENT]; + // We must wait for search services to initialize in order to access default + // search engine properties without triggering a synchronous initialization + try { + await Services.search.init(); + } catch { + // We continue anyway because we want the user to see their sponsored, + // saved, or visited shortcut tiles even if search engines are not + // available. + } + + // Get all frecent sites from history. + let frecent = []; + const cache = await this.frecentCache.request({ + // We need to overquery due to the top 5 alexa search + default search possibly being removed + numItems: numItems + SEARCH_FILTERS.length + 1, + topsiteFrecency: FRECENCY_THRESHOLD, + }); + for (let link of cache) { + const hostname = shortURL(link); + if (!this.shouldFilterSearchTile(hostname)) { + frecent.push({ + ...(searchShortcutsExperiment + ? await this.topSiteToSearchTopSite(link) + : link), + hostname, + }); + } + } + + // Get defaults. + let contileSponsored = []; + let notBlockedDefaultSites = []; + for (let link of DEFAULT_TOP_SITES) { + // For sponsored Yandex links, default filtering is reversed: we only + // show them if Yandex is the default search engine. + if (link.sponsored_position && link.hostname === "yandex") { + if (link.hostname !== this._currentSearchHostname) { + continue; + } + } else if (this.shouldFilterSearchTile(link.hostname)) { + continue; + } + // Drop blocked default sites. + if ( + lazy.NewTabUtils.blockedLinks.isBlocked({ + url: link.url, + }) + ) { + continue; + } + // If we've previously blocked a search shortcut, remove the default top site + // that matches the hostname + const searchProvider = getSearchProvider(shortURL(link)); + if ( + searchProvider && + lazy.NewTabUtils.blockedLinks.isBlocked({ url: searchProvider.url }) + ) { + continue; + } + if (link.sponsored_position) { + if (!prefValues[SHOW_SPONSORED_PREF]) { + continue; + } + contileSponsored[link.sponsored_position - 1] = link; + + // Unpin search shortcut if present for the sponsored link to be shown + // instead. + this._unpinSearchShortcut(link.hostname); + } else { + notBlockedDefaultSites.push( + searchShortcutsExperiment + ? await this.topSiteToSearchTopSite(link) + : link + ); + } + } + this._telemetryUtility.determineFilteredTilesAndSetToDismissed( + contileSponsored + ); + + const discoverySponsored = this.fetchDiscoveryStreamSpocs(); + this._telemetryUtility.setTiles(discoverySponsored); + + const sponsored = this._mergeSponsoredLinks({ + [SPONSORED_TILE_PARTNER_AMP]: contileSponsored, + [SPONSORED_TILE_PARTNER_MOZ_SALES]: discoverySponsored, + }); + + this._maybeCapSponsoredLinks(sponsored); + + // This will set all extra tiles to oversold, including moz-sales. + this._telemetryUtility.determineFilteredTilesAndSetToOversold(sponsored); + + // Get pinned links augmented with desired properties + let plainPinned = await this.pinnedCache.request(); + + // Insert search shortcuts if we need to. + // _maybeInsertSearchShortcuts returns true if any search shortcuts are + // inserted, meaning we need to expire and refresh the pinnedCache + if (await this._maybeInsertSearchShortcuts(plainPinned)) { + this.pinnedCache.expire(); + plainPinned = await this.pinnedCache.request(); + } + + const pinned = await Promise.all( + plainPinned.map(async link => { + if (!link) { + return link; + } + + // Drop pinned search shortcuts when their engine has been removed / hidden. + if (link.searchTopSite) { + const searchProvider = getSearchProvider(shortURL(link)); + if ( + !searchProvider || + !(await checkHasSearchEngine(searchProvider.keyword)) + ) { + return null; + } + } + + // Copy all properties from a frecent link and add more + const finder = other => other.url === link.url; + + // Remove frecent link's screenshot if pinned link has a custom one + const frecentSite = frecent.find(finder); + if (frecentSite && link.customScreenshotURL) { + delete frecentSite.screenshot; + } + // If the link is a frecent site, do not copy over 'isDefault', else check + // if the site is a default site + const copy = Object.assign( + {}, + frecentSite || { isDefault: !!notBlockedDefaultSites.find(finder) }, + link, + { hostname: shortURL(link) }, + { searchTopSite: !!link.searchTopSite } + ); + + // Add in favicons if we don't already have it + if (!copy.favicon) { + try { + lazy.NewTabUtils.activityStreamProvider._faviconBytesToDataURI( + await lazy.NewTabUtils.activityStreamProvider._addFavicons([copy]) + ); + + for (const prop of PINNED_FAVICON_PROPS_TO_MIGRATE) { + copy.__sharedCache.updateLink(prop, copy[prop]); + } + } catch (e) { + // Some issue with favicon, so just continue without one + } + } + + return copy; + }) + ); + + // Remove any duplicates from frecent and default sites + const [, dedupedSponsored, dedupedFrecent, dedupedDefaults] = + this.dedupe.group(pinned, sponsored, frecent, notBlockedDefaultSites); + const dedupedUnpinned = [...dedupedFrecent, ...dedupedDefaults]; + + // Remove adult sites if we need to + const checkedAdult = lazy.FilterAdult.filter(dedupedUnpinned); + + // Insert the original pinned sites into the deduped frecent and defaults. + let withPinned = insertPinned(checkedAdult, pinned); + // Insert sponsored sites at their desired position. + dedupedSponsored.forEach(link => { + if (!link) { + return; + } + let index = link.sponsored_position - 1; + if (index >= withPinned.length) { + withPinned[index] = link; + } else if (withPinned[index]?.sponsored_position) { + // We currently want DiscoveryStream spocs to replace existing spocs. + withPinned[index] = link; + } else { + withPinned.splice(index, 0, link); + } + }); + // Remove excess items after we inserted sponsored ones. + withPinned = withPinned.slice(0, numItems); + + // Now, get a tippy top icon, a rich icon, or screenshot for every item + for (const link of withPinned) { + if (link) { + // If there is a custom screenshot this is the only image we display + if (link.customScreenshotURL) { + this._fetchScreenshot(link, link.customScreenshotURL, isStartup); + } else if (link.searchTopSite && !link.isDefault) { + await this._attachTippyTopIconForSearchShortcut(link, link.label); + } else { + this._fetchIcon(link, isStartup); + } + + // Remove internal properties that might be updated after dispatch + delete link.__sharedCache; + + // Indicate that these links should get a frecency bonus when clicked + link.typedBonus = true; + } + } + + this._linksWithDefaults = withPinned; + + this._telemetryUtility.finalizeNewtabPingFields(dedupedSponsored); + return withPinned; + } + + /** + * Cap sponsored links if they're more than the specified maximum. + * + * @param {Array} links An array of sponsored links. Capping will be performed in-place. + */ + _maybeCapSponsoredLinks(links) { + // Set maximum sponsored top sites + const maxSponsored = + lazy.NimbusFeatures.pocketNewtab.getVariable( + NIMBUS_VARIABLE_MAX_SPONSORED + ) ?? MAX_NUM_SPONSORED; + if (links.length > maxSponsored) { + links.length = maxSponsored; + } + } + + /** + * Merge sponsored links from all the partners using SOV if present. + * For each tile position, the user is assigned to one partner via stable sampling. + * If the chosen partner doesn't have a tile to serve, another tile from a different + * partner is used as the replacement. + * + * @param {Object} sponsoredLinks An object with sponsored links from all the partners. + * @returns {Array} An array of merged sponsored links. + */ + _mergeSponsoredLinks(sponsoredLinks) { + const { positions: allocatedPositions, ready: sovReady } = + this.store.getState().TopSites.sov || {}; + if ( + !this._contile.sov || + !sovReady || + !lazy.NimbusFeatures.pocketNewtab.getVariable( + NIMBUS_VARIABLE_CONTILE_SOV_ENABLED + ) + ) { + return Object.values(sponsoredLinks).flat(); + } + + // AMP links might have empty slots, remove them as SOV doesn't need those. + sponsoredLinks[SPONSORED_TILE_PARTNER_AMP] = + sponsoredLinks[SPONSORED_TILE_PARTNER_AMP].filter(Boolean); + + let sponsored = []; + let chosenPartners = []; + + for (const allocation of allocatedPositions) { + let link = null; + const { assignedPartner } = allocation; + if (assignedPartner) { + // Unknown partners are allowed so that new parters can be added to Shepherd + // sooner without waiting for client changes. + link = sponsoredLinks[assignedPartner]?.shift(); + } + + if (!link) { + // If the chosen partner doesn't have a tile for this postion, choose any + // one from another group. For simplicity, we do _not_ do resampling here + // against the remaining partners. + for (const partner of SPONSORED_TILE_PARTNERS) { + if ( + partner === assignedPartner || + sponsoredLinks[partner].length === 0 + ) { + continue; + } + link = sponsoredLinks[partner].shift(); + break; + } + + if (!link) { + // No more links to be added across all the partners, just return. + if (chosenPartners.length) { + Glean.newtab.sovAllocation.set( + chosenPartners.map(entry => JSON.stringify(entry)) + ); + } + return sponsored; + } + } + + // Update the position fields. Note that postion is also 1-based in SOV. + link.sponsored_position = allocation.position; + if (link.pos !== undefined) { + // Pocket `pos` is 0-based. + link.pos = allocation.position - 1; + } + sponsored.push(link); + + chosenPartners.push({ + pos: allocation.position, + assigned: assignedPartner, // The assigned partner based on SOV + chosen: link.partner, + }); + } + // Record chosen partners to glean + if (chosenPartners.length) { + Glean.newtab.sovAllocation.set( + chosenPartners.map(entry => JSON.stringify(entry)) + ); + } + + // add the remaining contile sponsoredLinks when nimbus variable present + if ( + lazy.NimbusFeatures.pocketNewtab.getVariable( + NIMBUS_VARIABLE_CONTILE_MAX_NUM_SPONSORED + ) + ) { + return sponsored.concat(sponsoredLinks[SPONSORED_TILE_PARTNER_AMP]); + } + + return sponsored; + } + + /** + * Attach TippyTop icon to the given search shortcut + * + * Note that it queries the search form URL from search service For Yandex, + * and uses it to choose the best icon for its shortcut variants. + * + * @param {Object} link A link object with a `url` property + * @param {string} keyword Search keyword + */ + async _attachTippyTopIconForSearchShortcut(link, keyword) { + if ( + ["@\u044F\u043D\u0434\u0435\u043A\u0441", "@yandex"].includes(keyword) + ) { + let site = { url: link.url }; + site.url = (await getSearchFormURL(keyword)) || site.url; + this._tippyTopProvider.processSite(site); + link.tippyTopIcon = site.tippyTopIcon; + link.smallFavicon = site.smallFavicon; + link.backgroundColor = site.backgroundColor; + } else { + this._tippyTopProvider.processSite(link); + } + } + + /** + * Refresh the top sites data for content. + * @param {bool} options.broadcast Should the update be broadcasted. + * @param {bool} options.isStartup Being called while TopSitesFeed is initting. + */ + async refresh(options = {}) { + if (!this._startedUp && !options.isStartup) { + // Initial refresh still pending. + return; + } + this._startedUp = true; + + if (!this._tippyTopProvider.initialized) { + await this._tippyTopProvider.init(); + } + + const links = await this.getLinksWithDefaults({ + isStartup: options.isStartup, + }); + const newAction = { type: at.TOP_SITES_UPDATED, data: { links } }; + let storedPrefs; + try { + storedPrefs = (await this._storage.get(SECTION_ID)) || {}; + } catch (e) { + storedPrefs = {}; + console.error("Problem getting stored prefs for TopSites"); + } + newAction.data.pref = getDefaultOptions(storedPrefs); + + if (options.isStartup) { + newAction.meta = { + isStartup: true, + }; + } + + if (options.broadcast) { + // Broadcast an update to all open content pages + this.store.dispatch(ac.BroadcastToContent(newAction)); + } else { + // Don't broadcast only update the state and update the preloaded tab. + this.store.dispatch(ac.AlsoToPreloaded(newAction)); + } + } + + // Allocate ad positions to partners based on SOV via stable randomization. + async allocatePositions() { + // If the fetch to get sov fails for whatever reason, we can just return here. + // Code that uses this falls back to flattening allocations instead if this has failed. + if (!this._contile.sov) { + return; + } + // This sample input should ensure we return the same result for this allocation, + // even if called from other parts of the code. + const sampleInput = `${lazy.contextId}-${this._contile.sov.name}`; + const allocatedPositions = []; + for (const allocation of this._contile.sov.allocations) { + const allocatedPosition = { + position: allocation.position, + }; + allocatedPositions.push(allocatedPosition); + const ratios = allocation.allocation.map(alloc => alloc.percentage); + if (ratios.length) { + const index = await lazy.Sampling.ratioSample(sampleInput, ratios); + allocatedPosition.assignedPartner = + allocation.allocation[index].partner; + } + } + + this.store.dispatch( + ac.OnlyToMain({ + type: at.SOV_UPDATED, + data: { + ready: !!allocatedPositions.length, + positions: allocatedPositions, + }, + }) + ); + } + + async updateCustomSearchShortcuts(isStartup = false) { + if (!this.store.getState().Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT]) { + return; + } + + if (!this._tippyTopProvider.initialized) { + await this._tippyTopProvider.init(); + } + + // Populate the state with available search shortcuts + let searchShortcuts = []; + for (const engine of await Services.search.getAppProvidedEngines()) { + const shortcut = CUSTOM_SEARCH_SHORTCUTS.find(s => + engine.aliases.includes(s.keyword) + ); + if (shortcut) { + let clone = { ...shortcut }; + await this._attachTippyTopIconForSearchShortcut(clone, clone.keyword); + searchShortcuts.push(clone); + } + } + + this.store.dispatch( + ac.BroadcastToContent({ + type: at.UPDATE_SEARCH_SHORTCUTS, + data: { searchShortcuts }, + meta: { + isStartup, + }, + }) + ); + } + + async topSiteToSearchTopSite(site) { + const searchProvider = getSearchProvider(shortURL(site)); + if ( + !searchProvider || + !(await checkHasSearchEngine(searchProvider.keyword)) + ) { + return site; + } + return { + ...site, + searchTopSite: true, + label: searchProvider.keyword, + }; + } + + /** + * Get an image for the link preferring tippy top, rich favicon, screenshots. + */ + async _fetchIcon(link, isStartup = false) { + // Nothing to do if we already have a rich icon from the page + if (link.favicon && link.faviconSize >= MIN_FAVICON_SIZE) { + return; + } + + // Nothing more to do if we can use a default tippy top icon + this._tippyTopProvider.processSite(link); + if (link.tippyTopIcon) { + return; + } + + // Make a request for a better icon + this._requestRichIcon(link.url); + + // Also request a screenshot if we don't have one yet + await this._fetchScreenshot(link, link.url, isStartup); + } + + /** + * Fetch, cache and broadcast a screenshot for a specific topsite. + * @param link cached topsite object + * @param url where to fetch the image from + * @param isStartup Whether the screenshot is fetched while initting TopSitesFeed. + */ + async _fetchScreenshot(link, url, isStartup = false) { + // We shouldn't bother caching screenshots if they won't be shown. + if ( + link.screenshot || + !this.store.getState().Prefs.values[SHOWN_ON_NEWTAB_PREF] + ) { + return; + } + await lazy.Screenshots.maybeCacheScreenshot( + link, + url, + "screenshot", + screenshot => + this.store.dispatch( + ac.BroadcastToContent({ + data: { screenshot, url: link.url }, + type: at.SCREENSHOT_UPDATED, + meta: { + isStartup, + }, + }) + ) + ); + } + + /** + * Dispatch screenshot preview to target or notify if request failed. + * @param customScreenshotURL {string} The URL used to capture the screenshot + * @param target {string} Id of content process where to dispatch the result + */ + async getScreenshotPreview(url, target) { + const preview = (await lazy.Screenshots.getScreenshotForURL(url)) || ""; + this.store.dispatch( + ac.OnlyToOneContent( + { + data: { url, preview }, + type: at.PREVIEW_RESPONSE, + }, + target + ) + ); + } + + _requestRichIcon(url) { + this.store.dispatch({ + type: at.RICH_ICON_MISSING, + data: { url }, + }); + } + + updateSectionPrefs(collapsed) { + this.store.dispatch( + ac.BroadcastToContent({ + type: at.TOP_SITES_PREFS_UPDATED, + data: { pref: collapsed }, + }) + ); + } + + /** + * Inform others that top sites data has been updated due to pinned changes. + */ + _broadcastPinnedSitesUpdated() { + // Pinned data changed, so make sure we get latest + this.pinnedCache.expire(); + + // Refresh to update pinned sites with screenshots, trigger deduping, etc. + this.refresh({ broadcast: true }); + } + + /** + * Pin a site at a specific position saving only the desired keys. + * @param customScreenshotURL {string} User set URL of preview image for site + * @param label {string} User set string of custom site name + */ + async _pinSiteAt({ customScreenshotURL, label, url, searchTopSite }, index) { + const toPin = { url }; + if (label) { + toPin.label = label; + } + if (customScreenshotURL) { + toPin.customScreenshotURL = customScreenshotURL; + } + if (searchTopSite) { + toPin.searchTopSite = searchTopSite; + } + lazy.NewTabUtils.pinnedLinks.pin(toPin, index); + + await this._clearLinkCustomScreenshot({ customScreenshotURL, url }); + } + + async _clearLinkCustomScreenshot(site) { + // If screenshot url changed or was removed we need to update the cached link obj + if (site.customScreenshotURL !== undefined) { + const pinned = await this.pinnedCache.request(); + const link = pinned.find(pin => pin && pin.url === site.url); + if (link && link.customScreenshotURL !== site.customScreenshotURL) { + link.__sharedCache.updateLink("screenshot", undefined); + } + } + } + + /** + * Handle a pin action of a site to a position. + */ + async pin(action) { + let { site, index } = action.data; + index = this._adjustPinIndexForSponsoredLinks(site, index); + // If valid index provided, pin at that position + if (index >= 0) { + await this._pinSiteAt(site, index); + this._broadcastPinnedSitesUpdated(); + } else { + // Bug 1458658. If the top site is being pinned from an 'Add a Top Site' option, + // then we want to make sure to unblock that link if it has previously been + // blocked. We know if the site has been added because the index will be -1. + if (index === -1) { + lazy.NewTabUtils.blockedLinks.unblock({ url: site.url }); + this.frecentCache.expire(); + } + this.insert(action); + } + } + + /** + * Handle an unpin action of a site. + */ + unpin(action) { + const { site } = action.data; + lazy.NewTabUtils.pinnedLinks.unpin(site); + this._broadcastPinnedSitesUpdated(); + } + + unpinAllSearchShortcuts() { + Services.prefs.clearUserPref( + `browser.newtabpage.activity-stream.${SEARCH_SHORTCUTS_HAVE_PINNED_PREF}` + ); + for (let pinnedLink of lazy.NewTabUtils.pinnedLinks.links) { + if (pinnedLink && pinnedLink.searchTopSite) { + lazy.NewTabUtils.pinnedLinks.unpin(pinnedLink); + } + } + this.pinnedCache.expire(); + } + + _unpinSearchShortcut(vendor) { + for (let pinnedLink of lazy.NewTabUtils.pinnedLinks.links) { + if ( + pinnedLink && + pinnedLink.searchTopSite && + shortURL(pinnedLink) === vendor + ) { + lazy.NewTabUtils.pinnedLinks.unpin(pinnedLink); + this.pinnedCache.expire(); + + const prevInsertedShortcuts = this.store + .getState() + .Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF].split(","); + this.store.dispatch( + ac.SetPref( + SEARCH_SHORTCUTS_HAVE_PINNED_PREF, + prevInsertedShortcuts.filter(s => s !== vendor).join(",") + ) + ); + break; + } + } + } + + /** + * Reduces the given pinning index by the number of preceding sponsored + * sites, to accomodate for sponsored sites pushing pinned ones to the side, + * effectively increasing their index again. + */ + _adjustPinIndexForSponsoredLinks(site, index) { + if (!this._linksWithDefaults) { + return index; + } + // Adjust insertion index for sponsored sites since their position is + // fixed. + let adjustedIndex = index; + for (let i = 0; i < index; i++) { + const link = this._linksWithDefaults[i]; + if ( + link && + link.sponsored_position && + this._linksWithDefaults[i]?.url !== site.url + ) { + adjustedIndex--; + } + } + return adjustedIndex; + } + + /** + * Insert a site to pin at a position shifting over any other pinned sites. + */ + _insertPin(site, originalIndex, draggedFromIndex) { + let index = this._adjustPinIndexForSponsoredLinks(site, originalIndex); + + // Don't insert any pins past the end of the visible top sites. Otherwise, + // we can end up with a bunch of pinned sites that can never be unpinned again + // from the UI. + const topSitesCount = + this.store.getState().Prefs.values[ROWS_PREF] * + TOP_SITES_MAX_SITES_PER_ROW; + if (index >= topSitesCount) { + return; + } + + let pinned = lazy.NewTabUtils.pinnedLinks.links; + if (!pinned[index]) { + this._pinSiteAt(site, index); + } else { + pinned[draggedFromIndex] = null; + // Find the hole to shift the pinned site(s) towards. We shift towards the + // hole left by the site being dragged. + let holeIndex = index; + const indexStep = index > draggedFromIndex ? -1 : 1; + while (pinned[holeIndex]) { + holeIndex += indexStep; + } + if (holeIndex >= topSitesCount || holeIndex < 0) { + // There are no holes, so we will effectively unpin the last slot and shifting + // towards it. This only happens when adding a new top site to an already + // fully pinned grid. + holeIndex = topSitesCount - 1; + } + + // Shift towards the hole. + const shiftingStep = holeIndex > index ? -1 : 1; + while (holeIndex !== index) { + const nextIndex = holeIndex + shiftingStep; + this._pinSiteAt(pinned[nextIndex], holeIndex); + holeIndex = nextIndex; + } + this._pinSiteAt(site, index); + } + } + + /** + * Handle an insert (drop/add) action of a site. + */ + async insert(action) { + let { index } = action.data; + // Treat invalid pin index values (e.g., -1, undefined) as insert in the first position + if (!(index > 0)) { + index = 0; + } + + // Inserting a top site pins it in the specified slot, pushing over any link already + // pinned in the slot (unless it's the last slot, then it replaces). + this._insertPin( + action.data.site, + index, + action.data.draggedFromIndex !== undefined + ? action.data.draggedFromIndex + : this.store.getState().Prefs.values[ROWS_PREF] * + TOP_SITES_MAX_SITES_PER_ROW + ); + + await this._clearLinkCustomScreenshot(action.data.site); + this._broadcastPinnedSitesUpdated(); + } + + updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts }) { + // Unpin the deletedShortcuts. + deletedShortcuts.forEach(({ url }) => { + lazy.NewTabUtils.pinnedLinks.unpin({ url }); + }); + + // Pin the addedShortcuts. + const numberOfSlots = + this.store.getState().Prefs.values[ROWS_PREF] * + TOP_SITES_MAX_SITES_PER_ROW; + addedShortcuts.forEach(shortcut => { + // Find first hole in pinnedLinks. + let index = lazy.NewTabUtils.pinnedLinks.links.findIndex(link => !link); + if ( + index < 0 && + lazy.NewTabUtils.pinnedLinks.links.length + 1 < numberOfSlots + ) { + // pinnedLinks can have less slots than the total available. + index = lazy.NewTabUtils.pinnedLinks.links.length; + } + if (index >= 0) { + lazy.NewTabUtils.pinnedLinks.pin(shortcut, index); + } else { + // No slots available, we need to do an insert in first slot and push over other pinned links. + this._insertPin(shortcut, 0, numberOfSlots); + } + }); + + this._broadcastPinnedSitesUpdated(); + } + + onAction(action) { + switch (action.type) { + case at.INIT: + this.init(); + this.updateCustomSearchShortcuts(true /* isStartup */); + break; + case at.SYSTEM_TICK: + this.refresh({ broadcast: false }); + this._contile.periodicUpdate(); + break; + // All these actions mean we need new top sites + case at.PLACES_HISTORY_CLEARED: + case at.PLACES_LINKS_DELETED: + this.frecentCache.expire(); + this.refresh({ broadcast: true }); + break; + case at.PLACES_LINKS_CHANGED: + this.frecentCache.expire(); + this.refresh({ broadcast: false }); + break; + case at.PLACES_LINK_BLOCKED: + this.frecentCache.expire(); + this.pinnedCache.expire(); + this.refresh({ broadcast: true }); + break; + case at.PREF_CHANGED: + switch (action.data.name) { + case DEFAULT_SITES_PREF: + if (!this._useRemoteSetting) { + this.refreshDefaults(action.data.value); + } + break; + case ROWS_PREF: + case FILTER_DEFAULT_SEARCH_PREF: + case SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF: + this.refresh({ broadcast: true }); + break; + case SHOW_SPONSORED_PREF: + if ( + lazy.NimbusFeatures.newtab.getVariable( + NIMBUS_VARIABLE_CONTILE_ENABLED + ) + ) { + this._contile.refresh(); + } else { + this.refresh({ broadcast: true }); + } + if (!action.data.value) { + this._contile._resetContileCachePrefs(); + } + + break; + case SEARCH_SHORTCUTS_EXPERIMENT: + if (action.data.value) { + this.updateCustomSearchShortcuts(); + } else { + this.unpinAllSearchShortcuts(); + } + this.refresh({ broadcast: true }); + } + break; + case at.UPDATE_SECTION_PREFS: + if (action.data.id === SECTION_ID) { + this.updateSectionPrefs(action.data.value); + } + break; + case at.PREFS_INITIAL_VALUES: + if (!this._useRemoteSetting) { + this.refreshDefaults(action.data[DEFAULT_SITES_PREF]); + } + break; + case at.TOP_SITES_PIN: + this.pin(action); + break; + case at.TOP_SITES_UNPIN: + this.unpin(action); + break; + case at.TOP_SITES_INSERT: + this.insert(action); + break; + case at.PREVIEW_REQUEST: + this.getScreenshotPreview(action.data.url, action.meta.fromTarget); + break; + case at.UPDATE_PINNED_SEARCH_SHORTCUTS: + this.updatePinnedSearchShortcuts(action.data); + break; + case at.DISCOVERY_STREAM_SPOCS_UPDATE: + // Refresh to update sponsored topsites. + this.refresh({ broadcast: true, isStartup: action.meta.isStartup }); + break; + case at.UNINIT: + this.uninit(); + break; + } + } +} diff --git a/browser/components/newtab/lib/TopStoriesFeed.sys.mjs b/browser/components/newtab/lib/TopStoriesFeed.sys.mjs new file mode 100644 index 0000000000..be030649dd --- /dev/null +++ b/browser/components/newtab/lib/TopStoriesFeed.sys.mjs @@ -0,0 +1,731 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { + actionTypes as at, + actionCreators as ac, +} from "resource://activity-stream/common/Actions.sys.mjs"; +import { Prefs } from "resource://activity-stream/lib/ActivityStreamPrefs.sys.mjs"; +import { shortURL } from "resource://activity-stream/lib/ShortURL.sys.mjs"; +import { SectionsManager } from "resource://activity-stream/lib/SectionsManager.sys.mjs"; +import { PersistentCache } from "resource://activity-stream/lib/PersistentCache.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", + pktApi: "chrome://pocket/content/pktApi.sys.mjs", +}); + +export const STORIES_UPDATE_TIME = 30 * 60 * 1000; // 30 minutes +export const TOPICS_UPDATE_TIME = 3 * 60 * 60 * 1000; // 3 hours +const STORIES_NOW_THRESHOLD = 24 * 60 * 60 * 1000; // 24 hours +export const DEFAULT_RECS_EXPIRE_TIME = 60 * 60 * 1000; // 1 hour +export const SECTION_ID = "topstories"; +const IMPRESSION_SOURCE = "TOP_STORIES"; + +export const SPOC_IMPRESSION_TRACKING_PREF = + "feeds.section.topstories.spoc.impressions"; + +const DISCOVERY_STREAM_PREF_ENABLED = "discoverystream.enabled"; +const DISCOVERY_STREAM_PREF_ENABLED_PATH = + "browser.newtabpage.activity-stream.discoverystream.enabled"; +export const REC_IMPRESSION_TRACKING_PREF = + "feeds.section.topstories.rec.impressions"; +const PREF_USER_TOPSTORIES = "feeds.section.topstories"; +const MAX_LIFETIME_CAP = 500; // Guard against misconfiguration on the server +const DISCOVERY_STREAM_PREF = "discoverystream.config"; + +export class TopStoriesFeed { + constructor(ds) { + // Use discoverystream config pref default values for fast path and + // if needed lazy load activity stream top stories feed based on + // actual user preference when INIT and PREF_CHANGED is invoked + this.discoveryStreamEnabled = + ds && + ds.value && + JSON.parse(ds.value).enabled && + Services.prefs.getBoolPref(DISCOVERY_STREAM_PREF_ENABLED_PATH, false); + if (!this.discoveryStreamEnabled) { + this.initializeProperties(); + } + } + + initializeProperties() { + this.contentUpdateQueue = []; + this.spocCampaignMap = new Map(); + this.cache = new PersistentCache(SECTION_ID, true); + this._prefs = new Prefs(); + this.propertiesInitialized = true; + } + + async onInit() { + SectionsManager.enableSection(SECTION_ID, true /* isStartup */); + if (this.discoveryStreamEnabled) { + return; + } + + try { + const { options } = SectionsManager.sections.get(SECTION_ID); + const apiKey = this.getApiKeyFromPref(options.api_key_pref); + this.stories_endpoint = this.produceFinalEndpointUrl( + options.stories_endpoint, + apiKey + ); + this.topics_endpoint = this.produceFinalEndpointUrl( + options.topics_endpoint, + apiKey + ); + this.read_more_endpoint = options.read_more_endpoint; + this.stories_referrer = options.stories_referrer; + this.show_spocs = options.show_spocs; + this.storiesLastUpdated = 0; + this.topicsLastUpdated = 0; + this.storiesLoaded = false; + this.dispatchPocketCta(this._prefs.get("pocketCta"), false); + + // Cache is used for new page loads, which shouldn't have changed data. + // If we have changed data, cache should be cleared, + // and last updated should be 0, and we can fetch. + let { stories, topics } = await this.loadCachedData(); + if (this.storiesLastUpdated === 0) { + stories = await this.fetchStories(); + } + if (this.topicsLastUpdated === 0) { + topics = await this.fetchTopics(); + } + this.doContentUpdate({ stories, topics }, true); + this.storiesLoaded = true; + + // This is filtered so an update function can return true to retry on the next run + this.contentUpdateQueue = this.contentUpdateQueue.filter(update => + update() + ); + } catch (e) { + console.error(`Problem initializing top stories feed: ${e.message}`); + } + } + + init() { + SectionsManager.onceInitialized(this.onInit.bind(this)); + } + + async clearCache() { + await this.cache.set("stories", {}); + await this.cache.set("topics", {}); + await this.cache.set("spocs", {}); + } + + uninit() { + this.storiesLoaded = false; + SectionsManager.disableSection(SECTION_ID); + } + + getPocketState(target) { + const action = { + type: at.POCKET_LOGGED_IN, + data: lazy.pktApi.isUserLoggedIn(), + }; + this.store.dispatch(ac.OnlyToOneContent(action, target)); + } + + dispatchPocketCta(data, shouldBroadcast) { + const action = { type: at.POCKET_CTA, data: JSON.parse(data) }; + this.store.dispatch( + shouldBroadcast + ? ac.BroadcastToContent(action) + : ac.AlsoToPreloaded(action) + ); + } + + /** + * doContentUpdate - Updates topics and stories in the topstories section. + * + * Sections have one update action for the whole section. + * Redux creates a state race condition if you call the same action, + * twice, concurrently. Because of this, doContentUpdate is + * one place to update both topics and stories in a single action. + * + * Section updates used old topics if none are available, + * but clear stories if none are available. Because of this, if no + * stories are passed, we instead use the existing stories in state. + * + * @param {Object} This is an object with potential new stories or topics. + * @param {Boolean} shouldBroadcast If we should update existing tabs or not. For first page + * loads or pref changes, we want to update existing tabs, + * for system tick or other updates we do not. + */ + doContentUpdate({ stories, topics }, shouldBroadcast) { + let updateProps = {}; + if (stories) { + updateProps.rows = stories; + } else { + const { Sections } = this.store.getState(); + if (Sections && Sections.find) { + updateProps.rows = Sections.find(s => s.id === SECTION_ID).rows; + } + } + if (topics) { + Object.assign(updateProps, { + topics, + read_more_endpoint: this.read_more_endpoint, + }); + } + + // We should only be calling this once per init. + this.dispatchUpdateEvent(shouldBroadcast, updateProps); + } + + async fetchStories() { + if (!this.stories_endpoint) { + return null; + } + try { + const response = await fetch(this.stories_endpoint, { + credentials: "omit", + }); + if (!response.ok) { + throw new Error( + `Stories endpoint returned unexpected status: ${response.status}` + ); + } + + const body = await response.json(); + this.updateSettings(body.settings); + this.stories = this.rotate(this.transform(body.recommendations)); + this.cleanUpTopRecImpressionPref(); + + if (this.show_spocs && body.spocs) { + this.spocCampaignMap = new Map( + body.spocs.map(s => [s.id, `${s.campaign_id}`]) + ); + this.spocs = this.transform(body.spocs); + this.cleanUpCampaignImpressionPref(); + } + this.storiesLastUpdated = Date.now(); + body._timestamp = this.storiesLastUpdated; + this.cache.set("stories", body); + } catch (error) { + console.error(`Failed to fetch content: ${error.message}`); + } + return this.stories; + } + + async loadCachedData() { + const data = await this.cache.get(); + let stories = data.stories && data.stories.recommendations; + let topics = data.topics && data.topics.topics; + + if (stories && !!stories.length && this.storiesLastUpdated === 0) { + this.updateSettings(data.stories.settings); + this.stories = this.rotate(this.transform(stories)); + this.storiesLastUpdated = data.stories._timestamp; + if (data.stories.spocs && data.stories.spocs.length) { + this.spocCampaignMap = new Map( + data.stories.spocs.map(s => [s.id, `${s.campaign_id}`]) + ); + this.spocs = this.transform(data.stories.spocs); + this.cleanUpCampaignImpressionPref(); + } + } + if (topics && !!topics.length && this.topicsLastUpdated === 0) { + this.topics = topics; + this.topicsLastUpdated = data.topics._timestamp; + } + + return { topics: this.topics, stories: this.stories }; + } + + transform(items) { + if (!items) { + return []; + } + + const calcResult = items + .filter(s => !lazy.NewTabUtils.blockedLinks.isBlocked({ url: s.url })) + .map(s => { + let mapped = { + guid: s.id, + hostname: s.domain || shortURL(Object.assign({}, s, { url: s.url })), + type: + Date.now() - s.published_timestamp * 1000 <= STORIES_NOW_THRESHOLD + ? "now" + : "trending", + context: s.context, + icon: s.icon, + title: s.title, + description: s.excerpt, + image: this.normalizeUrl(s.image_src), + referrer: this.stories_referrer, + url: s.url, + score: s.item_score || 1, + spoc_meta: this.show_spocs + ? { campaign_id: s.campaign_id, caps: s.caps } + : {}, + }; + + // Very old cached spocs may not contain an `expiration_timestamp` property + if (s.expiration_timestamp) { + mapped.expiration_timestamp = s.expiration_timestamp; + } + + return mapped; + }) + .sort(this.compareScore); + + return calcResult; + } + + async fetchTopics() { + if (!this.topics_endpoint) { + return null; + } + try { + const response = await fetch(this.topics_endpoint, { + credentials: "omit", + }); + if (!response.ok) { + throw new Error( + `Topics endpoint returned unexpected status: ${response.status}` + ); + } + const body = await response.json(); + const { topics } = body; + if (topics) { + this.topics = topics; + this.topicsLastUpdated = Date.now(); + body._timestamp = this.topicsLastUpdated; + this.cache.set("topics", body); + } + } catch (error) { + console.error(`Failed to fetch topics: ${error.message}`); + } + return this.topics; + } + + dispatchUpdateEvent(shouldBroadcast, data) { + SectionsManager.updateSection(SECTION_ID, data, shouldBroadcast); + } + + compareScore(a, b) { + return b.score - a.score; + } + + updateSettings(settings = {}) { + this.spocsPerNewTabs = settings.spocsPerNewTabs || 1; // Probability of a new tab getting a spoc [0,1] + this.recsExpireTime = settings.recsExpireTime; + } + + // We rotate stories on the client so that + // active stories are at the front of the list, followed by stories that have expired + // impressions i.e. have been displayed for longer than recsExpireTime. + rotate(items) { + if (items.length <= 3) { + return items; + } + + const maxImpressionAge = Math.max( + this.recsExpireTime * 1000 || DEFAULT_RECS_EXPIRE_TIME, + DEFAULT_RECS_EXPIRE_TIME + ); + const impressions = this.readImpressionsPref(REC_IMPRESSION_TRACKING_PREF); + const expired = []; + const active = []; + for (const item of items) { + if ( + impressions[item.guid] && + Date.now() - impressions[item.guid] >= maxImpressionAge + ) { + expired.push(item); + } else { + active.push(item); + } + } + return active.concat(expired); + } + + getApiKeyFromPref(apiKeyPref) { + if (!apiKeyPref) { + return apiKeyPref; + } + + return ( + this._prefs.get(apiKeyPref) || Services.prefs.getCharPref(apiKeyPref) + ); + } + + produceFinalEndpointUrl(url, apiKey) { + if (!url) { + return url; + } + if (url.includes("$apiKey") && !apiKey) { + throw new Error(`An API key was specified but none configured: ${url}`); + } + return url.replace("$apiKey", apiKey); + } + + // Need to remove parenthesis from image URLs as React will otherwise + // fail to render them properly as part of the card template. + normalizeUrl(url) { + if (url) { + return url.replace(/\(/g, "%28").replace(/\)/g, "%29"); + } + return url; + } + + shouldShowSpocs() { + return this.show_spocs && this.store.getState().Prefs.values.showSponsored; + } + + dispatchSpocDone(target) { + const action = { type: at.POCKET_WAITING_FOR_SPOC, data: false }; + this.store.dispatch(ac.OnlyToOneContent(action, target)); + } + + filterSpocs() { + if (!this.shouldShowSpocs()) { + return []; + } + + if (Math.random() > this.spocsPerNewTabs) { + return []; + } + + if (!this.spocs || !this.spocs.length) { + // We have stories but no spocs so there's nothing to do and this update can be + // removed from the queue. + return []; + } + + // Filter spocs based on frequency caps + const impressions = this.readImpressionsPref(SPOC_IMPRESSION_TRACKING_PREF); + let spocs = this.spocs.filter(s => + this.isBelowFrequencyCap(impressions, s) + ); + + // Filter out expired spocs based on `expiration_timestamp` + spocs = spocs.filter(spoc => { + // If cached data is so old it doesn't contain this property, assume the spoc is ok to show + if (!(`expiration_timestamp` in spoc)) { + return true; + } + // `expiration_timestamp` is the number of seconds elapsed since January 1, 1970 00:00:00 UTC + return spoc.expiration_timestamp * 1000 > Date.now(); + }); + + return spocs; + } + + maybeAddSpoc(target) { + const updateContent = () => { + let spocs = this.filterSpocs(); + + if (!spocs.length) { + this.dispatchSpocDone(target); + return false; + } + + // Create a new array with a spoc inserted at index 2 + const section = this.store + .getState() + .Sections.find(s => s.id === SECTION_ID); + let rows = section.rows.slice(0, this.stories.length); + rows.splice(2, 0, Object.assign(spocs[0], { pinned: true })); + + // Send a content update to the target tab + const action = { + type: at.SECTION_UPDATE, + data: Object.assign({ rows }, { id: SECTION_ID }), + }; + this.store.dispatch(ac.OnlyToOneContent(action, target)); + this.dispatchSpocDone(target); + return false; + }; + + if (this.storiesLoaded) { + updateContent(); + } else { + // Delay updating tab content until initial data has been fetched + this.contentUpdateQueue.push(updateContent); + } + } + + // Frequency caps are based on campaigns, which may include multiple spocs. + // We currently support two types of frequency caps: + // - lifetime: Indicates how many times spocs from a campaign can be shown in total + // - period: Indicates how many times spocs from a campaign can be shown within a period + // + // So, for example, the feed configuration below defines that for campaign 1 no more + // than 5 spocs can be show in total, and no more than 2 per hour. + // "campaign_id": 1, + // "caps": { + // "lifetime": 5, + // "campaign": { + // "count": 2, + // "period": 3600 + // } + // } + isBelowFrequencyCap(impressions, spoc) { + const campaignImpressions = impressions[spoc.spoc_meta.campaign_id]; + if (!campaignImpressions) { + return true; + } + + const lifeTimeCap = Math.min( + spoc.spoc_meta.caps && spoc.spoc_meta.caps.lifetime, + MAX_LIFETIME_CAP + ); + const lifeTimeCapExceeded = campaignImpressions.length >= lifeTimeCap; + if (lifeTimeCapExceeded) { + return false; + } + + const campaignCap = + (spoc.spoc_meta.caps && spoc.spoc_meta.caps.campaign) || {}; + const campaignCapExceeded = + campaignImpressions.filter( + i => Date.now() - i < campaignCap.period * 1000 + ).length >= campaignCap.count; + return !campaignCapExceeded; + } + + // Clean up campaign impression pref by removing all campaigns that are no + // longer part of the response, and are therefore considered inactive. + cleanUpCampaignImpressionPref() { + const campaignIds = new Set(this.spocCampaignMap.values()); + this.cleanUpImpressionPref( + id => !campaignIds.has(id), + SPOC_IMPRESSION_TRACKING_PREF + ); + } + + // Clean up rec impression pref by removing all stories that are no + // longer part of the response. + cleanUpTopRecImpressionPref() { + const activeStories = new Set(this.stories.map(s => `${s.guid}`)); + this.cleanUpImpressionPref( + id => !activeStories.has(id), + REC_IMPRESSION_TRACKING_PREF + ); + } + + /** + * Cleans up the provided impression pref (spocs or recs). + * + * @param isExpired predicate (boolean-valued function) that returns whether or not + * the impression for the given key is expired. + * @param pref the impression pref to clean up. + */ + cleanUpImpressionPref(isExpired, pref) { + const impressions = this.readImpressionsPref(pref); + let changed = false; + + Object.keys(impressions).forEach(id => { + if (isExpired(id)) { + changed = true; + delete impressions[id]; + } + }); + + if (changed) { + this.writeImpressionsPref(pref, impressions); + } + } + + // Sets a pref mapping campaign IDs to timestamp arrays. + // The timestamps represent impressions which are used to calculate frequency caps. + recordCampaignImpression(campaignId) { + let impressions = this.readImpressionsPref(SPOC_IMPRESSION_TRACKING_PREF); + + const timeStamps = impressions[campaignId] || []; + timeStamps.push(Date.now()); + impressions = Object.assign(impressions, { [campaignId]: timeStamps }); + + this.writeImpressionsPref(SPOC_IMPRESSION_TRACKING_PREF, impressions); + } + + // Sets a pref mapping story (rec) IDs to a single timestamp (time of first impression). + // We use these timestamps to guarantee a story doesn't stay on top for longer than + // configured in the feed settings (settings.recsExpireTime). + recordTopRecImpressions(topItems) { + let impressions = this.readImpressionsPref(REC_IMPRESSION_TRACKING_PREF); + let changed = false; + + topItems.forEach(t => { + if (!impressions[t]) { + changed = true; + impressions = Object.assign(impressions, { [t]: Date.now() }); + } + }); + + if (changed) { + this.writeImpressionsPref(REC_IMPRESSION_TRACKING_PREF, impressions); + } + } + + readImpressionsPref(pref) { + const prefVal = this._prefs.get(pref); + return prefVal ? JSON.parse(prefVal) : {}; + } + + writeImpressionsPref(pref, impressions) { + this._prefs.set(pref, JSON.stringify(impressions)); + } + + async removeSpocs() { + // Quick hack so that SPOCS are removed from all open and preloaded tabs when + // they are disabled. The longer term fix should probably be to remove them + // in the Reducer. + await this.clearCache(); + this.uninit(); + this.init(); + } + + lazyLoadTopStories(options = {}) { + let { dsPref, userPref } = options; + if (!dsPref) { + dsPref = this.store.getState().Prefs.values[DISCOVERY_STREAM_PREF]; + } + if (!userPref) { + userPref = this.store.getState().Prefs.values[PREF_USER_TOPSTORIES]; + } + + try { + this.discoveryStreamEnabled = + JSON.parse(dsPref).enabled && + this.store.getState().Prefs.values[DISCOVERY_STREAM_PREF_ENABLED]; + } catch (e) { + // Load activity stream top stories if fail to determine discovery stream state + this.discoveryStreamEnabled = false; + } + + // Return without invoking initialization if top stories are loaded, or preffed off. + if (this.storiesLoaded || !userPref) { + return; + } + + if (!this.discoveryStreamEnabled && !this.propertiesInitialized) { + this.initializeProperties(); + } + this.init(); + } + + handleDisabled(action) { + switch (action.type) { + case at.INIT: + this.lazyLoadTopStories(); + break; + case at.PREF_CHANGED: + if (action.data.name === DISCOVERY_STREAM_PREF) { + this.lazyLoadTopStories({ dsPref: action.data.value }); + } + if (action.data.name === DISCOVERY_STREAM_PREF_ENABLED) { + this.lazyLoadTopStories(); + } + if (action.data.name === PREF_USER_TOPSTORIES) { + if (action.data.value) { + // init topstories if value if true. + this.lazyLoadTopStories({ userPref: action.data.value }); + } else { + this.uninit(); + } + } + break; + case at.UNINIT: + this.uninit(); + break; + } + } + + async onAction(action) { + if (this.discoveryStreamEnabled) { + this.handleDisabled(action); + return; + } + switch (action.type) { + // Check discoverystream pref and load activity stream top stories only if needed + case at.INIT: + this.lazyLoadTopStories(); + break; + case at.SYSTEM_TICK: + let stories; + let topics; + if (Date.now() - this.storiesLastUpdated >= STORIES_UPDATE_TIME) { + stories = await this.fetchStories(); + } + if (Date.now() - this.topicsLastUpdated >= TOPICS_UPDATE_TIME) { + topics = await this.fetchTopics(); + } + this.doContentUpdate({ stories, topics }, false); + break; + case at.UNINIT: + this.uninit(); + break; + case at.NEW_TAB_REHYDRATED: + this.getPocketState(action.meta.fromTarget); + this.maybeAddSpoc(action.meta.fromTarget); + break; + case at.SECTION_OPTIONS_CHANGED: + if (action.data === SECTION_ID) { + await this.clearCache(); + this.uninit(); + this.init(); + } + break; + case at.PLACES_LINK_BLOCKED: + if (this.spocs) { + this.spocs = this.spocs.filter(s => s.url !== action.data.url); + } + break; + case at.TELEMETRY_IMPRESSION_STATS: { + // We want to make sure we only track impressions from Top Stories, + // otherwise unexpected things that are not properly handled can happen. + // Example: Impressions from spocs on Discovery Stream can cause the + // Top Stories impressions pref to continuously grow, see bug #1523408 + if (action.data.source === IMPRESSION_SOURCE) { + const payload = action.data; + const viewImpression = !( + "click" in payload || + "block" in payload || + "pocket" in payload + ); + if (payload.tiles && viewImpression) { + if (this.shouldShowSpocs()) { + payload.tiles.forEach(t => { + if (this.spocCampaignMap.has(t.id)) { + this.recordCampaignImpression(this.spocCampaignMap.get(t.id)); + } + }); + } + const topRecs = payload.tiles + .filter(t => !this.spocCampaignMap.has(t.id)) + .map(t => t.id); + this.recordTopRecImpressions(topRecs); + } + } + break; + } + case at.PREF_CHANGED: + if (action.data.name === DISCOVERY_STREAM_PREF) { + this.lazyLoadTopStories({ dsPref: action.data.value }); + } + if (action.data.name === PREF_USER_TOPSTORIES) { + if (action.data.value) { + // init topstories if value if true. + this.lazyLoadTopStories({ userPref: action.data.value }); + } else { + this.uninit(); + } + } + // Check if spocs was disabled. Remove them if they were. + if (action.data.name === "showSponsored" && !action.data.value) { + await this.removeSpocs(); + } + if (action.data.name === "pocketCta") { + this.dispatchPocketCta(action.data.value, true); + } + break; + } + } +} diff --git a/browser/components/newtab/lib/UTEventReporting.sys.mjs b/browser/components/newtab/lib/UTEventReporting.sys.mjs new file mode 100644 index 0000000000..8da7824415 --- /dev/null +++ b/browser/components/newtab/lib/UTEventReporting.sys.mjs @@ -0,0 +1,62 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Note: the schema can be found in + * https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/Events.yaml + */ +const EXTRAS_FIELD_NAMES = [ + "addon_version", + "session_id", + "page", + "user_prefs", + "action_position", +]; + +export class UTEventReporting { + constructor() { + Services.telemetry.setEventRecordingEnabled("activity_stream", true); + this.sendUserEvent = this.sendUserEvent.bind(this); + this.sendSessionEndEvent = this.sendSessionEndEvent.bind(this); + } + + _createExtras(data) { + // Make a copy of the given data and delete/modify it as needed. + let utExtras = Object.assign({}, data); + for (let field of Object.keys(utExtras)) { + if (EXTRAS_FIELD_NAMES.includes(field)) { + utExtras[field] = String(utExtras[field]); + continue; + } + delete utExtras[field]; + } + return utExtras; + } + + sendUserEvent(data) { + let mainFields = ["event", "source"]; + let eventFields = mainFields.map(field => String(data[field]) || null); + + Services.telemetry.recordEvent( + "activity_stream", + "event", + ...eventFields, + this._createExtras(data) + ); + } + + sendSessionEndEvent(data) { + Services.telemetry.recordEvent( + "activity_stream", + "end", + "session", + String(data.session_duration), + this._createExtras(data) + ); + } + + uninit() { + Services.telemetry.setEventRecordingEnabled("activity_stream", false); + } +} diff --git a/browser/components/newtab/lib/cache.worker.js b/browser/components/newtab/lib/cache.worker.js new file mode 100644 index 0000000000..1195da05fa --- /dev/null +++ b/browser/components/newtab/lib/cache.worker.js @@ -0,0 +1,203 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* global ReactDOMServer, NewtabRenderUtils */ + +const PAGE_TEMPLATE_RESOURCE_PATH = + "resource://activity-stream/data/content/abouthomecache/page.html.template"; +const SCRIPT_TEMPLATE_RESOURCE_PATH = + "resource://activity-stream/data/content/abouthomecache/script.js.template"; + +// If we don't stub these functions out, React throws warnings in the console +// upon being loaded. +let window = self; +window.requestAnimationFrame = () => {}; +window.cancelAnimationFrame = () => {}; +window.ASRouterMessage = () => { + return Promise.resolve(); +}; +window.ASRouterAddParentListener = () => {}; +window.ASRouterRemoveParentListener = () => {}; + +/* import-globals-from /toolkit/components/workerloader/require.js */ +importScripts("resource://gre/modules/workers/require.js"); + +{ + let oldChromeUtils = ChromeUtils; + + // ChromeUtils is defined inside of a Worker, but we don't want the + // activity-stream.bundle.js to detect it when loading, since that results + // in it attempting to import JSMs on load, which is not allowed in + // a Worker. So we temporarily clear ChromeUtils so that activity-stream.bundle.js + // thinks its being loaded in content scope. + // + // eslint-disable-next-line no-implicit-globals, no-global-assign + ChromeUtils = undefined; + + /* import-globals-from ../vendor/react.js */ + /* import-globals-from ../vendor/react-dom.js */ + /* import-globals-from ../vendor/react-dom-server.js */ + /* import-globals-from ../vendor/redux.js */ + /* import-globals-from ../vendor/react-transition-group.js */ + /* import-globals-from ../vendor/prop-types.js */ + /* import-globals-from ../vendor/react-redux.js */ + /* import-globals-from ../data/content/activity-stream.bundle.js */ + importScripts( + "resource://activity-stream/vendor/react.js", + "resource://activity-stream/vendor/react-dom.js", + "resource://activity-stream/vendor/react-dom-server.js", + "resource://activity-stream/vendor/redux.js", + "resource://activity-stream/vendor/react-transition-group.js", + "resource://activity-stream/vendor/prop-types.js", + "resource://activity-stream/vendor/react-redux.js", + "resource://activity-stream/data/content/activity-stream.bundle.js" + ); + + // eslint-disable-next-line no-global-assign, no-implicit-globals + ChromeUtils = oldChromeUtils; +} + +let PromiseWorker = require("resource://gre/modules/workers/PromiseWorker.js"); + +let Agent = { + _templates: null, + + /** + * Synchronously loads the template files off of the file + * system, and returns them as an object. If the Worker has loaded + * these templates before, a cached copy of the templates is returned + * instead. + * + * @return Object + * An object with the following properties: + * + * pageTemplate (String): + * The template for the document markup. + * + * scriptTempate (String): + * The template for the script. + */ + getOrCreateTemplates() { + if (this._templates) { + return this._templates; + } + + const templateResources = new Map([ + ["pageTemplate", PAGE_TEMPLATE_RESOURCE_PATH], + ["scriptTemplate", SCRIPT_TEMPLATE_RESOURCE_PATH], + ]); + + this._templates = {}; + + for (let [name, path] of templateResources) { + const xhr = new XMLHttpRequest(); + // Using a synchronous XHR in a worker is fine. + xhr.open("GET", path, false); + xhr.responseType = "text"; + xhr.send(null); + this._templates[name] = xhr.responseText; + } + + return this._templates; + }, + + /** + * Constructs the cached about:home document using ReactDOMServer. This will + * be called when "construct" messages are sent to this PromiseWorker. + * + * @param state (Object) + * The most recent Activity Stream Redux state. + * @return Object + * An object with the following properties: + * + * page (String): + * The generated markup for the document. + * + * script (String): + * The generated script for the document. + */ + construct(state) { + // If anything in this function throws an exception, PromiseWorker + // runs the risk of leaving the Promise associated with this method + // forever unresolved. This is particularly bad when this method is + // called via AsyncShutdown, since the forever unresolved Promise can + // result in a AsyncShutdown timeout crash. + // + // To help ensure that no matter what, the Promise resolves with something, + // we wrap the whole operation in a try/catch. + try { + return this._construct(state); + } catch (e) { + console.error("about:home startup cache construction failed:", e); + return { page: null, script: null }; + } + }, + + /** + * Internal method that actually does the work of constructing the cached + * about:home document using ReactDOMServer. This should be called from + * `construct` only. + * + * @param state (Object) + * The most recent Activity Stream Redux state. + * @return Object + * An object with the following properties: + * + * page (String): + * The generated markup for the document. + * + * script (String): + * The generated script for the document. + */ + _construct(state) { + state.App.isForStartupCache = true; + + // ReactDOMServer.renderToString expects a Redux store to pull + // the state from, so we mock out a minimal store implementation. + let fakeStore = { + getState() { + return state; + }, + dispatch() {}, + }; + + let markup = ReactDOMServer.renderToString( + NewtabRenderUtils.NewTab({ + store: fakeStore, + isFirstrun: false, + }) + ); + + let { pageTemplate, scriptTemplate } = this.getOrCreateTemplates(); + let cacheTime = new Date().toUTCString(); + let page = pageTemplate + .replace("{{ MARKUP }}", markup) + .replace("{{ CACHE_TIME }}", cacheTime); + let script = scriptTemplate.replace( + "{{ STATE }}", + JSON.stringify(state, null, "\t") + ); + + return { page, script }; + }, +}; + +// This boilerplate connects the PromiseWorker to the Agent so +// that messages from the main thread map to methods on the +// Agent. +let worker = new PromiseWorker.AbstractWorker(); +worker.dispatch = function (method, args = []) { + return Agent[method](...args); +}; +worker.postMessage = function (result, ...transfers) { + self.postMessage(result, ...transfers); +}; +worker.close = function () { + self.close(); +}; + +self.addEventListener("message", msg => worker.handleMessage(msg)); +self.addEventListener("unhandledrejection", function (error) { + throw error.reason; +}); diff --git a/browser/components/newtab/loaders/inject-loader.js b/browser/components/newtab/loaders/inject-loader.js new file mode 100644 index 0000000000..8729baf270 --- /dev/null +++ b/browser/components/newtab/loaders/inject-loader.js @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Note: this is based on https://github.com/plasticine/inject-loader, +// patched to make istanbul work properly + +const loaderUtils = require("loader-utils"); +const QUOTE_REGEX_STRING = "['|\"]{1}"; + +const hasOnlyExcludeFlags = query => + Object.keys(query).filter(key => query[key] === true).length === 0; +const escapePath = path => path.replace("/", "\\/"); + +function createRequireStringRegex(query) { + const regexArray = []; + + // if there is no query then replace everything + if (Object.keys(query).length === 0) { + regexArray.push("([^\\)]+)"); + } else if (hasOnlyExcludeFlags(query)) { + // if there are only negation matches in the query then replace everything + // except them + Object.keys(query).forEach(key => + regexArray.push(`(?!${QUOTE_REGEX_STRING}${escapePath(key)})`) + ); + regexArray.push("([^\\)]+)"); + } else { + regexArray.push(`(${QUOTE_REGEX_STRING}(`); + regexArray.push( + Object.keys(query) + .map(key => escapePath(key)) + .join("|") + ); + regexArray.push(`)${QUOTE_REGEX_STRING})`); + } + + // Wrap the regex to match `require()` + regexArray.unshift("require\\("); + regexArray.push("\\)"); + + return new RegExp(regexArray.join(""), "g"); +} + +module.exports = function inject(src) { + if (this.cacheable) { + this.cacheable(); + } + const regex = createRequireStringRegex( + loaderUtils.urlToRequest(this.resourcePath) || {} + ); + + return `module.exports = function inject(injections) { + var module = {exports: {}}; + var exports = module.exports; + ${src.replace(regex, "(injections[$1] || /* istanbul ignore next */ $&)")} + return module.exports; +}\n`; +}; diff --git a/browser/components/newtab/metrics.yaml b/browser/components/newtab/metrics.yaml new file mode 100644 index 0000000000..04845fcda0 --- /dev/null +++ b/browser/components/newtab/metrics.yaml @@ -0,0 +1,1589 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# Adding a new metric? We have docs for that! +# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html + +--- +$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0 +$tags: + - 'Firefox :: New Tab Page' + +newtab: + locale: + type: string + description: > + The application's locale as of when newtab's TelemetryFeed was init. + Comes from `Services.local.appLocaleAsBCP47`. + Looks like `en-US`. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1766887 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786670 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1766887 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786670#c3 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105#c11 + data_sensitivity: + - interaction + notification_emails: + - anicholson@mozilla.com + - chutten@mozilla.com + - mmccorquodale@mozilla.com + - najiang@mozilla.com + - lina@mozilla.com + expires: never + send_in_pings: + - newtab + lifetime: application + + newtab_category: + type: string + description: > + The current setting of the newtab page. + One of ["enabled", "disabled", "extension"] or any value from + SiteClassifier like "known-hijacker" or "social-media". + Similar to Activity Stream's PAGE_TAKEOVER_DATA event's + `newtab_url_category`. + Sampled once after newtab init. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786612 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786612 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105#c11 + data_sensitivity: + - technical + notification_emails: + - anicholson@mozilla.com + - chutten@mozilla.com + - mmccorquodale@mozilla.com + - najiang@mozilla.com + - lina@mozilla.com + expires: never + send_in_pings: + - newtab + lifetime: application + + homepage_category: + type: string + description: > + The current setting of the home page. + One of ["enabled", "disabled", "extension"] or any value from + SiteClassifier like "known-hijacker" or "social-media". + Similar to Activity Stream's PAGE_TAKEOVER_DATA event's + `home_url_category`. + Sampled once after newtab init. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786612 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786612 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105#c11 + data_sensitivity: + - technical + notification_emails: + - anicholson@mozilla.com + - chutten@mozilla.com + - mmccorquodale@mozilla.com + - najiang@mozilla.com + - lina@mozilla.com + expires: never + send_in_pings: + - newtab + lifetime: application + + opened: + type: event + description: > + Recorded when newtab UI is opened via `about:newtab` or `about:home` or + `about:welcome` and has been made visible (see `visibility_event_rcvd_ts` + in + [detect-user-session-start.js](https://searchfox.org/mozilla-central/source/browser/components/newtab/content-src/lib/detect-user-session-start.js)). + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1766887 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786670 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1766887 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786670#c3 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105#c11 + data_sensitivity: + - interaction + notification_emails: + - anicholson@mozilla.com + - chutten@mozilla.com + - mmccorquodale@mozilla.com + - najiang@mozilla.com + - lina@mozilla.com + expires: never + extra_keys: + newtab_visit_id: &newtab_visit_id + description: > + The id of this newtab visit. + Allows you to separate multiple simultaneous newtabs and + build an event timeline of actions taken from this newtab. + type: string + source: + description: > + The source that opened this newtab. + One of + * `about:newtab` + * `about:home` + * `about:welcome` + * `other` + (See `ONBOARDING_ALLOWED_PAGE_VALUES`). + type: string + send_in_pings: + - newtab + + closed: + type: event + description: > + Recorded when newtab UI is closed by + * navigation + * closing the tab + + Doesn't mean that the newtab was ever visible to a user. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1766887 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786670 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1766887 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786670#c3 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105#c11 + data_sensitivity: + - interaction + notification_emails: + - anicholson@mozilla.com + - chutten@mozilla.com + - mmccorquodale@mozilla.com + - najiang@mozilla.com + - lina@mozilla.com + expires: never + extra_keys: + newtab_visit_id: *newtab_visit_id + send_in_pings: + - newtab + + blocked_sponsors: + type: string_list + description: > + The advertiser names that have been dismissed by the user. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1828234 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1828234#c1 + data_sensitivity: + - interaction + notification_emails: + - anicholson@mozilla.com + - chutten@mozilla.com + - mmccorquodale@mozilla.com + - najiang@mozilla.com + - lina@mozilla.com + - ttran@mozilla.com + expires: never + send_in_pings: + - newtab + lifetime: application + + sov_allocation: + type: string_list + description: > + The partner group assignment for sov + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1840311 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1840311#c3 + data_sensitivity: + - technical + notification_emails: + - anicholson@mozilla.com + - chutten@mozilla.com + - mmccorquodale@mozilla.com + - najiang@mozilla.com + - lina@mozilla.com + - ttran@mozilla.com + expires: never + send_in_pings: + - newtab + lifetime: application + +newtab.search: + enabled: + lifetime: application + type: boolean + description: > + Whether the search input is enabled on the newtab. + Corresponds to the value of the + `browser.newtabpage.activity-stream.showSearch` pref. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786612 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786612 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105#c11 + data_sensitivity: + - technical + notification_emails: + - anicholson@mozilla.com + - chutten@mozilla.com + - mmccorquodale@mozilla.com + - najiang@mozilla.com + - lina@mozilla.com + expires: never + send_in_pings: + - newtab + +newtab.handoff_preference: + enabled: + lifetime: application + type: boolean + description: > + Records whether the + browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar preference is + enabled or disabled + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1864496 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1864496 + data_sensitivity: + - interaction + expires: 128 + notification_emails: + - fx-search-telemetry@mozilla.com + + +topsites: + enabled: + lifetime: application + type: boolean + description: > + Whether "topsites" is enabled on the newtab. + AKA the "Shortcuts" section. + Corresponds to the value of the + `browser.newtabpage.activity-stream.feeds.topsites` pref. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786612 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786612 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105#c11 + data_sensitivity: + - technical + notification_emails: + - anicholson@mozilla.com + - chutten@mozilla.com + - mmccorquodale@mozilla.com + - najiang@mozilla.com + - lina@mozilla.com + expires: never + send_in_pings: + - newtab + + sponsored_enabled: + lifetime: application + type: boolean + description: > + Whether sponsored topsites are enabled on the newtab. + AKA the "Sponsored Shortcuts" section. + Corresponds to the value of the + `browser.newtabpage.activity-stream.showSponsoredTopSites` pref. + Can be `true` even if topsites.enabled is `false`. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786612 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786612 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105#c11 + data_sensitivity: + - technical + notification_emails: + - anicholson@mozilla.com + - chutten@mozilla.com + - mmccorquodale@mozilla.com + - najiang@mozilla.com + - lina@mozilla.com + expires: never + send_in_pings: + - newtab + + impression: + type: event + description: > + Recorded when topsite tiles are loaded. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1766887 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786670 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1820707 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1766887 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786670#c3 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105#c11 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1820707#c3 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1821556#c3 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1824842#c7 + data_sensitivity: + - interaction + notification_emails: + - anicholson@mozilla.com + - chutten@mozilla.com + - mmccorquodale@mozilla.com + - najiang@mozilla.com + - lina@mozilla.com + expires: never + extra_keys: + advertiser_name: &advertiser_name + description: > + The name of the advertiser of the tile + type: string + tile_id: &tile_id + description: > + The tile id of the advertiser provided by Contile. Like `74357`. + type: quantity + newtab_visit_id: *newtab_visit_id + is_sponsored: &is_sponsored + description: Whether the topsite tile was sponsored. + type: boolean + position: &topsite_position + description: The position (0-index) of the topsite tile. + type: quantity + send_in_pings: + - newtab + + click: + type: event + description: > + Recorded when a topsite tile is clicked. + Only happens on click. Not on middle-click. Not on "Open in new Tab"-like + options in the context menu. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1766887 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786670 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1820707 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1766887 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786670#c3 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105#c11 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1820707#c3 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1821556#c3 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1824842#c7 + data_sensitivity: + - interaction + notification_emails: + - anicholson@mozilla.com + - chutten@mozilla.com + - mmccorquodale@mozilla.com + - najiang@mozilla.com + - lina@mozilla.com + expires: never + extra_keys: + advertiser_name: *advertiser_name + tile_id: *tile_id + newtab_visit_id: *newtab_visit_id + is_sponsored: *is_sponsored + position: *topsite_position + send_in_pings: + - newtab + + show_privacy_click: + type: event + description: > + Recorded when the "Our Sponsors and Your Privacy" menu item in the three- + dots menu of a sponsored topsite is clicked. + Corresponds to the receipt of a dispatched `ABOUT_SPONSORED_TOP_SITES` + action by `TelemetryFeed`. + bugs: + - https://mozilla-hub.atlassian.net/browse/DENG-1364 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1857324 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1857324 + data_sensitivity: [interaction] + notification_emails: + - chutten@mozilla.com + - mmccorquodale@mozilla.com + - sbetancourt@mozilla.com + expires: never + extra_keys: + advertiser_name: *advertiser_name + tile_id: *tile_id + newtab_visit_id: *newtab_visit_id + position: *topsite_position + send_in_pings: + - newtab + + dismiss: + type: event + description: > + Recorded when the "Dismiss" menu item in the three-dots menu of a topsite + is clicked. + Corresponds to the receipt of a dispatched `BLOCK_URL` action by + `TelemetryFeed`. + Applies to both sponsored and non-sponsored topsites. + `advertiser_name` is only provided for sponsored topsites. + bugs: + - https://mozilla-hub.atlassian.net/browse/DENG-1363 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1857324 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1857324 + data_sensitivity: [interaction] + notification_emails: + - chutten@mozilla.com + - mmccorquodale@mozilla.com + - sbetancourt@mozilla.com + - kdemtchouk@mozilla.com + - mbowerman@mozilla.com + expires: never + extra_keys: + advertiser_name: *advertiser_name + tile_id: *tile_id + newtab_visit_id: *newtab_visit_id + is_sponsored: *is_sponsored + position: *topsite_position + send_in_pings: + - newtab + + pref_changed: + type: event + description: > + Recorded when specific topsites prefs have changed. + + The list of possible prefs is presently: + * browser.newtabpage.activity-stream.feeds.topsites + * browser.newtabpage.activity-stream.showSponsoredTopSites + bugs: + - https://mozilla-hub.atlassian.net/browse/D0-1293 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1857324 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1857324 + data_sensitivity: [interaction] + notification_emails: + - chutten@mozilla.com + - mmccorquodale@mozilla.com + - sbetancourt@mozilla.com + - kdemtchouk@mozilla.com + - mbowerman@mozilla.com + expires: never + extra_keys: + pref_name: + description: The full name of the pref whose value just changed. + type: string + new_value: + description: The new (current) value the pref just changed to. + type: boolean + send_in_pings: + - newtab + + rows: + lifetime: application + type: quantity + unit: integer + description: > + The number of topsite tile rows configured to be shown on the newtab + page. Corresponds to the value of the + `browser.newtabpage.activity-stream.topSitesRows` pref. This is not the + number of rows actually seen by the user: if the browser window is + partially off-screen, or isn't wide enough to accommodate eight tiles per + row, the actual number of rows may be different. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1821556 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1821556#c3 + data_sensitivity: + - interaction + notification_emails: + - anicholson@mozilla.com + - chutten@mozilla.com + - mmccorquodale@mozilla.com + - najiang@mozilla.com + - lina@mozilla.com + expires: never + send_in_pings: + - newtab + + sponsored_tiles_configured: + lifetime: application + type: quantity + unit: integer + description: > + The number of topsite tiles configured to be shown on newtab. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1862493 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi + data_sensitivity: + - technical + notification_emails: + - gleonard@mozilla.com + expires: never + send_in_pings: + - newtab + + sponsored_tiles_received: + lifetime: application + type: text + description: > + The stringified JSON of tiles processed for display (array of objects). + Includes tiles not displayed and reason for not displaying. + Fields included: advertiser, provider, display_position, + display_fail_reason. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi + data_sensitivity: + - web_activity + notification_emails: + - gleonard@mozilla.com + expires: never + send_in_pings: + - newtab + +pocket: + is_signed_in: + lifetime: application + type: boolean + description: > + Whether the Firefox user is signed in to Pocket. + Does not correspond to a pref, so its value is resampled at newtab's + component init and whenever there is a Discovery Stream user event. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786612 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786612 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105#c11 + data_sensitivity: + - technical + notification_emails: + - anicholson@mozilla.com + - chutten@mozilla.com + - mmccorquodale@mozilla.com + - najiang@mozilla.com + - lina@mozilla.com + expires: never + send_in_pings: + - newtab + + enabled: + lifetime: application + type: boolean + description: > + Whether Pocket is enabled on the newtab. + AKA the "Recommended by Pocket" section. + Corresponds to the value of the + `browser.newtabpage.activity-stream.feeds.section.topstories` pref. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786612 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786612 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105#c11 + data_sensitivity: + - technical + notification_emails: + - anicholson@mozilla.com + - chutten@mozilla.com + - mmccorquodale@mozilla.com + - najiang@mozilla.com + - lina@mozilla.com + expires: never + send_in_pings: + - newtab + + sponsored_stories_enabled: + lifetime: application + type: boolean + description: > + Whether Pocket sponsored stories are enabled on the newtab. + Corresponds to the value of the + `browser.newtabpage.activity-stream.showSponsored` pref. + Can be `true` even if pocket.enabled is `false`. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786612 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786612 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105#c11 + data_sensitivity: + - technical + notification_emails: + - anicholson@mozilla.com + - chutten@mozilla.com + - mmccorquodale@mozilla.com + - najiang@mozilla.com + - lina@mozilla.com + expires: never + send_in_pings: + - newtab + + impression: + type: event + description: > + Recorded when a pocket tile is visible to the user. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786612 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1854245 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1862670 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786612 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105#c11 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1854245 + data_sensitivity: + - interaction + notification_emails: + - anicholson@mozilla.com + - chutten@mozilla.com + - mmccorquodale@mozilla.com + - najiang@mozilla.com + - lina@mozilla.com + expires: never + extra_keys: + newtab_visit_id: *newtab_visit_id + is_sponsored: &is_sponsored_pocket + description: Whether the pocket tile was sponsored (has an ad shim). + type: boolean + position: &pocket_position + description: The position (0-index) of the pocket tile. + type: quantity + recommendation_id: &recommendation_id + description: > + The id from the Pocket API response that returned the recommendation. + Like "{61934fe5-fbb0-4f4e-b9dd-7eab5f6ee9cd}". + type: string + tile_id: &pocket_tile_id + description: > + A content identifier. + For organic Pocket recommendations it is an opaque id produced by + Pocket's recommendation systems. + For sponsored Pocket content it is Kevel's "ad ID". + type: quantity + send_in_pings: + - newtab + + click: + type: event + description: > + Recorded when a pocket tile is clicked. + Only happens on click. Not on middle-click. Not on "Open in new Tab"-like + options in the context menu. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786612 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1854245 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1862670 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786612 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105#c11 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1854245 + data_sensitivity: + - interaction + notification_emails: + - anicholson@mozilla.com + - chutten@mozilla.com + - mmccorquodale@mozilla.com + - najiang@mozilla.com + - lina@mozilla.com + expires: never + extra_keys: + newtab_visit_id: *newtab_visit_id + is_sponsored: *is_sponsored_pocket + position: *pocket_position + recommendation_id: *recommendation_id + tile_id: *pocket_tile_id + send_in_pings: + - newtab + + save: + type: event + description: > + Recorded when a user decides to save a pocket tile. + Does not mean it ends up successfully saved. + Just that the user clicked on "Save to Pocket" in the little pocket + tile menu. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786612 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1854245 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1862670 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786612 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105#c11 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1854245 + data_sensitivity: + - interaction + notification_emails: + - anicholson@mozilla.com + - chutten@mozilla.com + - mmccorquodale@mozilla.com + - najiang@mozilla.com + - lina@mozilla.com + expires: never + extra_keys: + newtab_visit_id: *newtab_visit_id + is_sponsored: *is_sponsored_pocket + position: *pocket_position + recommendation_id: *recommendation_id + tile_id: *pocket_tile_id + send_in_pings: + - newtab + + topic_click: + type: event + description: > + Recorded when a pocket "Popular Topic" is clicked. + Only happens on click. Not on middle-click. Not on "Open in new Tab"-like + options in the context menu. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786612 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1854245 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1786612 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1817105#c11 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1854245 + data_sensitivity: + - interaction + notification_emails: + - anicholson@mozilla.com + - chutten@mozilla.com + - mmccorquodale@mozilla.com + - najiang@mozilla.com + - lina@mozilla.com + expires: never + extra_keys: + newtab_visit_id: *newtab_visit_id + topic: + description: The topic that was clicked on. Like "entertainment". + type: string + send_in_pings: + - newtab + + shim: + type: text + lifetime: ping + description: | + Opaque partner identifier for a given ad impression or engagement action, + unique per market and region. + Pocket + [proxies requests to ad partners](https://github.com/Pocket/proxy-server/) + and provides them solely with market, region, and action to generate these + shims. Thus, though the contents of this field are obscure, they cannot + identify clients. + At time of writing this information is a comma-separated trio. + The first item is an index into the proxy server's list of acceptable http + endpoints for contacting the ad service. The second item is a + several-hundred-byte base64-encoded JSON-encoded struct with fields for, + amongst other things, market and region. The third is unknown, but appears + to be a signature or checksum. + This shim should not be sent with the client_id. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1862670 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1862670 + data_sensitivity: + - stored_content # Required for text type, and to encourage scrutiny + notification_emails: + - chutten@mozilla.com + - najiang@mozilla.com + expires: never + send_in_pings: + - spoc + + +messaging_system: + event_context_parse_error: + type: counter + lifetime: ping + description: | + How often we failed to parse event_context as JSON. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - technical + notification_emails: + - dmosedale@mozilla.com + - pmcmanis@mozilla.com + expires: never + send_in_pings: + - messaging-system + + event_reason: + type: string + lifetime: ping + description: | + The event_context's `reason`. Likely something like + "welcome-window-closed" or "app-shut-down",. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - interaction + notification_emails: + - dmosedale@mozilla.com + - pmcmanis@mozilla.com + expires: never + send_in_pings: + - messaging-system + + event_page: + type: string + lifetime: ping + description: | + The event_context's `page`. Almost always "about:welcome". + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - interaction + notification_emails: + - dmosedale@mozilla.com + - pmcmanis@mozilla.com + expires: never + send_in_pings: + - messaging-system + + event_source: + type: string + lifetime: ping + description: | + The event_context's `source`. Likely something like "primary_button". + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - interaction + notification_emails: + - dmosedale@mozilla.com + - pmcmanis@mozilla.com + expires: never + send_in_pings: + - messaging-system + + event_context: + type: text + lifetime: ping + description: | + The stringified JSON of `event_context`. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - web_activity + notification_emails: + - dmosedale@mozilla.com + - pmcmanis@mozilla.com + expires: never + send_in_pings: + - messaging-system + + event_screen_family: + type: text + lifetime: ping + description: | + A string identifier of the message family derived from the message id + (e.g. MR_WELCOME_DEFAULT). + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1867627 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - web_activity + notification_emails: + - pmcmanis@mozilla.com + - dmosedale@mozilla.com + - nsauermann@mozilla.com + expires: never + send_in_pings: + - messaging-system + + event_screen_id: + type: text + lifetime: ping + description: | + A string identifier of the message screen id + (e.g. AW_MOBILE_DOWNLOAD). + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1867627 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - web_activity + notification_emails: + - pmcmanis@mozilla.com + - dmosedale@mozilla.com + - nsauermann@mozilla.com + expires: never + send_in_pings: + - messaging-system + + event_screen_initials: + type: text + lifetime: ping + description: | + A string identifier of the message screen initials + (e.g. 'EMAG' for EASY_SETUP, MOBILE_DOWNLOADS, AMO, GRATITUDE). + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1867627 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - web_activity + notification_emails: + - pmcmanis@mozilla.com + - dmosedale@mozilla.com + - nsauermann@mozilla.com + expires: never + send_in_pings: + - messaging-system + + event_screen_index: + type: quantity + unit: integer + lifetime: ping + description: | + A number identifier of the screen index in a sequence of screens + (e.g. 0 for first message). + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - web_activity + notification_emails: + - pmcmanis@mozilla.com + - dmosedale@mozilla.com + - nsauermann@mozilla.com + expires: never + send_in_pings: + - messaging-system + + message_id: + type: text + lifetime: ping + description: | + A string identifier of the message in Activity Stream Router. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - web_activity + notification_emails: + - pmcmanis@mozilla.com + - dmosedale@mozilla.com + expires: never + send_in_pings: + - messaging-system + + event: + type: string + description: > + The type of event. Any user defined string + (e.g. “IMPRESSION”, “CLICK_BUTTON”, "INDEXEDDB_OPEN_FAILED", “SESSION_END”) + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - interaction + notification_emails: + - pmcmanis@mozilla.com + - dmosedale@mozilla.com + expires: never + send_in_pings: + - messaging-system + + ping_type: + type: string + description: > + Type of event the ping is capturing. + e.g. "cfr", "whats-new-panel", "onboarding" + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - interaction + notification_emails: + - pmcmanis@mozilla.com + - dmosedale@mozilla.com + expires: never + send_in_pings: + - messaging-system + + source: + type: string + description: > + The source of the interaction described by the other metrics. + e.g. "frecent_links", "newtab", "CFR" + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - interaction + notification_emails: + - pmcmanis@mozilla.com + - dmosedale@mozilla.com + expires: never + send_in_pings: + - messaging-system + + client_id: + type: uuid + lifetime: ping + description: | + The client_id according to Telemetry. + Might not always have a value due to policy around specific types of + ping being sent. Value may be the canary client id + `c0ffeec0-ffee-c0ff-eec0-ffeec0ffeec0` + in pings near when the data upload pref is disabled (if Telemetry gets + to go first), or between when a client_id has been removed and when it + has been regenerated. + Present only in some circumstances (see + [bug 1484035]https://bugzilla.mozilla.org/show_bug.cgi?id=1484035)). + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1755549 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1484035 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1755549 + data_sensitivity: + - technical + notification_emails: + - dmosedale@mozilla.com + - pmcmanis@mozilla.com + expires: never + send_in_pings: + - messaging-system + + locale: + type: string + lifetime: ping + description: > + The locale as supplied to the messaging system by + `Services.locale.appLocaleAsBCP47`. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - technical + notification_emails: + - pmcmanis@mozilla.com + - dmosedale@mozilla.com + expires: never + send_in_pings: + - messaging-system + + browser_session_id: + type: uuid + lifetime: ping + # Disable yamllint for long lines. + # yamllint disable + description: > + The Legacy Telemetry browser "session id". + Identifies a specific period from application start to shutdown. + See [the "main" ping docs](https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/data/main-ping.html) + for details. + # yamllint enable + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - technical + notification_emails: + - pmcmanis@mozilla.com + - dmosedale@mozilla.com + expires: never + send_in_pings: + - messaging-system + + impression_id: + type: uuid + lifetime: ping + description: > + The unique impression identifier for a specific client. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - technical + notification_emails: + - pmcmanis@mozilla.com + - dmosedale@mozilla.com + expires: never + send_in_pings: + - messaging-system + + bucket_id: + type: string + lifetime: ping + description: > + A name shared between multiple messages that may individually be too + targetted. + e.g. a message that gets shown on specific websites or a message asking + about personal information. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - technical + notification_emails: + - pmcmanis@mozilla.com + - dmosedale@mozilla.com + expires: never + send_in_pings: + - messaging-system + + addon_version: + type: string + lifetime: ping + description: > + Used to hold the system addon's version, + now is almost certainly an echo of the app's build id. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - technical + notification_emails: + - pmcmanis@mozilla.com + - dmosedale@mozilla.com + expires: never + send_in_pings: + - messaging-system + + unknown_key_count: + type: counter + description: | + The sum of all unknown keys counted. + Useful for testing. + Can be removed after bug 1600008 is resolved. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - technical + notification_emails: + - chutten@mozilla.com + expires: never + send_in_pings: + - messaging-system + + unknown_keys: + type: labeled_counter + description: | + Ping keys supplied to the messaging system for which + we did not have a corresponding metric mapped to how often they attempted + to be recorded. + You may have forgotten to define an appropriate metric in + `browser/components/newtab/metrics.yaml`. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - technical + notification_emails: + - dmosedale@mozilla.com + - pmcmanis@mozilla.com + expires: never + send_in_pings: + - messaging-system + + glean_ping_for_ping_failures: + type: counter + description: | + How often something went awry within + `AboutWelcome.submitGleanPingForPing`, preventing ping submission. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - technical + notification_emails: + - dmosedale@mozilla.com + - pmcmanis@mozilla.com + - chutten@mozilla.com + expires: never + send_in_pings: + - metrics + + invalid_nested_data: + type: labeled_counter + description: | + We received a ping with non-scalar data on a field of this name. + If this is existing pre-PingCentre-replacement data, you may need to + augment the logic in + `AboutWelcome.submitGleanPingForPing` like the other `handledKeys`. + If this is for new, post-PingCentre-replacement data, you should + probably prefer a flat structure. + If you're unsure, please ask in + [the #glean channel](https://chat.mozilla.org/#/room/#glean:mozilla.org). + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - technical + notification_emails: + - dmosedale@mozilla.com + - pmcmanis@mozilla.com + - chutten@mozilla.com + expires: never + send_in_pings: + - messaging-system + + +messaging_system.attribution: + source: + type: string + lifetime: ping + description: | + Attribution's source, possibly derived from the utm parameter of the same + name. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - interaction + notification_emails: + - dmosedale@mozilla.com + - pmcmanis@mozilla.com + expires: never + send_in_pings: + - messaging-system + + medium: + type: string + lifetime: ping + description: | + Attribution's medium, possibly derived from the utm parameter of the same + name. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - interaction + notification_emails: + - dmosedale@mozilla.com + - pmcmanis@mozilla.com + expires: never + send_in_pings: + - messaging-system + + campaign: + type: string + lifetime: ping + description: | + Attribution's campaign, possibly derived from the utm parameter of the + same name. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - interaction + notification_emails: + - dmosedale@mozilla.com + - pmcmanis@mozilla.com + expires: never + send_in_pings: + - messaging-system + + content: + type: string + lifetime: ping + description: | + Attribution's content, possibly derived from the utm parameter of the + same name. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - interaction + notification_emails: + - dmosedale@mozilla.com + - pmcmanis@mozilla.com + expires: never + send_in_pings: + - messaging-system + + experiment: + type: string + lifetime: ping + description: | + Attribution's experiment key. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - interaction + notification_emails: + - dmosedale@mozilla.com + - pmcmanis@mozilla.com + expires: never + send_in_pings: + - messaging-system + + variation: + type: string + lifetime: ping + description: | + Attribution's variation key. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - interaction + notification_emails: + - dmosedale@mozilla.com + - pmcmanis@mozilla.com + expires: never + send_in_pings: + - messaging-system + + ua: + type: string + lifetime: ping + description: | + Attribution's ua key. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - interaction + notification_emails: + - dmosedale@mozilla.com + - pmcmanis@mozilla.com + expires: never + send_in_pings: + - messaging-system + + dltoken: + type: string + lifetime: ping + description: | + String representation of the dltoken identifying the particular + installer used to install this Firefox. + Likely a UUID, if present. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - interaction + notification_emails: + - dmosedale@mozilla.com + - pmcmanis@mozilla.com + expires: never + send_in_pings: + - messaging-system + + msstoresignedin: + type: string + lifetime: ping + description: | + Either the string "true" or the string "false" to indicate whether the + attributed install came from the Microsoft store and, if so, whether the + user was signed in at the time. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1756209 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1756209 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - interaction + notification_emails: + - dmosedale@mozilla.com + - pmcmanis@mozilla.com + expires: never + send_in_pings: + - messaging-system + + dlsource: + type: string + lifetime: ping + description: | + Mozilla-specific download "source" name. Could be something like + "mozillaci" to identify that the installer came from + `{archive|ftp}.mozilla.org`. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1819997 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1819997 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - interaction + notification_emails: + - dmosedale@mozilla.com + - pmcmanis@mozilla.com + expires: never + send_in_pings: + - messaging-system + + unknown_keys: + type: labeled_counter + description: | + Attribution keys supplied to the messaging system for which + we did not have a corresponding metric, and the count of how + often that happened. + Either add this key to a list of known attribution keys in + `AboutWelcomeTelemetry` to suppress or define an appropriate metric in + `browser/components/newtab/metrics.yaml` to collect. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_sensitivity: + - technical + notification_emails: + - dmosedale@mozilla.com + - pmcmanis@mozilla.com + expires: never + send_in_pings: + - messaging-system + +top_sites: # Replacement for PingCentre "topsites-impression|click" pings. + ping_type: + type: string + description: > + The ping's type. In other situations might be designated by an event's + name or an interaction field. E.g. "topsites-impression", + "topsites-click". + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836283 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836283 + data_sensitivity: + - interaction + notification_emails: + - najiang@mozilla.com + expires: never + send_in_pings: + - top-sites + + position: + type: quantity + unit: topsite position + description: > + The position (1-based) of the topsites item being interatcted with. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836283 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836283 + data_sensitivity: + - interaction + notification_emails: + - najiang@mozilla.com + expires: never + send_in_pings: + - top-sites + + source: + type: string + description: > + The source of the interaction. Always set to "newtab". + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836283 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836283 + data_sensitivity: + - interaction + notification_emails: + - najiang@mozilla.com + expires: never + send_in_pings: + - top-sites + + tile_id: + type: string + description: > + String-encoded number for the tile's sponsored tile id. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836283 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836283 + data_sensitivity: + - interaction + notification_emails: + - najiang@mozilla.com + expires: never + send_in_pings: + - top-sites + + reporting_url: + type: url + description: > + The url to report this interaction to. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836283 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836283 + data_sensitivity: + - web_activity + notification_emails: + - najiang@mozilla.com + expires: never + send_in_pings: + - top-sites + + advertiser: + type: string + description: > + The name of the advertiser providing the sponsored TopSite. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836283 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836283 + data_sensitivity: + - interaction + - web_activity + notification_emails: + - najiang@mozilla.com + expires: never + send_in_pings: + - top-sites + + context_id: + type: uuid + description: > + An identifier to identify users for Contextual Services user interaction pings. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836283 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836283 + data_sensitivity: + - technical + notification_emails: + - najiang@mozilla.com + expires: never + send_in_pings: + - top-sites diff --git a/browser/components/newtab/moz.build b/browser/components/newtab/moz.build new file mode 100644 index 0000000000..0d3bddb968 --- /dev/null +++ b/browser/components/newtab/moz.build @@ -0,0 +1,35 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Firefox", "New Tab Page") + +BROWSER_CHROME_MANIFESTS += [ + "test/browser/abouthomecache/browser.toml", + "test/browser/browser.toml", +] + +SPHINX_TREES["docs"] = "docs" + +XPCSHELL_TESTS_MANIFESTS += [ + "test/xpcshell/xpcshell.toml", +] + +XPIDL_SOURCES += [ + "nsIAboutNewTabService.idl", +] + +XPIDL_MODULE = "browser-newtab" + +EXTRA_JS_MODULES += [ + "AboutNewTabService.sys.mjs", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +JAR_MANIFESTS += ["jar.mn"] diff --git a/browser/components/newtab/nsIAboutNewTabService.idl b/browser/components/newtab/nsIAboutNewTabService.idl new file mode 100644 index 0000000000..e8ddd0381f --- /dev/null +++ b/browser/components/newtab/nsIAboutNewTabService.idl @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +/** + * Allows to override about:newtab to point to a different location + * than the one specified within AboutRedirector.cpp + */ + +interface nsIChannel; +interface nsIURI; +interface nsILoadInfo; + +[scriptable, uuid(dfcd2adc-7867-4d3a-ba70-17501f208142)] +interface nsIAboutNewTabService : nsISupports +{ + /** + * Returns the default URL (local or activity stream depending on pref) + */ + readonly attribute ACString defaultURL; + + /** + * In the "privileged about content process", if about:home is being + * retrieved, the AboutRedirector will call this function to get the + * nsIChannel for the document. This gives the nsIAboutNewTabService + * the opportunity to provide a cached document for about:home. If + * no cache exists, the nsIChannel will be for the normal dynamically + * generated about:home document. + */ + nsIChannel aboutHomeChannel(in nsIURI aURI, + in nsILoadInfo aLoadInfo); + + /** + * Returns the about:welcome URL. + */ + readonly attribute ACString welcomeURL; +}; diff --git a/browser/components/newtab/package-lock.json b/browser/components/newtab/package-lock.json new file mode 100644 index 0000000000..7a9fead1b8 --- /dev/null +++ b/browser/components/newtab/package-lock.json @@ -0,0 +1,12455 @@ +{ + "name": "activity-streams", + "version": "1.14.3", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "activity-streams", + "version": "1.14.3", + "license": "MPL-2.0", + "dependencies": { + "@fluent/bundle": "0.17.1", + "@fluent/react": "0.15.0", + "react": "16.13.1", + "react-dom": "16.13.1", + "react-redux": "7.2.6", + "react-transition-group": "4.4.2", + "redux": "4.1.2" + }, + "devDependencies": { + "@babel/core": "7.23.5", + "@babel/preset-react": "7.23.3", + "@jsdevtools/coverage-istanbul-loader": "^3.0.5", + "acorn": "8.5.0", + "babel-loader": "8.2.3", + "babel-plugin-jsm-to-esmodules": "0.6.0", + "buffer": "6.0.3", + "chai": "4.3.4", + "enzyme": "3.11.0", + "enzyme-adapter-react-16": "1.15.6", + "joi-browser": "13.4.0", + "karma": "6.4.2", + "karma-chai": "0.1.0", + "karma-coverage-istanbul-reporter": "3.0.3", + "karma-firefox-launcher": "2.1.2", + "karma-json-reporter": "1.2.1", + "karma-mocha": "2.0.1", + "karma-mocha-reporter": "2.2.5", + "karma-sinon": "1.0.5", + "karma-sourcemap-loader": "0.3.8", + "karma-webpack": "5.0.0", + "loader-utils": "3.2.1", + "lodash": "4.17.21", + "mocha": "9.2.2", + "mock-raf": "1.0.1", + "npm-run-all": "4.1.5", + "postcss-scss": "4.0.6", + "prop-types": "15.7.2", + "raw-loader": "4.0.2", + "rimraf": "3.0.2", + "sass": "1.43.4", + "shelljs": "0.8.5", + "sinon": "12.0.1", + "stream-browserify": "3.0.0", + "util": "0.10.4", + "webpack": "5.89.0", + "webpack-cli": "4.9.1", + "yamscripts": "0.1.0" + }, + "engines": { + "//": "when changing node versions, also edit .nvmrc", + "firefox": ">=45.0 <=*", + "node": "16.19.*", + "npm": "8.19.3" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.5.tgz", + "integrity": "sha512-Cwc2XjUrG4ilcfOw4wBAK+enbdgwAcAJCfGUItPBKR7Mjw4aEfAFYrLxeRp4jWgtNIKn3n2AlBOfwwafl+42/g==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.5", + "@babel/parser": "^7.23.5", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.5", + "@babel/types": "^7.23.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.8.tgz", + "integrity": "sha512-KDqYz4PiOWvDFrdHLPhKtCThtIcKVy6avWD2oG4GEvyQ+XDZwHD4YQd+H2vNMnq2rkdxsDkU82T+Vk8U/WXHRQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", + "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.23.3.tgz", + "integrity": "sha512-GnvhtVfA2OAtzdX58FJxU19rhoGeQzyVndw3GgtdECQvQFXPEZIOVULHVZGAYmOgmqjXpVpfocAbSjh99V/Fqw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz", + "integrity": "sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.23.3", + "@babel/types": "^7.23.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz", + "integrity": "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==", + "dev": true, + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.23.3.tgz", + "integrity": "sha512-qMFdSS+TUhB7Q/3HVPnEdYJDQIk57jkntAwSuz9xfSE4n+3I+vHYCli3HoHawN1Z3RfCz/y1zXA/JXjG6cVImQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.23.3.tgz", + "integrity": "sha512-tbkHOS9axH6Ysf2OUEqoSZ6T3Fa2SrNH6WTWSPBboxKzdxNc9qOICeLXkNG0ZEwbQ1HY8liwOce4aN/Ceyuq6w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", + "@babel/plugin-transform-react-display-name": "^7.23.3", + "@babel/plugin-transform-react-jsx": "^7.22.15", + "@babel/plugin-transform-react-jsx-development": "^7.22.5", + "@babel/plugin-transform-react-pure-annotations": "^7.23.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz", + "integrity": "sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz", + "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@fluent/bundle": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@fluent/bundle/-/bundle-0.17.1.tgz", + "integrity": "sha512-CRFNT9QcSFAeFDneTF59eyv3JXFGhIIN4boUO2y22YmsuuKLyDk+N1I/NQUYz9Ab63e6V7T6vItoZIG/2oOOuw==", + "engines": { + "node": ">=12.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@fluent/react": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@fluent/react/-/react-0.15.0.tgz", + "integrity": "sha512-qUMfaHman+UciOELQc5hnFAv0VerUR6+9gEBCRk9RR66XS13syt91ZElNOTHWe2Ofv70cxAGaJ5Yff4MRPg5Ow==", + "dependencies": { + "@fluent/sequence": "^0.8.0", + "cached-iterable": "^0.3.0" + }, + "engines": { + "node": ">=14.0.0", + "npm": ">=7.0.0" + }, + "peerDependencies": { + "@fluent/bundle": ">=0.16.0 <0.18.0", + "react": ">=16.8.0" + } + }, + "node_modules/@fluent/sequence": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@fluent/sequence/-/sequence-0.8.0.tgz", + "integrity": "sha512-eV5QlEEVV/wR3AFQLXO67x4yPRPQXyqke0c8yucyMSeW36B3ecZyVFlY1UprzrfFV8iPJB4TAehDy/dLGbvQ1Q==", + "engines": { + "node": ">=14.0.0", + "npm": ">=7.0.0" + }, + "peerDependencies": { + "@fluent/bundle": ">= 0.13.0" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.21", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.21.tgz", + "integrity": "sha512-SRfKmRe1KvYnxjEMtxEr+J4HIeMX5YBg/qhRHpxEIGjhX1rshcHlnFUE9K0GazhVKWM7B+nARSkV8LuvJdJ5/g==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsdevtools/coverage-istanbul-loader": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@jsdevtools/coverage-istanbul-loader/-/coverage-istanbul-loader-3.0.5.tgz", + "integrity": "sha512-EUCPEkaRPvmHjWAAZkWMT7JDzpw7FKB00WTISaiXsbNOd5hCHg77XLA8sLYLFDo1zepYLo2w7GstN8YBqRXZfA==", + "dev": true, + "dependencies": { + "convert-source-map": "^1.7.0", + "istanbul-lib-instrument": "^4.0.3", + "loader-utils": "^2.0.0", + "merge-source-map": "^1.1.0", + "schema-utils": "^2.7.0" + } + }, + "node_modules/@jsdevtools/coverage-istanbul-loader/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/@jsdevtools/coverage-istanbul-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", + "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.1.3.tgz", + "integrity": "sha512-nhOb2dWPeb1sd3IQXL/dVPnKHDOAFfvichtBf4xV00/rU1QbPCQqKMbvIheIjqwVjh7qIgf2AHTHi391yMOMpQ==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", + "dev": true + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "8.56.2", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", + "integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.11.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz", + "integrity": "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" + }, + "node_modules/@types/react": { + "version": "18.2.47", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.47.tgz", + "integrity": "sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-redux": { + "version": "7.1.33", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", + "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" + }, + "node_modules/@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz", + "integrity": "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==", + "dev": true, + "peerDependencies": { + "webpack": "4.x.x || 5.x.x", + "webpack-cli": "4.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.5.0.tgz", + "integrity": "sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==", + "dev": true, + "dependencies": { + "envinfo": "^7.7.3" + }, + "peerDependencies": { + "webpack-cli": "4.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz", + "integrity": "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==", + "dev": true, + "peerDependencies": { + "webpack-cli": "4.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz", + "integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/airbnb-prop-types": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz", + "integrity": "sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg==", + "dev": true, + "dependencies": { + "array.prototype.find": "^2.1.1", + "function.prototype.name": "^1.1.2", + "is-regex": "^1.1.0", + "object-is": "^1.1.2", + "object.assign": "^4.1.0", + "object.entries": "^1.1.2", + "prop-types": "^15.7.2", + "prop-types-exact": "^1.2.0", + "react-is": "^16.13.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + }, + "peerDependencies": { + "react": "^0.14 || ^15.0.0 || ^16.0.0-alpha" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.filter": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array.prototype.filter/-/array.prototype.filter-1.0.3.tgz", + "integrity": "sha512-VizNcj/RGJiUyQBgzwxzE5oHdeuXY5hSbbmKMlphj1cy1Vl7Pn2asCGbSrru6hSQjmCzqTBPVWAF/whmEOVHbw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.find": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.2.2.tgz", + "integrity": "sha512-DRumkfW97iZGOfn+lIXbkVrXL04sfYKX+EfOodo8XboR5sxPDVvOjZTF/rysusa9lmhmSOeD6Vp6RKQP+eP4Tg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-loader": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.3.tgz", + "integrity": "sha512-n4Zeta8NC3QAsuyiizu0GkmRcQ6clkV9WFUnUf1iXP//IeSKbWjofW3UHyZVwlOB4y039YQKefawyTn64Zwbuw==", + "dev": true, + "dependencies": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^1.4.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + }, + "engines": { + "node": ">= 8.9" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "webpack": ">=2" + } + }, + "node_modules/babel-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/babel-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/babel-plugin-jsm-to-esmodules": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jsm-to-esmodules/-/babel-plugin-jsm-to-esmodules-0.6.0.tgz", + "integrity": "sha512-463Yuq2sLkjoGHl5vPYUQQONnDjxnmxZuhsR1swL5N76hDFGyYZAVd6HoS4E02jBF8bORpS4aFmdr1XjEZ0buQ==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/browserslist": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cached-iterable": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/cached-iterable/-/cached-iterable-0.3.0.tgz", + "integrity": "sha512-MDqM6TpBVebZD4UDtmlFp8EjVtRcsB6xt9aRdWymjk0fWVUUGgmt/V7o0H0gkI2Tkvv8B0ucjidZm4mLosdlWw==", + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001576", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz", + "integrity": "sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chai": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", + "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "dev": true, + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cors/node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/cross-spawn/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==", + "dev": true + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.4.629", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.629.tgz", + "integrity": "sha512-5UUkr3k3CZ/k+9Sw7vaaIMyOzMC0XbPyprKI3n0tbKDqkzTDOjK4izm7DxlkueRMim6ZZQ1ja9F7hoFVplHihA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", + "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", + "dev": true, + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/envinfo": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.0.tgz", + "integrity": "sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/enzyme": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.11.0.tgz", + "integrity": "sha512-Dw8/Gs4vRjxY6/6i9wU0V+utmQO9kvh9XLnz3LIudviOnVYDEe2ec+0k+NQoMamn1VrjKgCUOWj5jG/5M5M0Qw==", + "dev": true, + "dependencies": { + "array.prototype.flat": "^1.2.3", + "cheerio": "^1.0.0-rc.3", + "enzyme-shallow-equal": "^1.0.1", + "function.prototype.name": "^1.1.2", + "has": "^1.0.3", + "html-element-map": "^1.2.0", + "is-boolean-object": "^1.0.1", + "is-callable": "^1.1.5", + "is-number-object": "^1.0.4", + "is-regex": "^1.0.5", + "is-string": "^1.0.5", + "is-subset": "^0.1.1", + "lodash.escape": "^4.0.1", + "lodash.isequal": "^4.5.0", + "object-inspect": "^1.7.0", + "object-is": "^1.0.2", + "object.assign": "^4.1.0", + "object.entries": "^1.1.1", + "object.values": "^1.1.1", + "raf": "^3.4.1", + "rst-selector-parser": "^2.2.3", + "string.prototype.trim": "^1.2.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/enzyme-adapter-react-16": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.6.tgz", + "integrity": "sha512-yFlVJCXh8T+mcQo8M6my9sPgeGzj85HSHi6Apgf1Cvq/7EL/J9+1JoJmJsRxZgyTvPMAqOEpRSu/Ii/ZpyOk0g==", + "dev": true, + "dependencies": { + "enzyme-adapter-utils": "^1.14.0", + "enzyme-shallow-equal": "^1.0.4", + "has": "^1.0.3", + "object.assign": "^4.1.2", + "object.values": "^1.1.2", + "prop-types": "^15.7.2", + "react-is": "^16.13.1", + "react-test-renderer": "^16.0.0-0", + "semver": "^5.7.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + }, + "peerDependencies": { + "enzyme": "^3.0.0", + "react": "^16.0.0-0", + "react-dom": "^16.0.0-0" + } + }, + "node_modules/enzyme-adapter-react-16/node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/enzyme-adapter-react-16/node_modules/react-test-renderer": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.14.0.tgz", + "integrity": "sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg==", + "dev": true, + "dependencies": { + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "react-is": "^16.8.6", + "scheduler": "^0.19.1" + }, + "peerDependencies": { + "react": "^16.14.0" + } + }, + "node_modules/enzyme-adapter-react-16/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/enzyme-adapter-utils": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.14.1.tgz", + "integrity": "sha512-JZgMPF1QOI7IzBj24EZoDpaeG/p8Os7WeBZWTJydpsH7JRStc7jYbHE4CmNQaLqazaGFyLM8ALWA3IIZvxW3PQ==", + "dev": true, + "dependencies": { + "airbnb-prop-types": "^2.16.0", + "function.prototype.name": "^1.1.5", + "has": "^1.0.3", + "object.assign": "^4.1.4", + "object.fromentries": "^2.0.5", + "prop-types": "^15.8.1", + "semver": "^5.7.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + }, + "peerDependencies": { + "react": "0.13.x || 0.14.x || ^15.0.0-0 || ^16.0.0-0" + } + }, + "node_modules/enzyme-adapter-utils/node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/enzyme-adapter-utils/node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/enzyme-adapter-utils/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/enzyme-shallow-equal": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.5.tgz", + "integrity": "sha512-i6cwm7hN630JXenxxJFBKzgLC3hMTafFQXflvzHgPmDhOBhxUWDe8AeRv1qp2/uWJ2Y8z5yLWMzmAfkTOiOCZg==", + "dev": true, + "dependencies": { + "has": "^1.0.3", + "object-is": "^1.1.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", + "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.5", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.12", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", + "dev": true + }, + "node_modules/es-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", + "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", + "dev": true + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/execa/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/execa/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/execa/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true, + "engines": { + "node": ">=4.x" + } + }, + "node_modules/has": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", + "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/html-element-map": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/html-element-map/-/html-element-map-1.3.1.tgz", + "integrity": "sha512-6XMlxrAFX4UEEGxctfFnmrFaaZFNf9i5fNuV5wZ3WWQ4FVaNP1aX1LkX9j2mfEx1NpjeE/rL3nmgEn23GdFmrg==", + "dev": true, + "dependencies": { + "array.prototype.filter": "^1.0.0", + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-subset": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", + "integrity": "sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw==", + "dev": true + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.11" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-report/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/joi-browser": { + "version": "13.4.0", + "resolved": "https://registry.npmjs.org/joi-browser/-/joi-browser-13.4.0.tgz", + "integrity": "sha512-TfzJd2JaJ/lg/gU+q5j9rLAjnfUNF9DUmXTP9w+GfmG79LjFOXFeM7hIFuXCBcZCivUDFwd9l1btTV9rhHumtQ==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, + "node_modules/karma": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.2.tgz", + "integrity": "sha512-C6SU/53LB31BEgRg+omznBEMY4SjHU3ricV6zBcAe1EeILKkeScr+fZXtaI5WyDbkVowJxxAI6h73NcFPmXolQ==", + "dev": true, + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.4.1", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-chai": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/karma-chai/-/karma-chai-0.1.0.tgz", + "integrity": "sha512-mqKCkHwzPMhgTYca10S90aCEX9+HjVjjrBFAsw36Zj7BlQNbokXXCAe6Ji04VUMsxcY5RLP7YphpfO06XOubdg==", + "dev": true, + "peerDependencies": { + "chai": "*", + "karma": ">=0.10.9" + } + }, + "node_modules/karma-coverage-istanbul-reporter": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-3.0.3.tgz", + "integrity": "sha512-wE4VFhG/QZv2Y4CdAYWDbMmcAHeS926ZIji4z+FkB2aF/EposRb6DP6G5ncT/wXhqUfAb/d7kZrNKPonbvsATw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^3.0.6", + "istanbul-reports": "^3.0.2", + "minimatch": "^3.0.4" + }, + "funding": { + "url": "https://github.com/sponsors/mattlewis92" + } + }, + "node_modules/karma-firefox-launcher": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-2.1.2.tgz", + "integrity": "sha512-VV9xDQU1QIboTrjtGVD4NCfzIH7n01ZXqy/qpBhnOeGVOkG5JYPEm8kuSd7psHE6WouZaQ9Ool92g8LFweSNMA==", + "dev": true, + "dependencies": { + "is-wsl": "^2.2.0", + "which": "^2.0.1" + } + }, + "node_modules/karma-json-reporter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/karma-json-reporter/-/karma-json-reporter-1.2.1.tgz", + "integrity": "sha512-ASmvranNhUN0ctSuAZKeWISW9Nf4AteMcVy8rJVjS7Qk+qWgssag/nw+yivHWKDROztVFn7TdamHOETMPCkvgA==", + "dev": true, + "peerDependencies": { + "karma": ">=0.9" + } + }, + "node_modules/karma-mocha": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/karma-mocha/-/karma-mocha-2.0.1.tgz", + "integrity": "sha512-Tzd5HBjm8his2OA4bouAsATYEpZrp9vC7z5E5j4C5Of5Rrs1jY67RAwXNcVmd/Bnk1wgvQRou0zGVLey44G4tQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.3" + } + }, + "node_modules/karma-mocha-reporter": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/karma-mocha-reporter/-/karma-mocha-reporter-2.2.5.tgz", + "integrity": "sha512-Hr6nhkIp0GIJJrvzY8JFeHpQZNseuIakGac4bpw8K1+5F0tLb6l7uvXRa8mt2Z+NVwYgCct4QAfp2R2QP6o00w==", + "dev": true, + "dependencies": { + "chalk": "^2.1.0", + "log-symbols": "^2.1.0", + "strip-ansi": "^4.0.0" + }, + "peerDependencies": { + "karma": ">=0.13" + } + }, + "node_modules/karma-sinon": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/karma-sinon/-/karma-sinon-1.0.5.tgz", + "integrity": "sha512-wrkyAxJmJbn75Dqy17L/8aILJWFm7znd1CE8gkyxTBFnjMSOe2XTJ3P30T8SkxWZHmoHX0SCaUJTDBEoXs25Og==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + }, + "peerDependencies": { + "karma": ">=0.10", + "sinon": "*" + } + }, + "node_modules/karma-sourcemap-loader": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.8.tgz", + "integrity": "sha512-zorxyAakYZuBcHRJE+vbrK2o2JXLFWK8VVjiT/6P+ltLBUGUvqTEkUiQ119MGdOrK7mrmxXHZF1/pfT6GgIZ6g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2" + } + }, + "node_modules/karma-webpack": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-5.0.0.tgz", + "integrity": "sha512-+54i/cd3/piZuP3dr54+NcFeKOPnys5QeM1IY+0SPASwrtHsliXUiCL50iW+K9WWA7RvamC4macvvQ86l3KtaA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "webpack-merge": "^4.1.5" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.escape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", + "integrity": "sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw==", + "dev": true + }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "dependencies": { + "chalk": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dev": true, + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/merge-source-map": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", + "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", + "dev": true, + "dependencies": { + "source-map": "^0.6.1" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mocha": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", + "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", + "dev": true, + "dependencies": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.3", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "4.2.1", + "ms": "2.1.3", + "nanoid": "3.3.1", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "workerpool": "6.2.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/mocha/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/mocha/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/mocha/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/mocha/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mocha/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/mocha/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", + "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/mock-raf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mock-raf/-/mock-raf-1.0.1.tgz", + "integrity": "sha512-+25y56bblLzEnv+G4ODsHNck07A5uP5HFfu/1VBKeFrUXoFT9oru+R+jLxLz6rwdM5drUHFdqX9LYBsMP4dz/w==", + "dev": true, + "dependencies": { + "object-assign": "^3.0.0" + } + }, + "node_modules/moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", + "dev": true + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", + "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "dev": true, + "dependencies": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6" + }, + "bin": { + "nearley-railroad": "bin/nearley-railroad.js", + "nearley-test": "bin/nearley-test.js", + "nearley-unparse": "bin/nearley-unparse.js", + "nearleyc": "bin/nearleyc.js" + }, + "funding": { + "type": "individual", + "url": "https://nearley.js.org/#give-to-nearley" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node_modules/nise": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.7.tgz", + "integrity": "sha512-wWtNUhkT7k58uvWTB/Gy26eA/EJKtPZFVAhEilN5UYVmmGRYOURbejRUyKm0Uu9XVEW7K5nBOZfR8VMB4QR2RQ==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, + "node_modules/nise/node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", + "integrity": "sha512-jHP15vXVGeVh1HuaA2wY6lxk+whK/x4KBG88VXeRma7CCun7iGD5qPc4eYykQ9sdQvg8jkwFKsSxHln2ybW3xQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", + "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "dev": true, + "dependencies": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", + "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==", + "dev": true + }, + "node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.4.33", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", + "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true, + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.6.tgz", + "integrity": "sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + } + ], + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.19" + } + }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "node_modules/prop-types-exact": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/prop-types-exact/-/prop-types-exact-1.2.0.tgz", + "integrity": "sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA==", + "dev": true, + "dependencies": { + "has": "^1.0.3", + "object.assign": "^4.1.0", + "reflect.ownkeys": "^0.2.0" + } + }, + "node_modules/prop-types/node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "dev": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, + "node_modules/railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==", + "dev": true + }, + "node_modules/randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "dev": true, + "dependencies": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-loader": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz", + "integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/raw-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/raw-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/react": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz", + "integrity": "sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz", + "integrity": "sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.19.1" + }, + "peerDependencies": { + "react": "^16.13.1" + } + }, + "node_modules/react-dom/node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-redux": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz", + "integrity": "sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ==", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-redux/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "node_modules/react-transition-group": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", + "integrity": "sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/react/node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/redux": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz", + "integrity": "sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/reflect.ownkeys": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz", + "integrity": "sha512-qOLsBKHCpSOFKK1NUOCGC5VyeufB6lEsFe92AL2bhIJsacZS1qdoOZSbPk3MYKuT2cFlRDnulKXuuElIrMjGUg==", + "dev": true + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rst-selector-parser": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", + "integrity": "sha512-nDG1rZeP6oFTLN6yNDV/uiAvs1+FS/KlrEwh7+y7dpuApDBy6bI2HTBcc0/V8lv9OTqfyD34eF7au2pm8aBbhA==", + "dev": true, + "dependencies": { + "lodash.flattendeep": "^4.4.0", + "nearley": "^2.7.10" + } + }, + "node_modules/safe-array-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex-test": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", + "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "get-intrinsic": "^1.2.2", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/sass": { + "version": "1.43.4", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.43.4.tgz", + "integrity": "sha512-/ptG7KE9lxpGSYiXn7Ar+lKOv37xfWsZRtFYal2QHNigyVQDx685VFT/h7ejVr+R8w7H4tmUgtulsKl5YpveOg==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/scheduler": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "node_modules/scheduler/node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sinon": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-12.0.1.tgz", + "integrity": "sha512-iGu29Xhym33ydkAT+aNQFBINakjq69kKO6ByPvTsm3yyIACfyQttRTP03aBP/I8GfhFmLzrnKwNNkr0ORb1udg==", + "deprecated": "16.1.1", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.8.3", + "@sinonjs/fake-timers": "^8.1.0", + "@sinonjs/samsam": "^6.0.2", + "diff": "^5.0.0", + "nise": "^5.1.0", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/socket.io": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.4.tgz", + "integrity": "sha512-DcotgfP1Zg9iP/dH9zvAQcWrE0TtbMVwXmlV4T4mqsvY+gw+LqUGPfx2AoVyRk0FLME+GQhufDMyacFmw7ksqw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", + "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "dev": true, + "dependencies": { + "ws": "~8.11.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", + "dev": true + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "dev": true, + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dev": true, + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.padend": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.5.tgz", + "integrity": "sha512-DOB27b/2UTTD+4myKUFh+/fXWcu/UDyASIXfg+7VzoCNNGOfWvoyU/x5pvVHr++ztyt/oSYI1BcWBBG/hmlNjA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.26.0.tgz", + "integrity": "sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-webpack-plugin/node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/terser/node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.37", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.37.tgz", + "integrity": "sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "engines": { + "node": "*" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "dev": true, + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/util/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.89.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", + "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.9.1.tgz", + "integrity": "sha512-JYRFVuyFpzDxMDB+v/nanUdQYcZtqFPGzmlW4s+UkPMFhSpfRNmf1z4AwYcHJVdvEFAM7FFCQdNTpsBYhDLusQ==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^1.1.0", + "@webpack-cli/info": "^1.4.0", + "@webpack-cli/serve": "^1.6.0", + "colorette": "^2.0.14", + "commander": "^7.0.0", + "execa": "^5.0.0", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^2.2.0", + "rechoir": "^0.7.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "webpack": "4.x.x || 5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "@webpack-cli/migrate": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-cli/node_modules/interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/webpack-cli/node_modules/rechoir": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", + "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", + "dev": true, + "dependencies": { + "resolve": "^1.9.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/webpack-cli/node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-merge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.2.tgz", + "integrity": "sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g==", + "dev": true, + "dependencies": { + "lodash": "^4.17.15" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.4", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/workerpool": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", + "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yamljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", + "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + }, + "bin": { + "json2yaml": "bin/json2yaml", + "yaml2json": "bin/yaml2json" + } + }, + "node_modules/yamljs/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/yamscripts": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yamscripts/-/yamscripts-0.1.0.tgz", + "integrity": "sha512-i4ThS58KwsK83qSrrc8YZiBqgdl3WewWcWZ4fPdrh7A+qiRU9kXMcIKzngOC7VpJ2nTsWvHG6TcK3JHXpBxACA==", + "dev": true, + "dependencies": { + "colors": "^1.3.2", + "fs-extra": "^7.0.0", + "minimist": "^1.2.0", + "yamljs": "^0.3.0" + }, + "bin": { + "yamscripts": "bin/yamscripts.js" + } + }, + "node_modules/yamscripts/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + } + }, + "@babel/compat-data": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "dev": true + }, + "@babel/core": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.5.tgz", + "integrity": "sha512-Cwc2XjUrG4ilcfOw4wBAK+enbdgwAcAJCfGUItPBKR7Mjw4aEfAFYrLxeRp4jWgtNIKn3n2AlBOfwwafl+42/g==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.5", + "@babel/parser": "^7.23.5", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.5", + "@babel/types": "^7.23.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + } + }, + "@babel/generator": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "dev": true, + "requires": { + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "requires": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.15" + } + }, + "@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true + }, + "@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "dev": true + }, + "@babel/helpers": { + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.8.tgz", + "integrity": "sha512-KDqYz4PiOWvDFrdHLPhKtCThtIcKVy6avWD2oG4GEvyQ+XDZwHD4YQd+H2vNMnq2rkdxsDkU82T+Vk8U/WXHRQ==", + "dev": true, + "requires": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6" + } + }, + "@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", + "dev": true + }, + "@babel/plugin-syntax-jsx": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", + "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-react-display-name": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.23.3.tgz", + "integrity": "sha512-GnvhtVfA2OAtzdX58FJxU19rhoGeQzyVndw3GgtdECQvQFXPEZIOVULHVZGAYmOgmqjXpVpfocAbSjh99V/Fqw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-react-jsx": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz", + "integrity": "sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.23.3", + "@babel/types": "^7.23.4" + } + }, + "@babel/plugin-transform-react-jsx-development": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz", + "integrity": "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==", + "dev": true, + "requires": { + "@babel/plugin-transform-react-jsx": "^7.22.5" + } + }, + "@babel/plugin-transform-react-pure-annotations": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.23.3.tgz", + "integrity": "sha512-qMFdSS+TUhB7Q/3HVPnEdYJDQIk57jkntAwSuz9xfSE4n+3I+vHYCli3HoHawN1Z3RfCz/y1zXA/JXjG6cVImQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/preset-react": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.23.3.tgz", + "integrity": "sha512-tbkHOS9axH6Ysf2OUEqoSZ6T3Fa2SrNH6WTWSPBboxKzdxNc9qOICeLXkNG0ZEwbQ1HY8liwOce4aN/Ceyuq6w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", + "@babel/plugin-transform-react-display-name": "^7.23.3", + "@babel/plugin-transform-react-jsx": "^7.22.15", + "@babel/plugin-transform-react-jsx-development": "^7.22.5", + "@babel/plugin-transform-react-pure-annotations": "^7.23.3" + } + }, + "@babel/runtime": { + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz", + "integrity": "sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==", + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, + "@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + } + }, + "@babel/traverse": { + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz", + "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + }, + "@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true + }, + "@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true + }, + "@fluent/bundle": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@fluent/bundle/-/bundle-0.17.1.tgz", + "integrity": "sha512-CRFNT9QcSFAeFDneTF59eyv3JXFGhIIN4boUO2y22YmsuuKLyDk+N1I/NQUYz9Ab63e6V7T6vItoZIG/2oOOuw==" + }, + "@fluent/react": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@fluent/react/-/react-0.15.0.tgz", + "integrity": "sha512-qUMfaHman+UciOELQc5hnFAv0VerUR6+9gEBCRk9RR66XS13syt91ZElNOTHWe2Ofv70cxAGaJ5Yff4MRPg5Ow==", + "requires": { + "@fluent/sequence": "^0.8.0", + "cached-iterable": "^0.3.0" + } + }, + "@fluent/sequence": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@fluent/sequence/-/sequence-0.8.0.tgz", + "integrity": "sha512-eV5QlEEVV/wR3AFQLXO67x4yPRPQXyqke0c8yucyMSeW36B3ecZyVFlY1UprzrfFV8iPJB4TAehDy/dLGbvQ1Q==", + "requires": {} + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.21", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.21.tgz", + "integrity": "sha512-SRfKmRe1KvYnxjEMtxEr+J4HIeMX5YBg/qhRHpxEIGjhX1rshcHlnFUE9K0GazhVKWM7B+nARSkV8LuvJdJ5/g==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@jsdevtools/coverage-istanbul-loader": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@jsdevtools/coverage-istanbul-loader/-/coverage-istanbul-loader-3.0.5.tgz", + "integrity": "sha512-EUCPEkaRPvmHjWAAZkWMT7JDzpw7FKB00WTISaiXsbNOd5hCHg77XLA8sLYLFDo1zepYLo2w7GstN8YBqRXZfA==", + "dev": true, + "requires": { + "convert-source-map": "^1.7.0", + "istanbul-lib-instrument": "^4.0.3", + "loader-utils": "^2.0.0", + "merge-source-map": "^1.1.0", + "schema-utils": "^2.7.0" + }, + "dependencies": { + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + } + } + }, + "@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", + "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@sinonjs/samsam": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.1.3.tgz", + "integrity": "sha512-nhOb2dWPeb1sd3IQXL/dVPnKHDOAFfvichtBf4xV00/rU1QbPCQqKMbvIheIjqwVjh7qIgf2AHTHi391yMOMpQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, + "@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", + "dev": true + }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true + }, + "@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/eslint": { + "version": "8.56.2", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", + "integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "requires": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, + "@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "@types/node": { + "version": "20.11.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz", + "integrity": "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + }, + "@types/prop-types": { + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" + }, + "@types/react": { + "version": "18.2.47", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.47.tgz", + "integrity": "sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ==", + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-redux": { + "version": "7.1.33", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", + "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==", + "requires": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, + "@types/scheduler": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" + }, + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, + "@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "dev": true, + "requires": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "dev": true + }, + "@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "requires": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "@webpack-cli/configtest": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz", + "integrity": "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==", + "dev": true, + "requires": {} + }, + "@webpack-cli/info": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.5.0.tgz", + "integrity": "sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==", + "dev": true, + "requires": { + "envinfo": "^7.7.3" + } + }, + "@webpack-cli/serve": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz", + "integrity": "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==", + "dev": true, + "requires": {} + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "acorn": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz", + "integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==", + "dev": true + }, + "acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "requires": {} + }, + "airbnb-prop-types": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz", + "integrity": "sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg==", + "dev": true, + "requires": { + "array.prototype.find": "^2.1.1", + "function.prototype.name": "^1.1.2", + "is-regex": "^1.1.0", + "object-is": "^1.1.2", + "object.assign": "^4.1.0", + "object.entries": "^1.1.2", + "prop-types": "^15.7.2", + "prop-types-exact": "^1.2.0", + "react-is": "^16.13.1" + } + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "requires": {} + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + } + }, + "array.prototype.filter": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array.prototype.filter/-/array.prototype.filter-1.0.3.tgz", + "integrity": "sha512-VizNcj/RGJiUyQBgzwxzE5oHdeuXY5hSbbmKMlphj1cy1Vl7Pn2asCGbSrru6hSQjmCzqTBPVWAF/whmEOVHbw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.7" + } + }, + "array.prototype.find": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.2.2.tgz", + "integrity": "sha512-DRumkfW97iZGOfn+lIXbkVrXL04sfYKX+EfOodo8XboR5sxPDVvOjZTF/rysusa9lmhmSOeD6Vp6RKQP+eP4Tg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + } + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true + }, + "babel-loader": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.3.tgz", + "integrity": "sha512-n4Zeta8NC3QAsuyiizu0GkmRcQ6clkV9WFUnUf1iXP//IeSKbWjofW3UHyZVwlOB4y039YQKefawyTn64Zwbuw==", + "dev": true, + "requires": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^1.4.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + }, + "dependencies": { + "json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + } + } + }, + "babel-plugin-jsm-to-esmodules": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jsm-to-esmodules/-/babel-plugin-jsm-to-esmodules-0.6.0.tgz", + "integrity": "sha512-463Yuq2sLkjoGHl5vPYUQQONnDjxnmxZuhsR1swL5N76hDFGyYZAVd6HoS4E02jBF8bORpS4aFmdr1XjEZ0buQ==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dev": true, + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "browserslist": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + } + }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true + }, + "cached-iterable": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/cached-iterable/-/cached-iterable-0.3.0.tgz", + "integrity": "sha512-MDqM6TpBVebZD4UDtmlFp8EjVtRcsB6xt9aRdWymjk0fWVUUGgmt/V7o0H0gkI2Tkvv8B0ucjidZm4mLosdlWw==" + }, + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001576", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz", + "integrity": "sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg==", + "dev": true + }, + "chai": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", + "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "requires": { + "get-func-name": "^2.0.2" + } + }, + "cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "dev": true, + "requires": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + } + }, + "cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "requires": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true + }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "dev": true + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "requires": { + "object-assign": "^4", + "vary": "^1" + }, + "dependencies": { + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true + } + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true + }, + "csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true + }, + "date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, + "define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "requires": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true + }, + "di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true + }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true + }, + "discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==", + "dev": true + }, + "dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "requires": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.4.629", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.629.tgz", + "integrity": "sha512-5UUkr3k3CZ/k+9Sw7vaaIMyOzMC0XbPyprKI3n0tbKDqkzTDOjK4izm7DxlkueRMim6ZZQ1ja9F7hoFVplHihA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true + }, + "engine.io": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", + "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", + "dev": true, + "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0" + } + }, + "engine.io-parser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", + "dev": true + }, + "enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "dev": true + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + }, + "envinfo": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.0.tgz", + "integrity": "sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==", + "dev": true + }, + "enzyme": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.11.0.tgz", + "integrity": "sha512-Dw8/Gs4vRjxY6/6i9wU0V+utmQO9kvh9XLnz3LIudviOnVYDEe2ec+0k+NQoMamn1VrjKgCUOWj5jG/5M5M0Qw==", + "dev": true, + "requires": { + "array.prototype.flat": "^1.2.3", + "cheerio": "^1.0.0-rc.3", + "enzyme-shallow-equal": "^1.0.1", + "function.prototype.name": "^1.1.2", + "has": "^1.0.3", + "html-element-map": "^1.2.0", + "is-boolean-object": "^1.0.1", + "is-callable": "^1.1.5", + "is-number-object": "^1.0.4", + "is-regex": "^1.0.5", + "is-string": "^1.0.5", + "is-subset": "^0.1.1", + "lodash.escape": "^4.0.1", + "lodash.isequal": "^4.5.0", + "object-inspect": "^1.7.0", + "object-is": "^1.0.2", + "object.assign": "^4.1.0", + "object.entries": "^1.1.1", + "object.values": "^1.1.1", + "raf": "^3.4.1", + "rst-selector-parser": "^2.2.3", + "string.prototype.trim": "^1.2.1" + } + }, + "enzyme-adapter-react-16": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.6.tgz", + "integrity": "sha512-yFlVJCXh8T+mcQo8M6my9sPgeGzj85HSHi6Apgf1Cvq/7EL/J9+1JoJmJsRxZgyTvPMAqOEpRSu/Ii/ZpyOk0g==", + "dev": true, + "requires": { + "enzyme-adapter-utils": "^1.14.0", + "enzyme-shallow-equal": "^1.0.4", + "has": "^1.0.3", + "object.assign": "^4.1.2", + "object.values": "^1.1.2", + "prop-types": "^15.7.2", + "react-is": "^16.13.1", + "react-test-renderer": "^16.0.0-0", + "semver": "^5.7.0" + }, + "dependencies": { + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true + }, + "react-test-renderer": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.14.0.tgz", + "integrity": "sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "react-is": "^16.8.6", + "scheduler": "^0.19.1" + } + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + } + } + }, + "enzyme-adapter-utils": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.14.1.tgz", + "integrity": "sha512-JZgMPF1QOI7IzBj24EZoDpaeG/p8Os7WeBZWTJydpsH7JRStc7jYbHE4CmNQaLqazaGFyLM8ALWA3IIZvxW3PQ==", + "dev": true, + "requires": { + "airbnb-prop-types": "^2.16.0", + "function.prototype.name": "^1.1.5", + "has": "^1.0.3", + "object.assign": "^4.1.4", + "object.fromentries": "^2.0.5", + "prop-types": "^15.8.1", + "semver": "^5.7.1" + }, + "dependencies": { + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true + }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + } + } + }, + "enzyme-shallow-equal": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.5.tgz", + "integrity": "sha512-i6cwm7hN630JXenxxJFBKzgLC3hMTafFQXflvzHgPmDhOBhxUWDe8AeRv1qp2/uWJ2Y8z5yLWMzmAfkTOiOCZg==", + "dev": true, + "requires": { + "has": "^1.0.3", + "object-is": "^1.1.5" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", + "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.5", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.12", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.13" + } + }, + "es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", + "dev": true + }, + "es-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", + "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", + "dev": true + }, + "es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + } + }, + "es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "requires": { + "hasown": "^2.0.0" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + } + } + }, + "find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, + "flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "dev": true + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "requires": { + "is-callable": "^1.1.3" + } + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true + }, + "function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + } + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true + }, + "get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dev": true, + "requires": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "has": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", + "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", + "dev": true + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.2" + } + }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "requires": { + "function-bind": "^1.1.2" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + } + }, + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "html-element-map": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/html-element-map/-/html-element-map-1.3.1.tgz", + "integrity": "sha512-6XMlxrAFX4UEEGxctfFnmrFaaZFNf9i5fNuV5wZ3WWQ4FVaNP1aX1LkX9j2mfEx1NpjeE/rL3nmgEn23GdFmrg==", + "dev": true, + "requires": { + "array.prototype.filter": "^1.0.0", + "call-bind": "^1.0.2" + } + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "dependencies": { + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true + } + } + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "internal-slot": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + } + }, + "interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true + }, + "is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "requires": { + "hasown": "^2.0.0" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-subset": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", + "integrity": "sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw==", + "dev": true + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, + "requires": { + "which-typed-array": "^1.1.11" + } + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + } + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "dependencies": { + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "joi-browser": { + "version": "13.4.0", + "resolved": "https://registry.npmjs.org/joi-browser/-/joi-browser-13.4.0.tgz", + "integrity": "sha512-TfzJd2JaJ/lg/gU+q5j9rLAjnfUNF9DUmXTP9w+GfmG79LjFOXFeM7hIFuXCBcZCivUDFwd9l1btTV9rhHumtQ==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, + "karma": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.2.tgz", + "integrity": "sha512-C6SU/53LB31BEgRg+omznBEMY4SjHU3ricV6zBcAe1EeILKkeScr+fZXtaI5WyDbkVowJxxAI6h73NcFPmXolQ==", + "dev": true, + "requires": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.4.1", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + } + }, + "karma-chai": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/karma-chai/-/karma-chai-0.1.0.tgz", + "integrity": "sha512-mqKCkHwzPMhgTYca10S90aCEX9+HjVjjrBFAsw36Zj7BlQNbokXXCAe6Ji04VUMsxcY5RLP7YphpfO06XOubdg==", + "dev": true, + "requires": {} + }, + "karma-coverage-istanbul-reporter": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-3.0.3.tgz", + "integrity": "sha512-wE4VFhG/QZv2Y4CdAYWDbMmcAHeS926ZIji4z+FkB2aF/EposRb6DP6G5ncT/wXhqUfAb/d7kZrNKPonbvsATw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^3.0.6", + "istanbul-reports": "^3.0.2", + "minimatch": "^3.0.4" + } + }, + "karma-firefox-launcher": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-2.1.2.tgz", + "integrity": "sha512-VV9xDQU1QIboTrjtGVD4NCfzIH7n01ZXqy/qpBhnOeGVOkG5JYPEm8kuSd7psHE6WouZaQ9Ool92g8LFweSNMA==", + "dev": true, + "requires": { + "is-wsl": "^2.2.0", + "which": "^2.0.1" + } + }, + "karma-json-reporter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/karma-json-reporter/-/karma-json-reporter-1.2.1.tgz", + "integrity": "sha512-ASmvranNhUN0ctSuAZKeWISW9Nf4AteMcVy8rJVjS7Qk+qWgssag/nw+yivHWKDROztVFn7TdamHOETMPCkvgA==", + "dev": true, + "requires": {} + }, + "karma-mocha": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/karma-mocha/-/karma-mocha-2.0.1.tgz", + "integrity": "sha512-Tzd5HBjm8his2OA4bouAsATYEpZrp9vC7z5E5j4C5Of5Rrs1jY67RAwXNcVmd/Bnk1wgvQRou0zGVLey44G4tQ==", + "dev": true, + "requires": { + "minimist": "^1.2.3" + } + }, + "karma-mocha-reporter": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/karma-mocha-reporter/-/karma-mocha-reporter-2.2.5.tgz", + "integrity": "sha512-Hr6nhkIp0GIJJrvzY8JFeHpQZNseuIakGac4bpw8K1+5F0tLb6l7uvXRa8mt2Z+NVwYgCct4QAfp2R2QP6o00w==", + "dev": true, + "requires": { + "chalk": "^2.1.0", + "log-symbols": "^2.1.0", + "strip-ansi": "^4.0.0" + } + }, + "karma-sinon": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/karma-sinon/-/karma-sinon-1.0.5.tgz", + "integrity": "sha512-wrkyAxJmJbn75Dqy17L/8aILJWFm7znd1CE8gkyxTBFnjMSOe2XTJ3P30T8SkxWZHmoHX0SCaUJTDBEoXs25Og==", + "dev": true, + "requires": {} + }, + "karma-sourcemap-loader": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.8.tgz", + "integrity": "sha512-zorxyAakYZuBcHRJE+vbrK2o2JXLFWK8VVjiT/6P+ltLBUGUvqTEkUiQ119MGdOrK7mrmxXHZF1/pfT6GgIZ6g==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2" + } + }, + "karma-webpack": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-5.0.0.tgz", + "integrity": "sha512-+54i/cd3/piZuP3dr54+NcFeKOPnys5QeM1IY+0SPASwrtHsliXUiCL50iW+K9WWA7RvamC4macvvQ86l3KtaA==", + "dev": true, + "requires": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "webpack-merge": "^4.1.5" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true + }, + "loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "dev": true + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash.escape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", + "integrity": "sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw==", + "dev": true + }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "requires": { + "chalk": "^2.0.1" + } + }, + "log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dev": true, + "requires": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + } + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true + }, + "memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true + }, + "merge-source-map": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", + "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "requires": { + "minimist": "^1.2.6" + } + }, + "mocha": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", + "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", + "dev": true, + "requires": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.3", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "4.2.1", + "ms": "2.1.3", + "nanoid": "3.3.1", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "workerpool": "6.2.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "minimatch": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", + "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "mock-raf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mock-raf/-/mock-raf-1.0.1.tgz", + "integrity": "sha512-+25y56bblLzEnv+G4ODsHNck07A5uP5HFfu/1VBKeFrUXoFT9oru+R+jLxLz6rwdM5drUHFdqX9LYBsMP4dz/w==", + "dev": true, + "requires": { + "object-assign": "^3.0.0" + } + }, + "moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "nanoid": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", + "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", + "dev": true + }, + "nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "dev": true, + "requires": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6" + } + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "nise": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.7.tgz", + "integrity": "sha512-wWtNUhkT7k58uvWTB/Gy26eA/EJKtPZFVAhEilN5UYVmmGRYOURbejRUyKm0Uu9XVEW7K5nBOZfR8VMB4QR2RQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + } + } + }, + "node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + } + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + } + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + }, + "dependencies": { + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + } + } + }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "requires": { + "boolbase": "^1.0.0" + } + }, + "object-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", + "integrity": "sha512-jHP15vXVGeVh1HuaA2wY6lxk+whK/x4KBG88VXeRma7CCun7iGD5qPc4eYykQ9sdQvg8jkwFKsSxHln2ybW3xQ==", + "dev": true + }, + "object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true + }, + "object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "object.entries": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", + "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "object.fromentries": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "object.values": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "requires": { + "entities": "^4.4.0" + } + }, + "parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "dev": true, + "requires": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-to-regexp": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", + "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==", + "dev": true + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + } + } + }, + "postcss": { + "version": "8.4.33", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", + "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", + "dev": true, + "peer": true, + "requires": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "dependencies": { + "nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "peer": true + } + } + }, + "postcss-scss": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.6.tgz", + "integrity": "sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ==", + "dev": true, + "requires": {} + }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + }, + "dependencies": { + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + } + } + }, + "prop-types-exact": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/prop-types-exact/-/prop-types-exact-1.2.0.tgz", + "integrity": "sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA==", + "dev": true, + "requires": { + "has": "^1.0.3", + "object.assign": "^4.1.0", + "reflect.ownkeys": "^0.2.0" + } + }, + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true + }, + "qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + }, + "raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "dev": true, + "requires": { + "performance-now": "^2.1.0" + } + }, + "railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==", + "dev": true + }, + "randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "dev": true, + "requires": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + } + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "raw-loader": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz", + "integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "dependencies": { + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "react": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz", + "integrity": "sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + }, + "dependencies": { + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + } + } + }, + "react-dom": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz", + "integrity": "sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.19.1" + }, + "dependencies": { + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + } + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "react-redux": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz", + "integrity": "sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ==", + "requires": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "dependencies": { + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + } + } + }, + "react-transition-group": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", + "integrity": "sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "requires": { + "resolve": "^1.1.6" + } + }, + "redux": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz", + "integrity": "sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, + "reflect.ownkeys": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz", + "integrity": "sha512-qOLsBKHCpSOFKK1NUOCGC5VyeufB6lEsFe92AL2bhIJsacZS1qdoOZSbPk3MYKuT2cFlRDnulKXuuElIrMjGUg==", + "dev": true + }, + "regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "regexp.prototype.flags": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "rst-selector-parser": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", + "integrity": "sha512-nDG1rZeP6oFTLN6yNDV/uiAvs1+FS/KlrEwh7+y7dpuApDBy6bI2HTBcc0/V8lv9OTqfyD34eF7au2pm8aBbhA==", + "dev": true, + "requires": { + "lodash.flattendeep": "^4.4.0", + "nearley": "^2.7.10" + } + }, + "safe-array-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "safe-regex-test": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", + "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.5", + "get-intrinsic": "^1.2.2", + "is-regex": "^1.1.4" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "sass": { + "version": "1.43.4", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.43.4.tgz", + "integrity": "sha512-/ptG7KE9lxpGSYiXn7Ar+lKOv37xfWsZRtFYal2QHNigyVQDx685VFT/h7ejVr+R8w7H4tmUgtulsKl5YpveOg==", + "dev": true, + "requires": { + "chokidar": ">=3.0.0 <4.0.0" + } + }, + "scheduler": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + }, + "dependencies": { + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + } + } + }, + "schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dev": true, + "requires": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, + "set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, + "requires": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true + }, + "shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true + }, + "shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "sinon": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-12.0.1.tgz", + "integrity": "sha512-iGu29Xhym33ydkAT+aNQFBINakjq69kKO6ByPvTsm3yyIACfyQttRTP03aBP/I8GfhFmLzrnKwNNkr0ORb1udg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.8.3", + "@sinonjs/fake-timers": "^8.1.0", + "@sinonjs/samsam": "^6.0.2", + "diff": "^5.0.0", + "nise": "^5.1.0", + "supports-color": "^7.2.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "socket.io": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.4.tgz", + "integrity": "sha512-DcotgfP1Zg9iP/dH9zvAQcWrE0TtbMVwXmlV4T4mqsvY+gw+LqUGPfx2AoVyRk0FLME+GQhufDMyacFmw7ksqw==", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + } + }, + "socket.io-adapter": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", + "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "dev": true, + "requires": { + "ws": "~8.11.0" + } + }, + "socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "peer": true + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true + }, + "stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "dev": true, + "requires": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dev": true, + "requires": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "string.prototype.padend": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.5.tgz", + "integrity": "sha512-DOB27b/2UTTD+4myKUFh+/fXWcu/UDyASIXfg+7VzoCNNGOfWvoyU/x5pvVHr++ztyt/oSYI1BcWBBG/hmlNjA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "string.prototype.trim": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "string.prototype.trimstart": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true + }, + "terser": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.26.0.tgz", + "integrity": "sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==", + "dev": true, + "requires": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "dependencies": { + "acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true + } + } + }, + "terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "dependencies": { + "schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + } + } + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "requires": { + "rimraf": "^3.0.0" + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + } + }, + "ua-parser-js": { + "version": "0.7.37", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.37.tgz", + "integrity": "sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA==", + "dev": true + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true + }, + "update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "dev": true, + "requires": { + "inherits": "2.0.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true + }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true + }, + "watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + } + }, + "webpack": { + "version": "5.89.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", + "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", + "dev": true, + "requires": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "dependencies": { + "acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true + }, + "schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "webpack-cli": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.9.1.tgz", + "integrity": "sha512-JYRFVuyFpzDxMDB+v/nanUdQYcZtqFPGzmlW4s+UkPMFhSpfRNmf1z4AwYcHJVdvEFAM7FFCQdNTpsBYhDLusQ==", + "dev": true, + "requires": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^1.1.0", + "@webpack-cli/info": "^1.4.0", + "@webpack-cli/serve": "^1.6.0", + "colorette": "^2.0.14", + "commander": "^7.0.0", + "execa": "^5.0.0", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^2.2.0", + "rechoir": "^0.7.0", + "webpack-merge": "^5.7.3" + }, + "dependencies": { + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true + }, + "interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "dev": true + }, + "rechoir": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", + "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", + "dev": true, + "requires": { + "resolve": "^1.9.0" + } + }, + "webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + } + } + } + }, + "webpack-merge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.2.tgz", + "integrity": "sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g==", + "dev": true, + "requires": { + "lodash": "^4.17.15" + } + }, + "webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.4", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + } + }, + "wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "workerpool": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", + "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "dev": true, + "requires": {} + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "yamljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", + "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + } + } + }, + "yamscripts": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yamscripts/-/yamscripts-0.1.0.tgz", + "integrity": "sha512-i4ThS58KwsK83qSrrc8YZiBqgdl3WewWcWZ4fPdrh7A+qiRU9kXMcIKzngOC7VpJ2nTsWvHG6TcK3JHXpBxACA==", + "dev": true, + "requires": { + "colors": "^1.3.2", + "fs-extra": "^7.0.0", + "minimist": "^1.2.0", + "yamljs": "^0.3.0" + }, + "dependencies": { + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + } + } + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + } + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/browser/components/newtab/package.json b/browser/components/newtab/package.json new file mode 100644 index 0000000000..b878deb975 --- /dev/null +++ b/browser/components/newtab/package.json @@ -0,0 +1,115 @@ +{ + "name": "activity-streams", + "description": "A rich visual history feed and a reimagined home page make it easier than ever to find exactly what you're looking for in Firefox.\n\nLearn more about this Test Pilot experiment at https://testpilot.firefox.com/.", + "version": "1.14.3", + "author": "Mozilla (https://mozilla.org/)", + "bugs": { + "url": "https://github.com/mozilla/activity-stream/issues" + }, + "dependencies": { + "@fluent/bundle": "0.17.1", + "@fluent/react": "0.15.0", + "react": "16.13.1", + "react-dom": "16.13.1", + "react-redux": "7.2.6", + "react-transition-group": "4.4.2", + "redux": "4.1.2" + }, + "devDependencies": { + "@babel/core": "7.23.5", + "@babel/preset-react": "7.23.3", + "@jsdevtools/coverage-istanbul-loader": "^3.0.5", + "acorn": "8.5.0", + "babel-loader": "8.2.3", + "babel-plugin-jsm-to-esmodules": "0.6.0", + "buffer": "6.0.3", + "chai": "4.3.4", + "enzyme": "3.11.0", + "enzyme-adapter-react-16": "1.15.6", + "joi-browser": "13.4.0", + "karma": "6.4.2", + "karma-chai": "0.1.0", + "karma-coverage-istanbul-reporter": "3.0.3", + "karma-firefox-launcher": "2.1.2", + "karma-json-reporter": "1.2.1", + "karma-mocha": "2.0.1", + "karma-mocha-reporter": "2.2.5", + "karma-sinon": "1.0.5", + "karma-sourcemap-loader": "0.3.8", + "karma-webpack": "5.0.0", + "loader-utils": "3.2.1", + "lodash": "4.17.21", + "mocha": "9.2.2", + "mock-raf": "1.0.1", + "npm-run-all": "4.1.5", + "postcss-scss": "4.0.6", + "prop-types": "15.7.2", + "raw-loader": "4.0.2", + "rimraf": "3.0.2", + "sass": "1.43.4", + "shelljs": "0.8.5", + "sinon": "12.0.1", + "stream-browserify": "3.0.0", + "util": "0.10.4", + "webpack": "5.89.0", + "webpack-cli": "4.9.1", + "yamscripts": "0.1.0" + }, + "engines": { + "firefox": ">=45.0 <=*", + "//": "when changing node versions, also edit .nvmrc", + "node": "16.19.*", + "npm": "8.19.3" + }, + "homepage": "https://github.com/mozilla/activity-stream", + "keywords": [ + "mozilla", + "firefox", + "activity-stream" + ], + "license": "MPL-2.0", + "main": "bootstrap.js", + "repository": "mozilla/activity-stream", + "config": { + "mc_root": "../../..", + "newtab_path": "browser/components/newtab" + }, + "scripts": { + "bundle": "npm-run-all bundle:*", + "bundle:webpack": "webpack-cli --config webpack.system-addon.config.js", + "bundle:css": "sass content-src/styles:css --no-source-map", + "bundle:html": "rimraf prerendered && node ./bin/render-activity-stream-html.js", + "buildmc": "npm-run-all buildmc:*", + "buildmc:bundle": "npm run bundle", + "watchmc": "npm-run-all --parallel watchmc:*", + "watchmc:webpack": "npm run bundle:webpack -- --env development -w", + "watchmc:css": "npm run bundle:css -- --source-map --embed-sources --embed-source-map --load-path=content-src -w", + "testmc": "npm-run-all testmc:*", + "testmc:lint": "npm run lint", + "testmc:build": "npm run bundle:webpack", + "testmc:unit": "karma start karma.mc.config.js", + "tddmc": "karma start karma.mc.config.js --tdd", + "debugcoverage": "open logs/coverage/lcov-report/index.html", + "lint": "npm-run-all lint:*", + "lint:codespell": "(cd $npm_package_config_mc_root && ./mach lint -l codespell $npm_package_config_newtab_path)", + "lint:eslint": "(cd $npm_package_config_mc_root && ./mach lint -l eslint $npm_package_config_newtab_path)", + "lint:l10n": "(cd $npm_package_config_mc_root && ./mach lint -l l10n --warnings soft browser/locales/en-US/browser/newtab)", + "lint:license": "(cd $npm_package_config_mc_root && ./mach lint -l license $npm_package_config_newtab_path)", + "lint:stylelint": "(cd $npm_package_config_mc_root && ./mach lint -l stylelint $npm_package_config_newtab_path)", + "test": "npm run testmc", + "tdd": "npm run tddmc", + "vendor": "node ./bin/vendor.js", + "try": "node ./bin/try-runner.js", + "fix": "npm-run-all fix:*", + "fix:eslint": "npm run lint:eslint -- --fix", + "fix:stylelint": "npm run lint:stylelint -- --fix", + "help": "yamscripts help", + "yamscripts": "yamscripts compile", + "__": "# NOTE: THESE SCRIPTS ARE COMPILED!!! EDIT yamscripts.yml instead!!!" + }, + "title": "Activity Stream", + "permissions": { + "multiprocess": true, + "private-browsing": true + } +} diff --git a/browser/components/newtab/pings.yaml b/browser/components/newtab/pings.yaml new file mode 100644 index 0000000000..f23bc84833 --- /dev/null +++ b/browser/components/newtab/pings.yaml @@ -0,0 +1,75 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +--- +$schema: moz://mozilla.org/schemas/glean/pings/2-0-0 + +newtab: + description: | + Newtab-related instrumentation. + Can be disabled via the `newtabPingEnabled` variable of the `glean` Nimbus + feature, or the `browser.newtabpage.ping.enabled` pref. + reasons: + newtab_session_end: | + The newtab visit ended. + Could be by navigation, being closed, etc. + component_init: | + The newtab component init'd, + and the newtab and homepage settings have been categorized. + This is mostly to ensure we hear at least once from clients configured to + not show a newtab UI. + include_client_id: true + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1766887 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1766887 + notification_emails: + - chutten@mozilla.com + - mmccorquodale@mozilla.com + - anicholson@mozilla.com + - najiang@mozilla.com + +messaging-system: + description: | + This is a ping representing single events triggered by the messaging system + and captures some pings from About:Welcome, ASRouter, and other corners. + include_client_id: false + send_if_empty: false + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 + notification_emails: + - pmcmanis@mozilla.com + - dmosedale@mozilla.com + +top-sites: + description: | + A ping representing a single event happening with or to a TopSite. + Distinguishable by its `ping_type`. + Does not contain a `client_id`, preferring a `context_id` instead. + include_client_id: false + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836283 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836283 + notification_emails: + - najiang@mozilla.com + +spoc: + description: | + A ping for submitting the pocket sponsored content's `shim`. + Does not contain a `client_id`. + include_client_id: false + reasons: + impression: A sponsored story was impressed upon the client. + click: A sponsored story was clicked. + save: A sponsored story was saved to Pocket. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1862670 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1862670 + notification_emails: + - najiang@mozilla.com + - chutten@mozilla.com diff --git a/browser/components/newtab/prerendered/activity-stream-debug.html b/browser/components/newtab/prerendered/activity-stream-debug.html new file mode 100644 index 0000000000..b87303eb58 --- /dev/null +++ b/browser/components/newtab/prerendered/activity-stream-debug.html @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + diff --git a/browser/components/newtab/prerendered/activity-stream-noscripts.html b/browser/components/newtab/prerendered/activity-stream-noscripts.html new file mode 100644 index 0000000000..8186565ef3 --- /dev/null +++ b/browser/components/newtab/prerendered/activity-stream-noscripts.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + +
    + + + diff --git a/browser/components/newtab/prerendered/activity-stream.html b/browser/components/newtab/prerendered/activity-stream.html new file mode 100644 index 0000000000..86e7f292d8 --- /dev/null +++ b/browser/components/newtab/prerendered/activity-stream.html @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + diff --git a/browser/components/newtab/test/browser/abouthomecache/browser.toml b/browser/components/newtab/test/browser/abouthomecache/browser.toml new file mode 100644 index 0000000000..1994415d9a --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser.toml @@ -0,0 +1,52 @@ +[DEFAULT] +support-files = [ + "head.js", + "../topstories.json", +] +prefs = [ + "browser.tabs.remote.separatePrivilegedContentProcess=true", + "browser.startup.homepage.abouthome_cache.enabled=true", + "browser.startup.homepage.abouthome_cache.cache_on_shutdown=false", + "browser.startup.homepage.abouthome_cache.loglevel=All", + "browser.startup.homepage.abouthome_cache.testing=true", + "browser.startup.page=1", + "browser.newtabpage.activity-stream.discoverystream.endpoints=data:", + "browser.newtabpage.activity-stream.feeds.system.topstories=true", + "browser.newtabpage.activity-stream.feeds.section.topstories=true", + "browser.newtabpage.activity-stream.feeds.system.topstories=true", + "browser.newtabpage.activity-stream.feeds.section.topstories.options={\"provider_name\":\"\"}", + "browser.newtabpage.activity-stream.telemetry.structuredIngestion=false", + "browser.newtabpage.activity-stream.discoverystream.endpoints=https://example.com", + "dom.ipc.processPrelaunch.delayMs=0", + # Bug 1694957 is why we need dom.ipc.processPrelaunch.delayMs=0 +] + +["browser_basic_endtoend.js"] + +["browser_bump_version.js"] + +["browser_disabled.js"] + +["browser_experiments_api_control.js"] + +["browser_locale_change.js"] + +["browser_no_cache.js"] + +["browser_no_cache_on_SessionStartup_restore.js"] + +["browser_no_startup_actions.js"] + +["browser_overwrite_cache.js"] + +["browser_process_crash.js"] +skip-if = [ + "!crashreporter", + "os == 'mac' && fission", # Bug 1659427; medium frequency intermittent on osx: test timed out +] + +["browser_same_consumer.js"] + +["browser_sanitize.js"] + +["browser_shutdown_timeout.js"] diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_basic_endtoend.js b/browser/components/newtab/test/browser/abouthomecache/browser_basic_endtoend.js new file mode 100644 index 0000000000..bd42dd4af9 --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_basic_endtoend.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the about:home cache gets written on shutdown, and read + * from in the subsequent startup. + */ +add_task(async function test_basic_behaviour() { + await withFullyLoadedAboutHome(async browser => { + // First, clear the cache to test the base case. + await clearCache(); + await simulateRestart(browser); + await ensureCachedAboutHome(browser); + + // Next, test that a subsequent restart also shows the cached + // about:home. + await simulateRestart(browser); + await ensureCachedAboutHome(browser); + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_bump_version.js b/browser/components/newtab/test/browser/abouthomecache/browser_bump_version.js new file mode 100644 index 0000000000..726b9aa973 --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_bump_version.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that if the "version" metadata on the cache entry doesn't match + * the expectation that we ignore the cache and load the dynamic about:home + * document. + */ +add_task(async function test_bump_version() { + await withFullyLoadedAboutHome(async browser => { + // First, ensure that a pre-existing cache exists. + await simulateRestart(browser); + + let cacheEntry = await AboutHomeStartupCache.ensureCacheEntry(); + Assert.equal( + cacheEntry.getMetaDataElement("version"), + Services.appinfo.appBuildID, + "Cache entry should be versioned on the build ID" + ); + cacheEntry.setMetaDataElement("version", "somethingnew"); + // We don't need to shutdown write or ensure the cache wins the race, + // since we expect the cache to be blown away because the version number + // has been bumped. + await simulateRestart(browser, { + withAutoShutdownWrite: false, + ensureCacheWinsRace: false, + }); + await ensureDynamicAboutHome( + browser, + AboutHomeStartupCache.CACHE_RESULT_SCALARS.INVALIDATED + ); + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_disabled.js b/browser/components/newtab/test/browser/abouthomecache/browser_disabled.js new file mode 100644 index 0000000000..faa79b219c --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_disabled.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This file tests scenarios where the cache is disabled due to user + * configuration. + */ + +registerCleanupFunction(async () => { + // When the test completes, make sure we cleanup with a populated cache, + // since this is the default starting state for these tests. + await withFullyLoadedAboutHome(async browser => { + await simulateRestart(browser); + }); +}); + +/** + * Tests the case where the cache is disabled via the pref. + */ +add_task(async function test_cache_disabled() { + await withFullyLoadedAboutHome(async browser => { + await SpecialPowers.pushPrefEnv({ + set: [["browser.startup.homepage.abouthome_cache.enabled", false]], + }); + + await simulateRestart(browser); + + await ensureDynamicAboutHome( + browser, + AboutHomeStartupCache.CACHE_RESULT_SCALARS.DISABLED + ); + + await SpecialPowers.popPrefEnv(); + }); +}); + +/** + * Tests the case where the cache is disabled because the home page is + * not set at about:home. + */ +add_task(async function test_cache_custom_homepage() { + await withFullyLoadedAboutHome(async browser => { + await HomePage.set("https://example.com"); + await simulateRestart(browser); + + await ensureDynamicAboutHome( + browser, + AboutHomeStartupCache.CACHE_RESULT_SCALARS.NOT_LOADING_ABOUTHOME + ); + + HomePage.reset(); + }); +}); + +/** + * Tests the case where the cache is disabled because the session is + * configured to automatically be restored. + */ +add_task(async function test_cache_restore_session() { + await withFullyLoadedAboutHome(async browser => { + await SpecialPowers.pushPrefEnv({ + set: [["browser.startup.page", 3]], + }); + + await simulateRestart(browser); + + await ensureDynamicAboutHome( + browser, + AboutHomeStartupCache.CACHE_RESULT_SCALARS.NOT_LOADING_ABOUTHOME + ); + + await SpecialPowers.popPrefEnv(); + }); +}); + +/** + * Tests the case where the cache is disabled because about:newtab + * preloading is disabled. + */ +add_task(async function test_cache_no_preloading() { + await withFullyLoadedAboutHome(async browser => { + await SpecialPowers.pushPrefEnv({ + set: [["browser.newtab.preload", false]], + }); + + await simulateRestart(browser); + + await ensureDynamicAboutHome( + browser, + AboutHomeStartupCache.CACHE_RESULT_SCALARS.PRELOADING_DISABLED + ); + + await SpecialPowers.popPrefEnv(); + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_experiments_api_control.js b/browser/components/newtab/test/browser/abouthomecache/browser_experiments_api_control.js new file mode 100644 index 0000000000..a94f1fe055 --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_experiments_api_control.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ExperimentFakes } = ChromeUtils.importESModule( + "resource://testing-common/NimbusTestUtils.sys.mjs" +); + +registerCleanupFunction(async () => { + // When the test completes, make sure we cleanup with a populated cache, + // since this is the default starting state for these tests. + await withFullyLoadedAboutHome(async browser => { + await simulateRestart(browser); + }); +}); + +/** + * Tests that the ExperimentsAPI mechanism can be used to remotely + * enable and disable the about:home startup cache. + */ +add_task(async function test_experiments_api_control() { + // First, the disabled case. + await withFullyLoadedAboutHome(async browser => { + let doEnrollmentCleanup = await ExperimentFakes.enrollWithFeatureConfig({ + featureId: "abouthomecache", + value: { enabled: false }, + }); + + Assert.ok( + !NimbusFeatures.abouthomecache.getVariable("enabled"), + "NimbusFeatures should tell us that the about:home startup cache " + + "is disabled" + ); + + await simulateRestart(browser); + + await ensureDynamicAboutHome( + browser, + AboutHomeStartupCache.CACHE_RESULT_SCALARS.DISABLED + ); + + await doEnrollmentCleanup(); + }); + + // Now the enabled case. + await withFullyLoadedAboutHome(async browser => { + let doEnrollmentCleanup = await ExperimentFakes.enrollWithFeatureConfig({ + featureId: "abouthomecache", + value: { enabled: true }, + }); + + Assert.ok( + NimbusFeatures.abouthomecache.getVariable("enabled"), + "NimbusFeatures should tell us that the about:home startup cache " + + "is enabled" + ); + + await simulateRestart(browser); + await ensureCachedAboutHome(browser); + await doEnrollmentCleanup(); + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_locale_change.js b/browser/components/newtab/test/browser/abouthomecache/browser_locale_change.js new file mode 100644 index 0000000000..e9e3c619ec --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_locale_change.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the about:home startup cache is cleared if the app + * locale changes. + */ +add_task(async function test_locale_change() { + await withFullyLoadedAboutHome(async browser => { + await simulateRestart(browser); + await ensureCachedAboutHome(browser); + + Services.obs.notifyObservers(null, "intl:app-locales-changed"); + await AboutHomeStartupCache.ensureCacheEntry(); + + // We're testing that switching locales blows away the cache, so we + // bypass the automatic writing of the cache on shutdown, and we + // also don't need to wait for the cache to be available. + await simulateRestart(browser, { + withAutoShutdownWrite: false, + ensureCacheWinsRace: false, + }); + await ensureDynamicAboutHome( + browser, + AboutHomeStartupCache.CACHE_RESULT_SCALARS.DOES_NOT_EXIST + ); + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_no_cache.js b/browser/components/newtab/test/browser/abouthomecache/browser_no_cache.js new file mode 100644 index 0000000000..fdb51f8712 --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_no_cache.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +requestLongerTimeout(2); + +/** + * Test that if there's no cache written, that we load the dynamic + * about:home document on startup. + */ +add_task(async function test_no_cache() { + await withFullyLoadedAboutHome(async browser => { + await clearCache(); + // We're testing the no-cache case, so we bypass the automatic writing + // of the cache on shutdown, and we also don't need to wait for the + // cache to be available. + await simulateRestart(browser, { + withAutoShutdownWrite: false, + ensureCacheWinsRace: false, + }); + await ensureDynamicAboutHome( + browser, + AboutHomeStartupCache.CACHE_RESULT_SCALARS.DOES_NOT_EXIST + ); + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_no_cache_on_SessionStartup_restore.js b/browser/components/newtab/test/browser/abouthomecache/browser_no_cache_on_SessionStartup_restore.js new file mode 100644 index 0000000000..a312b2b44f --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_no_cache_on_SessionStartup_restore.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that if somehow about:newtab loads before about:home does, that we + * don't use the cache. This is because about:newtab doesn't use the cache, + * and so it'll inevitably be newer than what's in the about:home cache, + * which will put the about:home cache out of date the next time about:home + * eventually loads. + */ +add_task(async function test_no_cache_on_SessionStartup_restore() { + await withFullyLoadedAboutHome(async browser => { + await simulateRestart(browser, { skipAboutHomeLoad: true }); + + // We remove the preloaded browser to ensure that loading the next + // about:newtab occurs now, and not at preloading time. + NewTabPagePreloading.removePreloadedBrowser(window); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:newtab" + ); + + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + + // The cache is disqualified because about:newtab was loaded first. + // So now it's too late to use the cache. + await ensureDynamicAboutHome( + newWin.gBrowser.selectedBrowser, + AboutHomeStartupCache.CACHE_RESULT_SCALARS.LATE + ); + + await BrowserTestUtils.closeWindow(newWin); + await BrowserTestUtils.removeTab(tab); + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_no_startup_actions.js b/browser/components/newtab/test/browser/abouthomecache/browser_no_startup_actions.js new file mode 100644 index 0000000000..255b4c9d21 --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_no_startup_actions.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that upon initializing Activity Stream, the cached about:home + * document does not process any actions caused by that initialization. + * This is because the restored Redux state from the cache should be enough, + * and processing any of the initialization messages from Activity Stream + * could wipe out that state and cause flicker / unnecessary redraws. + */ +add_task(async function test_no_startup_actions() { + await withFullyLoadedAboutHome(async browser => { + // Make sure we have a cached document. We simulate a restart to ensure + // that we start with a cache... that we can then clear without a problem, + // before writing a new cache. This ensures that no matter what, we're in a + // state where we have a fresh cache, regardless of what's happened in earlier + // tests. + await simulateRestart(browser); + await clearCache(); + await simulateRestart(browser); + await ensureCachedAboutHome(browser); + + // Set up a listener to monitor for actions that get dispatched in the + // browser when we fire Activity Stream up again. + await SpecialPowers.spawn(browser, [], async () => { + let xrayWindow = ChromeUtils.waiveXrays(content); + xrayWindow.nonStartupActions = []; + xrayWindow.startupActions = []; + xrayWindow.RPMAddMessageListener("ActivityStream:MainToContent", msg => { + if (msg.data.meta.isStartup) { + xrayWindow.startupActions.push(msg.data); + } else { + xrayWindow.nonStartupActions.push(msg.data); + } + }); + }); + + // The following two statements seem to be enough to simulate Activity + // Stream starting up. + AboutNewTab.activityStream.uninit(); + AboutNewTab.onBrowserReady(); + + // Much of Activity Stream initializes asynchronously. This is the easiest way + // I could find to ensure that enough of the feeds had initialized to produce + // a meaningful cached document. + await TestUtils.waitForCondition(() => { + let feed = AboutNewTab.activityStream.store.feeds.get( + "feeds.discoverystreamfeed" + ); + return feed?.loaded; + }); + + // Wait an additional few seconds for any other actions to get displayed. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 2000)); + + let [startupActions, nonStartupActions] = await SpecialPowers.spawn( + browser, + [], + async () => { + let xrayWindow = ChromeUtils.waiveXrays(content); + return [xrayWindow.startupActions, xrayWindow.nonStartupActions]; + } + ); + + Assert.ok(!!startupActions.length, "Should have seen startup actions."); + info(`Saw ${startupActions.length} startup actions.`); + + Assert.equal( + nonStartupActions.length, + 0, + "Should be no non-startup actions." + ); + + if (nonStartupActions.length) { + for (let action of nonStartupActions) { + info(`Non-startup action: ${action.type}`); + } + } + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_overwrite_cache.js b/browser/components/newtab/test/browser/abouthomecache/browser_overwrite_cache.js new file mode 100644 index 0000000000..22df98794f --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_overwrite_cache.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that if a pre-existing about:home cache exists, that it can + * be overwritten with new information. + */ +add_task(async function test_overwrite_cache() { + await withFullyLoadedAboutHome(async browser => { + await simulateRestart(browser); + const TEST_ID = "test_overwrite_cache_h1"; + + // We need the CSP meta tag in about: pages, otherwise we hit assertions in + // debug builds. + await injectIntoCache( + ` + + + + + +

    Something new

    +
    + + + `, + "window.__FROM_STARTUP_CACHE__ = true;" + ); + await simulateRestart(browser, { withAutoShutdownWrite: false }); + + await SpecialPowers.spawn(browser, [TEST_ID], async testID => { + let target = content.document.getElementById(testID); + Assert.ok(target, "Found the target element"); + }); + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_process_crash.js b/browser/components/newtab/test/browser/abouthomecache/browser_process_crash.js new file mode 100644 index 0000000000..d3bfa383c2 --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_process_crash.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that if the "privileged about content process" crashes, that it + * drops its internal reference to the "privileged about content process" + * process manager, and that a subsequent restart of that process type + * results in a dynamic document load. Also tests that crashing of + * any other content process type doesn't clear the process manager + * reference. + */ +add_task(async function test_process_crash() { + await withFullyLoadedAboutHome(async browser => { + await simulateRestart(browser); + let origProcManager = AboutHomeStartupCache._procManager; + + await BrowserTestUtils.crashFrame(browser); + Assert.notEqual( + origProcManager, + AboutHomeStartupCache._procManager, + "Should have dropped the reference to the crashed process" + ); + }); + + await withFullyLoadedAboutHome(async browser => { + // The cache should still be considered "valid and used", since it was + // used successfully before the crash. + await ensureDynamicAboutHome( + browser, + AboutHomeStartupCache.CACHE_RESULT_SCALARS.VALID_AND_USED + ); + + // Now simulate a restart to attach the AboutHomeStartupCache to + // the new privileged about content process. + await simulateRestart(browser); + }); + + let latestProcManager = AboutHomeStartupCache._procManager; + + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + await BrowserTestUtils.withNewTab("http://example.com", async browser => { + await BrowserTestUtils.crashFrame(browser); + Assert.equal( + latestProcManager, + AboutHomeStartupCache._procManager, + "Should still have the reference to the privileged about process" + ); + }); +}); + +/** + * Tests that if the "privileged about content process" crashes while + * a cache request is still underway, that the cache request resolves with + * null input streams. + */ +add_task(async function test_process_crash_while_requesting_streams() { + await withFullyLoadedAboutHome(async browser => { + await simulateRestart(browser); + let cacheStreamsPromise = AboutHomeStartupCache.requestCache(); + await BrowserTestUtils.crashFrame(browser); + let cacheStreams = await cacheStreamsPromise; + + if (!cacheStreams.pageInputStream && !cacheStreams.scriptInputStream) { + Assert.ok(true, "Page and script input streams are null."); + } else { + // It's possible (but probably rare) the parent was able to receive the + // streams before the crash occurred. In that case, we'll make sure that + // we can still read the streams. + info("Received the streams. Checking that they're readable."); + Assert.ok( + cacheStreams.pageInputStream.available(), + "Bytes available for page stream" + ); + Assert.ok( + cacheStreams.scriptInputStream.available(), + "Bytes available for script stream" + ); + } + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_same_consumer.js b/browser/components/newtab/test/browser/abouthomecache/browser_same_consumer.js new file mode 100644 index 0000000000..75f8875f26 --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_same_consumer.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that if a page attempts to load the script stream without + * having also loaded the page stream, that it will fail and get + * the default non-cached script. + */ +add_task(async function test_same_consumer() { + await withFullyLoadedAboutHome(async browser => { + await simulateRestart(browser); + + // We need the CSP meta tag in about: pages, otherwise we hit assertions in + // debug builds. + // + // We inject a script that sets a __CACHE_CONSUMED__ property to true on + // the window element. We'll test to ensure that if we try to load the + // script cache from a different BrowsingContext that this property is + // not set. + await injectIntoCache( + ` + + + + + +

    A fake about:home page

    +
    + + `, + "window.__CACHE_CONSUMED__ = true;" + ); + await simulateRestart(browser, { withAutoShutdownWrite: false }); + + // Attempting to load the script from the cache should fail, and instead load + // the markup. + await BrowserTestUtils.withNewTab("about:home?jscache", async browser2 => { + await SpecialPowers.spawn(browser2, [], async () => { + Assert.ok( + !Cu.waiveXrays(content).__CACHE_CONSUMED__, + "Should not have found __CACHE_CONSUMED__ property" + ); + Assert.ok( + content.document.body.classList.contains("activity-stream"), + "Should have found activity-stream class on element" + ); + }); + }); + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_sanitize.js b/browser/components/newtab/test/browser/abouthomecache/browser_sanitize.js new file mode 100644 index 0000000000..4dc7ba2c89 --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_sanitize.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that when sanitizing places history, session store or downloads, that + * the about:home cache gets blown away. + */ + +add_task(async function test_sanitize() { + let testFlags = [ + ["downloads", Ci.nsIClearDataService.CLEAR_DOWNLOADS], + ["places history", Ci.nsIClearDataService.CLEAR_HISTORY], + ["session history", Ci.nsIClearDataService.CLEAR_SESSION_HISTORY], + ]; + + await withFullyLoadedAboutHome(async browser => { + for (let [type, flag] of testFlags) { + await simulateRestart(browser); + await ensureCachedAboutHome(browser); + + info( + "Testing that the about:home startup cache is cleared when " + + `clearing ${type}` + ); + + await new Promise((resolve, reject) => { + Services.clearData.deleteData(flag, { + onDataDeleted(resultFlags) { + if (!resultFlags) { + resolve(); + } else { + reject(new Error(`Failed with flags: ${resultFlags}`)); + } + }, + }); + }); + + // For the purposes of the test, we don't want the write-on-shutdown + // behaviour here (because we just want to test that the cache doesn't + // exist on startup if the history data was cleared). We also therefore + // don't need to ensure that the cache wins the race. + await simulateRestart(browser, { + withAutoShutdownWrite: false, + ensureCacheWinsRace: false, + }); + await ensureDynamicAboutHome( + browser, + AboutHomeStartupCache.CACHE_RESULT_SCALARS.DOES_NOT_EXIST + ); + } + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/browser_shutdown_timeout.js b/browser/components/newtab/test/browser/abouthomecache/browser_shutdown_timeout.js new file mode 100644 index 0000000000..b1600bfe00 --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/browser_shutdown_timeout.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that if there's a substantial delay in getting the cache + * streams from the privileged about content process for any reason + * during shutdown, that we timeout and let the AsyncShutdown proceed, + * rather than letting it block until AsyncShutdown causes a shutdown + * hang crash. + */ +add_task(async function test_shutdown_timeout() { + await withFullyLoadedAboutHome(async browser => { + // First, make sure the cache is populated so that later on, after + // the timeout, simulateRestart doesn't complain about not finding + // a pre-existing cache. This complaining only happens if this test + // is run in isolation. + await clearCache(); + await simulateRestart(browser); + + // Next, manually shutdown the AboutHomeStartupCacheChild so that + // it doesn't respond to requests to the cache streams. + await SpecialPowers.spawn(browser, [], async () => { + let { AboutHomeStartupCacheChild } = ChromeUtils.importESModule( + "resource:///modules/AboutNewTabService.sys.mjs" + ); + AboutHomeStartupCacheChild.uninit(); + }); + + // Then, manually dirty the cache state so that we attempt to write + // on shutdown. + AboutHomeStartupCache.onPreloadedNewTabMessage(); + + await simulateRestart(browser, { expectTimeout: true }); + + Assert.ok( + true, + "We reached here, which means shutdown didn't block forever." + ); + + // Clear the cache so that we're not in a half-persisted state. + await clearCache(); + }); +}); diff --git a/browser/components/newtab/test/browser/abouthomecache/head.js b/browser/components/newtab/test/browser/abouthomecache/head.js new file mode 100644 index 0000000000..5599b2bd10 --- /dev/null +++ b/browser/components/newtab/test/browser/abouthomecache/head.js @@ -0,0 +1,365 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let { AboutHomeStartupCache } = ChromeUtils.importESModule( + "resource:///modules/BrowserGlue.sys.mjs" +); +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); +const { DiscoveryStreamFeed } = ChromeUtils.importESModule( + "resource://activity-stream/lib/DiscoveryStreamFeed.sys.mjs" +); + +// Some Activity Stream preferences are JSON encoded, and quite complex. +// Hard-coding them here or in browser.ini makes them brittle to change. +// Instead, we pull the default prefs structures and set the values that +// we need and write them to preferences here dynamically. We do this in +// its own scope to avoid polluting the global scope. +{ + const { PREFS_CONFIG } = ChromeUtils.importESModule( + "resource://activity-stream/lib/ActivityStream.sys.mjs" + ); + + let defaultDSConfig = JSON.parse( + PREFS_CONFIG.get("discoverystream.config").getValue({ + geo: "US", + locale: "en-US", + }) + ); + + // Configure Activity Stream to query for the layout JSON file that points + // at the local top stories feed. + Services.prefs.setCharPref( + "browser.newtabpage.activity-stream.discoverystream.config", + JSON.stringify(defaultDSConfig) + ); +} + +/** + * Utility function that loads about:home in the current window in a new tab, and waits + * for the Discovery Stream cards to finish loading before running the taskFn function. + * Once taskFn exits, the about:home tab will be closed. + * + * @param {function} taskFn + * A function that will be run after about:home has finished loading. This can be + * an async function. + * @return {Promise} + * @resolves {undefined} + */ +function withFullyLoadedAboutHome(taskFn) { + const sandbox = sinon.createSandbox(); + sandbox + .stub(DiscoveryStreamFeed.prototype, "generateFeedUrl") + .returns( + "https://example.com/browser/browser/components/newtab/test/browser/topstories.json" + ); + + return BrowserTestUtils.withNewTab("about:home", async browser => { + await SpecialPowers.spawn(browser, [], async () => { + await ContentTaskUtils.waitForCondition( + () => + content.document.querySelectorAll( + "[data-section-id='topstories'] .ds-card-link" + ).length, + "Waiting for Discovery Stream to be rendered." + ); + }); + + await taskFn(browser); + sandbox.restore(); + }); +} + +/** + * Shuts down the AboutHomeStartupCache components in the parent process + * and privileged about content process, and then restarts them, simulating + * the parent process having restarted. + * + * @param browser () + * A with about:home running in it. This will be reloaded + * after the restart simultion is complete, and that reload will attempt + * to read any about:home cache contents. + * @param options (object, optional) + * + * An object with the following properties: + * + * withAutoShutdownWrite (boolean, optional): + * Whether or not the shutdown part of the simulation should cause the + * shutdown handler to run, which normally causes the cache to be + * written. Setting this to false is handy if the cache has been + * specially prepared for the subsequent startup, and we don't want to + * overwrite it. This defaults to true. + * + * ensureCacheWinsRace (boolean, optional): + * Ensures that the privileged about content process will be able to + * read the bytes from the streams sent down from the HTTP cache. Use + * this to avoid the HTTP cache "losing the race" against reading the + * about:home document from the omni.ja. This defaults to true. + * + * expectTimeout (boolean, optional): + * If true, indicates that it's expected that AboutHomeStartupCache will + * timeout when shutting down. If false, such timeouts will result in + * test failures. Defaults to false. + * + * skipAboutHomeLoad (boolean, optional): + * If true, doesn't automatically load about:home after the simulated + * restart. Defaults to false. + * + * @returns Promise + * @resolves undefined + * Resolves once the restart simulation is complete, and the + * pointed at about:home finishes reloading. + */ +async function simulateRestart( + browser, + { + withAutoShutdownWrite = true, + ensureCacheWinsRace = true, + expectTimeout = false, + skipAboutHomeLoad = false, + } = {} +) { + info("Simulating restart of the browser"); + if (browser.remoteType !== E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE) { + throw new Error( + "prepareLoadFromCache should only be called on a browser " + + "loaded in the privileged about content process." + ); + } + + if (withAutoShutdownWrite && AboutHomeStartupCache.initted) { + info("Simulating shutdown write"); + let timedOut = !(await AboutHomeStartupCache.onShutdown(expectTimeout)); + if (timedOut && !expectTimeout) { + Assert.ok( + false, + "AboutHomeStartupCache shutdown unexpectedly timed out." + ); + } else if (!timedOut && expectTimeout) { + Assert.ok(false, "AboutHomeStartupCache shutdown failed to time out."); + } + info("Shutdown write done"); + } else { + info("Intentionally skipping shutdown write"); + } + + AboutHomeStartupCache.uninit(); + + info("Waiting for AboutHomeStartupCacheChild to uninit"); + await SpecialPowers.spawn(browser, [], async () => { + let { AboutHomeStartupCacheChild } = ChromeUtils.importESModule( + "resource:///modules/AboutNewTabService.sys.mjs" + ); + AboutHomeStartupCacheChild.uninit(); + }); + info("AboutHomeStartupCacheChild uninitted"); + + AboutHomeStartupCache.init(); + + if (AboutHomeStartupCache.initted) { + let processManager = browser.messageManager.processMessageManager; + let pp = browser.browsingContext.currentWindowGlobal.domProcess; + let { childID } = pp; + AboutHomeStartupCache.onContentProcessCreated(childID, processManager, pp); + + info("Waiting for AboutHomeStartupCache cache entry"); + await AboutHomeStartupCache.ensureCacheEntry(); + info("Got AboutHomeStartupCache cache entry"); + + if (ensureCacheWinsRace) { + info("Ensuring cache bytes are available"); + await SpecialPowers.spawn(browser, [], async () => { + let { AboutHomeStartupCacheChild } = ChromeUtils.importESModule( + "resource:///modules/AboutNewTabService.sys.mjs" + ); + let pageStream = AboutHomeStartupCacheChild._pageInputStream; + let scriptStream = AboutHomeStartupCacheChild._scriptInputStream; + await ContentTaskUtils.waitForCondition(() => { + return pageStream.available() && scriptStream.available(); + }); + }); + } + } + + if (!skipAboutHomeLoad) { + info("Waiting for about:home to load"); + let loaded = BrowserTestUtils.browserLoaded(browser, false, "about:home"); + BrowserTestUtils.startLoadingURIString(browser, "about:home"); + await loaded; + info("about:home loaded"); + } +} + +/** + * Writes a page string and a script string into the cache for + * the next about:home load. + * + * @param page (String) + * The HTML content to write into the cache. This cannot be the empty + * string. Note that this string should contain a node that has an + * id of "root", in order for the newtab scripts to attach correctly. + * Otherwise, an exception might get thrown which can cause shutdown + * leaks. + * @param script (String) + * The JS content to write into the cache that can be loaded via + * about:home?jscache. This cannot be the empty string. + * @returns Promise + * @resolves undefined + * When the page and script content has been successfully written. + */ +async function injectIntoCache(page, script) { + if (!page || !script) { + throw new Error("Cannot injectIntoCache with falsey values"); + } + + if (!page.includes(`id="root"`)) { + throw new Error("Page markup must include a root node."); + } + + await AboutHomeStartupCache.ensureCacheEntry(); + + let pageInputStream = Cc[ + "@mozilla.org/io/string-input-stream;1" + ].createInstance(Ci.nsIStringInputStream); + + pageInputStream.setUTF8Data(page); + + let scriptInputStream = Cc[ + "@mozilla.org/io/string-input-stream;1" + ].createInstance(Ci.nsIStringInputStream); + + scriptInputStream.setUTF8Data(script); + + await AboutHomeStartupCache.populateCache(pageInputStream, scriptInputStream); +} + +/** + * Clears out any pre-existing about:home cache. + * @returns Promise + * @resolves undefined + * Resolves when the cache is cleared. + */ +async function clearCache() { + info("Test is clearing the cache"); + AboutHomeStartupCache.clearCache(); + await AboutHomeStartupCache.ensureCacheEntry(); + info("Test has cleared the cache."); +} + +/** + * Checks that the browser.startup.abouthome_cache_result scalar was + * recorded at a particular value. + * + * @param cacheResultScalar (Number) + * One of the AboutHomeStartupCache.CACHE_RESULT_SCALARS values. + */ +function assertCacheResultScalar(cacheResultScalar) { + let parentScalars = Services.telemetry.getSnapshotForScalars("main").parent; + Assert.equal( + parentScalars["browser.startup.abouthome_cache_result"], + cacheResultScalar, + "Expected the right value set to browser.startup.abouthome_cache_result " + + "scalar." + ); +} + +/** + * Tests that the about:home document loaded in a passed was + * one from the cache. + * + * We test for this by looking for some tell-tale signs of the cached + * document: + * + * 1. The about:home?jscache