summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/content-src/asrouter/schemas
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/newtab/content-src/asrouter/schemas')
-rw-r--r--browser/components/newtab/content-src/asrouter/schemas/BackgroundTaskMessagingExperiment.schema.json312
-rw-r--r--browser/components/newtab/content-src/asrouter/schemas/FxMSCommon.schema.json128
-rw-r--r--browser/components/newtab/content-src/asrouter/schemas/MessagingExperiment.schema.json1640
-rw-r--r--browser/components/newtab/content-src/asrouter/schemas/corpus/ReachExperiments.messages.json15
-rw-r--r--browser/components/newtab/content-src/asrouter/schemas/extract-test-corpus.js65
-rwxr-xr-xbrowser/components/newtab/content-src/asrouter/schemas/make-schemas.py475
-rw-r--r--browser/components/newtab/content-src/asrouter/schemas/message-format.md101
-rw-r--r--browser/components/newtab/content-src/asrouter/schemas/message-group.schema.json64
-rw-r--r--browser/components/newtab/content-src/asrouter/schemas/provider-response.schema.json67
9 files changed, 2867 insertions, 0 deletions
diff --git a/browser/components/newtab/content-src/asrouter/schemas/BackgroundTaskMessagingExperiment.schema.json b/browser/components/newtab/content-src/asrouter/schemas/BackgroundTaskMessagingExperiment.schema.json
new file mode 100644
index 0000000000..109a9d6cd1
--- /dev/null
+++ b/browser/components/newtab/content-src/asrouter/schemas/BackgroundTaskMessagingExperiment.schema.json
@@ -0,0 +1,312 @@
+{
+ "$schema": "https://json-schema.org/draft/2019-09/schema",
+ "$id": "resource://activity-stream/schemas/BackgroundTaskMessagingExperiment.schema.json",
+ "title": "Messaging Experiment",
+ "description": "A Firefox Messaging System message.",
+ "if": {
+ "type": "object",
+ "properties": {
+ "template": {
+ "const": "multi"
+ }
+ },
+ "required": [
+ "template"
+ ]
+ },
+ "then": {
+ "$ref": "resource://activity-stream/schemas/BackgroundTaskMessagingExperiment.schema.json#/$defs/MultiMessage"
+ },
+ "else": {
+ "$ref": "resource://activity-stream/schemas/BackgroundTaskMessagingExperiment.schema.json#/$defs/TemplatedMessage"
+ },
+ "$defs": {
+ "ToastNotification": {
+ "$schema": "https://json-schema.org/draft/2019-09/schema",
+ "$id": "file:///ToastNotification.schema.json",
+ "title": "ToastNotification",
+ "description": "A template for toast notifications displayed by the Alert service.",
+ "allOf": [
+ {
+ "$ref": "resource://activity-stream/schemas/BackgroundTaskMessagingExperiment.schema.json#/$defs/Message"
+ }
+ ],
+ "type": "object",
+ "properties": {
+ "content": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "$ref": "resource://activity-stream/schemas/BackgroundTaskMessagingExperiment.schema.json#/$defs/localizableText",
+ "description": "Id of localized string or message override of toast notification title"
+ },
+ "body": {
+ "$ref": "resource://activity-stream/schemas/BackgroundTaskMessagingExperiment.schema.json#/$defs/localizableText",
+ "description": "Id of localized string or message override of toast notification body"
+ },
+ "icon_url": {
+ "description": "The URL of the image used as an icon of the toast notification.",
+ "type": "string",
+ "format": "moz-url-format"
+ },
+ "image_url": {
+ "description": "The URL of an image to be displayed as part of the notification.",
+ "type": "string",
+ "format": "moz-url-format"
+ },
+ "launch_url": {
+ "description": "The URL to launch when the notification or an action button is clicked.",
+ "type": "string",
+ "format": "moz-url-format"
+ },
+ "requireInteraction": {
+ "type": "boolean",
+ "description": "Whether the toast notification should remain active until the user clicks or dismisses it, rather than closing automatically."
+ },
+ "tag": {
+ "type": "string",
+ "description": "An identifying tag for the toast notification."
+ },
+ "data": {
+ "type": "object",
+ "description": "Arbitrary data associated with the toast notification."
+ },
+ "actions": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "$ref": "resource://activity-stream/schemas/BackgroundTaskMessagingExperiment.schema.json#/$defs/localizableText",
+ "description": "The action text to be shown to the user."
+ },
+ "action": {
+ "type": "string",
+ "description": "Opaque identifer that identifies action."
+ },
+ "iconURL": {
+ "type": "string",
+ "format": "uri",
+ "description": "URL of an icon to display with the action."
+ },
+ "windowsSystemActivationType": {
+ "type": "boolean",
+ "description": "Whether to have Windows process the given `action`."
+ }
+ },
+ "required": [
+ "action",
+ "title"
+ ],
+ "additionalProperties": true
+ }
+ }
+ },
+ "additionalProperties": true,
+ "required": [
+ "title",
+ "body"
+ ]
+ },
+ "template": {
+ "type": "string",
+ "const": "toast_notification"
+ }
+ },
+ "required": [
+ "content",
+ "targeting",
+ "template",
+ "trigger"
+ ],
+ "additionalProperties": true
+ },
+ "Message": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "The message identifier"
+ },
+ "groups": {
+ "description": "Array of preferences used to control `enabled` status of the group. If any is `false` the group is disabled.",
+ "type": "array",
+ "items": {
+ "type": "string",
+ "description": "Preference name"
+ }
+ },
+ "template": {
+ "type": "string",
+ "description": "Which messaging template this message is using.",
+ "enum": [
+ "toast_notification"
+ ]
+ },
+ "frequency": {
+ "type": "object",
+ "description": "An object containing frequency cap information for a message.",
+ "properties": {
+ "lifetime": {
+ "type": "integer",
+ "description": "The maximum lifetime impressions for a message.",
+ "minimum": 1,
+ "maximum": 100
+ },
+ "custom": {
+ "type": "array",
+ "description": "An array of custom frequency cap definitions.",
+ "items": {
+ "description": "A frequency cap definition containing time and max impression information",
+ "type": "object",
+ "properties": {
+ "period": {
+ "type": "integer",
+ "description": "Period of time in milliseconds (e.g. 86400000 for one day)"
+ },
+ "cap": {
+ "type": "integer",
+ "description": "The maximum impressions for the message within the defined period.",
+ "minimum": 1,
+ "maximum": 100
+ }
+ },
+ "required": [
+ "period",
+ "cap"
+ ]
+ }
+ }
+ }
+ },
+ "priority": {
+ "description": "The priority of the message. If there are two competing messages to show, the one with the highest priority will be shown",
+ "type": "integer"
+ },
+ "order": {
+ "description": "The order in which messages should be shown. Messages will be shown in increasing order.",
+ "type": "integer"
+ },
+ "targeting": {
+ "description": "A JEXL expression representing targeting information",
+ "type": "string"
+ },
+ "trigger": {
+ "description": "An action to trigger potentially showing the message",
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "A string identifying the trigger action"
+ },
+ "params": {
+ "type": "array",
+ "description": "An optional array of string parameters for the trigger action",
+ "items": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ }
+ }
+ },
+ "required": [
+ "id"
+ ]
+ },
+ "provider": {
+ "description": "An identifier for the provider of this message, such as \"cfr\" or \"preview\".",
+ "type": "string"
+ }
+ },
+ "additionalProperties": true,
+ "dependentRequired": {
+ "content": [
+ "id",
+ "template"
+ ],
+ "template": [
+ "id",
+ "content"
+ ]
+ }
+ },
+ "localizedText": {
+ "type": "object",
+ "properties": {
+ "string_id": {
+ "description": "Id of localized string to be rendered.",
+ "type": "string"
+ }
+ },
+ "required": [
+ "string_id"
+ ]
+ },
+ "localizableText": {
+ "description": "Either a raw string or an object containing the string_id of the localized text",
+ "oneOf": [
+ {
+ "type": "string",
+ "description": "The string to be rendered."
+ },
+ {
+ "$ref": "resource://activity-stream/schemas/BackgroundTaskMessagingExperiment.schema.json#/$defs/localizedText"
+ }
+ ]
+ },
+ "TemplatedMessage": {
+ "description": "An FxMS message of one of a variety of types.",
+ "type": "object",
+ "allOf": [
+ {
+ "$ref": "resource://activity-stream/schemas/BackgroundTaskMessagingExperiment.schema.json#/$defs/Message"
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "template": {
+ "type": "string",
+ "enum": [
+ "toast_notification"
+ ]
+ }
+ },
+ "required": [
+ "template"
+ ]
+ },
+ "then": {
+ "$ref": "resource://activity-stream/schemas/BackgroundTaskMessagingExperiment.schema.json#/$defs/ToastNotification"
+ }
+ }
+ ]
+ },
+ "MultiMessage": {
+ "description": "An object containing an array of messages.",
+ "type": "object",
+ "properties": {
+ "template": {
+ "type": "string",
+ "const": "multi"
+ },
+ "messages": {
+ "type": "array",
+ "description": "An array of messages.",
+ "items": {
+ "$ref": "resource://activity-stream/schemas/BackgroundTaskMessagingExperiment.schema.json#/$defs/TemplatedMessage"
+ }
+ }
+ },
+ "required": [
+ "template",
+ "messages"
+ ]
+ }
+ }
+}
diff --git a/browser/components/newtab/content-src/asrouter/schemas/FxMSCommon.schema.json b/browser/components/newtab/content-src/asrouter/schemas/FxMSCommon.schema.json
new file mode 100644
index 0000000000..51dbd3efa6
--- /dev/null
+++ b/browser/components/newtab/content-src/asrouter/schemas/FxMSCommon.schema.json
@@ -0,0 +1,128 @@
+{
+ "description": "Common elements used across FxMS schemas",
+ "$id": "file:///FxMSCommon.schema.json",
+ "$defs": {
+ "Message": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "The message identifier"
+ },
+ "groups": {
+ "description": "Array of preferences used to control `enabled` status of the group. If any is `false` the group is disabled.",
+ "type": "array",
+ "items": {
+ "type": "string",
+ "description": "Preference name"
+ }
+ },
+ "template": {
+ "type": "string",
+ "description": "Which messaging template this message is using."
+ },
+ "frequency": {
+ "type": "object",
+ "description": "An object containing frequency cap information for a message.",
+ "properties": {
+ "lifetime": {
+ "type": "integer",
+ "description": "The maximum lifetime impressions for a message.",
+ "minimum": 1,
+ "maximum": 100
+ },
+ "custom": {
+ "type": "array",
+ "description": "An array of custom frequency cap definitions.",
+ "items": {
+ "description": "A frequency cap definition containing time and max impression information",
+ "type": "object",
+ "properties": {
+ "period": {
+ "type": "integer",
+ "description": "Period of time in milliseconds (e.g. 86400000 for one day)"
+ },
+ "cap": {
+ "type": "integer",
+ "description": "The maximum impressions for the message within the defined period.",
+ "minimum": 1,
+ "maximum": 100
+ }
+ },
+ "required": ["period", "cap"]
+ }
+ }
+ }
+ },
+ "priority": {
+ "description": "The priority of the message. If there are two competing messages to show, the one with the highest priority will be shown",
+ "type": "integer"
+ },
+ "order": {
+ "description": "The order in which messages should be shown. Messages will be shown in increasing order.",
+ "type": "integer"
+ },
+ "targeting": {
+ "description": "A JEXL expression representing targeting information",
+ "type": "string"
+ },
+ "trigger": {
+ "description": "An action to trigger potentially showing the message",
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "A string identifying the trigger action"
+ },
+ "params": {
+ "type": "array",
+ "description": "An optional array of string parameters for the trigger action",
+ "items": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ }
+ }
+ },
+ "required": ["id"]
+ },
+ "provider": {
+ "description": "An identifier for the provider of this message, such as \"cfr\" or \"preview\".",
+ "type": "string"
+ }
+ },
+ "additionalProperties": true,
+ "dependentRequired": {
+ "content": ["id", "template"],
+ "template": ["id", "content"]
+ }
+ },
+ "localizedText": {
+ "type": "object",
+ "properties": {
+ "string_id": {
+ "description": "Id of localized string to be rendered.",
+ "type": "string"
+ }
+ },
+ "required": ["string_id"]
+ },
+ "localizableText": {
+ "description": "Either a raw string or an object containing the string_id of the localized text",
+ "oneOf": [
+ {
+ "type": "string",
+ "description": "The string to be rendered."
+ },
+ {
+ "$ref": "#/$defs/localizedText"
+ }
+ ]
+ }
+ }
+}
diff --git a/browser/components/newtab/content-src/asrouter/schemas/MessagingExperiment.schema.json b/browser/components/newtab/content-src/asrouter/schemas/MessagingExperiment.schema.json
new file mode 100644
index 0000000000..abd36cf6cf
--- /dev/null
+++ b/browser/components/newtab/content-src/asrouter/schemas/MessagingExperiment.schema.json
@@ -0,0 +1,1640 @@
+{
+ "$schema": "https://json-schema.org/draft/2019-09/schema",
+ "$id": "resource://activity-stream/schemas/MessagingExperiment.schema.json",
+ "title": "Messaging Experiment",
+ "description": "A Firefox Messaging System message.",
+ "if": {
+ "type": "object",
+ "properties": {
+ "template": {
+ "const": "multi"
+ }
+ },
+ "required": [
+ "template"
+ ]
+ },
+ "then": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/MultiMessage"
+ },
+ "else": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/TemplatedMessage"
+ },
+ "$defs": {
+ "CFRUrlbarChiclet": {
+ "$schema": "https://json-schema.org/draft/2019-09/schema",
+ "$id": "file:///CFRUrlbarChiclet.schema.json",
+ "title": "CFRUrlbarChiclet",
+ "description": "A template with a chiclet button with text.",
+ "allOf": [
+ {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/Message"
+ }
+ ],
+ "type": "object",
+ "properties": {
+ "content": {
+ "type": "object",
+ "properties": {
+ "category": {
+ "type": "string",
+ "description": "Attribute used for different groups of messages from the same provider"
+ },
+ "layout": {
+ "type": "string",
+ "description": "Describes how content should be displayed.",
+ "enum": [
+ "chiclet_open_url"
+ ]
+ },
+ "bucket_id": {
+ "type": "string",
+ "description": "A bucket identifier for the addon. This is used in order to anonymize telemetry for history-sensitive targeting."
+ },
+ "notification_text": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
+ "description": "The text in the small blue chicklet that appears in the URL bar. This can be a reference to a localized string in Firefox or just a plain string."
+ },
+ "active_color": {
+ "type": "string",
+ "description": "Background color of the button"
+ },
+ "action": {
+ "type": "object",
+ "properties": {
+ "url": {
+ "description": "The page to open when the button is clicked.",
+ "type": "string",
+ "format": "moz-url-format"
+ },
+ "where": {
+ "description": "Should it open in a new tab or the current tab",
+ "type": "string",
+ "enum": [
+ "current",
+ "tabshifted"
+ ]
+ }
+ },
+ "additionalProperties": true,
+ "required": [
+ "url",
+ "where"
+ ]
+ }
+ },
+ "additionalProperties": true,
+ "required": [
+ "layout",
+ "category",
+ "bucket_id",
+ "notification_text",
+ "action"
+ ]
+ },
+ "template": {
+ "type": "string",
+ "const": "cfr_urlbar_chiclet"
+ }
+ },
+ "required": [
+ "targeting",
+ "trigger"
+ ]
+ },
+ "ExtensionDoorhanger": {
+ "$schema": "https://json-schema.org/draft/2019-09/schema",
+ "$id": "file:///ExtensionDoorhanger.schema.json",
+ "title": "ExtensionDoorhanger",
+ "description": "A template with a heading, addon icon, title and description. No markup allowed.",
+ "allOf": [
+ {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/Message"
+ }
+ ],
+ "type": "object",
+ "properties": {
+ "content": {
+ "type": "object",
+ "properties": {
+ "category": {
+ "type": "string",
+ "description": "Attribute used for different groups of messages from the same provider"
+ },
+ "layout": {
+ "type": "string",
+ "description": "Attribute used for different groups of messages from the same provider",
+ "enum": [
+ "short_message",
+ "icon_and_message",
+ "addon_recommendation"
+ ]
+ },
+ "anchor_id": {
+ "type": "string",
+ "description": "A DOM element ID that the pop-over will be anchored."
+ },
+ "alt_anchor_id": {
+ "type": "string",
+ "description": "An alternate DOM element ID that the pop-over will be anchored."
+ },
+ "bucket_id": {
+ "type": "string",
+ "description": "A bucket identifier for the addon. This is used in order to anonymize telemetry for history-sensitive targeting."
+ },
+ "skip_address_bar_notifier": {
+ "type": "boolean",
+ "description": "Skip the 'Recommend' notifier and show directly."
+ },
+ "persistent_doorhanger": {
+ "type": "boolean",
+ "description": "Prevent the doorhanger from being dismissed if user interacts with the page or switches between applications."
+ },
+ "show_in_private_browsing": {
+ "type": "boolean",
+ "description": "Whether to allow the message to be shown in private browsing mode. Defaults to false."
+ },
+ "notification_text": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
+ "description": "The text in the small blue chicklet that appears in the URL bar. This can be a reference to a localized string in Firefox or just a plain string."
+ },
+ "info_icon": {
+ "type": "object",
+ "description": "The small icon displayed in the top right corner of the pop-over. Should be 19x19px, svg or png. Defaults to a small question mark.",
+ "properties": {
+ "label": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "attributes": {
+ "type": "object",
+ "properties": {
+ "tooltiptext": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
+ "description": "Text for button tooltip used to provide information about the doorhanger."
+ }
+ },
+ "required": [
+ "tooltiptext"
+ ]
+ }
+ },
+ "required": [
+ "attributes"
+ ]
+ },
+ {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/localizedText"
+ }
+ ]
+ },
+ "sumo_path": {
+ "type": "string",
+ "description": "Last part of the path in the URL to the support page with the information about the doorhanger.",
+ "examples": [
+ "extensionpromotions",
+ "extensionrecommendations"
+ ]
+ }
+ }
+ },
+ "learn_more": {
+ "type": "string",
+ "description": "Last part of the path in the SUMO URL to the support page with the information about the doorhanger.",
+ "examples": [
+ "extensionpromotions",
+ "extensionrecommendations"
+ ]
+ },
+ "heading_text": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
+ "description": "The larger heading text displayed in the pop-over. This can be a reference to a localized string in Firefox or just a plain string."
+ },
+ "icon": {
+ "$ref": "file:///ExtensionDoorhanger.schema.json#/$defs/linkUrl",
+ "description": "The icon displayed in the pop-over. Should be 32x32px or 64x64px and png/svg."
+ },
+ "icon_dark_theme": {
+ "type": "string",
+ "description": "Pop-over icon, dark theme variant. Should be 32x32px or 64x64px and png/svg."
+ },
+ "icon_class": {
+ "type": "string",
+ "description": "CSS class of the pop-over icon."
+ },
+ "addon": {
+ "description": "Addon information including AMO URL.",
+ "type": "object",
+ "properties": {
+ "id": {
+ "$ref": "file:///ExtensionDoorhanger.schema.json#/$defs/plainText",
+ "description": "Unique addon ID"
+ },
+ "title": {
+ "$ref": "file:///ExtensionDoorhanger.schema.json#/$defs/plainText",
+ "description": "Addon name"
+ },
+ "author": {
+ "$ref": "file:///ExtensionDoorhanger.schema.json#/$defs/plainText",
+ "description": "Addon author"
+ },
+ "icon": {
+ "$ref": "file:///ExtensionDoorhanger.schema.json#/$defs/linkUrl",
+ "description": "The icon displayed in the pop-over. Should be 64x64px and png/svg."
+ },
+ "rating": {
+ "type": "string",
+ "description": "Star rating"
+ },
+ "users": {
+ "type": "string",
+ "description": "Installed users"
+ },
+ "amo_url": {
+ "$ref": "file:///ExtensionDoorhanger.schema.json#/$defs/linkUrl",
+ "description": "Link that offers more information related to the addon."
+ }
+ },
+ "required": [
+ "title",
+ "author",
+ "icon",
+ "amo_url"
+ ]
+ },
+ "text": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
+ "description": "The body text displayed in the pop-over. This can be a reference to a localized string in Firefox or just a plain string."
+ },
+ "descriptionDetails": {
+ "description": "Additional information and steps on how to use",
+ "type": "object",
+ "properties": {
+ "steps": {
+ "description": "Array of string_ids",
+ "type": "array",
+ "items": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/localizedText",
+ "description": "Id of string to localized addon description"
+ }
+ }
+ },
+ "required": [
+ "steps"
+ ]
+ },
+ "buttons": {
+ "description": "The label and functionality for the buttons in the pop-over.",
+ "type": "object",
+ "properties": {
+ "primary": {
+ "type": "object",
+ "properties": {
+ "label": {
+ "type": "object",
+ "oneOf": [
+ {
+ "properties": {
+ "value": {
+ "$ref": "file:///ExtensionDoorhanger.schema.json#/$defs/plainText",
+ "description": "Button label override used when a localized version is not available."
+ },
+ "attributes": {
+ "type": "object",
+ "properties": {
+ "accesskey": {
+ "type": "string",
+ "description": "A single character to be used as a shortcut key for the secondary button. This should be one of the characters that appears in the button label."
+ }
+ },
+ "required": [
+ "accesskey"
+ ],
+ "description": "Button attributes."
+ }
+ },
+ "required": [
+ "value",
+ "attributes"
+ ]
+ },
+ {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/localizedText"
+ }
+ ],
+ "description": "Id of localized string or message override."
+ },
+ "action": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Action dispatched by the button."
+ },
+ "data": {
+ "properties": {
+ "url": {
+ "type": "string",
+ "$comment": "This is dynamically generated from the addon.id. See CFRPageActions.jsm",
+ "description": "URL used in combination with the primary action dispatched."
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "secondary": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "label": {
+ "type": "object",
+ "oneOf": [
+ {
+ "properties": {
+ "value": {
+ "allOf": [
+ {
+ "$ref": "file:///ExtensionDoorhanger.schema.json#/$defs/plainText"
+ },
+ {
+ "description": "Button label override used when a localized version is not available."
+ }
+ ]
+ },
+ "attributes": {
+ "type": "object",
+ "properties": {
+ "accesskey": {
+ "type": "string",
+ "description": "A single character to be used as a shortcut key for the secondary button. This should be one of the characters that appears in the button label."
+ }
+ },
+ "required": [
+ "accesskey"
+ ],
+ "description": "Button attributes."
+ }
+ },
+ "required": [
+ "value",
+ "attributes"
+ ]
+ },
+ {
+ "properties": {
+ "string_id": {
+ "allOf": [
+ {
+ "$ref": "file:///ExtensionDoorhanger.schema.json#/$defs/plainText"
+ },
+ {
+ "description": "Id of localized string for button"
+ }
+ ]
+ }
+ },
+ "required": [
+ "string_id"
+ ]
+ }
+ ],
+ "description": "Id of localized string or message override."
+ },
+ "action": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Action dispatched by the button."
+ },
+ "data": {
+ "properties": {
+ "url": {
+ "allOf": [
+ {
+ "$ref": "file:///ExtensionDoorhanger.schema.json#/$defs/linkUrl"
+ },
+ {
+ "description": "URL used in combination with the primary action dispatched."
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "additionalProperties": true,
+ "required": [
+ "layout",
+ "bucket_id",
+ "heading_text",
+ "text",
+ "buttons"
+ ],
+ "if": {
+ "properties": {
+ "skip_address_bar_notifier": {
+ "anyOf": [
+ {
+ "const": "false"
+ },
+ {
+ "const": null
+ }
+ ]
+ }
+ }
+ },
+ "then": {
+ "required": [
+ "category",
+ "notification_text"
+ ]
+ }
+ },
+ "template": {
+ "type": "string",
+ "enum": [
+ "cfr_doorhanger",
+ "milestone_message"
+ ]
+ }
+ },
+ "additionalProperties": true,
+ "required": [
+ "targeting",
+ "trigger"
+ ],
+ "$defs": {
+ "plainText": {
+ "description": "Plain text (no HTML allowed)",
+ "type": "string"
+ },
+ "linkUrl": {
+ "description": "Target for links or buttons",
+ "type": "string",
+ "format": "uri"
+ }
+ }
+ },
+ "InfoBar": {
+ "$schema": "https://json-schema.org/draft/2019-09/schema",
+ "$id": "file:///InfoBar.schema.json",
+ "title": "InfoBar",
+ "description": "A template with an image, test and buttons.",
+ "allOf": [
+ {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/Message"
+ }
+ ],
+ "type": "object",
+ "properties": {
+ "content": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Should the message be global (persisted across tabs) or local (disappear when switching to a different tab).",
+ "enum": [
+ "global",
+ "tab"
+ ]
+ },
+ "text": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
+ "description": "The text show in the notification box."
+ },
+ "priority": {
+ "description": "Infobar priority level https://searchfox.org/mozilla-central/rev/3aef835f6cb12e607154d56d68726767172571e4/toolkit/content/widgets/notificationbox.js#387",
+ "type": "number",
+ "minumum": 0,
+ "exclusiveMaximum": 10
+ },
+ "buttons": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "label": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
+ "description": "The text label of the button."
+ },
+ "primary": {
+ "type": "boolean",
+ "description": "Is this the primary button?"
+ },
+ "accessKey": {
+ "type": "string",
+ "description": "Keyboard shortcut letter."
+ },
+ "action": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Action dispatched by the button."
+ },
+ "data": {
+ "type": "object"
+ }
+ },
+ "required": [
+ "type"
+ ],
+ "additionalProperties": true
+ },
+ "supportPage": {
+ "type": "string",
+ "description": "A page title on SUMO to link to"
+ }
+ },
+ "required": [
+ "label",
+ "action"
+ ],
+ "additionalProperties": true
+ }
+ }
+ },
+ "additionalProperties": true,
+ "required": [
+ "text",
+ "buttons"
+ ]
+ },
+ "template": {
+ "type": "string",
+ "const": "infobar"
+ }
+ },
+ "additionalProperties": true,
+ "required": [
+ "targeting",
+ "trigger"
+ ],
+ "$defs": {
+ "plainText": {
+ "description": "Plain text (no HTML allowed)",
+ "type": "string"
+ },
+ "linkUrl": {
+ "description": "Target for links or buttons",
+ "type": "string",
+ "format": "uri"
+ }
+ }
+ },
+ "NewtabPromoMessage": {
+ "$schema": "https://json-schema.org/draft/2019-09/schema",
+ "$id": "file:///NewtabPromoMessage.schema.json",
+ "title": "PBNewtabPromoMessage",
+ "description": "Message shown on the private browsing newtab page.",
+ "allOf": [
+ {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/Message"
+ }
+ ],
+ "type": "object",
+ "properties": {
+ "content": {
+ "type": "object",
+ "properties": {
+ "hideDefault": {
+ "type": "boolean",
+ "description": "Should we hide the default promo after the experiment promo is dismissed."
+ },
+ "infoEnabled": {
+ "type": "boolean",
+ "description": "Should we show the info section."
+ },
+ "infoIcon": {
+ "type": "string",
+ "description": "Icon shown in the left side of the info section. Default is the private browsing icon."
+ },
+ "infoTitle": {
+ "type": "string",
+ "description": "Is the title in the info section enabled."
+ },
+ "infoTitleEnabled": {
+ "type": "boolean",
+ "description": "Is the title in the info section enabled."
+ },
+ "infoBody": {
+ "type": "string",
+ "description": "Text content in the info section."
+ },
+ "infoLinkText": {
+ "type": "string",
+ "description": "Text for the link in the info section."
+ },
+ "infoLinkUrl": {
+ "type": "string",
+ "description": "URL for the info section link.",
+ "format": "moz-url-format"
+ },
+ "promoEnabled": {
+ "type": "boolean",
+ "description": "Should we show the promo section."
+ },
+ "promoType": {
+ "type": "string",
+ "description": "Promo type used to determine if promo should show to a given user",
+ "enum": [
+ "FOCUS",
+ "VPN",
+ "PIN",
+ "COOKIE_BANNERS",
+ "OTHER"
+ ]
+ },
+ "promoSectionStyle": {
+ "type": "string",
+ "description": "Sets the position of the promo section. Possible values are: top, below-search, bottom. Default bottom.",
+ "enum": [
+ "top",
+ "below-search",
+ "bottom"
+ ]
+ },
+ "promoTitle": {
+ "type": "string",
+ "description": "The text content of the promo section."
+ },
+ "promoTitleEnabled": {
+ "type": "boolean",
+ "description": "Should we show text content in the promo section."
+ },
+ "promoLinkText": {
+ "type": "string",
+ "description": "The text of the link in the promo box."
+ },
+ "promoHeader": {
+ "type": "string",
+ "description": "The title of the promo section."
+ },
+ "promoButton": {
+ "type": "object",
+ "properties": {
+ "action": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Action dispatched by the button."
+ },
+ "data": {
+ "type": "object"
+ }
+ },
+ "required": [
+ "type"
+ ],
+ "additionalProperties": true
+ }
+ },
+ "required": [
+ "action"
+ ]
+ },
+ "promoLinkType": {
+ "type": "string",
+ "description": "Type of promo link type. Possible values: link, button. Default is link.",
+ "enum": [
+ "link",
+ "button"
+ ]
+ },
+ "promoImageLarge": {
+ "type": "string",
+ "description": "URL for image used on the left side of the promo box, larger, showcases some feature. Default off.",
+ "format": "uri"
+ },
+ "promoImageSmall": {
+ "type": "string",
+ "description": "URL for image used on the right side of the promo box, smaller, usually a logo. Default off.",
+ "format": "uri"
+ }
+ },
+ "additionalProperties": true,
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "promoEnabled": {
+ "const": true
+ }
+ },
+ "required": [
+ "promoEnabled"
+ ]
+ },
+ "then": {
+ "required": [
+ "promoButton"
+ ]
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "infoEnabled": {
+ "const": true
+ }
+ },
+ "required": [
+ "infoEnabled"
+ ]
+ },
+ "then": {
+ "required": [
+ "infoLinkText"
+ ],
+ "if": {
+ "properties": {
+ "infoTitleEnabled": {
+ "const": true
+ }
+ },
+ "required": [
+ "infoTitleEnabled"
+ ]
+ },
+ "then": {
+ "required": [
+ "infoTitle"
+ ]
+ }
+ }
+ }
+ ]
+ },
+ "template": {
+ "type": "string",
+ "const": "pb_newtab"
+ }
+ },
+ "additionalProperties": true,
+ "required": [
+ "targeting"
+ ]
+ },
+ "ProtectionsPanelMessage": {
+ "$schema": "https://json-schema.org/draft/2019-09/schema",
+ "$id": "file:///ProtectionsPanelMessage.schema.json",
+ "title": "ProtectionsPanelMessage",
+ "description": "A message shown in the protections panel.",
+ "allOf": [
+ {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/Message"
+ }
+ ],
+ "type": "object",
+ "properties": {
+ "content": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "description": "The message title.",
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/localizableText"
+ },
+ "body": {
+ "description": "The body of the message.",
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/localizableText"
+ },
+ "link_text": {
+ "description": "The text of the call to action link.",
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/localizableText"
+ },
+ "cta_type": {
+ "description": "The type of URL open action.",
+ "type": "string",
+ "enum": [
+ "OPEN_URL",
+ "OPEN_PROTECTION_REPORT",
+ "OPEN_ABOUT_PAGE"
+ ]
+ },
+ "cta_url": {
+ "description": "The URL to open when the call to action is clicked",
+ "type": "string",
+ "format": "moz-url-format"
+ },
+ "cta_where": {
+ "description": "How to open the cta.",
+ "type": "string",
+ "enum": [
+ "current",
+ "tabshifted",
+ "tab",
+ "save",
+ "window"
+ ]
+ }
+ },
+ "dependantSchemas": {
+ "link_text": [
+ "cta_type",
+ "cta_url"
+ ],
+ "cta_type": [
+ "link_text"
+ ],
+ "cta_url": [
+ "link_text"
+ ],
+ "cta_where": [
+ "link_text"
+ ]
+ },
+ "additionalProperties": false,
+ "required": [
+ "title",
+ "body"
+ ]
+ },
+ "template": {
+ "type": "string",
+ "const": "protections_panel"
+ },
+ "trigger": {
+ "description": "An action to trigger potentially showing the message. The action ID `protectionsPanelOpen` is required.",
+ "const": {
+ "id": "protectionsPanelOpen"
+ }
+ }
+ },
+ "required": [
+ "content",
+ "template",
+ "trigger"
+ ],
+ "additionalProperties": true
+ },
+ "Spotlight": {
+ "$schema": "https://json-schema.org/draft/2019-09/schema",
+ "$id": "file:///Spotlight.schema.json",
+ "title": "Spotlight",
+ "description": "A template with an image, title, content and two buttons.",
+ "allOf": [
+ {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/Message"
+ }
+ ],
+ "type": "object",
+ "properties": {
+ "content": {
+ "type": "object",
+ "properties": {
+ "template": {
+ "type": "string",
+ "description": "Specify the layout template for the Spotlight",
+ "const": "multistage"
+ },
+ "backdrop": {
+ "type": "string",
+ "description": "Background css behind modal content"
+ },
+ "logo": {
+ "type": "object",
+ "properties": {
+ "imageURL": {
+ "type": "string",
+ "description": "URL for image to use with the content"
+ },
+ "imageId": {
+ "type": "string",
+ "description": "The ID for a remotely hosted image"
+ },
+ "size": {
+ "type": "string",
+ "description": "The logo size."
+ }
+ },
+ "additionalProperties": true
+ },
+ "screens": {
+ "type": "array",
+ "description": "Collection of individual screen content"
+ },
+ "transitions": {
+ "type": "boolean",
+ "description": "Show transitions within and between screens"
+ },
+ "disableHistoryUpdates": {
+ "type": "boolean",
+ "description": "Don't alter the browser session's history stack - used with messaging surfaces like Feature Callouts"
+ },
+ "startScreen": {
+ "type": "integer",
+ "description": "Index of first screen to show from message, defaulting to 0"
+ }
+ },
+ "additionalProperties": true
+ },
+ "template": {
+ "type": "string",
+ "description": "Specify whether the surface is shown as a Spotlight modal or an in-surface Feature Callout dialog",
+ "enum": [
+ "spotlight",
+ "feature_callout"
+ ]
+ }
+ },
+ "additionalProperties": true,
+ "required": [
+ "targeting"
+ ]
+ },
+ "ToastNotification": {
+ "$schema": "https://json-schema.org/draft/2019-09/schema",
+ "$id": "file:///ToastNotification.schema.json",
+ "title": "ToastNotification",
+ "description": "A template for toast notifications displayed by the Alert service.",
+ "allOf": [
+ {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/Message"
+ }
+ ],
+ "type": "object",
+ "properties": {
+ "content": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
+ "description": "Id of localized string or message override of toast notification title"
+ },
+ "body": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
+ "description": "Id of localized string or message override of toast notification body"
+ },
+ "icon_url": {
+ "description": "The URL of the image used as an icon of the toast notification.",
+ "type": "string",
+ "format": "moz-url-format"
+ },
+ "image_url": {
+ "description": "The URL of an image to be displayed as part of the notification.",
+ "type": "string",
+ "format": "moz-url-format"
+ },
+ "launch_url": {
+ "description": "The URL to launch when the notification or an action button is clicked.",
+ "type": "string",
+ "format": "moz-url-format"
+ },
+ "requireInteraction": {
+ "type": "boolean",
+ "description": "Whether the toast notification should remain active until the user clicks or dismisses it, rather than closing automatically."
+ },
+ "tag": {
+ "type": "string",
+ "description": "An identifying tag for the toast notification."
+ },
+ "data": {
+ "type": "object",
+ "description": "Arbitrary data associated with the toast notification."
+ },
+ "actions": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
+ "description": "The action text to be shown to the user."
+ },
+ "action": {
+ "type": "string",
+ "description": "Opaque identifer that identifies action."
+ },
+ "iconURL": {
+ "type": "string",
+ "format": "uri",
+ "description": "URL of an icon to display with the action."
+ },
+ "windowsSystemActivationType": {
+ "type": "boolean",
+ "description": "Whether to have Windows process the given `action`."
+ }
+ },
+ "required": [
+ "action",
+ "title"
+ ],
+ "additionalProperties": true
+ }
+ }
+ },
+ "additionalProperties": true,
+ "required": [
+ "title",
+ "body"
+ ]
+ },
+ "template": {
+ "type": "string",
+ "const": "toast_notification"
+ }
+ },
+ "required": [
+ "content",
+ "targeting",
+ "template",
+ "trigger"
+ ],
+ "additionalProperties": true
+ },
+ "ToolbarBadgeMessage": {
+ "$schema": "https://json-schema.org/draft/2019-09/schema",
+ "$id": "file:///ToolbarBadgeMessage.schema.json",
+ "title": "ToolbarBadgeMessage",
+ "description": "A template that specifies to which element in the browser toolbar to add a notification.",
+ "allOf": [
+ {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/Message"
+ }
+ ],
+ "type": "object",
+ "properties": {
+ "content": {
+ "type": "object",
+ "properties": {
+ "target": {
+ "type": "string"
+ },
+ "action": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": true,
+ "required": [
+ "id"
+ ],
+ "description": "Optional action to take in addition to showing the notification"
+ },
+ "delay": {
+ "type": "number",
+ "description": "Optional delay in ms after which to show the notification"
+ },
+ "badgeDescription": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/localizedText",
+ "description": "This is used in combination with the badged button to offer a text based alternative to the visual badging. Example 'New Feature: What's New'"
+ }
+ },
+ "additionalProperties": true,
+ "required": [
+ "target"
+ ]
+ },
+ "template": {
+ "type": "string",
+ "const": "toolbar_badge"
+ }
+ },
+ "additionalProperties": true,
+ "required": [
+ "targeting"
+ ]
+ },
+ "UpdateAction": {
+ "$schema": "https://json-schema.org/draft/2019-09/schema",
+ "$id": "file:///UpdateAction.schema.json",
+ "title": "UpdateActionMessage",
+ "description": "A template for messages that execute predetermined actions.",
+ "allOf": [
+ {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/Message"
+ }
+ ],
+ "type": "object",
+ "properties": {
+ "content": {
+ "type": "object",
+ "properties": {
+ "action": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "data": {
+ "type": "object",
+ "description": "Additional data provided as argument when executing the action",
+ "properties": {
+ "url": {
+ "type": "string",
+ "description": "URL data to be used as argument to the action"
+ },
+ "expireDelta": {
+ "type": "number",
+ "description": "Expiration timestamp to be used as argument to the action"
+ }
+ }
+ }
+ },
+ "additionalProperties": true,
+ "description": "Optional action to take in addition to showing the notification",
+ "required": [
+ "id",
+ "data"
+ ]
+ }
+ },
+ "additionalProperties": true,
+ "required": [
+ "action"
+ ]
+ },
+ "template": {
+ "type": "string",
+ "const": "update_action"
+ }
+ },
+ "required": [
+ "targeting"
+ ]
+ },
+ "WhatsNewMessage": {
+ "$schema": "https://json-schema.org/draft/2019-09/schema",
+ "$id": "file:///WhatsNewMessage.schema.json",
+ "title": "WhatsNewMessage",
+ "description": "A template for the messages that appear in the What's New panel.",
+ "allOf": [
+ {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/Message"
+ }
+ ],
+ "type": "object",
+ "properties": {
+ "content": {
+ "type": "object",
+ "properties": {
+ "layout": {
+ "description": "Different message layouts",
+ "enum": [
+ "tracking-protections"
+ ]
+ },
+ "bucket_id": {
+ "type": "string",
+ "description": "A bucket identifier for the addon. This is used in order to anonymize telemetry for history-sensitive targeting."
+ },
+ "published_date": {
+ "type": "integer",
+ "description": "The date/time (number of milliseconds elapsed since January 1, 1970 00:00:00 UTC) the message was published."
+ },
+ "title": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
+ "description": "Id of localized string or message override of What's New message title"
+ },
+ "subtitle": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
+ "description": "Id of localized string or message override of What's New message subtitle"
+ },
+ "body": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
+ "description": "Id of localized string or message override of What's New message body"
+ },
+ "link_text": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
+ "description": "(optional) Id of localized string or message override of What's New message link text"
+ },
+ "cta_url": {
+ "description": "Target URL for the What's New message.",
+ "type": "string",
+ "format": "moz-url-format"
+ },
+ "cta_type": {
+ "description": "Type of url open action",
+ "enum": [
+ "OPEN_URL",
+ "OPEN_ABOUT_PAGE",
+ "OPEN_PROTECTION_REPORT"
+ ]
+ },
+ "cta_where": {
+ "description": "How to open the cta: new window, tab, focused, unfocused.",
+ "enum": [
+ "current",
+ "tabshifted",
+ "tab",
+ "save",
+ "window"
+ ]
+ },
+ "icon_url": {
+ "description": "(optional) URL for the What's New message icon.",
+ "type": "string",
+ "format": "uri"
+ },
+ "icon_alt": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/localizableText",
+ "description": "Alt text for image."
+ }
+ },
+ "additionalProperties": true,
+ "required": [
+ "published_date",
+ "title",
+ "body",
+ "cta_url",
+ "bucket_id"
+ ]
+ },
+ "template": {
+ "type": "string",
+ "const": "whatsnew_panel_message"
+ }
+ },
+ "required": [
+ "order"
+ ],
+ "additionalProperties": true
+ },
+ "Message": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "The message identifier"
+ },
+ "groups": {
+ "description": "Array of preferences used to control `enabled` status of the group. If any is `false` the group is disabled.",
+ "type": "array",
+ "items": {
+ "type": "string",
+ "description": "Preference name"
+ }
+ },
+ "template": {
+ "type": "string",
+ "description": "Which messaging template this message is using.",
+ "enum": [
+ "cfr_urlbar_chiclet",
+ "cfr_doorhanger",
+ "milestone_message",
+ "infobar",
+ "pb_newtab",
+ "protections_panel",
+ "spotlight",
+ "feature_callout",
+ "toast_notification",
+ "toolbar_badge",
+ "update_action",
+ "whatsnew_panel_message"
+ ]
+ },
+ "frequency": {
+ "type": "object",
+ "description": "An object containing frequency cap information for a message.",
+ "properties": {
+ "lifetime": {
+ "type": "integer",
+ "description": "The maximum lifetime impressions for a message.",
+ "minimum": 1,
+ "maximum": 100
+ },
+ "custom": {
+ "type": "array",
+ "description": "An array of custom frequency cap definitions.",
+ "items": {
+ "description": "A frequency cap definition containing time and max impression information",
+ "type": "object",
+ "properties": {
+ "period": {
+ "type": "integer",
+ "description": "Period of time in milliseconds (e.g. 86400000 for one day)"
+ },
+ "cap": {
+ "type": "integer",
+ "description": "The maximum impressions for the message within the defined period.",
+ "minimum": 1,
+ "maximum": 100
+ }
+ },
+ "required": [
+ "period",
+ "cap"
+ ]
+ }
+ }
+ }
+ },
+ "priority": {
+ "description": "The priority of the message. If there are two competing messages to show, the one with the highest priority will be shown",
+ "type": "integer"
+ },
+ "order": {
+ "description": "The order in which messages should be shown. Messages will be shown in increasing order.",
+ "type": "integer"
+ },
+ "targeting": {
+ "description": "A JEXL expression representing targeting information",
+ "type": "string"
+ },
+ "trigger": {
+ "description": "An action to trigger potentially showing the message",
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "A string identifying the trigger action"
+ },
+ "params": {
+ "type": "array",
+ "description": "An optional array of string parameters for the trigger action",
+ "items": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ }
+ }
+ },
+ "required": [
+ "id"
+ ]
+ },
+ "provider": {
+ "description": "An identifier for the provider of this message, such as \"cfr\" or \"preview\".",
+ "type": "string"
+ }
+ },
+ "additionalProperties": true,
+ "dependentRequired": {
+ "content": [
+ "id",
+ "template"
+ ],
+ "template": [
+ "id",
+ "content"
+ ]
+ }
+ },
+ "localizedText": {
+ "type": "object",
+ "properties": {
+ "string_id": {
+ "description": "Id of localized string to be rendered.",
+ "type": "string"
+ }
+ },
+ "required": [
+ "string_id"
+ ]
+ },
+ "localizableText": {
+ "description": "Either a raw string or an object containing the string_id of the localized text",
+ "oneOf": [
+ {
+ "type": "string",
+ "description": "The string to be rendered."
+ },
+ {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/localizedText"
+ }
+ ]
+ },
+ "TemplatedMessage": {
+ "description": "An FxMS message of one of a variety of types.",
+ "type": "object",
+ "allOf": [
+ {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/Message"
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "template": {
+ "type": "string",
+ "enum": [
+ "cfr_urlbar_chiclet"
+ ]
+ }
+ },
+ "required": [
+ "template"
+ ]
+ },
+ "then": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/CFRUrlbarChiclet"
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "template": {
+ "type": "string",
+ "enum": [
+ "cfr_doorhanger",
+ "milestone_message"
+ ]
+ }
+ },
+ "required": [
+ "template"
+ ]
+ },
+ "then": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/ExtensionDoorhanger"
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "template": {
+ "type": "string",
+ "enum": [
+ "infobar"
+ ]
+ }
+ },
+ "required": [
+ "template"
+ ]
+ },
+ "then": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/InfoBar"
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "template": {
+ "type": "string",
+ "enum": [
+ "pb_newtab"
+ ]
+ }
+ },
+ "required": [
+ "template"
+ ]
+ },
+ "then": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/NewtabPromoMessage"
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "template": {
+ "type": "string",
+ "enum": [
+ "protections_panel"
+ ]
+ }
+ },
+ "required": [
+ "template"
+ ]
+ },
+ "then": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/ProtectionsPanelMessage"
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "template": {
+ "type": "string",
+ "enum": [
+ "spotlight",
+ "feature_callout"
+ ]
+ }
+ },
+ "required": [
+ "template"
+ ]
+ },
+ "then": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/Spotlight"
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "template": {
+ "type": "string",
+ "enum": [
+ "toast_notification"
+ ]
+ }
+ },
+ "required": [
+ "template"
+ ]
+ },
+ "then": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/ToastNotification"
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "template": {
+ "type": "string",
+ "enum": [
+ "toolbar_badge"
+ ]
+ }
+ },
+ "required": [
+ "template"
+ ]
+ },
+ "then": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/ToolbarBadgeMessage"
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "template": {
+ "type": "string",
+ "enum": [
+ "update_action"
+ ]
+ }
+ },
+ "required": [
+ "template"
+ ]
+ },
+ "then": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/UpdateAction"
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "template": {
+ "type": "string",
+ "enum": [
+ "whatsnew_panel_message"
+ ]
+ }
+ },
+ "required": [
+ "template"
+ ]
+ },
+ "then": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/WhatsNewMessage"
+ }
+ }
+ ]
+ },
+ "MultiMessage": {
+ "description": "An object containing an array of messages.",
+ "type": "object",
+ "properties": {
+ "template": {
+ "type": "string",
+ "const": "multi"
+ },
+ "messages": {
+ "type": "array",
+ "description": "An array of messages.",
+ "items": {
+ "$ref": "resource://activity-stream/schemas/MessagingExperiment.schema.json#/$defs/TemplatedMessage"
+ }
+ }
+ },
+ "required": [
+ "template",
+ "messages"
+ ]
+ }
+ }
+}
diff --git a/browser/components/newtab/content-src/asrouter/schemas/corpus/ReachExperiments.messages.json b/browser/components/newtab/content-src/asrouter/schemas/corpus/ReachExperiments.messages.json
new file mode 100644
index 0000000000..1ccfefe478
--- /dev/null
+++ b/browser/components/newtab/content-src/asrouter/schemas/corpus/ReachExperiments.messages.json
@@ -0,0 +1,15 @@
+[
+ {
+ "trigger": {
+ "id": "defaultBrowserCheck"
+ },
+ "targeting": "source == 'startup' && !isMajorUpgrade && !activeNotifications && totalBookmarksCount == 5"
+ },
+ {
+ "groups": ["eco"],
+ "trigger": {
+ "id": "defaultBrowserCheck"
+ },
+ "targeting": "source == 'startup' && !isMajorUpgrade && !activeNotifications && totalBookmarksCount == 5"
+ }
+]
diff --git a/browser/components/newtab/content-src/asrouter/schemas/extract-test-corpus.js b/browser/components/newtab/content-src/asrouter/schemas/extract-test-corpus.js
new file mode 100644
index 0000000000..f8d3f5ac81
--- /dev/null
+++ b/browser/components/newtab/content-src/asrouter/schemas/extract-test-corpus.js
@@ -0,0 +1,65 @@
+/* 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";
+
+const { CFRMessageProvider } = ChromeUtils.importESModule(
+ "resource://activity-stream/lib/CFRMessageProvider.sys.mjs"
+);
+const { OnboardingMessageProvider } = ChromeUtils.import(
+ "resource://activity-stream/lib/OnboardingMessageProvider.jsm"
+);
+const { PanelTestProvider } = ChromeUtils.importESModule(
+ "resource://activity-stream/lib/PanelTestProvider.sys.mjs"
+);
+
+const CWD = Services.dirsvc.get("CurWorkD", Ci.nsIFile).path;
+const CORPUS_DIR = PathUtils.join(CWD, "corpus");
+
+const CORPUS = [
+ {
+ name: "CFRMessageProvider.messages.json",
+ provider: CFRMessageProvider,
+ },
+ {
+ name: "OnboardingMessageProvider.messages.json",
+ provider: OnboardingMessageProvider,
+ },
+ {
+ name: "PanelTestProvider.messages.json",
+ provider: PanelTestProvider,
+ },
+ {
+ name: "PanelTestProvider_toast_notification.messages.json",
+ provider: PanelTestProvider,
+ filter: message => message.template === "toast_notification",
+ },
+];
+
+let exit = false;
+async function main() {
+ try {
+ await IOUtils.makeDirectory(CORPUS_DIR);
+
+ for (const entry of CORPUS) {
+ const { name, provider } = entry;
+ const filter = entry.filter ?? (() => true);
+ const messages = await provider.getMessages();
+ const json = `${JSON.stringify(messages.filter(filter), undefined, 2)}\n`;
+
+ const path = PathUtils.join(CORPUS_DIR, name);
+ await IOUtils.writeUTF8(path, json);
+ }
+ } finally {
+ exit = true;
+ }
+}
+
+main();
+
+// We need to spin the event loop here, otherwise everything goes out of scope.
+Services.tm.spinEventLoopUntil(
+ "extract-test-corpus.js: waiting for completion",
+ () => exit
+);
diff --git a/browser/components/newtab/content-src/asrouter/schemas/make-schemas.py b/browser/components/newtab/content-src/asrouter/schemas/make-schemas.py
new file mode 100755
index 0000000000..7fbf815aa0
--- /dev/null
+++ b/browser/components/newtab/content-src/asrouter/schemas/make-schemas.py
@@ -0,0 +1,475 @@
+#!/usr/bin/env python3
+# 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/.
+
+"""Firefox Messaging System Messaging Experiment schema generator
+
+The Firefox Messaging System handles several types of messages. This program
+patches and combines those schemas into a single schema
+(MessagingExperiment.schema.json) which is used to validate messaging
+experiments coming from Nimbus.
+
+Definitions from FxMsCommon.schema.json are bundled into this schema. This
+allows all of the FxMS schemas to reference common definitions, e.g.
+`localizableText` for translatable strings, via referencing the common schema.
+The bundled schema will be re-written so that the references now point at the
+top-level, generated schema.
+
+Additionally, all self-references in each messaging schema will be rewritten
+into absolute references, referencing each sub-schemas `$id`. This is requried
+due to the JSONSchema validation library used by Experimenter not fully
+supporting self-references and bundled schema.
+"""
+
+import json
+import sys
+from argparse import ArgumentParser
+from itertools import chain
+from pathlib import Path
+from typing import Any, Dict, List, NamedTuple, Union
+from urllib.parse import urlparse
+
+import jsonschema
+
+
+class SchemaDefinition(NamedTuple):
+ """A definition of a schema that is to be bundled."""
+
+ #: The $id of the generated schema.
+ schema_id: str
+
+ #: The path of the generated schema.
+ schema_path: Path
+
+ #: The message types that will be bundled into the schema.
+ message_types: Dict[str, Path]
+
+ #: What common definitions to bundle into the schema.
+ #:
+ #: If `True`, all definitions will be bundled.
+ #: If `False`, no definitons will be bundled.
+ #: If a list, only the named definitions will be bundled.
+ bundle_common: Union[bool, List[str]]
+
+ #: The testing corpus for the schema.
+ test_corpus: Dict[str, Path]
+
+
+SCHEMA_DIR = Path("..", "templates")
+
+SCHEMAS = [
+ SchemaDefinition(
+ schema_id="resource://activity-stream/schemas/MessagingExperiment.schema.json",
+ schema_path=Path("MessagingExperiment.schema.json"),
+ message_types={
+ "CFRUrlbarChiclet": (
+ SCHEMA_DIR / "CFR" / "templates" / "CFRUrlbarChiclet.schema.json"
+ ),
+ "ExtensionDoorhanger": (
+ SCHEMA_DIR / "CFR" / "templates" / "ExtensionDoorhanger.schema.json"
+ ),
+ "InfoBar": SCHEMA_DIR / "CFR" / "templates" / "InfoBar.schema.json",
+ "NewtabPromoMessage": (
+ SCHEMA_DIR / "PBNewtab" / "NewtabPromoMessage.schema.json"
+ ),
+ "ProtectionsPanelMessage": (
+ SCHEMA_DIR / "OnboardingMessage" / "ProtectionsPanelMessage.schema.json"
+ ),
+ "Spotlight": SCHEMA_DIR / "OnboardingMessage" / "Spotlight.schema.json",
+ "ToastNotification": (
+ SCHEMA_DIR / "ToastNotification" / "ToastNotification.schema.json"
+ ),
+ "ToolbarBadgeMessage": (
+ SCHEMA_DIR / "OnboardingMessage" / "ToolbarBadgeMessage.schema.json"
+ ),
+ "UpdateAction": (
+ SCHEMA_DIR / "OnboardingMessage" / "UpdateAction.schema.json"
+ ),
+ "WhatsNewMessage": (
+ SCHEMA_DIR / "OnboardingMessage" / "WhatsNewMessage.schema.json"
+ ),
+ },
+ bundle_common=True,
+ test_corpus={
+ "ReachExperiments": Path("corpus", "ReachExperiments.messages.json"),
+ # These are generated via extract-test-corpus.js
+ "CFRMessageProvider": Path("corpus", "CFRMessageProvider.messages.json"),
+ "OnboardingMessageProvider": Path(
+ "corpus", "OnboardingMessageProvider.messages.json"
+ ),
+ "PanelTestProvider": Path("corpus", "PanelTestProvider.messages.json"),
+ },
+ ),
+ SchemaDefinition(
+ schema_id=(
+ "resource://activity-stream/schemas/"
+ "BackgroundTaskMessagingExperiment.schema.json"
+ ),
+ schema_path=Path("BackgroundTaskMessagingExperiment.schema.json"),
+ message_types={
+ "ToastNotification": (
+ SCHEMA_DIR / "ToastNotification" / "ToastNotification.schema.json"
+ ),
+ },
+ bundle_common=True,
+ # These are generated via extract-test-corpus.js
+ test_corpus={
+ # Just the "toast_notification" messages.
+ "PanelTestProvider": Path(
+ "corpus", "PanelTestProvider_toast_notification.messages.json"
+ ),
+ },
+ ),
+]
+
+COMMON_SCHEMA_NAME = "FxMSCommon.schema.json"
+COMMON_SCHEMA_PATH = Path(COMMON_SCHEMA_NAME)
+
+
+class NestedRefResolver(jsonschema.RefResolver):
+ """A custom ref resolver that handles bundled schema.
+
+ This is the resolver used by Experimenter.
+ """
+
+ def __init__(self, schema):
+ super().__init__(base_uri=None, referrer=None)
+
+ if "$id" in schema:
+ self.store[schema["$id"]] = schema
+
+ if "$defs" in schema:
+ for dfn in schema["$defs"].values():
+ if "$id" in dfn:
+ self.store[dfn["$id"]] = dfn
+
+
+def read_schema(path):
+ """Read a schema from disk and parse it as JSON."""
+ with path.open("r") as f:
+ return json.load(f)
+
+
+def extract_template_values(template):
+ """Extract the possible template values (either via JSON Schema enum or const)."""
+ enum = template.get("enum")
+ if enum:
+ return enum
+
+ const = template.get("const")
+ if const:
+ return [const]
+
+
+def patch_schema(schema, bundled_id, schema_id=None):
+ """Patch the given schema.
+
+ The JSON schema validator that Experimenter uses
+ (https://pypi.org/project/jsonschema/) does not support relative references,
+ nor does it support bundled schemas. We rewrite the schema so that all
+ relative refs are transformed into absolute refs via the schema's `$id`.
+
+ Additionally, we merge in the contents of FxMSCommon.schema.json, so all
+ refs relative to that schema will be transformed to become relative to this
+ schema.
+
+ See-also: https://github.com/python-jsonschema/jsonschema/issues/313
+ """
+ if schema_id is None:
+ schema_id = schema["$id"]
+
+ def patch_impl(schema):
+ ref = schema.get("$ref")
+
+ if ref:
+ uri = urlparse(ref)
+ if (
+ uri.scheme == ""
+ and uri.netloc == ""
+ and uri.path == ""
+ and uri.fragment != ""
+ ):
+ schema["$ref"] = f"{schema_id}#{uri.fragment}"
+ elif (uri.scheme, uri.path) == ("file", f"/{COMMON_SCHEMA_NAME}"):
+ schema["$ref"] = f"{bundled_id}#{uri.fragment}"
+
+ # If `schema` is object-like, inspect each of its indivual properties
+ # and patch them.
+ properties = schema.get("properties")
+ if properties:
+ for prop in properties.keys():
+ patch_impl(properties[prop])
+
+ # If `schema` is array-like, inspect each of its items and patch them.
+ items = schema.get("items")
+ if items:
+ patch_impl(items)
+
+ # Patch each `if`, `then`, `else`, and `not` sub-schema that is present.
+ for key in ("if", "then", "else", "not"):
+ if key in schema:
+ patch_impl(schema[key])
+
+ # Patch the items of each `oneOf`, `allOf`, and `anyOf` sub-schema that
+ # is present.
+ for key in ("oneOf", "allOf", "anyOf"):
+ subschema = schema.get(key)
+ if subschema:
+ for i, alternate in enumerate(subschema):
+ patch_impl(alternate)
+
+ # Patch the top-level type defined in the schema.
+ patch_impl(schema)
+
+ # Patch each named definition in the schema.
+ for key in ("$defs", "definitions"):
+ defns = schema.get(key)
+ if defns:
+ for defn_name, defn_value in defns.items():
+ patch_impl(defn_value)
+
+ return schema
+
+
+def bundle_schema(schema_def: SchemaDefinition):
+ """Create a bundled schema based on the schema definition."""
+ # Patch each message type schema to resolve all self-references to be
+ # absolute and rewrite # references to FxMSCommon.schema.json to be relative
+ # to the new schema (because we are about to bundle its definitions).
+ defs = {
+ name: patch_schema(read_schema(path), bundled_id=schema_def.schema_id)
+ for name, path in schema_def.message_types.items()
+ }
+
+ # Bundle the definitions from FxMSCommon.schema.json into this schema.
+ if schema_def.bundle_common:
+
+ def dfn_filter(name):
+ if schema_def.bundle_common is True:
+ return True
+
+ return name in schema_def.bundle_common
+
+ common_schema = patch_schema(
+ read_schema(COMMON_SCHEMA_PATH),
+ bundled_id=schema_def.schema_id,
+ schema_id=schema_def.schema_id,
+ )
+
+ # patch_schema mutates the given schema, so we read a new copy in for
+ # each bundle operation.
+ defs.update(
+ {
+ name: dfn
+ for name, dfn in common_schema["$defs"].items()
+ if dfn_filter(name)
+ }
+ )
+
+ # Ensure all bundled schemas have an $id so that $refs inside the
+ # bundled schema work correctly (i.e, they will reference the subschema
+ # and not the bundle).
+ for name in schema_def.message_types.keys():
+ subschema = defs[name]
+ if "$id" not in subschema:
+ raise ValueError(f"Schema {name} is missing an $id")
+
+ props = subschema["properties"]
+ if "template" not in props:
+ raise ValueError(f"Schema {name} is missing a template")
+
+ template = props["template"]
+ if "enum" not in template and "const" not in template:
+ raise ValueError(f"Schema {name} should have const or enum template")
+
+ templates = {
+ name: extract_template_values(defs[name]["properties"]["template"])
+ for name in schema_def.message_types.keys()
+ }
+
+ # Ensure that each schema has a unique set of template values.
+ for a in templates.keys():
+ a_keys = set(templates[a])
+
+ for b in templates.keys():
+ if a == b:
+ continue
+
+ b_keys = set(templates[b])
+ intersection = a_keys.intersection(b_keys)
+
+ if len(intersection):
+ raise ValueError(
+ f"Schema {a} and {b} have overlapping template values: "
+ f"{', '.join(intersection)}"
+ )
+
+ all_templates = list(chain.from_iterable(templates.values()))
+
+ # Enforce that one of the templates must match (so that one of the if
+ # branches will match).
+ defs["Message"]["properties"]["template"]["enum"] = all_templates
+ defs["TemplatedMessage"] = {
+ "description": "An FxMS message of one of a variety of types.",
+ "type": "object",
+ "allOf": [
+ # Ensure each message has all the fields defined in the base
+ # Message type.
+ #
+ # This is slightly redundant because each message should
+ # already inherit from this message type, but it is easier
+ # to add this requirement here than to verify that each
+ # message's schema is properly inheriting.
+ {"$ref": f"{schema_def.schema_id}#/$defs/Message"},
+ # For each message type, create a subschema that says if the
+ # template field matches a value for a message type defined
+ # in MESSAGE_TYPES, then the message must also match the
+ # schema for that message type.
+ #
+ # This is done using `allOf: [{ if, then }]` instead of `oneOf: []`
+ # because it provides better error messages. Using `if-then`
+ # will only show validation errors for the sub-schema that
+ # matches template, whereas using `oneOf` will show
+ # validation errors for *all* sub-schemas, which makes
+ # debugging messages much harder.
+ *(
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "template": {
+ "type": "string",
+ "enum": templates[message_type],
+ },
+ },
+ "required": ["template"],
+ },
+ "then": {"$ref": f"{schema_def.schema_id}#/$defs/{message_type}"},
+ }
+ for message_type in schema_def.message_types
+ ),
+ ],
+ }
+ defs["MultiMessage"] = {
+ "description": "An object containing an array of messages.",
+ "type": "object",
+ "properties": {
+ "template": {"type": "string", "const": "multi"},
+ "messages": {
+ "type": "array",
+ "description": "An array of messages.",
+ "items": {"$ref": f"{schema_def.schema_id}#/$defs/TemplatedMessage"},
+ },
+ },
+ "required": ["template", "messages"],
+ }
+
+ # Generate the combined schema.
+ return {
+ "$schema": "https://json-schema.org/draft/2019-09/schema",
+ "$id": schema_def.schema_id,
+ "title": "Messaging Experiment",
+ "description": "A Firefox Messaging System message.",
+ # A message must be one of:
+ # - An object that contains id, template, and content fields
+ # - An object that contains none of the above fields (empty message)
+ # - An array of messages like the above
+ "if": {
+ "type": "object",
+ "properties": {"template": {"const": "multi"}},
+ "required": ["template"],
+ },
+ "then": {
+ "$ref": f"{schema_def.schema_id}#/$defs/MultiMessage",
+ },
+ "else": {
+ "$ref": f"{schema_def.schema_id}#/$defs/TemplatedMessage",
+ },
+ "$defs": defs,
+ }
+
+
+def check_diff(schema_def: SchemaDefinition, schema: Dict[str, Any]):
+ """Check the generated schema matches the on-disk schema."""
+ print(f" Checking {schema_def.schema_path} for differences...")
+
+ with schema_def.schema_path.open("r") as f:
+ on_disk = json.load(f)
+
+ if on_disk != schema:
+ print(f"{schema_def.schema_path} does not match generated schema:")
+ print("Generated schema:")
+ json.dump(schema, sys.stdout, indent=2)
+ print("\n\nOn Disk schema:")
+ json.dump(on_disk, sys.stdout, indent=2)
+ print("\n\n")
+
+ raise ValueError("Schemas do not match!")
+
+
+def validate_corpus(schema_def: SchemaDefinition, schema: Dict[str, Any]):
+ """Check that the schema validates.
+
+ This uses the same validation configuration that is used in Experimenter.
+ """
+ print(" Validating messages with Experimenter JSON Schema validator...")
+
+ resolver = NestedRefResolver(schema)
+
+ for provider, provider_path in schema_def.test_corpus.items():
+ print(f" Validating messages from {provider}:")
+
+ try:
+ with provider_path.open("r") as f:
+ messages = json.load(f)
+ except FileNotFoundError as e:
+ if not provider_path.parent.exists():
+ new_exc = Exception(
+ f"Could not find {provider_path}: Did you run "
+ "`mach xpcshell extract-test-corpus.js` ?"
+ )
+ raise new_exc from e
+
+ raise e
+
+ for i, message in enumerate(messages):
+ template = message.get("template", "(no template)")
+ msg_id = message.get("id", f"index {i}")
+
+ print(
+ f" Validating {msg_id} {template} message with {schema_def.schema_path}..."
+ )
+ jsonschema.validate(instance=message, schema=schema, resolver=resolver)
+
+ print()
+
+
+def main(check=False):
+ """Generate Nimbus feature schemas for Firefox Messaging System."""
+ for schema_def in SCHEMAS:
+ print(f"Generating {schema_def.schema_path} ...")
+ schema = bundle_schema(schema_def)
+
+ if check:
+ print(f"Checking {schema_def.schema_path} ...")
+ check_diff(schema_def, schema)
+ validate_corpus(schema_def, schema)
+ else:
+ with schema_def.schema_path.open("wb") as f:
+ print(f"Writing {schema_def.schema_path} ...")
+ f.write(json.dumps(schema, indent=2).encode("utf-8"))
+ f.write(b"\n")
+
+
+if __name__ == "__main__":
+ parser = ArgumentParser(description=main.__doc__)
+ parser.add_argument(
+ "--check",
+ action="store_true",
+ help="Check that the generated schemas have not changed and run validation tests.",
+ default=False,
+ )
+ args = parser.parse_args()
+
+ main(args.check)
diff --git a/browser/components/newtab/content-src/asrouter/schemas/message-format.md b/browser/components/newtab/content-src/asrouter/schemas/message-format.md
new file mode 100644
index 0000000000..debcce0572
--- /dev/null
+++ b/browser/components/newtab/content-src/asrouter/schemas/message-format.md
@@ -0,0 +1,101 @@
+## Activity Stream Router message format
+
+Field name | Type | Required | Description | Example / Note
+--- | --- | --- | --- | ---
+`id` | `string` | Yes | A unique identifier for the message that should not conflict with any other previous message | `ONBOARDING_1`
+`template` | `string` | Yes | An id matching an existing Activity Stream Router template | [See example](https://github.com/mozilla/activity-stream/blob/33669c67c2269078a6d3d6d324fb48175d98f634/system-addon/content-src/message-center/templates/SimpleSnippet.jsx)
+`content` | `object` | Yes | An object containing all variables/props to be rendered in the template. Subset of allowed tags detailed below. | [See example below](#html-subset)
+`bundled` | `integer` | No | The number of messages of the same template this one should be shown with | [See example below](#a-bundled-message-example)
+`order` | `integer` | No | If bundled with other messages of the same template, which order should this one be placed in? Defaults to 0 if no order is desired | [See example below](#a-bundled-message-example)
+`campaign` | `string` | No | Campaign id that the message belongs to | `RustWebAssembly`
+`targeting` | `string` `JEXL` | No | A [JEXL expression](http://normandy.readthedocs.io/en/latest/user/filter_expressions.html#jexl-basics) with all targeting information needed in order to decide if the message is shown | Not yet implemented, [Examples](#targeting-attributes)
+`trigger` | `string` | No | An event or condition upon which the message will be immediately shown. This can be combined with `targeting`. Messages that define a trigger will not be shown during non-trigger-based passive message rotation.
+`trigger.params` | `[string]` | No | A set of hostnames passed down as parameters to the trigger condition. Used to restrict the number of domains where the trigger/message is valid. | [See example below](#trigger-params)
+`trigger.patterns` | `[string]` | No | A set of patterns that match multiple hostnames passed down as parameters to the trigger condition. Used to restrict the number of domains where the trigger/message is valid. | [See example below](#trigger-patterns)
+`frequency` | `object` | No | A definition for frequency cap information for the message
+`frequency.lifetime` | `integer` | No | The maximum number of lifetime impressions for the message.
+`frequency.custom` | `array` | No | An array of frequency cap definition objects including `period`, a time period in milliseconds, and `cap`, a max number of impressions for that period.
+
+### Message example
+```javascript
+{
+ id: "ONBOARDING_1",
+ template: "simple_snippet",
+ content: {
+ title: "Find it faster",
+ body: "Access all of your favorite search engines with a click. Search the whole Web or just one website from the search box."
+ },
+ targeting: "usesFirefoxSync && !addonsInfo.addons['activity-stream@mozilla.org']",
+ frequency: {
+ lifetime: 20,
+ custom: [{period: 86400000, cap: 5}, {period: 3600000, cap: 1}]
+ }
+}
+```
+
+### A Bundled Message example
+The following 2 messages have a `bundled` property, indicating that they should be shown together, since they have the same template. The number `2` indicates that this message should be shown in a bundle of 2 messages of the same template. The order property defines that ONBOARDING_2 should be shown after ONBOARDING_3 in the bundle.
+```javascript
+{
+ id: "ONBOARDING_2",
+ template: "onboarding",
+ bundled: 2,
+ order: 2,
+ content: {
+ title: "Private Browsing",
+ body: "Browse by yourself. Private Browsing with Tracking Protection blocks online trackers that follow you around the web."
+ },
+ targeting: "",
+ trigger: "firstRun"
+}
+{
+ id: "ONBOARDING_3",
+ template: "onboarding",
+ bundled: 2,
+ order: 1,
+ content: {
+ title: "Find it faster",
+ body: "Access all of your favorite search engines with a click. Search the whole Web or just one website from the search box."
+ },
+ targeting: "",
+ trigger: "firstRun"
+}
+```
+
+### HTML subset
+The following tags are allowed in the content of the snippet: `i, b, u, strong, em, br`.
+
+Links cannot be rendered using regular anchor tags because [Fluent does not allow for href attributes](https://github.com/projectfluent/fluent.js/blob/a03d3aa833660f8c620738b26c80e46b1a4edb05/fluent-dom/src/overlay.js#L13). They will be wrapped in custom tags, for example `<cta>link</cta>` and the url will be provided as part of the payload:
+```
+{
+ "id": "7899",
+ "content": {
+ "text": "Use the CMD (CTRL) + T keyboard shortcut to <cta>open a new tab quickly!</cta>",
+ "links": {
+ "cta": {
+ "url": "https://support.mozilla.org/en-US/kb/keyboard-shortcuts-perform-firefox-tasks-quickly"
+ }
+ }
+ }
+}
+```
+If a tag that is not on the allowed is used, the text content will be extracted and displayed.
+
+Grouping multiple allowed elements is not possible, only the first level will be used: `<u><b>text</b></u>` will be interpreted as `<u>text</u>`.
+
+### Trigger params
+A set of hostnames that need to exactly match the location of the selected tab in order for the trigger to execute.
+```
+["github.com", "wwww.github.com"]
+```
+More examples in the [CFRMessageProvider](https://github.com/mozilla/activity-stream/blob/e76ce12fbaaac1182aa492b84fc038f78c3acc33/lib/CFRMessageProvider.jsm#L40-L47).
+
+### Trigger patterns
+A set of patterns that can match multiple hostnames. When the location of the selected tab matches one of the patterns it can execute a trigger.
+```
+["*://*.github.com"] // can match `github.com` but also match `https://gist.github.com/`
+```
+More [MatchPattern examples](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns#Examples).
+
+### Targeting attributes
+(This section has moved to [targeting-attributes.md](../docs/targeting-attributes.md)).
diff --git a/browser/components/newtab/content-src/asrouter/schemas/message-group.schema.json b/browser/components/newtab/content-src/asrouter/schemas/message-group.schema.json
new file mode 100644
index 0000000000..421acf159a
--- /dev/null
+++ b/browser/components/newtab/content-src/asrouter/schemas/message-group.schema.json
@@ -0,0 +1,64 @@
+{
+ "title": "MessageGroup",
+ "description": "Configuration object for groups of Messaging System messages",
+ "type": "object",
+ "version": "1.0.0",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "A unique identifier for the message that should not conflict with any other previous message."
+ },
+ "enabled": {
+ "type": "boolean",
+ "description": "Enables or disables all messages associated with this group."
+ },
+ "userPreferences": {
+ "type": "array",
+ "description": "Collection of preferences that control if the group is enabled.",
+ "items": {
+ "type": "string",
+ "description": "Preference name"
+ }
+ },
+ "frequency": {
+ "type": "object",
+ "description": "An object containing frequency cap information for a message.",
+ "properties": {
+ "lifetime": {
+ "type": "integer",
+ "description": "The maximum lifetime impressions for a message.",
+ "minimum": 1,
+ "maximum": 100
+ },
+ "custom": {
+ "type": "array",
+ "description": "An array of custom frequency cap definitions.",
+ "items": {
+ "description": "A frequency cap definition containing time and max impression information",
+ "type": "object",
+ "properties": {
+ "period": {
+ "type": "integer",
+ "description": "Period of time in milliseconds (e.g. 86400000 for one day)"
+ },
+ "cap": {
+ "type": "integer",
+ "description": "The maximum impressions for the message within the defined period.",
+ "minimum": 1,
+ "maximum": 100
+ }
+ },
+ "required": ["period", "cap"]
+ }
+ }
+ }
+ },
+ "type": {
+ "type": "string",
+ "description": "Local auto-generated group or remote group configuration from RS.",
+ "enum": ["remote-settings", "local", "default"]
+ }
+ },
+ "required": ["id", "enabled", "type"],
+ "additionalProperties": true
+}
diff --git a/browser/components/newtab/content-src/asrouter/schemas/provider-response.schema.json b/browser/components/newtab/content-src/asrouter/schemas/provider-response.schema.json
new file mode 100644
index 0000000000..f0a92705be
--- /dev/null
+++ b/browser/components/newtab/content-src/asrouter/schemas/provider-response.schema.json
@@ -0,0 +1,67 @@
+{
+ "title": "ProviderResponse",
+ "description": "A response object for remote providers of AS Router",
+ "type": "object",
+ "version": "6.1.0",
+ "properties": {
+ "messages": {
+ "type": "array",
+ "description": "An array of router messages",
+ "items": {
+ "title": "RouterMessage",
+ "description": "A definition of an individual message",
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "A unique identifier for the message that should not conflict with any other previous message"
+ },
+ "template": {
+ "type": "string",
+ "description": "An id matching an existing Activity Stream Router template",
+ "enum": ["simple_snippet"]
+ },
+ "bundled": {
+ "type": "integer",
+ "description": "The number of messages of the same template this one should be shown with (optional)"
+ },
+ "order": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "If bundled with other messages of the same template, which order should this one be placed in? (optional - defaults to 0)"
+ },
+ "content": {
+ "type": "object",
+ "description": "An object containing all variables/props to be rendered in the template. See individual template schemas for details."
+ },
+ "targeting": {
+ "type": "string",
+ "description": "A JEXL expression representing targeting information"
+ },
+ "trigger": {
+ "type": "object",
+ "description": "An action to trigger potentially showing the message",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "A string identifying the trigger action",
+ "enum": ["firstRun", "openURL"]
+ },
+ "params": {
+ "type": "array",
+ "description": "An optional array of string parameters for the trigger action",
+ "items": {
+ "type": "string",
+ "description": "A parameter for the trigger action"
+ }
+ }
+ },
+ "required": ["id"]
+ }
+ },
+ "required": ["id", "template", "content"]
+ }
+ }
+ },
+ "required": ["messages"]
+}