From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- .../BackgroundTaskMessagingExperiment.schema.json | 312 ++++ .../asrouter/schemas/FxMSCommon.schema.json | 128 ++ .../schemas/MessagingExperiment.schema.json | 1640 ++++++++++++++++++++ .../schemas/corpus/ReachExperiments.messages.json | 15 + .../asrouter/schemas/extract-test-corpus.js | 65 + .../content-src/asrouter/schemas/make-schemas.py | 475 ++++++ .../content-src/asrouter/schemas/message-format.md | 101 ++ .../asrouter/schemas/message-group.schema.json | 64 + .../asrouter/schemas/provider-response.schema.json | 67 + 9 files changed, 2867 insertions(+) create mode 100644 browser/components/newtab/content-src/asrouter/schemas/BackgroundTaskMessagingExperiment.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/schemas/FxMSCommon.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/schemas/MessagingExperiment.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/schemas/corpus/ReachExperiments.messages.json create mode 100644 browser/components/newtab/content-src/asrouter/schemas/extract-test-corpus.js create mode 100755 browser/components/newtab/content-src/asrouter/schemas/make-schemas.py create mode 100644 browser/components/newtab/content-src/asrouter/schemas/message-format.md create mode 100644 browser/components/newtab/content-src/asrouter/schemas/message-group.schema.json create mode 100644 browser/components/newtab/content-src/asrouter/schemas/provider-response.schema.json (limited to 'browser/components/newtab/content-src/asrouter/schemas') 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 `link` and the url will be provided as part of the payload: +``` +{ + "id": "7899", + "content": { + "text": "Use the CMD (CTRL) + T keyboard shortcut to open a new tab quickly!", + "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: `text` will be interpreted as `text`. + +### 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"] +} -- cgit v1.2.3