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/asrouter/.eslintrc.js | 155 + browser/components/asrouter/README.md | 23 + .../asrouter/actors/ASRouterChild.sys.mjs | 123 + .../asrouter/actors/ASRouterParent.sys.mjs | 98 + browser/components/asrouter/bin/import-rollouts.js | 369 + .../asrouter/content-src/asrouter-utils.js | 79 + .../components/ASRouterAdmin/ASRouterAdmin.jsx | 1498 +++ .../components/ASRouterAdmin/ASRouterAdmin.scss | 353 + .../components/ASRouterAdmin/CopyButton.jsx | 33 + .../ASRouterAdmin/ImpressionsSection.jsx | 146 + .../components/ASRouterAdmin/SimpleHashRouter.jsx | 35 + .../content-src/components/Button/Button.jsx | 32 + .../content-src/components/Button/_Button.scss | 51 + .../ConditionalWrapper/ConditionalWrapper.jsx | 9 + .../ImpressionsWrapper/ImpressionsWrapper.jsx | 76 + .../BackgroundTaskMessagingExperiment.schema.json | 305 + .../content-src/schemas/FxMSCommon.schema.json | 128 + .../schemas/MessagingExperiment.schema.json | 1366 +++ .../schemas/corpus/ReachExperiments.messages.json | 15 + .../content-src/schemas/extract-test-corpus.js | 65 + .../asrouter/content-src/schemas/make-schemas.py | 472 + .../asrouter/content-src/schemas/message-format.md | 111 + .../content-src/schemas/message-group.schema.json | 64 + .../schemas/provider-response.schema.json | 67 + .../content-src/styles/_feature-callout-theme.scss | 92 + .../content-src/styles/_feature-callout.scss | 775 ++ .../asrouter/content-src/styles/_shopping.scss | 209 + .../CFR/templates/CFRUrlbarChiclet.schema.json | 66 + .../CFR/templates/ExtensionDoorhanger.schema.json | 320 + .../templates/CFR/templates/InfoBar.schema.json | 89 + .../OnboardingMessage/Spotlight.schema.json | 66 + .../ToolbarBadgeMessage.schema.json | 45 + .../OnboardingMessage/UpdateAction.schema.json | 47 + .../OnboardingMessage/WhatsNewMessage.schema.json | 73 + .../PBNewtab/NewtabPromoMessage.schema.json | 153 + .../ToastNotification.schema.json | 113 + .../asrouter/content/asrouter-admin.bundle.js | 1936 +++ .../asrouter/content/asrouter-admin.html | 38 + .../components/ASRouterAdmin/ASRouterAdmin.css | 546 + browser/components/asrouter/content/render.js | 7 + browser/components/asrouter/docs/about-welcome.md | 105 + .../components/asrouter/docs/aboutwelcome-1.png | Bin 0 -> 91928 bytes .../components/asrouter/docs/aboutwelcome-2.png | Bin 0 -> 103305 bytes .../asrouter/docs/aboutwelcome-res-1.png | Bin 0 -> 86697 bytes .../asrouter/docs/aboutwelcome-res-2.png | Bin 0 -> 104842 bytes .../components/asrouter/docs/cfr-doorhanger.png | Bin 0 -> 70244 bytes .../asrouter/docs/cfr_doorhanger_screenshot.png | Bin 0 -> 257709 bytes .../docs/contextual-feature-recommendation.md | 83 + browser/components/asrouter/docs/debugging-docs.md | 32 + .../components/asrouter/docs/debugging-guide.png | Bin 0 -> 247644 bytes .../components/asrouter/docs/feature-callout.md | 614 + .../components/asrouter/docs/feature-callout.png | Bin 0 -> 188755 bytes browser/components/asrouter/docs/first-run.md | 68 + browser/components/asrouter/docs/index.rst | 108 + browser/components/asrouter/docs/infobar.png | Bin 0 -> 106424 bytes browser/components/asrouter/docs/infobars.md | 60 + .../asrouter/docs/message-routing-overview.png | Bin 0 -> 50250 bytes browser/components/asrouter/docs/moments-page.md | 64 + browser/components/asrouter/docs/moments.png | 0 .../components/asrouter/docs/private-browsing.md | 59 + .../components/asrouter/docs/private-browsing.png | Bin 0 -> 139858 bytes browser/components/asrouter/docs/remote_cfr.md | 82 + browser/components/asrouter/docs/selected-PB.png | Bin 0 -> 24388 bytes .../asrouter/docs/simple-cfr-template.rst | 37 + browser/components/asrouter/docs/spotlight.md | 90 + browser/components/asrouter/docs/spotlight.png | Bin 0 -> 52974 bytes .../asrouter/docs/targeting-attributes.md | 1033 ++ .../components/asrouter/docs/targeting-guide.md | 37 + browser/components/asrouter/docs/telemetry.md | 90 + browser/components/asrouter/jar.mn | 11 + browser/components/asrouter/karma.mc.config.js | 214 + .../components/asrouter/modules/ASRouter.sys.mjs | 2079 ++++ .../asrouter/modules/ASRouterDefaultConfig.sys.mjs | 64 + .../asrouter/modules/ASRouterNewTabHook.sys.mjs | 117 + .../ASRouterParentProcessMessageHandler.sys.mjs | 171 + .../asrouter/modules/ASRouterPreferences.sys.mjs | 241 + .../asrouter/modules/ASRouterTargeting.sys.mjs | 1308 ++ .../modules/ASRouterTriggerListeners.sys.mjs | 1439 +++ .../asrouter/modules/ActorConstants.sys.mjs | 49 + .../asrouter/modules/CFRMessageProvider.sys.mjs | 820 ++ .../asrouter/modules/CFRPageActions.sys.mjs | 1086 ++ .../asrouter/modules/FeatureCallout.sys.mjs | 2100 ++++ .../asrouter/modules/FeatureCalloutBroker.sys.mjs | 215 + .../modules/FeatureCalloutMessages.sys.mjs | 1299 ++ .../components/asrouter/modules/InfoBar.sys.mjs | 169 + .../modules/MessagingExperimentConstants.sys.mjs | 37 + .../asrouter/modules/MomentsPageHub.sys.mjs | 171 + .../modules/OnboardingMessageProvider.sys.mjs | 1414 +++ .../asrouter/modules/PageEventManager.sys.mjs | 135 + .../asrouter/modules/PanelTestProvider.sys.mjs | 771 ++ .../components/asrouter/modules/RemoteL10n.sys.mjs | 249 + .../components/asrouter/modules/Spotlight.sys.mjs | 78 + .../asrouter/modules/ToastNotification.sys.mjs | 138 + .../asrouter/modules/ToolbarBadgeHub.sys.mjs | 308 + .../asrouter/modules/ToolbarPanelHub.sys.mjs | 544 + browser/components/asrouter/moz.build | 67 + browser/components/asrouter/package-lock.json | 11917 +++++++++++++++++++ browser/components/asrouter/package.json | 82 + .../tests/InflightAssetsMessageProvider.sys.mjs | 340 + .../tests/NimbusRolloutMessageProvider.sys.mjs | 199 + .../components/asrouter/tests/browser/browser.toml | 44 + .../tests/browser/browser_asrouter_bug1761522.js | 234 + .../tests/browser/browser_asrouter_bug1800087.js | 48 + .../asrouter/tests/browser/browser_asrouter_cfr.js | 932 ++ .../browser_asrouter_experimentsAPILoader.js | 505 + .../browser/browser_asrouter_group_frequency.js | 188 + .../browser/browser_asrouter_group_userprefs.js | 158 + .../tests/browser/browser_asrouter_infobar.js | 223 + .../browser/browser_asrouter_momentspagehub.js | 116 + .../tests/browser/browser_asrouter_targeting.js | 1706 +++ .../browser/browser_asrouter_toast_notification.js | 139 + .../tests/browser/browser_asrouter_toolbarbadge.js | 149 + .../browser/browser_feature_callout_in_chrome.js | 1122 ++ .../tests/browser/browser_feature_callout_panel.js | 430 + .../tests/browser/browser_trigger_listeners.js | 430 + browser/components/asrouter/tests/browser/head.js | 66 + .../asrouter/tests/unit/ASRouter.test.js | 2870 +++++ .../asrouter/tests/unit/ASRouterChild.test.js | 71 + .../asrouter/tests/unit/ASRouterNewTabHook.test.js | 153 + .../asrouter/tests/unit/ASRouterParent.test.js | 83 + .../ASRouterParentProcessMessageHandler.test.js | 428 + .../tests/unit/ASRouterPreferences.test.js | 480 + .../asrouter/tests/unit/ASRouterTargeting.test.js | 574 + .../tests/unit/ASRouterTriggerListeners.test.js | 833 ++ .../asrouter/tests/unit/CFRMessageProvider.test.js | 32 + .../asrouter/tests/unit/CFRPageActions.test.js | 1414 +++ .../asrouter/tests/unit/MessageLoaderUtils.test.js | 459 + .../asrouter/tests/unit/ModalOverlay.test.jsx | 69 + .../asrouter/tests/unit/MomentsPageHub.test.js | 336 + .../asrouter/tests/unit/RemoteL10n.test.js | 217 + .../asrouter/tests/unit/TargetingDocs.test.js | 88 + .../asrouter/tests/unit/ToolbarBadgeHub.test.js | 652 + .../asrouter/tests/unit/ToolbarPanelHub.test.js | 762 ++ .../asrouter/tests/unit/asrouter-utils.test.js | 118 + .../components/asrouter/tests/unit/constants.js | 131 + .../content-src/components/ASRouterAdmin.test.jsx | 262 + .../unit/templates/ExtensionDoorhanger.test.jsx | 112 + .../components/asrouter/tests/unit/unit-entry.js | 727 ++ browser/components/asrouter/tests/xpcshell/head.js | 98 + .../xpcshell/test_ASRouterTargeting_attribution.js | 94 + .../xpcshell/test_ASRouterTargeting_snapshot.js | 172 + .../test_ASRouter_getTargetingParameters.js | 73 + .../tests/xpcshell/test_CFRMessageProvider.js | 32 + .../xpcshell/test_InflightAssetsMessageProvider.js | 41 + .../xpcshell/test_NimbusRolloutMessageProvider.js | 41 + .../xpcshell/test_OnboardingMessageProvider.js | 229 + .../tests/xpcshell/test_PanelTestProvider.js | 84 + .../tests/xpcshell/test_reach_experiments.js | 97 + .../tests/xpcshell/test_remoteExperiments.js | 37 + .../asrouter/tests/xpcshell/xpcshell.toml | 24 + .../asrouter/webpack.asrouter-admin.config.js | 35 + browser/components/asrouter/yamscripts.yml | 47 + 152 files changed, 59107 insertions(+) create mode 100644 browser/components/asrouter/.eslintrc.js create mode 100644 browser/components/asrouter/README.md create mode 100644 browser/components/asrouter/actors/ASRouterChild.sys.mjs create mode 100644 browser/components/asrouter/actors/ASRouterParent.sys.mjs create mode 100644 browser/components/asrouter/bin/import-rollouts.js create mode 100644 browser/components/asrouter/content-src/asrouter-utils.js create mode 100644 browser/components/asrouter/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx create mode 100644 browser/components/asrouter/content-src/components/ASRouterAdmin/ASRouterAdmin.scss create mode 100644 browser/components/asrouter/content-src/components/ASRouterAdmin/CopyButton.jsx create mode 100644 browser/components/asrouter/content-src/components/ASRouterAdmin/ImpressionsSection.jsx create mode 100644 browser/components/asrouter/content-src/components/ASRouterAdmin/SimpleHashRouter.jsx create mode 100644 browser/components/asrouter/content-src/components/Button/Button.jsx create mode 100644 browser/components/asrouter/content-src/components/Button/_Button.scss create mode 100644 browser/components/asrouter/content-src/components/ConditionalWrapper/ConditionalWrapper.jsx create mode 100644 browser/components/asrouter/content-src/components/ImpressionsWrapper/ImpressionsWrapper.jsx create mode 100644 browser/components/asrouter/content-src/schemas/BackgroundTaskMessagingExperiment.schema.json create mode 100644 browser/components/asrouter/content-src/schemas/FxMSCommon.schema.json create mode 100644 browser/components/asrouter/content-src/schemas/MessagingExperiment.schema.json create mode 100644 browser/components/asrouter/content-src/schemas/corpus/ReachExperiments.messages.json create mode 100644 browser/components/asrouter/content-src/schemas/extract-test-corpus.js create mode 100755 browser/components/asrouter/content-src/schemas/make-schemas.py create mode 100644 browser/components/asrouter/content-src/schemas/message-format.md create mode 100644 browser/components/asrouter/content-src/schemas/message-group.schema.json create mode 100644 browser/components/asrouter/content-src/schemas/provider-response.schema.json create mode 100644 browser/components/asrouter/content-src/styles/_feature-callout-theme.scss create mode 100644 browser/components/asrouter/content-src/styles/_feature-callout.scss create mode 100644 browser/components/asrouter/content-src/styles/_shopping.scss create mode 100644 browser/components/asrouter/content-src/templates/CFR/templates/CFRUrlbarChiclet.schema.json create mode 100644 browser/components/asrouter/content-src/templates/CFR/templates/ExtensionDoorhanger.schema.json create mode 100644 browser/components/asrouter/content-src/templates/CFR/templates/InfoBar.schema.json create mode 100644 browser/components/asrouter/content-src/templates/OnboardingMessage/Spotlight.schema.json create mode 100644 browser/components/asrouter/content-src/templates/OnboardingMessage/ToolbarBadgeMessage.schema.json create mode 100644 browser/components/asrouter/content-src/templates/OnboardingMessage/UpdateAction.schema.json create mode 100644 browser/components/asrouter/content-src/templates/OnboardingMessage/WhatsNewMessage.schema.json create mode 100644 browser/components/asrouter/content-src/templates/PBNewtab/NewtabPromoMessage.schema.json create mode 100644 browser/components/asrouter/content-src/templates/ToastNotification/ToastNotification.schema.json create mode 100644 browser/components/asrouter/content/asrouter-admin.bundle.js create mode 100644 browser/components/asrouter/content/asrouter-admin.html create mode 100644 browser/components/asrouter/content/components/ASRouterAdmin/ASRouterAdmin.css create mode 100644 browser/components/asrouter/content/render.js create mode 100644 browser/components/asrouter/docs/about-welcome.md create mode 100644 browser/components/asrouter/docs/aboutwelcome-1.png create mode 100644 browser/components/asrouter/docs/aboutwelcome-2.png create mode 100644 browser/components/asrouter/docs/aboutwelcome-res-1.png create mode 100644 browser/components/asrouter/docs/aboutwelcome-res-2.png create mode 100644 browser/components/asrouter/docs/cfr-doorhanger.png create mode 100644 browser/components/asrouter/docs/cfr_doorhanger_screenshot.png create mode 100644 browser/components/asrouter/docs/contextual-feature-recommendation.md create mode 100644 browser/components/asrouter/docs/debugging-docs.md create mode 100644 browser/components/asrouter/docs/debugging-guide.png create mode 100644 browser/components/asrouter/docs/feature-callout.md create mode 100644 browser/components/asrouter/docs/feature-callout.png create mode 100644 browser/components/asrouter/docs/first-run.md create mode 100644 browser/components/asrouter/docs/index.rst create mode 100644 browser/components/asrouter/docs/infobar.png create mode 100644 browser/components/asrouter/docs/infobars.md create mode 100644 browser/components/asrouter/docs/message-routing-overview.png create mode 100644 browser/components/asrouter/docs/moments-page.md create mode 100644 browser/components/asrouter/docs/moments.png create mode 100644 browser/components/asrouter/docs/private-browsing.md create mode 100644 browser/components/asrouter/docs/private-browsing.png create mode 100644 browser/components/asrouter/docs/remote_cfr.md create mode 100644 browser/components/asrouter/docs/selected-PB.png create mode 100644 browser/components/asrouter/docs/simple-cfr-template.rst create mode 100644 browser/components/asrouter/docs/spotlight.md create mode 100644 browser/components/asrouter/docs/spotlight.png create mode 100644 browser/components/asrouter/docs/targeting-attributes.md create mode 100644 browser/components/asrouter/docs/targeting-guide.md create mode 100644 browser/components/asrouter/docs/telemetry.md create mode 100644 browser/components/asrouter/jar.mn create mode 100644 browser/components/asrouter/karma.mc.config.js create mode 100644 browser/components/asrouter/modules/ASRouter.sys.mjs create mode 100644 browser/components/asrouter/modules/ASRouterDefaultConfig.sys.mjs create mode 100644 browser/components/asrouter/modules/ASRouterNewTabHook.sys.mjs create mode 100644 browser/components/asrouter/modules/ASRouterParentProcessMessageHandler.sys.mjs create mode 100644 browser/components/asrouter/modules/ASRouterPreferences.sys.mjs create mode 100644 browser/components/asrouter/modules/ASRouterTargeting.sys.mjs create mode 100644 browser/components/asrouter/modules/ASRouterTriggerListeners.sys.mjs create mode 100644 browser/components/asrouter/modules/ActorConstants.sys.mjs create mode 100644 browser/components/asrouter/modules/CFRMessageProvider.sys.mjs create mode 100644 browser/components/asrouter/modules/CFRPageActions.sys.mjs create mode 100644 browser/components/asrouter/modules/FeatureCallout.sys.mjs create mode 100644 browser/components/asrouter/modules/FeatureCalloutBroker.sys.mjs create mode 100644 browser/components/asrouter/modules/FeatureCalloutMessages.sys.mjs create mode 100644 browser/components/asrouter/modules/InfoBar.sys.mjs create mode 100644 browser/components/asrouter/modules/MessagingExperimentConstants.sys.mjs create mode 100644 browser/components/asrouter/modules/MomentsPageHub.sys.mjs create mode 100644 browser/components/asrouter/modules/OnboardingMessageProvider.sys.mjs create mode 100644 browser/components/asrouter/modules/PageEventManager.sys.mjs create mode 100644 browser/components/asrouter/modules/PanelTestProvider.sys.mjs create mode 100644 browser/components/asrouter/modules/RemoteL10n.sys.mjs create mode 100644 browser/components/asrouter/modules/Spotlight.sys.mjs create mode 100644 browser/components/asrouter/modules/ToastNotification.sys.mjs create mode 100644 browser/components/asrouter/modules/ToolbarBadgeHub.sys.mjs create mode 100644 browser/components/asrouter/modules/ToolbarPanelHub.sys.mjs create mode 100644 browser/components/asrouter/moz.build create mode 100644 browser/components/asrouter/package-lock.json create mode 100644 browser/components/asrouter/package.json create mode 100644 browser/components/asrouter/tests/InflightAssetsMessageProvider.sys.mjs create mode 100644 browser/components/asrouter/tests/NimbusRolloutMessageProvider.sys.mjs create mode 100644 browser/components/asrouter/tests/browser/browser.toml create mode 100644 browser/components/asrouter/tests/browser/browser_asrouter_bug1761522.js create mode 100644 browser/components/asrouter/tests/browser/browser_asrouter_bug1800087.js create mode 100644 browser/components/asrouter/tests/browser/browser_asrouter_cfr.js create mode 100644 browser/components/asrouter/tests/browser/browser_asrouter_experimentsAPILoader.js create mode 100644 browser/components/asrouter/tests/browser/browser_asrouter_group_frequency.js create mode 100644 browser/components/asrouter/tests/browser/browser_asrouter_group_userprefs.js create mode 100644 browser/components/asrouter/tests/browser/browser_asrouter_infobar.js create mode 100644 browser/components/asrouter/tests/browser/browser_asrouter_momentspagehub.js create mode 100644 browser/components/asrouter/tests/browser/browser_asrouter_targeting.js create mode 100644 browser/components/asrouter/tests/browser/browser_asrouter_toast_notification.js create mode 100644 browser/components/asrouter/tests/browser/browser_asrouter_toolbarbadge.js create mode 100644 browser/components/asrouter/tests/browser/browser_feature_callout_in_chrome.js create mode 100644 browser/components/asrouter/tests/browser/browser_feature_callout_panel.js create mode 100644 browser/components/asrouter/tests/browser/browser_trigger_listeners.js create mode 100644 browser/components/asrouter/tests/browser/head.js create mode 100644 browser/components/asrouter/tests/unit/ASRouter.test.js create mode 100644 browser/components/asrouter/tests/unit/ASRouterChild.test.js create mode 100644 browser/components/asrouter/tests/unit/ASRouterNewTabHook.test.js create mode 100644 browser/components/asrouter/tests/unit/ASRouterParent.test.js create mode 100644 browser/components/asrouter/tests/unit/ASRouterParentProcessMessageHandler.test.js create mode 100644 browser/components/asrouter/tests/unit/ASRouterPreferences.test.js create mode 100644 browser/components/asrouter/tests/unit/ASRouterTargeting.test.js create mode 100644 browser/components/asrouter/tests/unit/ASRouterTriggerListeners.test.js create mode 100644 browser/components/asrouter/tests/unit/CFRMessageProvider.test.js create mode 100644 browser/components/asrouter/tests/unit/CFRPageActions.test.js create mode 100644 browser/components/asrouter/tests/unit/MessageLoaderUtils.test.js create mode 100644 browser/components/asrouter/tests/unit/ModalOverlay.test.jsx create mode 100644 browser/components/asrouter/tests/unit/MomentsPageHub.test.js create mode 100644 browser/components/asrouter/tests/unit/RemoteL10n.test.js create mode 100644 browser/components/asrouter/tests/unit/TargetingDocs.test.js create mode 100644 browser/components/asrouter/tests/unit/ToolbarBadgeHub.test.js create mode 100644 browser/components/asrouter/tests/unit/ToolbarPanelHub.test.js create mode 100644 browser/components/asrouter/tests/unit/asrouter-utils.test.js create mode 100644 browser/components/asrouter/tests/unit/constants.js create mode 100644 browser/components/asrouter/tests/unit/content-src/components/ASRouterAdmin.test.jsx create mode 100644 browser/components/asrouter/tests/unit/templates/ExtensionDoorhanger.test.jsx create mode 100644 browser/components/asrouter/tests/unit/unit-entry.js create mode 100644 browser/components/asrouter/tests/xpcshell/head.js create mode 100644 browser/components/asrouter/tests/xpcshell/test_ASRouterTargeting_attribution.js create mode 100644 browser/components/asrouter/tests/xpcshell/test_ASRouterTargeting_snapshot.js create mode 100644 browser/components/asrouter/tests/xpcshell/test_ASRouter_getTargetingParameters.js create mode 100644 browser/components/asrouter/tests/xpcshell/test_CFRMessageProvider.js create mode 100644 browser/components/asrouter/tests/xpcshell/test_InflightAssetsMessageProvider.js create mode 100644 browser/components/asrouter/tests/xpcshell/test_NimbusRolloutMessageProvider.js create mode 100644 browser/components/asrouter/tests/xpcshell/test_OnboardingMessageProvider.js create mode 100644 browser/components/asrouter/tests/xpcshell/test_PanelTestProvider.js create mode 100644 browser/components/asrouter/tests/xpcshell/test_reach_experiments.js create mode 100644 browser/components/asrouter/tests/xpcshell/test_remoteExperiments.js create mode 100644 browser/components/asrouter/tests/xpcshell/xpcshell.toml create mode 100644 browser/components/asrouter/webpack.asrouter-admin.config.js create mode 100644 browser/components/asrouter/yamscripts.yml (limited to 'browser/components/asrouter') diff --git a/browser/components/asrouter/.eslintrc.js b/browser/components/asrouter/.eslintrc.js new file mode 100644 index 0000000000..7a67e797e6 --- /dev/null +++ b/browser/components/asrouter/.eslintrc.js @@ -0,0 +1,155 @@ +/* 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", "tests/unit/**/*.js"], + parserOptions: { + sourceType: "module", + }, + }, + { + files: ["bin/**", "content-src/**", "tests/unit/**"], + env: { + node: true, + }, + }, + { + // Use a configuration that's appropriate for modules, workers and + // non-production files. + files: ["tests/**", "modules/**"], + rules: { + "no-implicit-globals": "off", + }, + }, + { + files: ["content-src/**", "tests/unit/**"], + rules: { + // Disallow commonjs in these directories. + "import/no-commonjs": 2, + }, + }, + { + // These tests simulate the browser environment. + files: "tests/unit/**", + env: { + browser: true, + mocha: true, + }, + globals: { + assert: true, + chai: true, + sinon: true, + }, + }, + { + files: "tests/**", + 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/asrouter/README.md b/browser/components/asrouter/README.md new file mode 100644 index 0000000000..213093af86 --- /dev/null +++ b/browser/components/asrouter/README.md @@ -0,0 +1,23 @@ +# Activity Stream Router + +## Preferences `browser.newtab.activity-stream.asrouter.*` + +Name | Used for | Type | Example value +--- | --- | --- | --- +`allowHosts` | Allow a host in order to fetch messages from its endpoint | `[String]` | `["gist.github.com", "gist.githubusercontent.com", "localhost:8000"]` +`providers.cfr` | Message provider options for cfr | `Object` | [see below](#message-providers) +`providers.onboarding` | Message provider options for onboarding | `Object` | [see below](#message-providers) +`useRemoteL10n` | Controls whether to use the remote Fluent files for l10n, default as `true` | `Boolean` | `[true|false]` + +### Message providers examples + +```json +{ + "id" : "onboarding", + "enabled": true, + "type" : "local", + "localProvider" : "OnboardingMessageProvider" +} +``` + +### [Message format documentation](https://github.com/mozilla/activity-stream/blob/master/content-src/asrouter/schemas/message-format.md) diff --git a/browser/components/asrouter/actors/ASRouterChild.sys.mjs b/browser/components/asrouter/actors/ASRouterChild.sys.mjs new file mode 100644 index 0000000000..8e5fd5ccf5 --- /dev/null +++ b/browser/components/asrouter/actors/ASRouterChild.sys.mjs @@ -0,0 +1,123 @@ +/* vim: set ts=2 sw=2 sts=2 et tw=80: */ +/* 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 +// MESSAGE_TYPE_LIST and MESSAGE_TYPE_HASH, 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 { MESSAGE_TYPE_LIST, MESSAGE_TYPE_HASH: msg } = + ChromeUtils.importESModule( + "resource:///modules/asrouter/ActorConstants.sys.mjs" + ); + +const VALID_TYPES = new Set(MESSAGE_TYPE_LIST); + +export class ASRouterChild extends JSWindowActorChild { + constructor() { + super(); + this.observers = new Set(); + } + + didDestroy() { + this.observers.clear(); + } + + actorCreated() { + // NOTE: DOMDocElementInserted may be called multiple times per + // PWindowGlobal due to the initial about:blank document's window global + // being re-used. + const window = this.contentWindow; + Cu.exportFunction(this.asRouterMessage.bind(this), window, { + defineAs: "ASRouterMessage", + }); + Cu.exportFunction(this.addParentListener.bind(this), window, { + defineAs: "ASRouterAddParentListener", + }); + Cu.exportFunction(this.removeParentListener.bind(this), window, { + defineAs: "ASRouterRemoveParentListener", + }); + } + + handleEvent(event) { + // DOMDocElementCreated is only used to create the actor. + } + + addParentListener(listener) { + this.observers.add(listener); + } + + removeParentListener(listener) { + this.observers.delete(listener); + } + + receiveMessage({ name, data }) { + switch (name) { + case "UpdateAdminState": + case "ClearProviders": { + this.observers.forEach(listener => { + let result = Cu.cloneInto( + { + type: name, + data, + }, + this.contentWindow + ); + listener(result); + }); + break; + } + } + } + + wrapPromise(promise) { + return new this.contentWindow.Promise((resolve, reject) => + promise.then(resolve, reject) + ); + } + + sendQuery(aName, aData = null) { + return this.wrapPromise( + new Promise(resolve => { + super.sendQuery(aName, aData).then(result => { + resolve(Cu.cloneInto(result, this.contentWindow)); + }); + }) + ); + } + + asRouterMessage({ type, data }) { + // Some legacy privileged addons send this message, but it got removed from + // VALID_TYPES in bug 1715158. Thankfully, these addons don't appear to + // require any actions from this message - just a Promise that resolves. + if (type === "NEWTAB_MESSAGE_REQUEST") { + return this.wrapPromise(Promise.resolve()); + } + + if (VALID_TYPES.has(type)) { + switch (type) { + case msg.DISABLE_PROVIDER: + case msg.ENABLE_PROVIDER: + case msg.EXPIRE_QUERY_CACHE: + case msg.FORCE_WHATSNEW_PANEL: + case msg.CLOSE_WHATSNEW_PANEL: + case msg.FORCE_PRIVATE_BROWSING_WINDOW: + case msg.IMPRESSION: + case msg.RESET_PROVIDER_PREF: + case msg.SET_PROVIDER_USER_PREF: + case msg.USER_ACTION: { + return this.sendAsyncMessage(type, data); + } + default: { + // these messages need a response + return this.sendQuery(type, data); + } + } + } + throw new Error(`Unexpected type "${type}"`); + } +} diff --git a/browser/components/asrouter/actors/ASRouterParent.sys.mjs b/browser/components/asrouter/actors/ASRouterParent.sys.mjs new file mode 100644 index 0000000000..aab909df05 --- /dev/null +++ b/browser/components/asrouter/actors/ASRouterParent.sys.mjs @@ -0,0 +1,98 @@ +/* vim: set ts=2 sw=2 sts=2 et tw=80: */ +/* 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, since +// it doesn't seem to understand using static import for sys.mjs +// files. +// eslint-disable-next-line mozilla/use-static-import +const { ASRouterNewTabHook } = ChromeUtils.importESModule( + "resource:///modules/asrouter/ASRouterNewTabHook.sys.mjs" +); + +import { ASRouterDefaultConfig } from "resource:///modules/asrouter/ASRouterDefaultConfig.sys.mjs"; + +export class ASRouterTabs { + constructor({ asRouterNewTabHook }) { + this.actors = new Set(); + this.destroy = () => {}; + // This is one of several entrypoints to ASRouter Initialization. There is + // another one in BrowserGlue, and another in BackgroundTaskUtils. + asRouterNewTabHook.createInstance(ASRouterDefaultConfig()); + this.loadingMessageHandler = asRouterNewTabHook + .getInstance() + .then(initializer => { + const parentProcessMessageHandler = initializer.connect({ + clearChildMessages: ids => this.messageAll("ClearMessages", ids), + clearChildProviders: ids => this.messageAll("ClearProviders", ids), + updateAdminState: state => this.messageAll("UpdateAdminState", state), + }); + this.destroy = () => { + initializer.disconnect(); + }; + return parentProcessMessageHandler; + }); + } + + get size() { + return this.actors.size; + } + + messageAll(message, data) { + return Promise.all( + [...this.actors].map(a => a.sendAsyncMessage(message, data)) + ); + } + + registerActor(actor) { + this.actors.add(actor); + } + + unregisterActor(actor) { + this.actors.delete(actor); + } +} + +const defaultTabsFactory = () => + new ASRouterTabs({ asRouterNewTabHook: ASRouterNewTabHook }); + +export class ASRouterParent extends JSWindowActorParent { + static tabs = null; + + static nextTabId = 0; + + constructor({ tabsFactory } = { tabsFactory: defaultTabsFactory }) { + super(); + this.tabsFactory = tabsFactory; + } + + actorCreated() { + ASRouterParent.tabs = ASRouterParent.tabs || this.tabsFactory(); + this.tabsFactory = null; + this.tabId = ++ASRouterParent.nextTabId; + ASRouterParent.tabs.registerActor(this); + } + + didDestroy() { + ASRouterParent.tabs.unregisterActor(this); + if (ASRouterParent.tabs.size < 1) { + ASRouterParent.tabs.destroy(); + ASRouterParent.tabs = null; + } + } + + getTab() { + return { + id: this.tabId, + browser: this.browsingContext.embedderElement, + }; + } + + receiveMessage({ name, data }) { + return ASRouterParent.tabs.loadingMessageHandler.then(handler => { + return handler.handleMessage(name, data, this.getTab()); + }); + } +} diff --git a/browser/components/asrouter/bin/import-rollouts.js b/browser/components/asrouter/bin/import-rollouts.js new file mode 100644 index 0000000000..63f8a555b4 --- /dev/null +++ b/browser/components/asrouter/bin/import-rollouts.js @@ -0,0 +1,369 @@ +/* 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 is a script to import Nimbus experiments from a given collection into + * browser/components/asrouter/tests/NimbusRolloutMessageProvider.sys.mjs. By + * default, it only imports messaging rollouts. This is done so that the content + * of off-train rollouts can be easily searched. That way, when we are cleaning + * up old assets (such as Fluent strings), we don't accidentally delete strings + * that live rollouts are using because it was too difficult to find whether + * they were in use. + * + * This works by fetching the message records from the Nimbus collection and + * then writing them to the file. The messages are converted from JSON to JS. + * The file is structured like this: + * export const NimbusRolloutMessageProvider = { + * getMessages() { + * return [ + * { ...message1 }, + * { ...message2 }, + * ]; + * }, + * }; + */ + +/* eslint-disable max-depth, no-console */ +const chalk = require("chalk"); +const https = require("https"); +const path = require("path"); +const { pathToFileURL } = require("url"); +const fs = require("fs"); +const util = require("util"); +const prettier = require("prettier"); +const jsonschema = require("../../../../third_party/js/cfworker/json-schema.js"); + +const DEFAULT_COLLECTION_ID = "nimbus-desktop-experiments"; +const BASE_URL = + "https://firefox.settings.services.mozilla.com/v1/buckets/main/collections/"; +const EXPERIMENTER_URL = "https://experimenter.services.mozilla.com/nimbus/"; +const OUTPUT_PATH = "./tests/NimbusRolloutMessageProvider.sys.mjs"; +const LICENSE_STRING = `/* 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/. */`; + +function fetchJSON(url) { + return new Promise((resolve, reject) => { + https + .get(url, resp => { + let data = ""; + resp.on("data", chunk => { + data += chunk; + }); + resp.on("end", () => resolve(JSON.parse(data))); + }) + .on("error", reject); + }); +} + +function isMessageValid(validator, obj) { + if (validator) { + const result = validator.validate(obj); + return result.valid && result.errors.length === 0; + } + return true; +} + +async function getMessageValidators(skipValidation) { + if (skipValidation) { + return { experimentValidator: null, messageValidators: {} }; + } + + async function getSchema(filePath) { + const file = await util.promisify(fs.readFile)(filePath, "utf8"); + return JSON.parse(file); + } + + async function getValidator(filePath, { common = false } = {}) { + const schema = await getSchema(filePath); + const validator = new jsonschema.Validator(schema); + + if (common) { + const commonSchema = await getSchema( + "./content-src/schemas/FxMSCommon.schema.json" + ); + validator.addSchema(commonSchema); + } + + return validator; + } + + const experimentValidator = await getValidator( + "./content-src/schemas/MessagingExperiment.schema.json" + ); + + const messageValidators = { + cfr_doorhanger: await getValidator( + "./content-src/templates/CFR/templates/ExtensionDoorhanger.schema.json", + { common: true } + ), + cfr_urlbar_chiclet: await getValidator( + "./content-src/templates/CFR/templates/CFRUrlbarChiclet.schema.json", + { common: true } + ), + infobar: await getValidator( + "./content-src/templates/CFR/templates/InfoBar.schema.json", + { common: true } + ), + pb_newtab: await getValidator( + "./content-src/templates/PBNewtab/NewtabPromoMessage.schema.json", + { common: true } + ), + spotlight: await getValidator( + "./content-src/templates/OnboardingMessage/Spotlight.schema.json", + { common: true } + ), + toast_notification: await getValidator( + "./content-src/templates/ToastNotification/ToastNotification.schema.json", + { common: true } + ), + toolbar_badge: await getValidator( + "./content-src/templates/OnboardingMessage/ToolbarBadgeMessage.schema.json", + { common: true } + ), + update_action: await getValidator( + "./content-src/templates/OnboardingMessage/UpdateAction.schema.json", + { common: true } + ), + whatsnew_panel_message: await getValidator( + "./content-src/templates/OnboardingMessage/WhatsNewMessage.schema.json", + { common: true } + ), + feature_callout: await getValidator( + // For now, Feature Callout and Spotlight share a common schema + "./content-src/templates/OnboardingMessage/Spotlight.schema.json", + { common: true } + ), + }; + + messageValidators.milestone_message = messageValidators.cfr_doorhanger; + + return { experimentValidator, messageValidators }; +} + +function annotateMessage({ message, slug, minVersion, maxVersion, url }) { + const comments = []; + if (slug) { + comments.push(`// Nimbus slug: ${slug}`); + } + let versionRange = ""; + if (minVersion) { + versionRange = minVersion; + if (maxVersion) { + versionRange += `-${maxVersion}`; + } else { + versionRange += "+"; + } + } else if (maxVersion) { + versionRange = `0-${maxVersion}`; + } + if (versionRange) { + comments.push(`// Version range: ${versionRange}`); + } + if (url) { + comments.push(`// Recipe: ${url}`); + } + return JSON.stringify(message, null, 2).replace( + /^{/, + `{ ${comments.join("\n")}` + ); +} + +async function format(content) { + const config = await prettier.resolveConfig("./.prettierrc.js"); + return prettier.format(content, { ...config, filepath: OUTPUT_PATH }); +} + +async function main() { + const { default: meow } = await import("meow"); + const { MESSAGING_EXPERIMENTS_DEFAULT_FEATURES } = await import( + "../modules/MessagingExperimentConstants.sys.mjs" + ); + + const fileUrl = pathToFileURL(__filename); + + const cli = meow( + ` + Usage + $ node bin/import-rollouts.js [options] + + Options + -c ID, --collection ID The Nimbus collection ID to import from + default: ${DEFAULT_COLLECTION_ID} + -e, --experiments Import all messaging experiments, not just rollouts + -s, --skip-validation Skip validation of experiments and messages + -h, --help Show this help message + + Examples + $ node bin/import-rollouts.js --collection nimbus-preview + $ ./mach npm run import-rollouts --prefix=browser/components/newtab -- -e + `, + { + 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: "import-rollouts", + 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: { + collection: { + type: "string", + alias: "c", + default: DEFAULT_COLLECTION_ID, + }, + experiments: { + type: "boolean", + alias: "e", + default: false, + }, + skipValidation: { + type: "boolean", + alias: "s", + default: false, + }, + }, + } + ); + + const RECORDS_URL = `${BASE_URL}${cli.flags.collection}/records`; + + console.log(`Fetching records from ${chalk.underline.yellow(RECORDS_URL)}`); + + const { data: records } = await fetchJSON(RECORDS_URL); + + if (!Array.isArray(records)) { + throw new TypeError( + `Expected records to be an array, got ${typeof records}` + ); + } + + const recipes = records.filter( + record => + record.application === "firefox-desktop" && + record.featureIds.some(id => + MESSAGING_EXPERIMENTS_DEFAULT_FEATURES.includes(id) + ) && + (record.isRollout || cli.flags.experiments) + ); + + const importItems = []; + const { experimentValidator, messageValidators } = await getMessageValidators( + cli.flags.skipValidation + ); + for (const recipe of recipes) { + const { slug: experimentSlug, branches, targeting } = recipe; + if (!(experimentSlug && Array.isArray(branches) && branches.length)) { + continue; + } + console.log( + `Processing ${recipe.isRollout ? "rollout" : "experiment"}: ${chalk.blue( + experimentSlug + )}${ + branches.length > 1 + ? ` with ${chalk.underline(`${String(branches.length)} branches`)}` + : "" + }` + ); + const recipeUrl = `${EXPERIMENTER_URL}${experimentSlug}/summary`; + const [, minVersion] = + targeting?.match(/\(version\|versionCompare\(\'([0-9]+)\.!\'\) >= 0/) || + []; + const [, maxVersion] = + targeting?.match(/\(version\|versionCompare\(\'([0-9]+)\.\*\'\) <= 0/) || + []; + let branchIndex = branches.length > 1 ? 1 : 0; + for (const branch of branches) { + const { slug: branchSlug, features } = branch; + console.log( + ` Processing branch${ + branchIndex > 0 ? ` ${branchIndex} of ${branches.length}` : "" + }: ${chalk.blue(branchSlug)}` + ); + branchIndex += 1; + const url = `${recipeUrl}#${branchSlug}`; + if (!Array.isArray(features)) { + continue; + } + for (const feature of features) { + if ( + feature.enabled && + MESSAGING_EXPERIMENTS_DEFAULT_FEATURES.includes(feature.featureId) && + feature.value && + typeof feature.value === "object" && + feature.value.template + ) { + if (!isMessageValid(experimentValidator, feature.value)) { + console.log( + ` ${chalk.red( + "✗" + )} Skipping invalid value for branch: ${chalk.blue(branchSlug)}` + ); + continue; + } + const messages = ( + feature.value.template === "multi" && + Array.isArray(feature.value.messages) + ? feature.value.messages + : [feature.value] + ).filter(m => m && m.id); + let msgIndex = messages.length > 1 ? 1 : 0; + for (const message of messages) { + let messageLogString = `message${ + msgIndex > 0 ? ` ${msgIndex} of ${messages.length}` : "" + }: ${chalk.italic.green(message.id)}`; + if (!isMessageValid(messageValidators[message.template], message)) { + console.log( + ` ${chalk.red("✗")} Skipping invalid ${messageLogString}` + ); + continue; + } + console.log(` Importing ${messageLogString}`); + let slug = `${experimentSlug}:${branchSlug}`; + if (msgIndex > 0) { + slug += ` (message ${msgIndex} of ${messages.length})`; + } + msgIndex += 1; + importItems.push({ message, slug, minVersion, maxVersion, url }); + } + } + } + } + } + + const content = `${LICENSE_STRING} + +/** + * This file is generated by browser/components/asrouter/bin/import-rollouts.js + * Run the following from the repository root to regenerate it: + * ./mach npm run import-rollouts --prefix=browser/components/asrouter + */ + +export const NimbusRolloutMessageProvider = { + getMessages() { + return [${importItems.map(annotateMessage).join(",\n")}]; + }, +}; +`; + + const formattedContent = await format(content); + + await util.promisify(fs.writeFile)(OUTPUT_PATH, formattedContent); + + console.log( + `${chalk.green("✓")} Wrote ${chalk.underline.green( + `${String(importItems.length)} ${ + importItems.length === 1 ? "message" : "messages" + }` + )} to ${chalk.underline.yellow(path.resolve(OUTPUT_PATH))}` + ); +} + +main(); diff --git a/browser/components/asrouter/content-src/asrouter-utils.js b/browser/components/asrouter/content-src/asrouter-utils.js new file mode 100644 index 0000000000..65d25cb907 --- /dev/null +++ b/browser/components/asrouter/content-src/asrouter-utils.js @@ -0,0 +1,79 @@ +/* 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 { MESSAGE_TYPE_HASH as msg } from "modules/ActorConstants.sys.mjs"; +import { actionCreators as ac } from "common/Actions.sys.mjs"; + +export const ASRouterUtils = { + addListener(listener) { + if (global.ASRouterAddParentListener) { + global.ASRouterAddParentListener(listener); + } + }, + removeListener(listener) { + if (global.ASRouterRemoveParentListener) { + global.ASRouterRemoveParentListener(listener); + } + }, + sendMessage(action) { + if (global.ASRouterMessage) { + return global.ASRouterMessage(action); + } + throw new Error(`Unexpected call:\n${JSON.stringify(action, null, 3)}`); + }, + blockById(id, options) { + return ASRouterUtils.sendMessage({ + type: msg.BLOCK_MESSAGE_BY_ID, + data: { id, ...options }, + }); + }, + modifyMessageJson(content) { + return ASRouterUtils.sendMessage({ + type: msg.MODIFY_MESSAGE_JSON, + data: { content }, + }); + }, + executeAction(button_action) { + return ASRouterUtils.sendMessage({ + type: msg.USER_ACTION, + data: button_action, + }); + }, + unblockById(id) { + return ASRouterUtils.sendMessage({ + type: msg.UNBLOCK_MESSAGE_BY_ID, + data: { id }, + }); + }, + blockBundle(bundle) { + return ASRouterUtils.sendMessage({ + type: msg.BLOCK_BUNDLE, + data: { bundle }, + }); + }, + unblockBundle(bundle) { + return ASRouterUtils.sendMessage({ + type: msg.UNBLOCK_BUNDLE, + data: { bundle }, + }); + }, + overrideMessage(id) { + return ASRouterUtils.sendMessage({ + type: msg.OVERRIDE_MESSAGE, + data: { id }, + }); + }, + editState(key, value) { + return ASRouterUtils.sendMessage({ + type: msg.EDIT_STATE, + data: { [key]: value }, + }); + }, + sendTelemetry(ping) { + return ASRouterUtils.sendMessage(ac.ASRouterUserEvent(ping)); + }, + getPreviewEndpoint() { + return null; + }, +}; diff --git a/browser/components/asrouter/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx b/browser/components/asrouter/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx new file mode 100644 index 0000000000..f16dbacbd8 --- /dev/null +++ b/browser/components/asrouter/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx @@ -0,0 +1,1498 @@ +/* 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 { ASRouterUtils } from "../../asrouter-utils"; +import React from "react"; +import ReactDOM from "react-dom"; +import { SimpleHashRouter } from "./SimpleHashRouter"; +import { CopyButton } from "./CopyButton"; +import { ImpressionsSection } from "./ImpressionsSection"; + +const Row = props => ( + + {props.children} + +); + +function relativeTime(timestamp) { + if (!timestamp) { + return ""; + } + const seconds = Math.floor((Date.now() - timestamp) / 1000); + const minutes = Math.floor((Date.now() - timestamp) / 60000); + if (seconds < 2) { + return "just now"; + } else if (seconds < 60) { + return `${seconds} seconds ago`; + } else if (minutes === 1) { + return "1 minute ago"; + } else if (minutes < 600) { + return `${minutes} minutes ago`; + } + return new Date(timestamp).toLocaleString(); +} + +export class ToggleStoryButton extends React.PureComponent { + constructor(props) { + super(props); + this.handleClick = this.handleClick.bind(this); + } + + handleClick() { + this.props.onClick(this.props.story); + } + + render() { + return ; + } +} + +export class ToggleMessageJSON extends React.PureComponent { + constructor(props) { + super(props); + this.handleClick = this.handleClick.bind(this); + } + + handleClick() { + this.props.toggleJSON(this.props.msgId); + } + + render() { + let iconName = this.props.isCollapsed + ? "icon icon-arrowhead-forward-small" + : "icon icon-arrowhead-down-small"; + 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 ASRouterAdminInner extends React.PureComponent { + constructor(props) { + super(props); + this.handleEnabledToggle = this.handleEnabledToggle.bind(this); + this.handleUserPrefToggle = this.handleUserPrefToggle.bind(this); + this.onChangeMessageFilter = this.onChangeMessageFilter.bind(this); + this.onChangeMessageGroupsFilter = + this.onChangeMessageGroupsFilter.bind(this); + this.unblockAll = this.unblockAll.bind(this); + this.handleClearAllImpressionsByProvider = + this.handleClearAllImpressionsByProvider.bind(this); + this.handleExpressionEval = this.handleExpressionEval.bind(this); + this.onChangeTargetingParameters = + this.onChangeTargetingParameters.bind(this); + this.onChangeAttributionParameters = + this.onChangeAttributionParameters.bind(this); + this.setAttribution = this.setAttribution.bind(this); + this.onCopyTargetingParams = this.onCopyTargetingParams.bind(this); + this.onNewTargetingParams = this.onNewTargetingParams.bind(this); + this.handleOpenPB = this.handleOpenPB.bind(this); + this.selectPBMessage = this.selectPBMessage.bind(this); + this.resetPBJSON = this.resetPBJSON.bind(this); + this.resetPBMessageState = this.resetPBMessageState.bind(this); + this.toggleJSON = this.toggleJSON.bind(this); + this.toggleAllMessages = this.toggleAllMessages.bind(this); + this.resetGroups = this.resetGroups.bind(this); + this.onMessageFromParent = this.onMessageFromParent.bind(this); + this.setStateFromParent = this.setStateFromParent.bind(this); + this.setState = this.setState.bind(this); + this.state = { + messageFilter: "all", + messageGroupsFilter: "all", + collapsedMessages: [], + modifiedMessages: [], + selectedPBMessage: "", + evaluationStatus: {}, + stringTargetingParameters: null, + newStringTargetingParameters: null, + copiedToClipboard: false, + attributionParameters: { + source: "addons.mozilla.org", + medium: "referral", + campaign: "non-fx-button", + content: `rta:${btoa("uBlock0@raymondhill.net")}`, + experiment: "ua-onboarding", + variation: "chrome", + ua: "Google Chrome 123", + dltoken: "00000000-0000-0000-0000-000000000000", + }, + }; + } + + onMessageFromParent({ type, data }) { + // These only exists due to onPrefChange events in ASRouter + switch (type) { + case "UpdateAdminState": { + this.setStateFromParent(data); + break; + } + } + } + + setStateFromParent(data) { + this.setState(data); + if (!this.state.stringTargetingParameters) { + const stringTargetingParameters = {}; + for (const param of Object.keys(data.targetingParameters)) { + stringTargetingParameters[param] = JSON.stringify( + data.targetingParameters[param], + null, + 2 + ); + } + this.setState({ stringTargetingParameters }); + } + } + + componentWillMount() { + ASRouterUtils.addListener(this.onMessageFromParent); + const endpoint = ASRouterUtils.getPreviewEndpoint(); + ASRouterUtils.sendMessage({ + type: "ADMIN_CONNECT_STATE", + data: { endpoint }, + }).then(this.setStateFromParent); + } + + componentWillUnmount() { + ASRouterUtils.removeListener(this.onMessageFromParent); + } + + handleBlock(msg) { + return () => ASRouterUtils.blockById(msg.id); + } + + handleUnblock(msg) { + return () => ASRouterUtils.unblockById(msg.id); + } + + resetJSON(msg) { + // reset the displayed JSON for the given message + document.getElementById(`${msg.id}-textarea`).value = JSON.stringify( + msg, + null, + 2 + ); + // remove the message from the list of modified IDs + let index = this.state.modifiedMessages.indexOf(msg.id); + this.setState(prevState => ({ + modifiedMessages: [ + ...prevState.modifiedMessages.slice(0, index), + ...prevState.modifiedMessages.slice(index + 1), + ], + })); + } + + handleOverride(id) { + return () => + ASRouterUtils.overrideMessage(id).then(state => { + this.setStateFromParent(state); + }); + } + + resetPBMessageState() { + // Iterate over Private Browsing messages and block/unblock each one to clear impressions + const PBMessages = this.state.messages.filter( + message => message.template === "pb_newtab" + ); // messages from state go here + + PBMessages.forEach(message => { + if (message?.id) { + ASRouterUtils.blockById(message.id); + ASRouterUtils.unblockById(message.id); + } + }); + // Clear the selected messages & radio buttons + document.getElementById("clear radio").checked = true; + this.selectPBMessage("clear"); + } + + resetPBJSON(msg) { + // reset the displayed JSON for the given message + document.getElementById(`${msg.id}-textarea`).value = JSON.stringify( + msg, + null, + 2 + ); + } + + handleOpenPB() { + ASRouterUtils.sendMessage({ + type: "FORCE_PRIVATE_BROWSING_WINDOW", + data: { message: { content: this.state.selectedPBMessage } }, + }); + } + + expireCache() { + ASRouterUtils.sendMessage({ type: "EXPIRE_QUERY_CACHE" }); + } + + resetPref() { + ASRouterUtils.sendMessage({ type: "RESET_PROVIDER_PREF" }); + } + + resetGroups(id, value) { + ASRouterUtils.sendMessage({ + type: "RESET_GROUPS_STATE", + }).then(this.setStateFromParent); + } + + handleExpressionEval() { + const context = {}; + for (const param of Object.keys(this.state.stringTargetingParameters)) { + const value = this.state.stringTargetingParameters[param]; + context[param] = value ? JSON.parse(value) : null; + } + ASRouterUtils.sendMessage({ + type: "EVALUATE_JEXL_EXPRESSION", + data: { + expression: this.refs.expressionInput.value, + context, + }, + }).then(this.setStateFromParent); + } + + onChangeTargetingParameters(event) { + const { name } = event.target; + const { value } = event.target; + + this.setState(({ stringTargetingParameters }) => { + let targetingParametersError = null; + const updatedParameters = { ...stringTargetingParameters }; + updatedParameters[name] = value; + try { + JSON.parse(value); + } catch (e) { + console.error(`Error parsing value of parameter ${name}`); + targetingParametersError = { id: name }; + } + + return { + copiedToClipboard: false, + evaluationStatus: {}, + stringTargetingParameters: updatedParameters, + targetingParametersError, + }; + }); + } + + unblockAll() { + return ASRouterUtils.sendMessage({ + type: "UNBLOCK_ALL", + }).then(this.setStateFromParent); + } + + handleClearAllImpressionsByProvider() { + const providerId = this.state.messageFilter; + if (!providerId) { + return; + } + const userPrefInfo = this.state.userPrefs; + + const isUserEnabled = + providerId in userPrefInfo ? userPrefInfo[providerId] : true; + + ASRouterUtils.sendMessage({ + type: "DISABLE_PROVIDER", + data: providerId, + }); + if (!isUserEnabled) { + ASRouterUtils.sendMessage({ + type: "SET_PROVIDER_USER_PREF", + data: { id: providerId, value: true }, + }); + } + ASRouterUtils.sendMessage({ + type: "ENABLE_PROVIDER", + data: providerId, + }); + } + + handleEnabledToggle(event) { + const provider = this.state.providerPrefs.find( + p => p.id === event.target.dataset.provider + ); + const userPrefInfo = this.state.userPrefs; + + const isUserEnabled = + provider.id in userPrefInfo ? userPrefInfo[provider.id] : true; + const isSystemEnabled = provider.enabled; + const isEnabling = event.target.checked; + + if (isEnabling) { + if (!isUserEnabled) { + ASRouterUtils.sendMessage({ + type: "SET_PROVIDER_USER_PREF", + data: { id: provider.id, value: true }, + }); + } + if (!isSystemEnabled) { + ASRouterUtils.sendMessage({ + type: "ENABLE_PROVIDER", + data: provider.id, + }); + } + } else { + ASRouterUtils.sendMessage({ + type: "DISABLE_PROVIDER", + data: provider.id, + }); + } + + this.setState({ messageFilter: "all" }); + } + + handleUserPrefToggle(event) { + const action = { + type: "SET_PROVIDER_USER_PREF", + data: { id: event.target.dataset.provider, value: event.target.checked }, + }; + ASRouterUtils.sendMessage(action); + this.setState({ messageFilter: "all" }); + } + + onChangeMessageFilter(event) { + this.setState({ messageFilter: event.target.value }); + } + + onChangeMessageGroupsFilter(event) { + this.setState({ messageGroupsFilter: event.target.value }); + } + + // Simulate a copy event that sets to clipboard all targeting paramters and values + onCopyTargetingParams(event) { + const stringTargetingParameters = { + ...this.state.stringTargetingParameters, + }; + for (const key of Object.keys(stringTargetingParameters)) { + // If the value is not set the parameter will be lost when we stringify + if (stringTargetingParameters[key] === undefined) { + stringTargetingParameters[key] = null; + } + } + const setClipboardData = e => { + e.preventDefault(); + e.clipboardData.setData( + "text", + JSON.stringify(stringTargetingParameters, null, 2) + ); + document.removeEventListener("copy", setClipboardData); + this.setState({ copiedToClipboard: true }); + }; + + document.addEventListener("copy", setClipboardData); + + document.execCommand("copy"); + } + + onNewTargetingParams(event) { + this.setState({ newStringTargetingParameters: event.target.value }); + event.target.classList.remove("errorState"); + this.refs.targetingParamsEval.innerText = ""; + + try { + const stringTargetingParameters = JSON.parse(event.target.value); + this.setState({ stringTargetingParameters }); + } catch (e) { + event.target.classList.add("errorState"); + this.refs.targetingParamsEval.innerText = e.message; + } + } + + toggleJSON(msgId) { + if (this.state.collapsedMessages.includes(msgId)) { + let index = this.state.collapsedMessages.indexOf(msgId); + this.setState(prevState => ({ + collapsedMessages: [ + ...prevState.collapsedMessages.slice(0, index), + ...prevState.collapsedMessages.slice(index + 1), + ], + })); + } else { + this.setState(prevState => ({ + collapsedMessages: prevState.collapsedMessages.concat(msgId), + })); + } + } + + handleChange(msgId) { + if (!this.state.modifiedMessages.includes(msgId)) { + this.setState(prevState => ({ + modifiedMessages: prevState.modifiedMessages.concat(msgId), + })); + } + } + + renderMessageItem(msg) { + const isBlockedByGroup = this.state.groups + .filter(group => msg.groups.includes(group.id)) + .some(group => !group.enabled); + const msgProvider = + this.state.providers.find(provider => provider.id === msg.provider) || {}; + const isProviderExcluded = + msgProvider.exclude && msgProvider.exclude.includes(msg.id); + const isMessageBlocked = + this.state.messageBlockList.includes(msg.id) || + this.state.messageBlockList.includes(msg.campaign); + const isBlocked = + isMessageBlocked || isBlockedByGroup || isProviderExcluded; + const impressions = this.state.messageImpressions[msg.id] + ? this.state.messageImpressions[msg.id].length + : 0; + const isCollapsed = this.state.collapsedMessages.includes(msg.id); + const isModified = this.state.modifiedMessages.includes(msg.id); + const aboutMessagePreviewSupported = [ + "infobar", + "spotlight", + "cfr_doorhanger", + ].includes(msg.template); + + let itemClassName = "message-item"; + if (isBlocked) { + itemClassName += " blocked"; + } + + return ( + + + + {msg.id}
+
+ + + + + + + { + // eslint-disable-next-line no-nested-ternary + isBlocked ? null : isModified ? ( + + ) : ( + + ) + } + {isBlocked ? null : ( + + )} + {aboutMessagePreviewSupported ? ( + + `about:messagepreview?json=${encodeURIComponent(btoa(text))}` + } + label="Share" + copiedLabel="Copied!" + inputSelector={`#${msg.id}-textarea`} + className={"button share"} + /> + ) : null} +
({impressions} impressions) + + + {isBlocked && ( + + Block reason: + {isBlockedByGroup && " Blocked by group"} + {isProviderExcluded && " Excluded by provider"} + {isMessageBlocked && " Message blocked"} + + )} + +
+              
+            
+ + + + ); + } + + selectPBMessage(msgId) { + if (msgId === "clear") { + this.setState({ + selectedPBMessage: "", + }); + } else { + let selected = document.getElementById(`${msgId} radio`); + let msg = JSON.parse(document.getElementById(`${msgId}-textarea`).value); + + if (selected.checked) { + this.setState({ + selectedPBMessage: msg?.content, + }); + } else { + this.setState({ + selectedPBMessage: "", + }); + } + } + } + + modifyJson(content) { + const message = JSON.parse( + document.getElementById(`${content.id}-textarea`).value + ); + return ASRouterUtils.modifyMessageJson(message).then(state => { + this.setStateFromParent(state); + }); + } + + renderPBMessageItem(msg) { + const isBlocked = + this.state.messageBlockList.includes(msg.id) || + this.state.messageBlockList.includes(msg.campaign); + const impressions = this.state.messageImpressions[msg.id] + ? this.state.messageImpressions[msg.id].length + : 0; + + const isCollapsed = this.state.collapsedMessages.includes(msg.id); + + let itemClassName = "message-item"; + if (isBlocked) { + itemClassName += " blocked"; + } + + return ( + + + + {msg.id}
+
({impressions} impressions) +
+ + + + + + this.selectPBMessage(msg.id)} + disabled={isBlocked} + /> + + + + +
+            
+          
+ + + ); + } + + toggleAllMessages(messagesToShow) { + if (this.state.collapsedMessages.length) { + this.setState({ + collapsedMessages: [], + }); + } else { + Array.prototype.forEach.call(messagesToShow, msg => { + this.setState(prevState => ({ + collapsedMessages: prevState.collapsedMessages.concat(msg.id), + })); + }); + } + } + + renderMessages() { + if (!this.state.messages) { + return null; + } + const messagesToShow = + this.state.messageFilter === "all" + ? this.state.messages + : this.state.messages.filter( + message => + message.provider === this.state.messageFilter && + message.template !== "pb_newtab" + ); + + return ( +
+ +

+ {" "} + + To modify a message, change the JSON and click 'Modify' to see your + changes. Click 'Reset' to restore the JSON to the original. Click + 'Share' to copy a link to the clipboard that can be used to preview + the message by opening the link in Nightly/local builds. + +

+ + + {messagesToShow.map(msg => this.renderMessageItem(msg))} + +
+
+ ); + } + + renderMessagesByGroup() { + if (!this.state.messages) { + return null; + } + const messagesToShow = + this.state.messageGroupsFilter === "all" + ? this.state.messages.filter(m => m.groups.length) + : this.state.messages.filter(message => + message.groups.includes(this.state.messageGroupsFilter) + ); + + return ( + + {messagesToShow.map(msg => this.renderMessageItem(msg))} +
+ ); + } + + renderPBMessages() { + if (!this.state.messages) { + return null; + } + const messagesToShow = this.state.messages.filter( + message => message.template === "pb_newtab" + ); + return ( + + + {messagesToShow.map(msg => this.renderPBMessageItem(msg))} + +
+ ); + } + + renderMessageFilter() { + if (!this.state.providers) { + return null; + } + + return ( +

+ + Show messages from{" "} + + {this.state.messageFilter !== "all" && + !this.state.messageFilter.includes("_local_testing") ? ( + + ) : null} +

+ ); + } + + renderMessageGroupsFilter() { + if (!this.state.groups) { + return null; + } + + return ( +

+ Show messages from {/* eslint-disable-next-line jsx-a11y/no-onchange */} + +

+ ); + } + + renderTableHead() { + return ( + + + + Provider ID + Source + Cohort + Last Updated + + + ); + } + + renderProviders() { + const providersConfig = this.state.providerPrefs; + const providerInfo = this.state.providers; + const userPrefInfo = this.state.userPrefs; + + return ( + + {this.renderTableHead()} + + {providersConfig.map((provider, i) => { + const isTestProvider = provider.id.includes("_local_testing"); + const info = providerInfo.find(p => p.id === provider.id) || {}; + const isUserEnabled = + provider.id in userPrefInfo ? userPrefInfo[provider.id] : true; + const isSystemEnabled = isTestProvider || provider.enabled; + + let label = "local"; + if (provider.type === "remote") { + label = ( + + endpoint ( + + {info.url} + + ) + + ); + } else if (provider.type === "remote-settings") { + label = `remote settings (${provider.collection})`; + } else if (provider.type === "remote-experiments") { + label = ( + + remote settings ( + + nimbus-desktop-experiments + + ) + + ); + } + + let reasonsDisabled = []; + if (!isSystemEnabled) { + reasonsDisabled.push("system pref"); + } + if (!isUserEnabled) { + reasonsDisabled.push("user pref"); + } + if (reasonsDisabled.length) { + label = `disabled via ${reasonsDisabled.join(", ")}`; + } + + return ( + + + + + + + + ); + })} + +
+ {isTestProvider ? ( + + ) : ( + + )} + {provider.id} + + {label} + + {provider.cohort} + {info.lastUpdated + ? new Date(info.lastUpdated).toLocaleString() + : ""} +
+ ); + } + + renderTargetingParameters() { + // There was no error and the result is truthy + const success = + this.state.evaluationStatus.success && + !!this.state.evaluationStatus.result; + const result = + JSON.stringify(this.state.evaluationStatus.result, null, 2) || + "(Empty result)"; + + return ( + + + + + + +
+

Evaluate JEXL expression

+
+

+