diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-12 05:43:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-12 05:43:14 +0000 |
commit | 8dd16259287f58f9273002717ec4d27e97127719 (patch) | |
tree | 3863e62a53829a84037444beab3abd4ed9dfc7d0 /toolkit/mozapps | |
parent | Releasing progress-linux version 126.0.1-1~progress7.99u1. (diff) | |
download | firefox-8dd16259287f58f9273002717ec4d27e97127719.tar.xz firefox-8dd16259287f58f9273002717ec4d27e97127719.zip |
Merging upstream version 127.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/mozapps')
53 files changed, 1507 insertions, 6083 deletions
diff --git a/toolkit/mozapps/defaultagent/BackgroundTask_defaultagent.sys.mjs b/toolkit/mozapps/defaultagent/BackgroundTask_defaultagent.sys.mjs index c9da853cb5..d5323a0e30 100644 --- a/toolkit/mozapps/defaultagent/BackgroundTask_defaultagent.sys.mjs +++ b/toolkit/mozapps/defaultagent/BackgroundTask_defaultagent.sys.mjs @@ -331,7 +331,7 @@ function makeAlert(options) { let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); alert.init( options.name, - "chrome://branding/content/about-logo@2x.png", + "chrome://global/content/defaultagent/fox-doodle-peek.png", options.title, options.body, true /* aTextClickable */, diff --git a/toolkit/mozapps/defaultagent/assets/fox-doodle-peek.png b/toolkit/mozapps/defaultagent/assets/fox-doodle-peek.png Binary files differnew file mode 100644 index 0000000000..844f2cef62 --- /dev/null +++ b/toolkit/mozapps/defaultagent/assets/fox-doodle-peek.png diff --git a/toolkit/mozapps/defaultagent/jar.mn b/toolkit/mozapps/defaultagent/jar.mn new file mode 100644 index 0000000000..a38790f9a2 --- /dev/null +++ b/toolkit/mozapps/defaultagent/jar.mn @@ -0,0 +1,6 @@ +# 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/. + +toolkit.jar: + content/global/defaultagent/fox-doodle-peek.png (assets/fox-doodle-peek.png) diff --git a/toolkit/mozapps/defaultagent/moz.build b/toolkit/mozapps/defaultagent/moz.build index f8ae506d9c..3b209ab6e1 100644 --- a/toolkit/mozapps/defaultagent/moz.build +++ b/toolkit/mozapps/defaultagent/moz.build @@ -6,6 +6,8 @@ SPHINX_TREES["default-browser-agent"] = "docs" +JAR_MANIFESTS += ["jar.mn"] + DIRS += ["proxy"] UNIFIED_SOURCES += [ diff --git a/toolkit/mozapps/extensions/AbuseReporter.sys.mjs b/toolkit/mozapps/extensions/AbuseReporter.sys.mjs index 944cef507c..966e2a6dd5 100644 --- a/toolkit/mozapps/extensions/AbuseReporter.sys.mjs +++ b/toolkit/mozapps/extensions/AbuseReporter.sys.mjs @@ -2,310 +2,39 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; - import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; -const PREF_ABUSE_REPORT_URL = "extensions.abuseReport.url"; -const PREF_AMO_DETAILS_API_URL = "extensions.abuseReport.amoDetailsURL"; - -// Name associated with the report dialog window. -const DIALOG_WINDOW_NAME = "addons-abuse-report-dialog"; - // Maximum length of the string properties sent to the API endpoint. const MAX_STRING_LENGTH = 255; -// Minimum time between report submissions (in ms). -const MIN_MS_BETWEEN_SUBMITS = 30000; - -// The addon types currently supported by the integrated abuse report panel. -const SUPPORTED_ADDON_TYPES = [ +const AMO_SUPPORTED_ADDON_TYPES = [ "extension", "theme", "sitepermission", // TODO(Bug 1789718): Remove after the deprecated XPIProvider-based implementation is also removed. "sitepermission-deprecated", + "dictionary", ]; -// An expanded set of addon types supported when the abuse report hosted on AMO is enabled -// (based on the "extensions.abuseReport.amoFormEnabled" pref). -const AMO_SUPPORTED_ADDON_TYPES = [...SUPPORTED_ADDON_TYPES, "dictionary"]; - const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { - AMTelemetry: "resource://gre/modules/AddonManager.sys.mjs", AddonManager: "resource://gre/modules/AddonManager.sys.mjs", ClientID: "resource://gre/modules/ClientID.sys.mjs", }); -XPCOMUtils.defineLazyPreferenceGetter( - lazy, - "ABUSE_REPORT_URL", - PREF_ABUSE_REPORT_URL -); - -XPCOMUtils.defineLazyPreferenceGetter( - lazy, - "AMO_DETAILS_API_URL", - PREF_AMO_DETAILS_API_URL -); - -// Whether the abuse report feature should open a form hosted by the url -// derived from the one set on the extensions.abuseReport.amoFormURL pref -// or use the abuse report panel integrated in Firefox. -XPCOMUtils.defineLazyPreferenceGetter( - lazy, - "ABUSE_REPORT_AMO_FORM_ENABLED", - "extensions.abuseReport.amoFormEnabled", - true -); - -const PRIVATE_REPORT_PROPS = Symbol("privateReportProps"); - -const ERROR_TYPES = Object.freeze([ - "ERROR_ABORTED_SUBMIT", - "ERROR_ADDON_NOTFOUND", - "ERROR_CLIENT", - "ERROR_NETWORK", - "ERROR_UNKNOWN", - "ERROR_RECENT_SUBMIT", - "ERROR_SERVER", - "ERROR_AMODETAILS_NOTFOUND", - "ERROR_AMODETAILS_FAILURE", -]); - -export class AbuseReportError extends Error { - constructor(errorType, errorInfo = undefined) { - if (!ERROR_TYPES.includes(errorType)) { - throw new Error(`Unknown AbuseReportError type "${errorType}"`); - } - - let message = errorInfo ? `${errorType} - ${errorInfo}` : errorType; - - super(message); - this.name = "AbuseReportError"; - this.errorType = errorType; - this.errorInfo = errorInfo; - } -} - -/** - * Create an error info string from a fetch response object. - * - * @param {Response} response - * A fetch response object to convert into an errorInfo string. - * - * @returns {Promise<string>} - * The errorInfo string to be included in an AbuseReportError. - */ -async function responseToErrorInfo(response) { - return JSON.stringify({ - status: response.status, - responseText: await response.text().catch(() => ""), - }); -} - /** - * A singleton object used to create new AbuseReport instances for a given addonId - * and enforce a minium amount of time between two report submissions . + * A singleton used to manage abuse reports for add-ons. */ export const AbuseReporter = { - _lastReportTimestamp: null, - - get amoFormEnabled() { - return lazy.ABUSE_REPORT_AMO_FORM_ENABLED; - }, - getAMOFormURL({ addonId }) { return Services.urlFormatter .formatURLPref("extensions.abuseReport.amoFormURL") .replace(/%addonID%/g, addonId); }, - // Error types. - updateLastReportTimestamp() { - this._lastReportTimestamp = Date.now(); - }, - - getTimeFromLastReport() { - const currentTimestamp = Date.now(); - if (this._lastReportTimestamp > currentTimestamp) { - // Reset the last report timestamp if it is in the future. - this._lastReportTimestamp = null; - } - - if (!this._lastReportTimestamp) { - return Infinity; - } - - return currentTimestamp - this._lastReportTimestamp; - }, - isSupportedAddonType(addonType) { - if (this.amoFormEnabled) { - return AMO_SUPPORTED_ADDON_TYPES.includes(addonType); - } - return SUPPORTED_ADDON_TYPES.includes(addonType); - }, - - /** - * Create an AbuseReport instance, given the addonId and a reportEntryPoint. - * - * @param {string} addonId - * The id of the addon to create the report instance for. - * @param {object} options - * @param {string} options.reportEntryPoint - * An identifier that represent the entry point for the report flow. - * - * @returns {Promise<AbuseReport>} - * Returns a promise that resolves to an instance of the AbuseReport - * class, which represent an ongoing report. - */ - async createAbuseReport(addonId, { reportEntryPoint } = {}) { - let addon = await lazy.AddonManager.getAddonByID(addonId); - - if (!addon) { - // The addon isn't installed, query the details from the AMO API endpoint. - addon = await this.queryAMOAddonDetails(addonId, reportEntryPoint); - } - - if (!addon) { - lazy.AMTelemetry.recordReportEvent({ - addonId, - errorType: "ERROR_ADDON_NOTFOUND", - reportEntryPoint, - }); - throw new AbuseReportError("ERROR_ADDON_NOTFOUND"); - } - - const reportData = await this.getReportData(addon); - - return new AbuseReport({ - addon, - reportData, - reportEntryPoint, - }); - }, - - /** - * Retrieves the addon details from the AMO API endpoint, used to create - * abuse reports on non-installed addon-ons. - * - * For the addon details that may be translated (e.g. addon name, description etc.) - * the function will try to retrieve the string localized in the same locale used - * by Gecko (and fallback to "en-US" if that locale is unavailable). - * - * The addon creator properties are set to the first author available. - * - * @param {string} addonId - * The id of the addon to retrieve the details available on AMO. - * @param {string} reportEntryPoint - * The entry point for the report flow (to be included in the telemetry - * recorded in case of failures). - * - * @returns {Promise<AMOAddonDetails|null>} - * Returns a promise that resolves to an AMOAddonDetails object, - * which has the subset of the AddonWrapper properties which are - * needed by the abuse report panel or the report data sent to - * the abuse report API endpoint), or null if it fails to - * retrieve the details from AMO. - * - * @typedef {object} AMOAddonDetails - * @prop {string} id - * @prop {string} name - * @prop {string} version - * @prop {string} description - * @prop {string} type - * @prop {string} iconURL - * @prop {string} homepageURL - * @prop {string} supportURL - * @prop {AMOAddonCreator} creator - * @prop {boolean} isRecommended - * @prop {number} signedState=AddonManager.SIGNEDSTATE_UNKNOWN - * @prop {object} installTelemetryInfo={ source: "not_installed" } - * - * @typedef {object} AMOAddonCreator - * @prop {string} name - * @prop {string} url - */ - async queryAMOAddonDetails(addonId, reportEntryPoint) { - let details; - try { - // This should be the API endpoint documented at: - // https://addons-server.readthedocs.io/en/latest/topics/api/addons.html#detail - details = await fetch(`${lazy.AMO_DETAILS_API_URL}/${addonId}`, { - credentials: "omit", - referrerPolicy: "no-referrer", - headers: { "Content-Type": "application/json" }, - }).then(async response => { - if (response.status === 200) { - return response.json(); - } - - let errorInfo = await responseToErrorInfo(response).catch( - () => undefined - ); - - if (response.status === 404) { - // Record a different telemetry event for 404 errors. - throw new AbuseReportError("ERROR_AMODETAILS_NOTFOUND", errorInfo); - } - - throw new AbuseReportError("ERROR_AMODETAILS_FAILURE", errorInfo); - }); - } catch (err) { - // Log the original error in the browser console. - Cu.reportError(err); - - lazy.AMTelemetry.recordReportEvent({ - addonId, - errorType: err.errorType || "ERROR_AMODETAILS_FAILURE", - reportEntryPoint, - }); - - return null; - } - - const locale = Services.locale.appLocaleAsBCP47; - - // Get a string value from a translated value - // (https://addons-server.readthedocs.io/en/latest/topics/api/overview.html#api-overview-translations) - const getTranslatedValue = value => { - if (typeof value === "string") { - return value; - } - return value && (value[locale] || value["en-US"]); - }; - - const getAuthorField = fieldName => - details.authors && details.authors[0] && details.authors[0][fieldName]; - - // Normalize type "statictheme" (which is the type used on the AMO API side) - // into "theme" (because it is the type we use and expect on the Firefox side - // for this addon type). - const addonType = details.type === "statictheme" ? "theme" : details.type; - - return { - id: addonId, - name: getTranslatedValue(details.name), - version: details.current_version.version, - description: getTranslatedValue(details.summary), - type: addonType, - iconURL: details.icon_url, - homepageURL: getTranslatedValue(details.homepage), - supportURL: getTranslatedValue(details.support_url), - // Set the addon creator to the first author in the AMO details. - creator: { - name: getAuthorField("name"), - url: getAuthorField("url"), - }, - isRecommended: details.is_recommended, - // Set signed state to unknown because it isn't installed. - signedState: lazy.AddonManager.SIGNEDSTATE_UNKNOWN, - // Set the installTelemetryInfo.source to "not_installed". - installTelemetryInfo: { source: "not_installed" }, - }; + return AMO_SUPPORTED_ADDON_TYPES.includes(addonType); }, /** @@ -392,312 +121,4 @@ export const AbuseReporter = { return data; }, - - /** - * Helper function that returns a reference to a report dialog window - * already opened (if any). - * - * @returns {Window?} - */ - getOpenDialog() { - return Services.ww.getWindowByName(DIALOG_WINDOW_NAME); - }, - - /** - * Helper function that opens an abuse report form in a new dialog window. - * - * @param {string} addonId - * The addonId being reported. - * @param {string} reportEntryPoint - * The entry point from which the user has triggered the abuse report - * flow. - * @param {XULElement} browser - * The browser element (if any) that is opening the report window. - * - * @return {Promise<AbuseReportDialog>} - * Returns an AbuseReportDialog object, rejects if it fails to open - * the dialog. - * - * @typedef {object} AbuseReportDialog - * An object that represents the abuse report dialog. - * @prop {function} close - * A method that closes the report dialog (used by the caller - * to close the dialog when the user chooses to close the window - * that started the abuse report flow). - * @prop {Promise<AbuseReport|undefined>} promiseReport - * A promise resolved to an AbuseReport instance if the report should - * be submitted, or undefined if the user has cancelled the report. - * Rejects if it fails to create an AbuseReport instance or to open - * the abuse report window. - */ - async openDialog(addonId, reportEntryPoint, browser) { - const chromeWin = browser && browser.ownerGlobal; - if (!chromeWin) { - throw new Error("Abuse Reporter dialog cancelled, opener tab closed"); - } - - const dialogWin = this.getOpenDialog(); - - if (dialogWin) { - // If an abuse report dialog is already open, cancel the - // previous report flow and start a new one. - const { deferredReport, promiseReport } = - dialogWin.arguments[0].wrappedJSObject; - deferredReport.resolve({ userCancelled: true }); - await promiseReport; - } - - const report = await AbuseReporter.createAbuseReport(addonId, { - reportEntryPoint, - }); - - if (!SUPPORTED_ADDON_TYPES.includes(report.addon.type)) { - throw new Error( - `Addon type "${report.addon.type}" is not currently supported by the integrated abuse reporting feature` - ); - } - - const params = Cc["@mozilla.org/array;1"].createInstance( - Ci.nsIMutableArray - ); - - const dialogInit = { - report, - openWebLink(url) { - chromeWin.openWebLinkIn(url, "tab", { - relatedToCurrent: true, - }); - }, - }; - - params.appendElement(dialogInit); - - let win; - function closeDialog() { - if (win && !win.closed) { - win.close(); - } - } - - const promiseReport = new Promise((resolve, reject) => { - dialogInit.deferredReport = { resolve, reject }; - }).then( - ({ userCancelled }) => { - closeDialog(); - return userCancelled ? undefined : report; - }, - err => { - Cu.reportError( - `Unexpected abuse report panel error: ${err} :: ${err.stack}` - ); - closeDialog(); - return Promise.reject({ - message: "Unexpected abuse report panel error", - }); - } - ); - - const promiseReportPanel = new Promise((resolve, reject) => { - dialogInit.deferredReportPanel = { resolve, reject }; - }); - - dialogInit.promiseReport = promiseReport; - dialogInit.promiseReportPanel = promiseReportPanel; - - win = Services.ww.openWindow( - chromeWin, - "chrome://mozapps/content/extensions/abuse-report-frame.html", - DIALOG_WINDOW_NAME, - // Set the dialog window options (including a reasonable initial - // window height size, eventually adjusted by the panel once it - // has been rendered its content). - "dialog,centerscreen,height=700", - params - ); - - return { - close: closeDialog, - promiseReport, - - // Properties used in tests - promiseReportPanel, - window: win, - }; - }, }; - -/** - * Represents an ongoing abuse report. Instances of this class are created - * by the `AbuseReporter.createAbuseReport` method. - * - * This object is used by the reporting UI panel and message bars to: - * - * - get an errorType in case of a report creation error (e.g. because of a - * previously submitted report) - * - get the addon details used inside the reporting panel - * - submit the abuse report (and re-submit if a previous submission failed - * and the user choose to retry to submit it again) - * - abort an ongoing submission - * - * @param {object} options - * @param {AddonWrapper|null} options.addon - * AddonWrapper instance for the extension/theme being reported. - * (May be null if the extension has not been found). - * @param {object|null} options.reportData - * An object which contains addon and environment details to send as part of a submission - * (may be null if the report has a createErrorType). - * @param {string} options.reportEntryPoint - * A string that identify how the report has been triggered. - */ -class AbuseReport { - constructor({ addon, reportData, reportEntryPoint }) { - this[PRIVATE_REPORT_PROPS] = { - aborted: false, - abortController: new AbortController(), - addon, - reportData, - reportEntryPoint, - // message and reason are initially null, and then set by the panel - // using the related set method. - message: null, - reason: null, - }; - } - - recordTelemetry(errorType) { - const { addon, reportEntryPoint } = this; - lazy.AMTelemetry.recordReportEvent({ - addonId: addon.id, - addonType: addon.type, - errorType, - reportEntryPoint, - }); - } - - /** - * Submit the current report, given a reason and a message. - * - * @returns {Promise<void>} - * Resolves once the report has been successfully submitted. - * It rejects with an AbuseReportError if the report couldn't be - * submitted for a known reason (or another Error type otherwise). - */ - async submit() { - const { - aborted, - abortController, - message, - reason, - reportData, - reportEntryPoint, - } = this[PRIVATE_REPORT_PROPS]; - - // Record telemetry event and throw an AbuseReportError. - const rejectReportError = async (errorType, { response } = {}) => { - this.recordTelemetry(errorType); - - // Leave errorInfo empty if there is no response or fails to - // be converted into an error info object. - const errorInfo = response - ? await responseToErrorInfo(response).catch(() => undefined) - : undefined; - - throw new AbuseReportError(errorType, errorInfo); - }; - - if (aborted) { - // Report aborted before being actually submitted. - return rejectReportError("ERROR_ABORTED_SUBMIT"); - } - - // Prevent submit of a new abuse report in less than MIN_MS_BETWEEN_SUBMITS. - let msFromLastReport = AbuseReporter.getTimeFromLastReport(); - if (msFromLastReport < MIN_MS_BETWEEN_SUBMITS) { - return rejectReportError("ERROR_RECENT_SUBMIT"); - } - - let response; - try { - response = await fetch(lazy.ABUSE_REPORT_URL, { - signal: abortController.signal, - method: "POST", - credentials: "omit", - referrerPolicy: "no-referrer", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - ...reportData, - report_entry_point: reportEntryPoint, - message, - reason, - }), - }); - } catch (err) { - if (err.name === "AbortError") { - return rejectReportError("ERROR_ABORTED_SUBMIT"); - } - Cu.reportError(err); - return rejectReportError("ERROR_NETWORK"); - } - - if (response.ok && response.status >= 200 && response.status < 400) { - // Ensure that the response is also a valid json format. - try { - await response.json(); - } catch (err) { - this.recordTelemetry("ERROR_UNKNOWN"); - throw err; - } - AbuseReporter.updateLastReportTimestamp(); - this.recordTelemetry(); - return undefined; - } - - if (response.status >= 400 && response.status < 500) { - return rejectReportError("ERROR_CLIENT", { response }); - } - - if (response.status >= 500 && response.status < 600) { - return rejectReportError("ERROR_SERVER", { response }); - } - - // We got an unexpected HTTP status code. - return rejectReportError("ERROR_UNKNOWN", { response }); - } - - /** - * Abort the report submission. - */ - abort() { - const { abortController } = this[PRIVATE_REPORT_PROPS]; - abortController.abort(); - this[PRIVATE_REPORT_PROPS].aborted = true; - } - - get addon() { - return this[PRIVATE_REPORT_PROPS].addon; - } - - get reportEntryPoint() { - return this[PRIVATE_REPORT_PROPS].reportEntryPoint; - } - - /** - * Set the open message (called from the panel when the user submit the report) - * - * @parm {string} message - * An optional string which contains a description for the reported issue. - */ - setMessage(message) { - this[PRIVATE_REPORT_PROPS].message = message; - } - - /** - * Set the report reason (called from the panel when the user submit the report) - * - * @parm {string} reason - * String identifier for the report reason. - */ - setReason(reason) { - this[PRIVATE_REPORT_PROPS].reason = reason; - } -} diff --git a/toolkit/mozapps/extensions/AddonManager.sys.mjs b/toolkit/mozapps/extensions/AddonManager.sys.mjs index a07a651895..9890e15bf4 100644 --- a/toolkit/mozapps/extensions/AddonManager.sys.mjs +++ b/toolkit/mozapps/extensions/AddonManager.sys.mjs @@ -26,7 +26,6 @@ const MOZ_COMPATIBILITY_NIGHTLY = ![ const INTL_LOCALES_CHANGED = "intl:app-locales-changed"; -const PREF_AMO_ABUSEREPORT = "extensions.abuseReport.amWebAPI.enabled"; const PREF_BLOCKLIST_PINGCOUNTVERSION = "extensions.blocklist.pingCountVersion"; const PREF_EM_UPDATE_ENABLED = "extensions.update.enabled"; const PREF_EM_LAST_APP_VERSION = "extensions.lastAppVersion"; @@ -81,7 +80,6 @@ var AsyncShutdown = realAsyncShutdown; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { - AbuseReporter: "resource://gre/modules/AbuseReporter.sys.mjs", AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs", Extension: "resource://gre/modules/Extension.sys.mjs", RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", @@ -3280,7 +3278,7 @@ var AddonManagerInternal = { // the customConfirmationUI preference and responding to the // "addon-install-confirmation" notification. If the application // does not implement its own prompt, use the built-in xul dialog. - if (info.addon.userPermissions) { + if (info.addon.installPermissions) { let subject = { wrappedJSObject: { target: browser, @@ -3288,7 +3286,7 @@ var AddonManagerInternal = { }, }; subject.wrappedJSObject.info.permissions = - info.addon.userPermissions; + info.addon.installPermissions; Services.obs.notifyObservers( subject, "webextension-permission-prompt" @@ -3628,54 +3626,6 @@ var AddonManagerInternal = { } } }, - - async addonReportAbuse(target, id) { - if (!Services.prefs.getBoolPref(PREF_AMO_ABUSEREPORT, false)) { - return Promise.reject({ - message: "amWebAPI reportAbuse not supported", - }); - } - - let existingDialog = lazy.AbuseReporter.getOpenDialog(); - if (existingDialog) { - existingDialog.close(); - } - - const dialog = await lazy.AbuseReporter.openDialog( - id, - "amo", - target - ).catch(err => { - Cu.reportError(err); - return Promise.reject({ - message: "Error creating abuse report", - }); - }); - - return dialog.promiseReport.then( - async report => { - if (!report) { - return false; - } - - await report.submit().catch(err => { - Cu.reportError(err); - return Promise.reject({ - message: "Error submitting abuse report", - }); - }); - - return true; - }, - err => { - Cu.reportError(err); - dialog.close(); - return Promise.reject({ - message: "Error creating abuse report", - }); - } - ); - }, }, }; @@ -5251,40 +5201,6 @@ AMTelemetry = { }, /** - * Record an event on abuse report submissions. - * - * @params {object} opts - * @params {string} opts.addonId - * The id of the addon being reported. - * @params {string} [opts.addonType] - * The type of the addon being reported (only present for an existing - * addonId). - * @params {string} [opts.errorType] - * The AbuseReport errorType for a submission failure. - * @params {string} opts.reportEntryPoint - * The entry point of the abuse report. - */ - recordReportEvent({ addonId, addonType, errorType, reportEntryPoint }) { - this.recordEvent({ - method: "report", - object: reportEntryPoint, - value: addonId, - extra: this.formatExtraVars({ - addon_type: addonType, - error_type: errorType, - }), - }); - Glean.addonsManager.report.record( - this.formatExtraVars({ - addon_id: addonId, - addon_type: addonType, - entry_point: reportEntryPoint, - error_type: errorType, - }) - ); - }, - - /** * @params {object} opts * @params {nsIURI} opts.displayURI */ diff --git a/toolkit/mozapps/extensions/amWebAPI.sys.mjs b/toolkit/mozapps/extensions/amWebAPI.sys.mjs index ae1b94cac7..64d4bf78d2 100644 --- a/toolkit/mozapps/extensions/amWebAPI.sys.mjs +++ b/toolkit/mozapps/extensions/amWebAPI.sys.mjs @@ -2,17 +2,6 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; - -const lazy = {}; - -XPCOMUtils.defineLazyPreferenceGetter( - lazy, - "AMO_ABUSEREPORT", - "extensions.abuseReport.amWebAPI.enabled", - false -); - const MSG_PROMISE_REQUEST = "WebAPIPromiseRequest"; const MSG_PROMISE_RESULT = "WebAPIPromiseResult"; const MSG_INSTALL_EVENT = "WebAPIInstallEvent"; @@ -255,14 +244,6 @@ export class WebAPI extends APIObject { }); } - reportAbuse(id) { - return this._apiTask("addonReportAbuse", [id]); - } - - get abuseReportPanelEnabled() { - return lazy.AMO_ABUSEREPORT; - } - eventListenerAdded() { if (this.listenerCount == 0) { this.broker.setAddonListener(data => { diff --git a/toolkit/mozapps/extensions/content/aboutaddons.css b/toolkit/mozapps/extensions/content/aboutaddons.css index 2f5157bea3..bb0bf04dda 100644 --- a/toolkit/mozapps/extensions/content/aboutaddons.css +++ b/toolkit/mozapps/extensions/content/aboutaddons.css @@ -185,8 +185,7 @@ search-addons > search-textbox { max-width: var(--section-width); } -global-warnings, -#abuse-reports-messages { +global-warnings { margin-inline-start: var(--main-margin-start); max-width: var(--section-width); } @@ -645,7 +644,7 @@ panel-item[action="report"]::part(button) { .addon-inline-options { width: 100%; - background-color: white; + background-color: Canvas; margin-block: 4px; /* * Makes sure the browser minimal height is going to be the same as when @@ -675,6 +674,7 @@ addon-permissions-list > .addon-detail-row { background-position: 0 center; background-size: 1.6rem 1.6rem; background-repeat: no-repeat; + word-break: break-all; } .addon-permissions-list > li:dir(rtl) { diff --git a/toolkit/mozapps/extensions/content/aboutaddons.html b/toolkit/mozapps/extensions/content/aboutaddons.html index d0930ef42d..b48c219c05 100644 --- a/toolkit/mozapps/extensions/content/aboutaddons.html +++ b/toolkit/mozapps/extensions/content/aboutaddons.html @@ -30,7 +30,6 @@ <link rel="localization" href="branding/brand.ftl" /> <link rel="localization" href="toolkit/about/aboutAddons.ftl" /> - <link rel="localization" href="toolkit/about/abuseReports.ftl" /> <!-- Defer scripts so all the templates are loaded by the time they run. --> <script @@ -163,13 +162,6 @@ ></addon-page-header> <addon-page-options id="page-options"></addon-page-options> - <message-bar-stack - id="abuse-reports-messages" - reverse - max-message-bar-count="3" - > - </message-bar-stack> - <div id="main"></div> </div> </div> diff --git a/toolkit/mozapps/extensions/content/aboutaddonsCommon.js b/toolkit/mozapps/extensions/content/aboutaddonsCommon.js index fd91ba58be..5aa5f7ea0a 100644 --- a/toolkit/mozapps/extensions/content/aboutaddonsCommon.js +++ b/toolkit/mozapps/extensions/content/aboutaddonsCommon.js @@ -150,13 +150,13 @@ function shouldShowPermissionsPrompt(addon) { return false; } - let perms = addon.userPermissions; + let perms = addon.installPermissions; return perms?.origins.length || perms?.permissions.length; } function showPermissionsPrompt(addon) { return new Promise(resolve => { - const permissions = addon.userPermissions; + const permissions = addon.installPermissions; const target = getBrowserElement(); const onAddonEnabled = () => { diff --git a/toolkit/mozapps/extensions/content/abuse-report-frame.html b/toolkit/mozapps/extensions/content/abuse-report-frame.html deleted file mode 100644 index 64148ecece..0000000000 --- a/toolkit/mozapps/extensions/content/abuse-report-frame.html +++ /dev/null @@ -1,213 +0,0 @@ -<!-- 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/. --> - -<!DOCTYPE html> -<html> - <head> - <title></title> - <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" /> - <link - rel="stylesheet" - href="chrome://mozapps/content/extensions/aboutaddons.css" - /> - <link - rel="stylesheet" - href="chrome://mozapps/content/extensions/abuse-report-panel.css" - /> - - <link rel="localization" href="branding/brand.ftl" /> - <link rel="localization" href="toolkit/about/aboutAddons.ftl" /> - <link rel="localization" href="toolkit/about/abuseReports.ftl" /> - <script - type="module" - src="chrome://global/content/elements/moz-button-group.mjs" - ></script> - <script - type="module" - src="chrome://global/content/elements/moz-support-link.mjs" - ></script> - <script - defer - src="chrome://mozapps/content/extensions/abuse-report-panel.js" - ></script> - </head> - - <body> - <addon-abuse-report></addon-abuse-report> - - <!-- WebComponents Templates --> - <template id="tmpl-modal"> - <div class="modal-overlay-outer"></div> - <div class="modal-panel-container"></div> - </template> - - <template id="tmpl-abuse-report"> - <form class="addon-abuse-report" onsubmit="return false;"> - <div class="abuse-report-header"> - <img class="card-heading-icon addon-icon" /> - <div class="card-contents"> - <span class="addon-name"></span> - <span - class="addon-author-box" - data-l10n-args='{"author-name": "author placeholder"}' - data-l10n-id="abuse-report-addon-authored-by" - > - <a - data-l10n-name="author-name" - class="author" - href="#" - target="_blank" - ></a> - </span> - </div> - </div> - <button class="abuse-report-close-icon" type="button"></button> - <div class="abuse-report-contents"> - <abuse-report-reasons-panel></abuse-report-reasons-panel> - <abuse-report-submit-panel hidden></abuse-report-submit-panel> - </div> - <div class="abuse-report-buttons"> - <moz-button-group class="abuse-report-reasons-buttons"> - <button - class="abuse-report-cancel" - type="button" - data-l10n-id="abuse-report-cancel-button" - ></button> - <button - class="primary abuse-report-next" - type="button" - data-l10n-id="abuse-report-next-button" - ></button> - </moz-button-group> - <moz-button-group class="abuse-report-submit-buttons" hidden> - <button - class="abuse-report-goback" - type="button" - data-l10n-id="abuse-report-goback-button" - ></button> - <button - class="primary abuse-report-submit" - type="button" - data-l10n-id="abuse-report-submit-button" - ></button> - </moz-button-group> - </div> - </form> - </template> - - <template id="tmpl-reasons-panel"> - <h2 class="abuse-report-title"></h2> - <hr /> - <p class="abuse-report-subtitle" data-l10n-id="abuse-report-subtitle"></p> - <ul class="abuse-report-reasons"> - <li is="abuse-report-reason-listitem" report-reason="other"></li> - </ul> - <p> - <span data-l10n-id="abuse-report-learnmore-intro"></span> - <a - is="moz-support-link" - target="_blank" - support-page="reporting-extensions-and-themes-abuse" - data-l10n-id="abuse-report-learnmore-link" - > - </a> - </p> - </template> - - <template id="tmpl-submit-panel"> - <h2 class="abuse-reason-title"></h2> - <abuse-report-reason-suggestions></abuse-report-reason-suggestions> - <hr /> - <p - class="abuse-report-subtitle" - data-l10n-id="abuse-report-submit-description" - ></p> - <textarea name="message" data-l10n-id="abuse-report-textarea"></textarea> - <p class="abuse-report-note" data-l10n-id="abuse-report-submit-note"></p> - </template> - - <template id="tmpl-reason-listitem"> - <label> - <input type="radio" name="reason" class="radio" /> - <span class="reason-description"></span> - <span hidden class="abuse-report-note reason-example"></span> - </label> - </template> - - <template id="tmpl-suggestions-settings"> - <p data-l10n-id="abuse-report-settings-suggestions"></p> - <p></p> - <ul> - <li> - <a - is="moz-support-link" - target="_blank" - data-l10n-id="abuse-report-settings-suggestions-search" - support-page="prefs-search" - > - </a> - </li> - <li> - <a - is="moz-support-link" - target="_blank" - data-l10n-id="abuse-report-settings-suggestions-homepage" - support-page="prefs-homepage" - > - </a> - </li> - </ul> - </template> - - <template id="tmpl-suggestions-policy"> - <p data-l10n-id="abuse-report-policy-suggestions"> - <a - class="abuse-policy-learnmore" - target="_blank" - data-l10n-name="report-infringement-link" - > - </a> - </p> - </template> - - <template id="tmpl-suggestions-broken-extension"> - <p data-l10n-id="abuse-report-broken-suggestions-extension"> - <a - class="extension-support-link" - target="_blank" - data-l10n-name="support-link" - > - </a> - </p> - - <p></p - ></template> - - <template id="tmpl-suggestions-broken-theme"> - <p data-l10n-id="abuse-report-broken-suggestions-theme"> - <a - class="extension-support-link" - target="_blank" - data-l10n-name="support-link" - > - </a> - </p> - - <p></p - ></template> - - <template id="tmpl-suggestions-broken-sitepermission"> - <p data-l10n-id="abuse-report-broken-suggestions-sitepermission"> - <a - class="extension-support-link" - target="_blank" - data-l10n-name="support-link" - > - </a> - </p> - - <p></p - ></template> - </body> -</html> diff --git a/toolkit/mozapps/extensions/content/abuse-report-panel.css b/toolkit/mozapps/extensions/content/abuse-report-panel.css deleted file mode 100644 index aa2243c162..0000000000 --- a/toolkit/mozapps/extensions/content/abuse-report-panel.css +++ /dev/null @@ -1,181 +0,0 @@ -/* 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/. */ - -/* Abuse Reports card */ - -:root { - --close-icon-url: url("chrome://global/skin/icons/close.svg"); - --close-icon-size: 20px; - - --modal-panel-min-width: 60%; - --modal-panel-margin-top: 36px; - --modal-panel-margin-bottom: 36px; - --modal-panel-margin: 20%; - --modal-panel-padding: 40px; - - --line-height: 20px; - --textarea-height: 220px; - --listitem-padding-bottom: 14px; - --list-radio-column-size: 28px; - --note-font-size: 14px; - --note-font-weight: 400; - --subtitle-font-size: 16px; - --subtitle-font-weight: 600; -} - -/* Ensure that the document (embedded in the XUL about:addons using a - XUL browser) has a transparent background */ -html { - background-color: transparent; -} - -.modal-overlay-outer { - background: var(--grey-90-a60); - width: 100%; - height: 100%; - position: fixed; - z-index: -1; -} - -.modal-panel-container { - padding-top: var(--modal-panel-margin-top); - padding-bottom: var(--modal-panel-margin-bottom); - padding-left: var(--modal-panel-margin); - padding-right: var(--modal-panel-margin); -} - -.addon-abuse-report { - min-width: var(--modal-panel-min-width); - padding: var(--modal-panel-padding); - display: flex; - flex-direction: column; - position: relative; -} - -.addon-abuse-report:hover { - /* Avoid the card box highlighting on hover. */ - box-shadow: none; -} - - -.abuse-report-close-icon { - /* position the close button in the panel upper-right corner */ - position: absolute; - top: 12px; - inset-inline-end: 16px; -} - -button.abuse-report-close-icon { - background: var(--close-icon-url) no-repeat center center; - -moz-context-properties: fill; - color: inherit !important; - fill: currentColor; - min-width: auto; - min-height: auto; - width: var(--close-icon-size); - height: var(--close-icon-size); - margin: 0; - padding: 0; -} - -button.abuse-report-close-icon:hover { - fill-opacity: 0.1; -} - -button.abuse-report-close-icon:hover:active { - fill-opacity: 0.2; -} - -.abuse-report-header { - display: flex; - flex-direction: row; -} - -.abuse-report-contents, -.abuse-report-contents > hr { - width: 100%; -} - -.abuse-report-note { - color: var(--text-color-deemphasized); - font-size: var(--note-font-size); - font-weight: var(--note-font-weight); - line-height: var(--line-height); -} - -.abuse-report-subtitle { - font-size: var(--subtitle-font-size); - font-weight: var(--subtitle-font-weight); - line-height: var(--line-height); -} - -ul.abuse-report-reasons { - list-style-type: none; - padding-inline-start: 0; -} - -ul.abuse-report-reasons > li { - display: flex; - padding-bottom: var(--listitem-padding-bottom); -} - -ul.abuse-report-reasons > li > label { - display: grid; - grid-template-columns: var(--list-radio-column-size) auto; - grid-template-rows: 50% auto; - width: 100%; - line-height: var(--line-height); - font-size: var(--subtitle-font-size); - font-weight: var(--note-font-weight); - margin-inline-start: 4px; -} - -ul.abuse-report-reasons > li > label > [type="radio"] { - grid-column: 1; -} - -ul.abuse-report-reasons > li > label > span { - grid-column: 2; -} - -abuse-report-submit-panel textarea { - width: 100%; - height: var(--textarea-height); - resize: none; - box-sizing: border-box; -} - -/* Adapt styles for the panel opened in its own dialog window */ - -html.dialog-window { - background-color: var(--in-content-box-background); - height: 100%; - min-width: 740px; -} - -html.dialog-window body { - overflow: hidden; - min-height: 100%; - display: flex; - flex-direction: column; -} - -html.dialog-window .abuse-report-close-icon { - display: none; -} - -html.dialog-window addon-abuse-report { - flex-grow: 1; - display: flex; - /* Ensure that the dialog window starts from a reasonable initial size */ - --modal-panel-min-width: 700px; -} - -html.dialog-window addon-abuse-report form { - display: flex; -} - -html.dialog-window addon-abuse-report form .abuse-report-contents { - flex-grow: 1; -} diff --git a/toolkit/mozapps/extensions/content/abuse-report-panel.js b/toolkit/mozapps/extensions/content/abuse-report-panel.js deleted file mode 100644 index 53e0d76db0..0000000000 --- a/toolkit/mozapps/extensions/content/abuse-report-panel.js +++ /dev/null @@ -1,873 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -/* eslint max-len: ["error", 80] */ - -"use strict"; - -ChromeUtils.defineESModuleGetters(this, { - AbuseReporter: "resource://gre/modules/AbuseReporter.sys.mjs", -}); - -const IS_DIALOG_WINDOW = window.arguments && window.arguments.length; - -let openWebLink = IS_DIALOG_WINDOW - ? window.arguments[0].wrappedJSObject.openWebLink - : url => { - window.windowRoot.ownerGlobal.openWebLinkIn(url, "tab", { - relatedToCurrent: true, - }); - }; - -const showOnAnyType = () => false; -const hideOnAnyType = () => true; -const hideOnAddonTypes = hideForTypes => { - return addonType => hideForTypes.includes(addonType); -}; - -// The reasons string used as a key in this Map is expected to stay in sync -// with the reasons string used in the "abuseReports.ftl" locale file and -// the suggestions templates included in abuse-report-frame.html. -const ABUSE_REASONS = (window.ABUSE_REPORT_REASONS = { - damage: { - isExampleHidden: showOnAnyType, - isReasonHidden: hideOnAddonTypes(["theme"]), - }, - spam: { - isExampleHidden: showOnAnyType, - isReasonHidden: hideOnAddonTypes(["sitepermission"]), - }, - settings: { - hasSuggestions: true, - isExampleHidden: hideOnAnyType, - isReasonHidden: hideOnAddonTypes(["theme", "sitepermission"]), - }, - deceptive: { - isExampleHidden: showOnAnyType, - isReasonHidden: hideOnAddonTypes(["sitepermission"]), - }, - broken: { - hasAddonTypeL10nId: true, - hasAddonTypeSuggestionTemplate: true, - hasSuggestions: true, - isExampleHidden: hideOnAddonTypes(["theme"]), - isReasonHidden: showOnAnyType, - requiresSupportURL: true, - }, - policy: { - hasSuggestions: true, - isExampleHidden: hideOnAnyType, - isReasonHidden: hideOnAddonTypes(["sitepermission"]), - }, - unwanted: { - isExampleHidden: showOnAnyType, - isReasonHidden: hideOnAddonTypes(["theme"]), - }, - other: { - isExampleHidden: hideOnAnyType, - isReasonHidden: showOnAnyType, - }, -}); - -// Maps the reason id to the last version of the related fluent id. -// NOTE: when changing the localized string, increase the `-vN` suffix -// in the abuseReports.ftl fluent file and update this mapping table. -const REASON_L10N_STRING_MAPPING = { - "abuse-report-damage-reason": "abuse-report-damage-reason-v2", - "abuse-report-spam-reason": "abuse-report-spam-reason-v2", - "abuse-report-settings-reason": "abuse-report-settings-reason-v2", - "abuse-report-deceptive-reason": "abuse-report-deceptive-reason-v2", - "abuse-report-broken-reason-extension": - "abuse-report-broken-reason-extension-v2", - "abuse-report-broken-reason-sitepermission": - "abuse-report-broken-reason-sitepermission-v2", - "abuse-report-broken-reason-theme": "abuse-report-broken-reason-theme-v2", - "abuse-report-policy-reason": "abuse-report-policy-reason-v2", - "abuse-report-unwanted-reason": "abuse-report-unwanted-reason-v2", -}; - -function getReasonL10nId(reason, addonType) { - let reasonId = `abuse-report-${reason}-reason`; - // Special case reasons that have a addonType-specific - // l10n id. - if (ABUSE_REASONS[reason].hasAddonTypeL10nId) { - reasonId += `-${addonType}`; - } - // Map the reason to the corresponding versionized fluent string, using the - // mapping table above, if available. - return REASON_L10N_STRING_MAPPING[reasonId] || reasonId; -} - -function getSuggestionsTemplate({ addonType, reason, supportURL }) { - const reasonInfo = ABUSE_REASONS[reason]; - - if ( - !addonType || - !reasonInfo.hasSuggestions || - (reasonInfo.requiresSupportURL && !supportURL) - ) { - return null; - } - - let templateId = `tmpl-suggestions-${reason}`; - // Special case reasons that have a addonType-specific - // suggestion template. - if (reasonInfo.hasAddonTypeSuggestionTemplate) { - templateId += `-${addonType}`; - } - - return document.getElementById(templateId); -} - -// Map of the learnmore links metadata, keyed by link element class. -const LEARNMORE_LINKS = { - ".abuse-policy-learnmore": { - baseURL: "https://www.mozilla.org/%LOCALE%/", - path: "about/legal/report-infringement/", - }, -}; - -// Format links that match the selector in the LEARNMORE_LINKS map -// found in a given container element. -function formatLearnMoreURLs(containerEl) { - for (const [linkClass, linkInfo] of Object.entries(LEARNMORE_LINKS)) { - for (const element of containerEl.querySelectorAll(linkClass)) { - const baseURL = Services.urlFormatter.formatURL(linkInfo.baseURL); - - element.href = baseURL + linkInfo.path; - } - } -} - -// Define a set of getters from a Map<propertyName, selector>. -function defineElementSelectorsGetters(object, propsMap) { - const props = Object.entries(propsMap).reduce((acc, entry) => { - const [name, selector] = entry; - acc[name] = { get: () => object.querySelector(selector) }; - return acc; - }, {}); - Object.defineProperties(object, props); -} - -// Define a set of properties getters and setters for a -// Map<propertyName, attributeName>. -function defineElementAttributesProperties(object, propsMap) { - const props = Object.entries(propsMap).reduce((acc, entry) => { - const [name, attr] = entry; - acc[name] = { - get: () => object.getAttribute(attr), - set: value => { - object.setAttribute(attr, value); - }, - }; - return acc; - }, {}); - Object.defineProperties(object, props); -} - -// Return an object with properties associated to elements -// found using the related selector in the propsMap. -function getElements(containerEl, propsMap) { - return Object.entries(propsMap).reduce((acc, entry) => { - const [name, selector] = entry; - let elements = containerEl.querySelectorAll(selector); - acc[name] = elements.length > 1 ? elements : elements[0]; - return acc; - }, {}); -} - -function dispatchCustomEvent(el, eventName, detail) { - el.dispatchEvent(new CustomEvent(eventName, { detail })); -} - -// This WebComponent extends the li item to represent an abuse report reason -// and it is responsible for: -// - embedding a photon styled radio buttons -// - localizing the reason list item -// - optionally embedding a localized example, positioned -// below the reason label, and adjusts the item height -// accordingly -class AbuseReasonListItem extends HTMLLIElement { - constructor() { - super(); - defineElementAttributesProperties(this, { - addonType: "addon-type", - reason: "report-reason", - checked: "checked", - }); - } - - connectedCallback() { - this.update(); - } - - async update() { - if (this.reason !== "other" && !this.addonType) { - return; - } - - const { reason, checked, addonType } = this; - - this.textContent = ""; - const content = document.importNode(this.template.content, true); - - if (reason) { - const reasonId = `abuse-reason-${reason}`; - const reasonInfo = ABUSE_REASONS[reason] || {}; - - const { labelEl, descriptionEl, radioEl } = getElements(content, { - labelEl: "label", - descriptionEl: ".reason-description", - radioEl: "input[type=radio]", - }); - - labelEl.setAttribute("for", reasonId); - radioEl.id = reasonId; - radioEl.value = reason; - radioEl.checked = !!checked; - - // This reason has a different localized description based on the - // addon type. - document.l10n.setAttributes( - descriptionEl, - getReasonL10nId(reason, addonType) - ); - - // Show the reason example if supported for the addon type. - if (!reasonInfo.isExampleHidden(addonType)) { - const exampleEl = content.querySelector(".reason-example"); - document.l10n.setAttributes( - exampleEl, - `abuse-report-${reason}-example` - ); - exampleEl.hidden = false; - } - } - - formatLearnMoreURLs(content); - - this.appendChild(content); - } - - get template() { - return document.getElementById("tmpl-reason-listitem"); - } -} - -// This WebComponents implements the first step of the abuse -// report submission and embeds a randomized reasons list. -class AbuseReasonsPanel extends HTMLElement { - constructor() { - super(); - defineElementAttributesProperties(this, { - addonType: "addon-type", - }); - } - - connectedCallback() { - this.update(); - } - - update() { - if (!this.isConnected || !this.addonType) { - return; - } - - const { addonType } = this; - - this.textContent = ""; - const content = document.importNode(this.template.content, true); - - const { titleEl, listEl } = getElements(content, { - titleEl: ".abuse-report-title", - listEl: "ul.abuse-report-reasons", - }); - - // Change the title l10n-id if the addon type is theme. - document.l10n.setAttributes(titleEl, `abuse-report-title-${addonType}`); - - // Create the randomized list of reasons. - const reasons = Object.keys(ABUSE_REASONS) - .filter(reason => reason !== "other") - .sort(() => Math.random() - 0.5); - - for (const reason of reasons) { - const reasonInfo = ABUSE_REASONS[reason]; - if (!reasonInfo || reasonInfo.isReasonHidden(addonType)) { - // Skip an extension only reason while reporting a theme. - continue; - } - const item = document.createElement("li", { - is: "abuse-report-reason-listitem", - }); - item.reason = reason; - item.addonType = addonType; - - listEl.prepend(item); - } - - listEl.firstElementChild.checked = true; - formatLearnMoreURLs(content); - - this.appendChild(content); - } - - get template() { - return document.getElementById("tmpl-reasons-panel"); - } -} - -// This WebComponent is responsible for the suggestions, which are: -// - generated based on a template keyed by abuse report reason -// - localized by assigning fluent ids generated from the abuse report reason -// - learn more and extension support url are then generated when the -// specific reason expects it -class AbuseReasonSuggestions extends HTMLElement { - constructor() { - super(); - defineElementAttributesProperties(this, { - extensionSupportURL: "extension-support-url", - reason: "report-reason", - }); - } - - update() { - const { addonType, extensionSupportURL, reason } = this; - - this.textContent = ""; - - let template = getSuggestionsTemplate({ - addonType, - reason, - supportURL: extensionSupportURL, - }); - - if (template) { - let content = document.importNode(template.content, true); - - formatLearnMoreURLs(content); - - let extSupportLink = content.querySelector("a.extension-support-link"); - if (extSupportLink) { - extSupportLink.href = extensionSupportURL; - } - - this.appendChild(content); - this.hidden = false; - } else { - this.hidden = true; - } - } -} - -// This WebComponents implements the last step of the abuse report submission. -class AbuseSubmitPanel extends HTMLElement { - constructor() { - super(); - defineElementAttributesProperties(this, { - addonType: "addon-type", - reason: "report-reason", - extensionSupportURL: "extensionSupportURL", - }); - defineElementSelectorsGetters(this, { - _textarea: "textarea", - _title: ".abuse-reason-title", - _suggestions: "abuse-report-reason-suggestions", - }); - } - - connectedCallback() { - this.render(); - } - - render() { - this.textContent = ""; - this.appendChild(document.importNode(this.template.content, true)); - } - - update() { - if (!this.isConnected || !this.addonType) { - return; - } - const { addonType, reason, _suggestions, _title } = this; - document.l10n.setAttributes(_title, getReasonL10nId(reason, addonType)); - _suggestions.reason = reason; - _suggestions.addonType = addonType; - _suggestions.extensionSupportURL = this.extensionSupportURL; - _suggestions.update(); - } - - clear() { - this._textarea.value = ""; - } - - get template() { - return document.getElementById("tmpl-submit-panel"); - } -} - -// This WebComponent provides the abuse report -class AbuseReport extends HTMLElement { - constructor() { - super(); - this._report = null; - defineElementSelectorsGetters(this, { - _form: "form", - _textarea: "textarea", - _radioCheckedReason: "[type=radio]:checked", - _reasonsPanel: "abuse-report-reasons-panel", - _submitPanel: "abuse-report-submit-panel", - _reasonsPanelButtons: ".abuse-report-reasons-buttons", - _submitPanelButtons: ".abuse-report-submit-buttons", - _iconClose: ".abuse-report-close-icon", - _btnNext: "button.abuse-report-next", - _btnCancel: "button.abuse-report-cancel", - _btnGoBack: "button.abuse-report-goback", - _btnSubmit: "button.abuse-report-submit", - _addonAuthorContainer: ".abuse-report-header .addon-author-box", - _addonIconElement: ".abuse-report-header img.addon-icon", - _addonNameElement: ".abuse-report-header .addon-name", - _linkAddonAuthor: ".abuse-report-header .addon-author-box a.author", - }); - } - - connectedCallback() { - this.render(); - - this.addEventListener("click", this); - - // Start listening to keydown events (to close the modal - // when Escape has been pressed and to handling the keyboard - // navigation). - document.addEventListener("keydown", this); - } - - disconnectedCallback() { - this.textContent = ""; - this.removeEventListener("click", this); - document.removeEventListener("keydown", this); - } - - handleEvent(evt) { - if (!this.isConnected || !this.addon) { - return; - } - - switch (evt.type) { - case "keydown": - if (evt.key === "Escape") { - // Prevent Esc to close the panel if the textarea is - // empty. - if (this.message && !this._submitPanel.hidden) { - return; - } - this.cancel(); - } - if (!IS_DIALOG_WINDOW) { - // Workaround keyboard navigation issues when - // the panel is running in its own dialog window. - this.handleKeyboardNavigation(evt); - } - break; - case "click": - if (evt.target === this._iconClose || evt.target === this._btnCancel) { - // NOTE: clear the focus on the clicked element to ensure that - // -moz-focusring pseudo class is not still set on the element - // when the panel is going to be shown again (See Bug 1560949). - evt.target.blur(); - this.cancel(); - } - if (evt.target === this._btnNext) { - this.switchToSubmitMode(); - } - if (evt.target === this._btnGoBack) { - this.switchToListMode(); - } - if (evt.target === this._btnSubmit) { - this.submit(); - } - if (evt.target.localName === "a") { - evt.preventDefault(); - evt.stopPropagation(); - const url = evt.target.getAttribute("href"); - // Ignore if url is empty. - if (url) { - openWebLink(url); - } - } - break; - } - } - - handleKeyboardNavigation(evt) { - if ( - evt.keyCode !== evt.DOM_VK_TAB || - evt.altKey || - evt.controlKey || - evt.metaKey - ) { - return; - } - - const fm = Services.focus; - const backward = evt.shiftKey; - - const isFirstFocusableElement = el => { - // Also consider the document body as a valid first focusable element. - if (el === document.body) { - return true; - } - // XXXrpl unfortunately there is no way to get the first focusable element - // without asking the focus manager to move focus to it (similar strategy - // is also being used in about:prefereces subdialog.js). - const rv = el == fm.moveFocus(window, null, fm.MOVEFOCUS_FIRST, 0); - fm.setFocus(el, 0); - return rv; - }; - - // If the focus is exiting the panel while navigating - // backward, focus the previous element sibling on the - // Firefox UI. - if (backward && isFirstFocusableElement(evt.target)) { - evt.preventDefault(); - evt.stopImmediatePropagation(); - const chromeWin = window.windowRoot.ownerGlobal; - Services.focus.moveFocus( - chromeWin, - null, - Services.focus.MOVEFOCUS_BACKWARD, - Services.focus.FLAG_BYKEY - ); - } - } - - render() { - this.textContent = ""; - const formTemplate = document.importNode(this.template.content, true); - if (IS_DIALOG_WINDOW) { - this.appendChild(formTemplate); - } else { - // Append the report form inside a modal overlay when the report panel - // is a sub-frame of the about:addons tab. - const modalTemplate = document.importNode( - this.modalTemplate.content, - true - ); - - this.appendChild(modalTemplate); - this.querySelector(".modal-panel-container").appendChild(formTemplate); - - // Add the card styles to the form. - this.querySelector("form").classList.add("card"); - } - } - - async update() { - if (!this.addon) { - return; - } - - const { - addonId, - addonType, - _addonAuthorContainer, - _addonIconElement, - _addonNameElement, - _linkAddonAuthor, - _reasonsPanel, - _submitPanel, - } = this; - - // Ensure that the first step of the abuse submission is the one - // currently visible. - this.switchToListMode(); - - // Cancel the abuse report if the addon is not an extension or theme. - if (!AbuseReporter.isSupportedAddonType(addonType)) { - Cu.reportError( - new Error( - `Closing abuse report panel on unexpected addon type: ${addonType}` - ) - ); - this.cancel(); - return; - } - - _addonNameElement.textContent = this.addonName; - - if (this.authorName) { - _linkAddonAuthor.href = this.authorURL || this.homepageURL; - _linkAddonAuthor.textContent = this.authorName; - document.l10n.setAttributes( - _linkAddonAuthor.parentNode, - "abuse-report-addon-authored-by", - { "author-name": this.authorName } - ); - _addonAuthorContainer.hidden = false; - } else { - _addonAuthorContainer.hidden = true; - } - - _addonIconElement.setAttribute("src", this.iconURL); - - _reasonsPanel.addonType = this.addonType; - _reasonsPanel.update(); - - _submitPanel.addonType = this.addonType; - _submitPanel.reason = this.reason; - _submitPanel.extensionSupportURL = this.supportURL; - _submitPanel.update(); - - this.focus(); - - dispatchCustomEvent(this, "abuse-report:updated", { - addonId, - panel: "reasons", - }); - } - - setAbuseReport(abuseReport) { - this._report = abuseReport; - // Clear the textarea from any previously entered content. - this._submitPanel.clear(); - - if (abuseReport) { - this.update(); - this.hidden = false; - } else { - this.hidden = true; - } - } - - focus() { - if (!this.isConnected || !this.addon) { - return; - } - if (this._reasonsPanel.hidden) { - const { _textarea } = this; - _textarea.focus(); - _textarea.select(); - } else { - const { _radioCheckedReason } = this; - if (_radioCheckedReason) { - _radioCheckedReason.focus(); - } - } - } - - cancel() { - if (!this.isConnected || !this.addon) { - return; - } - this._report = null; - dispatchCustomEvent(this, "abuse-report:cancel"); - } - - submit() { - if (!this.isConnected || !this.addon) { - return; - } - this._report.setMessage(this.message); - this._report.setReason(this.reason); - dispatchCustomEvent(this, "abuse-report:submit", { - addonId: this.addonId, - report: this._report, - }); - } - - switchToSubmitMode() { - if (!this.isConnected || !this.addon) { - return; - } - this._submitPanel.reason = this.reason; - this._submitPanel.update(); - this._reasonsPanel.hidden = true; - this._reasonsPanelButtons.hidden = true; - this._submitPanel.hidden = false; - this._submitPanelButtons.hidden = false; - // Adjust the focused element when switching to the submit panel. - this.focus(); - dispatchCustomEvent(this, "abuse-report:updated", { - addonId: this.addonId, - panel: "submit", - }); - } - - switchToListMode() { - if (!this.isConnected || !this.addon) { - return; - } - this._submitPanel.hidden = true; - this._submitPanelButtons.hidden = true; - this._reasonsPanel.hidden = false; - this._reasonsPanelButtons.hidden = false; - // Adjust the focused element when switching back to the list of reasons. - this.focus(); - dispatchCustomEvent(this, "abuse-report:updated", { - addonId: this.addonId, - panel: "reasons", - }); - } - - get addon() { - return this._report?.addon; - } - - get addonId() { - return this.addon?.id; - } - - get addonName() { - return this.addon?.name; - } - - get addonType() { - // TODO(Bug 1789718): Remove after the deprecated XPIProvider-based - // implementation is also removed. - if (this.addon?.type === "sitepermission-deprecated") { - return "sitepermission"; - } - return this.addon?.type; - } - - get addonCreator() { - return this.addon?.creator; - } - - get homepageURL() { - return this.addon?.homepageURL || this.authorURL || ""; - } - - get authorName() { - // The author name may be missing on some of the test extensions - // (or for temporarily installed add-ons). - return this.addonCreator?.name || ""; - } - - get authorURL() { - return this.addonCreator?.url || ""; - } - - get iconURL() { - if (this.addonType === "sitepermission") { - return "chrome://mozapps/skin/extensions/category-sitepermission.svg"; - } - return ( - this.addon?.iconURL || - // Some extensions (e.g. static theme addons) may not have an icon, - // and so we fallback to use the generic extension icon. - "chrome://mozapps/skin/extensions/extensionGeneric.svg" - ); - } - - get supportURL() { - let url = this.addon?.supportURL || this.homepageURL || ""; - if (!url && this.addonType === "sitepermission" && this.addon?.siteOrigin) { - return this.addon.siteOrigin; - } - return url; - } - - get message() { - return this._form.elements.message.value; - } - - get reason() { - return this._form.elements.reason.value; - } - - get modalTemplate() { - return document.getElementById("tmpl-modal"); - } - - get template() { - return document.getElementById("tmpl-abuse-report"); - } -} - -customElements.define("abuse-report-reason-listitem", AbuseReasonListItem, { - extends: "li", -}); -customElements.define( - "abuse-report-reason-suggestions", - AbuseReasonSuggestions -); -customElements.define("abuse-report-reasons-panel", AbuseReasonsPanel); -customElements.define("abuse-report-submit-panel", AbuseSubmitPanel); -customElements.define("addon-abuse-report", AbuseReport); - -// The panel has been opened in a new dialog window. -if (IS_DIALOG_WINDOW) { - // CSS customizations when panel is in its own window - // (vs. being an about:addons subframe). - document.documentElement.className = "dialog-window"; - - const { report, deferredReport, deferredReportPanel } = - window.arguments[0].wrappedJSObject; - - window.addEventListener( - "unload", - () => { - // If the window has been closed resolve the deferredReport - // promise and reject the deferredReportPanel one, in case - // they haven't been resolved yet. - deferredReport.resolve({ userCancelled: true }); - deferredReportPanel.reject(new Error("report dialog closed")); - }, - { once: true } - ); - - document.l10n.setAttributes( - document.querySelector("head > title"), - "abuse-report-dialog-title", - { - "addon-name": report.addon.name, - } - ); - - const el = document.querySelector("addon-abuse-report"); - el.addEventListener("abuse-report:submit", () => { - deferredReport.resolve({ - userCancelled: false, - report, - }); - }); - el.addEventListener( - "abuse-report:cancel", - () => { - // Resolve the report panel deferred (in case the report - // has been cancelled automatically before it has been fully - // rendered, e.g. in case of non-supported addon types). - deferredReportPanel.resolve(el); - // Resolve the deferred report as cancelled. - deferredReport.resolve({ userCancelled: true }); - }, - { once: true } - ); - - // Adjust window size (if needed) once the fluent strings have been - // added to the document and the document has been flushed. - el.addEventListener( - "abuse-report:updated", - async () => { - const form = document.querySelector("form"); - await document.l10n.translateFragment(form); - const { scrollWidth, scrollHeight } = await window.promiseDocumentFlushed( - () => form - ); - // Resolve promiseReportPanel once the panel completed the initial render - // (used in tests). - deferredReportPanel.resolve(el); - if ( - window.innerWidth !== scrollWidth || - window.innerHeight !== scrollHeight - ) { - const width = window.outerWidth - window.innerWidth + scrollWidth; - const height = window.outerHeight - window.innerHeight + scrollHeight; - window.resizeTo(width, height); - } - }, - { once: true } - ); - el.setAbuseReport(report); -} diff --git a/toolkit/mozapps/extensions/content/abuse-reports.js b/toolkit/mozapps/extensions/content/abuse-reports.js index 38fa0f9f46..d978aa1293 100644 --- a/toolkit/mozapps/extensions/content/abuse-reports.js +++ b/toolkit/mozapps/extensions/content/abuse-reports.js @@ -4,188 +4,22 @@ /* eslint max-len: ["error", 80] */ /* import-globals-from aboutaddonsCommon.js */ -/* exported openAbuseReport */ +/* exported AbuseReporter, openAbuseReport */ /* global windowRoot */ /** * This script is part of the HTML about:addons page and it provides some - * helpers used for the Abuse Reporting submission (and related message bars). + * helpers used for abuse reports. */ const { AbuseReporter } = ChromeUtils.importESModule( "resource://gre/modules/AbuseReporter.sys.mjs" ); -// Message Bars definitions. -const ABUSE_REPORT_MESSAGE_BARS = { - // Idle message-bar (used while the submission is still ongoing). - submitting: { - actions: ["cancel"], - l10n: { - id: "abuse-report-messagebar-submitting2", - actionIds: { - cancel: "abuse-report-messagebar-action-cancel", - }, - }, - }, - // Submitted report message-bar. - submitted: { - actions: ["remove", "keep"], - dismissable: true, - l10n: { - id: "abuse-report-messagebar-submitted2", - actionIdsPerAddonType: { - extension: { - remove: "abuse-report-messagebar-action-remove-extension", - keep: "abuse-report-messagebar-action-keep-extension", - }, - sitepermission: { - remove: "abuse-report-messagebar-action-remove-sitepermission", - keep: "abuse-report-messagebar-action-keep-sitepermission", - }, - theme: { - remove: "abuse-report-messagebar-action-remove-theme", - keep: "abuse-report-messagebar-action-keep-theme", - }, - }, - }, - }, - // Submitted report message-bar (with no remove actions). - "submitted-no-remove-action": { - dismissable: true, - l10n: { id: "abuse-report-messagebar-submitted-noremove2" }, - }, - // Submitted report and remove addon message-bar. - "submitted-and-removed": { - dismissable: true, - l10n: { - idsPerAddonType: { - extension: "abuse-report-messagebar-removed-extension2", - sitepermission: "abuse-report-messagebar-removed-sitepermission2", - theme: "abuse-report-messagebar-removed-theme2", - }, - }, - }, - // The "aborted report" message bar is rendered as a generic informative one, - // because aborting a report is triggered by a user choice. - ERROR_ABORTED_SUBMIT: { - type: "info", - dismissable: true, - l10n: { id: "abuse-report-messagebar-aborted2" }, - }, - // Errors message bars. - ERROR_ADDON_NOTFOUND: { - type: "error", - dismissable: true, - l10n: { id: "abuse-report-messagebar-error2" }, - }, - ERROR_CLIENT: { - type: "error", - dismissable: true, - l10n: { id: "abuse-report-messagebar-error2" }, - }, - ERROR_NETWORK: { - actions: ["retry", "cancel"], - type: "error", - l10n: { - id: "abuse-report-messagebar-error2", - actionIds: { - retry: "abuse-report-messagebar-action-retry", - cancel: "abuse-report-messagebar-action-cancel", - }, - }, - }, - ERROR_RECENT_SUBMIT: { - actions: ["retry", "cancel"], - type: "error", - l10n: { - id: "abuse-report-messagebar-error-recent-submit2", - actionIds: { - retry: "abuse-report-messagebar-action-retry", - cancel: "abuse-report-messagebar-action-cancel", - }, - }, - }, - ERROR_SERVER: { - actions: ["retry", "cancel"], - type: "error", - l10n: { - id: "abuse-report-messagebar-error2", - actionIds: { - retry: "abuse-report-messagebar-action-retry", - cancel: "abuse-report-messagebar-action-cancel", - }, - }, - }, - ERROR_UNKNOWN: { - actions: ["retry", "cancel"], - type: "error", - l10n: { - id: "abuse-report-messagebar-error2", - actionIds: { - retry: "abuse-report-messagebar-action-retry", - cancel: "abuse-report-messagebar-action-cancel", - }, - }, - }, -}; +async function openAbuseReport({ addonId }) { + // TODO: `reportEntryPoint` is also passed to this function but we aren't + // using it currently. Maybe we should? -async function openAbuseReport({ addonId, reportEntryPoint }) { - try { - const reportDialog = await AbuseReporter.openDialog( - addonId, - reportEntryPoint, - window.docShell.chromeEventHandler - ); - - // Warn the user before the about:addons tab while an - // abuse report dialog is still open, and close the - // report dialog if the user choose to close the related - // about:addons tab. - const beforeunloadListener = evt => evt.preventDefault(); - const unloadListener = () => reportDialog.close(); - const clearUnloadListeners = () => { - window.removeEventListener("beforeunload", beforeunloadListener); - window.removeEventListener("unload", unloadListener); - }; - window.addEventListener("beforeunload", beforeunloadListener); - window.addEventListener("unload", unloadListener); - - reportDialog.promiseReport - .then( - report => { - if (report) { - submitReport({ report }); - } - }, - err => { - Cu.reportError( - `Unexpected abuse report panel error: ${err} :: ${err.stack}` - ); - reportDialog.close(); - } - ) - .then(clearUnloadListeners); - } catch (err) { - // Log the detailed error to the browser console. - Cu.reportError(err); - document.dispatchEvent( - new CustomEvent("abuse-report:create-error", { - detail: { - addonId, - addon: err.addon, - errorType: err.errorType, - }, - }) - ); - } -} - -// Unlike the openAbuseReport function, technically this method wouldn't need -// to be async, but it is so that both the implementations will be providing -// the same type signatures (returning a promise) to the callers, independently -// from which abuse reporting feature is enabled. -async function openAbuseReportAMOForm({ addonId }) { const amoUrl = AbuseReporter.getAMOFormURL({ addonId }); windowRoot.ownerGlobal.openTrustedLinkIn(amoUrl, "tab", { // Make sure the newly open tab is going to be focused, independently @@ -194,183 +28,4 @@ async function openAbuseReportAMOForm({ addonId }) { }); } -window.openAbuseReport = AbuseReporter.amoFormEnabled - ? openAbuseReportAMOForm - : openAbuseReport; - -// Helper function used to create abuse report message bars in the -// HTML about:addons page. -function createReportMessageBar( - definitionId, - { addonId, addonName, addonType }, - { onclose, onaction } = {} -) { - const barInfo = ABUSE_REPORT_MESSAGE_BARS[definitionId]; - if (!barInfo) { - throw new Error(`message-bar definition not found: ${definitionId}`); - } - const { dismissable, actions, type, l10n } = barInfo; - - // TODO(Bug 1789718): Remove after the deprecated XPIProvider-based - // implementation is also removed. - const mappingAddonType = - addonType === "sitepermission-deprecated" ? "sitepermission" : addonType; - - const getMessageL10n = () => { - return l10n.idsPerAddonType - ? l10n.idsPerAddonType[mappingAddonType] - : l10n.id; - }; - const getActionL10n = action => { - return l10n.actionIdsPerAddonType - ? l10n.actionIdsPerAddonType[mappingAddonType][action] - : l10n.actionIds[action]; - }; - - const messagebar = document.createElement("moz-message-bar"); - - document.l10n.setAttributes(messagebar, getMessageL10n(), { - "addon-name": addonName || addonId, - }); - messagebar.setAttribute("data-l10n-attrs", "message"); - - actions?.forEach(action => { - const buttonEl = document.createElement("button"); - buttonEl.addEventListener("click", () => onaction && onaction(action)); - document.l10n.setAttributes(buttonEl, getActionL10n(action)); - buttonEl.setAttribute("slot", "actions"); - messagebar.appendChild(buttonEl); - }); - - messagebar.setAttribute("type", type || "info"); - messagebar.dismissable = dismissable; - messagebar.addEventListener("message-bar:close", onclose, { once: true }); - - document.getElementById("abuse-reports-messages").append(messagebar); - - document.dispatchEvent( - new CustomEvent("abuse-report:new-message-bar", { - detail: { definitionId, messagebar }, - }) - ); - return messagebar; -} - -async function submitReport({ report }) { - const { addon } = report; - const addonId = addon.id; - const addonName = addon.name; - const addonType = addon.type; - - // Ensure that the tab that originated the report dialog is selected - // when the user is submitting the report. - const { gBrowser } = window.windowRoot.ownerGlobal; - if (gBrowser && gBrowser.getTabForBrowser) { - let tab = gBrowser.getTabForBrowser(window.docShell.chromeEventHandler); - gBrowser.selectedTab = tab; - } - - // Create a message bar while we are still submitting the report. - const mbSubmitting = createReportMessageBar( - "submitting", - { addonId, addonName, addonType }, - { - onaction: action => { - if (action === "cancel") { - report.abort(); - mbSubmitting.remove(); - } - }, - } - ); - - try { - await report.submit(); - mbSubmitting.remove(); - - // Create a submitted message bar when the submission has been - // successful. - let barId; - if ( - !(addon.permissions & AddonManager.PERM_CAN_UNINSTALL) && - !isPending(addon, "uninstall") - ) { - // Do not offer remove action if the addon can't be uninstalled. - barId = "submitted-no-remove-action"; - } else if (report.reportEntryPoint === "uninstall") { - // With reportEntryPoint "uninstall" a specific message bar - // is going to be used. - barId = "submitted-and-removed"; - } else { - // All the other reportEntryPoint ("menu" and "toolbar_context_menu") - // use the same kind of message bar. - barId = "submitted"; - } - - const mbInfo = createReportMessageBar( - barId, - { - addonId, - addonName, - addonType, - }, - { - onaction: action => { - mbInfo.remove(); - // action "keep" doesn't require any further action, - // just handle "remove". - if (action === "remove") { - report.addon.uninstall(true); - } - }, - } - ); - } catch (err) { - // Log the complete error in the console. - console.error("Error submitting abuse report for", addonId, err); - mbSubmitting.remove(); - // The report has a submission error, create a error message bar which - // may optionally allow the user to retry to submit the same report. - const barId = - err.errorType in ABUSE_REPORT_MESSAGE_BARS - ? err.errorType - : "ERROR_UNKNOWN"; - - const mbError = createReportMessageBar( - barId, - { - addonId, - addonName, - addonType, - }, - { - onaction: action => { - mbError.remove(); - switch (action) { - case "retry": - submitReport({ report }); - break; - case "cancel": - report.abort(); - break; - } - }, - } - ); - } -} - -document.addEventListener("abuse-report:submit", ({ detail }) => { - submitReport(detail); -}); - -document.addEventListener("abuse-report:create-error", ({ detail }) => { - const { addonId, addon, errorType } = detail; - const barId = - errorType in ABUSE_REPORT_MESSAGE_BARS ? errorType : "ERROR_UNKNOWN"; - createReportMessageBar(barId, { - addonId, - addonName: addon && addon.name, - addonType: addon && addon.type, - }); -}); +window.openAbuseReport = openAbuseReport; diff --git a/toolkit/mozapps/extensions/internal/XPIDatabase.sys.mjs b/toolkit/mozapps/extensions/internal/XPIDatabase.sys.mjs index d7541167fa..e70322d3a4 100644 --- a/toolkit/mozapps/extensions/internal/XPIDatabase.sys.mjs +++ b/toolkit/mozapps/extensions/internal/XPIDatabase.sys.mjs @@ -200,6 +200,7 @@ const PROP_JSON_FIELDS = [ "incognito", "userPermissions", "optionalPermissions", + "requestedPermissions", "sitePermissions", "siteOrigin", "icons", @@ -1426,6 +1427,21 @@ AddonWrapper = class { return addon.location.name == KEY_APP_PROFILE; } + /** + * Returns true if the addon is configured to be installed + * by enterprise policy. + */ + get isInstalledByEnterprisePolicy() { + const policySettings = Services.policies?.getExtensionSettings(this.id); + return ["force_installed", "normal_installed"].includes( + policySettings?.installation_mode + ); + } + + /** + * Required permissions that extension has access to based on its manifest. + * In mv3 this doesn't include host_permissions. + */ get userPermissions() { return addonFor(this).userPermissions; } @@ -1434,6 +1450,49 @@ AddonWrapper = class { return addonFor(this).optionalPermissions; } + /** + * Additional permissions that extension is requesting in its manifest. + * Currently this is host_permissions in MV3. + */ + get requestedPermissions() { + return addonFor(this).requestedPermissions; + } + + /** + * A helper that returns all permissions for the install prompt. + */ + get installPermissions() { + let required = this.userPermissions; + if (!required) { + return null; + } + let requested = this.requestedPermissions; + // Currently this can't result in duplicates, but if logic of what goes + // into these lists changes, make sure to check for dupes. + let perms = { + origins: required.origins.concat(requested?.origins ?? []), + permissions: required.permissions.concat(requested?.permissions ?? []), + }; + return perms; + } + + get optionalOriginsNormalized() { + const { permissions } = this.userPermissions; + const { origins } = this.optionalPermissions; + + const { patterns } = new MatchPatternSet(origins, { + restrictSchemes: !( + this.isPrivileged && permissions?.includes("mozillaAddons") + ), + ignorePath: true, + }); + + // De-dup the normalized host permission patterns. + return patterns + ? [...new Set(patterns.map(matcher => matcher.pattern))] + : []; + } + isCompatibleWith(aAppVersion, aPlatformVersion) { return addonFor(this).isCompatibleWith(aAppVersion, aPlatformVersion); } diff --git a/toolkit/mozapps/extensions/internal/XPIInstall.sys.mjs b/toolkit/mozapps/extensions/internal/XPIInstall.sys.mjs index 4a26785da8..5bb81a5f60 100644 --- a/toolkit/mozapps/extensions/internal/XPIInstall.sys.mjs +++ b/toolkit/mozapps/extensions/internal/XPIInstall.sys.mjs @@ -97,7 +97,6 @@ const PREF_XPI_FILE_WHITELISTED = "xpinstall.whitelist.fileRequest"; const PREF_XPI_WHITELIST_REQUIRED = "xpinstall.whitelist.required"; const PREF_XPI_WEAK_SIGNATURES_ALLOWED = "xpinstall.signatures.weakSignaturesTemporarilyAllowed"; -const PREF_XPI_WEAK_SIGNATURES_ALLOWED_DEFAULT = true; const PREF_SELECTED_THEME = "extensions.activeThemeID"; @@ -543,8 +542,9 @@ async function loadManifestFromWebManifest(aPackage, aLocation) { // WebExtensions don't use iconURLs addon.iconURL = null; addon.icons = manifest.icons || {}; - addon.userPermissions = extension.manifestPermissions; + addon.userPermissions = extension.getRequiredPermissions(); addon.optionalPermissions = extension.manifestOptionalPermissions; + addon.requestedPermissions = extension.getRequestedPermissions(); addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT; function getLocale(aLocale) { @@ -1664,12 +1664,13 @@ class AddonInstall { this.addon.signedDate && !hasStrongSignature(this.addon) ) { - const addonAllowedByPolicies = Services.policies.getExtensionSettings( - this.addon.id - )?.temporarily_allow_weak_signatures; + const addonAllowedByPolicies = + Services.policies?.getExtensionSettings( + this.addon.id + )?.temporarily_allow_weak_signatures; const globallyAllowedByPolicies = - Services.policies.getExtensionSettings( + Services.policies?.getExtensionSettings( "*" )?.temporarily_allow_weak_signatures; @@ -4411,10 +4412,7 @@ export var XPIInstall = { }, isWeakSignatureInstallAllowed() { - return Services.prefs.getBoolPref( - PREF_XPI_WEAK_SIGNATURES_ALLOWED, - PREF_XPI_WEAK_SIGNATURES_ALLOWED_DEFAULT - ); + return Services.prefs.getBoolPref(PREF_XPI_WEAK_SIGNATURES_ALLOWED, false); }, getWeakSignatureInstallPrefName() { diff --git a/toolkit/mozapps/extensions/jar.mn b/toolkit/mozapps/extensions/jar.mn index a881ed3843..fde7b1fa23 100644 --- a/toolkit/mozapps/extensions/jar.mn +++ b/toolkit/mozapps/extensions/jar.mn @@ -11,9 +11,6 @@ toolkit.jar: content/mozapps/extensions/aboutaddonsCommon.js (content/aboutaddonsCommon.js) content/mozapps/extensions/aboutaddons.css (content/aboutaddons.css) content/mozapps/extensions/abuse-reports.js (content/abuse-reports.js) - content/mozapps/extensions/abuse-report-frame.html (content/abuse-report-frame.html) - content/mozapps/extensions/abuse-report-panel.css (content/abuse-report-panel.css) - content/mozapps/extensions/abuse-report-panel.js (content/abuse-report-panel.js) content/mozapps/extensions/drag-drop-addon-installer.js (content/drag-drop-addon-installer.js) content/mozapps/extensions/shortcuts.css (content/shortcuts.css) content/mozapps/extensions/shortcuts.js (content/shortcuts.js) diff --git a/toolkit/mozapps/extensions/metrics.yaml b/toolkit/mozapps/extensions/metrics.yaml index d5935ae436..0e36368b56 100644 --- a/toolkit/mozapps/extensions/metrics.yaml +++ b/toolkit/mozapps/extensions/metrics.yaml @@ -208,50 +208,6 @@ addons_manager: type: quantity expires: 132 - report: - type: event - description: An abuse report submitted by a user for a given extension. - bugs: - - https://bugzilla.mozilla.org/1544927 - - https://bugzilla.mozilla.org/1580561 - - https://bugzilla.mozilla.org/1590736 - - https://bugzilla.mozilla.org/1630596 - - https://bugzilla.mozilla.org/1672570 - - https://bugzilla.mozilla.org/1714251 - - https://bugzilla.mozilla.org/1749878 - - https://bugzilla.mozilla.org/1780746 - - https://bugzilla.mozilla.org/1781974 - - https://bugzilla.mozilla.org/1817100 - - https://bugzilla.mozilla.org/1820153 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1820153#c3 - notification_emails: - - addons-dev-internal@mozilla.com - extra_keys: - addon_id: - description: Id of the addon being reported. - type: string - addon_type: - description: | - The type of the add-on being reported (missing on - ERROR_ADDON_NOT_FOUND, ERROR_AMODETAILS_NOTFOUND - and ERROR_AMODETAILS_FAILURE). - type: string - entry_point: - description: | - Report entry point, one of: amo, menu, - toolbar_context_menu, unified_context_menu, uninstall. - type: string - error_type: - description: | - AbuseReport Error Type (included in case of submission - failures). The error types include ERROR_ABORTED_SUBMIT, - ERROR_ADDON_NOT_FOUND, ERROR_CLIENT, ERROR_NETWORK, - ERROR_UNKNOWN, ERROR_RECENT_SUBMIT, ERROR_SERVER, - ERROR_AMODETAILS_NOTFOUND, ERROR_AMODETAILS_FAILURE. - type: string - expires: 132 - report_suspicious_site: type: event description: | @@ -343,6 +299,9 @@ blocklist: - rwu@mozilla.com expires: 132 telemetry_mirror: BLOCKLIST_MLBF_SOURCE + no_lint: + - GIFFT_NON_PING_LIFETIME + mlbf_generation_time: type: datetime description: > diff --git a/toolkit/mozapps/extensions/test/browser/browser.toml b/toolkit/mozapps/extensions/test/browser/browser.toml index 6556b95584..b43064da16 100644 --- a/toolkit/mozapps/extensions/test/browser/browser.toml +++ b/toolkit/mozapps/extensions/test/browser/browser.toml @@ -55,6 +55,7 @@ prefs = [ ["browser_addon_list_reordering.js"] ["browser_amo_abuse_report.js"] +support-files = ["head_abuse_report.js"] ["browser_bug572561.js"] @@ -77,12 +78,6 @@ skip-if = ["true"] # Bug 1626824 ["browser_history_navigation.js"] https_first_disabled = true -["browser_html_abuse_report.js"] -support-files = ["head_abuse_report.js"] - -["browser_html_abuse_report_dialog.js"] -support-files = ["head_abuse_report.js"] - ["browser_html_detail_permissions.js"] ["browser_html_detail_view.js"] @@ -100,10 +95,10 @@ support-files = ["head_disco.js"] ["browser_html_list_view_recommendations.js"] skip-if = ["a11y_checks"] # Bug 1858037 to investigate intermittent a11y_checks results for .popup-notification-primary-button.primary.footer-button -["browser_html_message_bar.js"] - ["browser_html_options_ui.js"] +["browser_html_options_ui_dark_theme.js"] + ["browser_html_options_ui_in_tab.js"] ["browser_html_pending_updates.js"] @@ -169,9 +164,6 @@ https_first_disabled = true ["browser_webapi.js"] -["browser_webapi_abuse_report.js"] -support-files = ["head_abuse_report.js"] - ["browser_webapi_access.js"] https_first_disabled = true diff --git a/toolkit/mozapps/extensions/test/browser/browser_amo_abuse_report.js b/toolkit/mozapps/extensions/test/browser/browser_amo_abuse_report.js index b470cf2d82..7cd2d5d57d 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_amo_abuse_report.js +++ b/toolkit/mozapps/extensions/test/browser/browser_amo_abuse_report.js @@ -15,26 +15,7 @@ add_setup(async () => { ], }); - // Explicitly flip the amoFormEnabled pref on builds where the pref is - // expected to not be set to true by default. - if (AppConstants.MOZ_APP_NAME != "firefox") { - await SpecialPowers.pushPrefEnv({ - set: [["extensions.abuseReport.amoFormEnabled", true]], - }); - } - - const { AbuseReporter } = ChromeUtils.importESModule( - "resource://gre/modules/AbuseReporter.sys.mjs" - ); - - Assert.equal( - AbuseReporter.amoFormEnabled, - true, - "Expect AMO abuse report form to be enabled" - ); - - // Setting up MockProvider to mock various addon types - // as installed. + // Setting up MockProvider to mock various addon types as installed. await AbuseReportTestUtils.setup(); }); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_abuse_report.js b/toolkit/mozapps/extensions/test/browser/browser_html_abuse_report.js deleted file mode 100644 index f20e5b357b..0000000000 --- a/toolkit/mozapps/extensions/test/browser/browser_html_abuse_report.js +++ /dev/null @@ -1,1091 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -/* eslint max-len: ["error", 80] */ - -loadTestSubscript("head_abuse_report.js"); - -add_setup(async function () { - // Make sure the integrated abuse report panel is the one enabled - // while this test file runs (instead of the AMO hosted form). - // NOTE: behaviors expected when amoFormEnabled is true are tested - // in the separate browser_amo_abuse_report.js test file. - await SpecialPowers.pushPrefEnv({ - set: [["extensions.abuseReport.amoFormEnabled", false]], - }); - await AbuseReportTestUtils.setup(); -}); - -/** - * Base tests on abuse report panel webcomponents. - */ - -// This test case verified that the abuse report panels contains a radio -// button for all the expected "abuse report reasons", they are grouped -// together under the same form field named "reason". -add_task(async function test_abusereport_issuelist() { - const extension = await installTestExtension(); - - const abuseReportEl = await AbuseReportTestUtils.openReport(extension.id); - - const reasonsPanel = abuseReportEl._reasonsPanel; - const radioButtons = reasonsPanel.querySelectorAll("[type=radio]"); - const selectedRadios = reasonsPanel.querySelectorAll("[type=radio]:checked"); - - is(selectedRadios.length, 1, "Expect only one radio button selected"); - is( - selectedRadios[0], - radioButtons[0], - "Expect the first radio button to be selected" - ); - - is( - abuseReportEl.reason, - radioButtons[0].value, - `The reason property has the expected value: ${radioButtons[0].value}` - ); - - const reasons = Array.from(radioButtons).map(el => el.value); - Assert.deepEqual( - reasons.sort(), - AbuseReportTestUtils.getReasons(abuseReportEl).sort(), - `Got a radio button for the expected reasons` - ); - - for (const radio of radioButtons) { - const reasonInfo = AbuseReportTestUtils.getReasonInfo( - abuseReportEl, - radio.value - ); - const expectExampleHidden = - reasonInfo && reasonInfo.isExampleHidden("extension"); - is( - radio.parentNode.querySelector(".reason-example").hidden, - expectExampleHidden, - `Got expected visibility on the example for reason "${radio.value}"` - ); - } - - info("Change the selected reason to " + radioButtons[3].value); - radioButtons[3].checked = true; - is( - abuseReportEl.reason, - radioButtons[3].value, - "The reason property has the expected value" - ); - - await extension.unload(); - await closeAboutAddons(); -}); - -// This test case verifies that the abuse report panel: -// - switches from its "reasons list" mode to its "submit report" mode when the -// "next" button is clicked -// - goes back to the "reasons list" mode when the "go back" button is clicked -// - the abuse report panel is closed when the "close" icon is clicked -add_task(async function test_abusereport_submitpanel() { - const extension = await installTestExtension(); - - const abuseReportEl = await AbuseReportTestUtils.openReport(extension.id); - - ok( - !abuseReportEl._reasonsPanel.hidden, - "The list of abuse reasons is the currently visible" - ); - ok( - abuseReportEl._submitPanel.hidden, - "The submit panel is the currently hidden" - ); - - let onceUpdated = AbuseReportTestUtils.promiseReportUpdated( - abuseReportEl, - "submit" - ); - const MozButtonGroup = - abuseReportEl.ownerGlobal.customElements.get("moz-button-group"); - - ok(MozButtonGroup, "Expect MozButtonGroup custom element to be defined"); - - const assertButtonInMozButtonGroup = ( - btnEl, - { expectPrimary = false } = {} - ) => { - // Let's include the l10n id into the assertion messages, - // to make it more likely to be immediately clear which - // button hit a failure if any of the following assertion - // fails. - let l10nId = btnEl.getAttribute("data-l10n-id"); - is( - btnEl.classList.contains("primary"), - expectPrimary, - `Expect button ${l10nId} to have${ - expectPrimary ? "" : " NOT" - } the primary class set` - ); - - ok( - btnEl.parentElement instanceof MozButtonGroup, - `Expect button ${l10nId} to be slotted inside the expected custom element` - ); - - is( - btnEl.getAttribute("slot"), - expectPrimary ? "primary" : null, - `Expect button ${l10nId} slot to ${ - expectPrimary ? "" : "NOT " - } be set to primary` - ); - }; - - // Verify button group from the initial panel. - assertButtonInMozButtonGroup(abuseReportEl._btnNext, { expectPrimary: true }); - assertButtonInMozButtonGroup(abuseReportEl._btnCancel, { - expectPrimary: false, - }); - await AbuseReportTestUtils.clickPanelButton(abuseReportEl._btnNext); - await onceUpdated; - // Verify button group from the submit panel mode. - assertButtonInMozButtonGroup(abuseReportEl._btnSubmit, { - expectPrimary: true, - }); - assertButtonInMozButtonGroup(abuseReportEl._btnGoBack, { - expectPrimary: false, - }); - onceUpdated = AbuseReportTestUtils.promiseReportUpdated( - abuseReportEl, - "reasons" - ); - await AbuseReportTestUtils.clickPanelButton(abuseReportEl._btnGoBack); - await onceUpdated; - - const onceReportClosed = - AbuseReportTestUtils.promiseReportClosed(abuseReportEl); - await AbuseReportTestUtils.clickPanelButton(abuseReportEl._btnCancel); - await onceReportClosed; - - await extension.unload(); - await closeAboutAddons(); -}); - -// This test case verifies that the abuse report panel sends the expected data -// in the "abuse-report:submit" event detail. -add_task(async function test_abusereport_submit() { - // Reset the timestamp of the last report between tests. - AbuseReporter._lastReportTimestamp = null; - const extension = await installTestExtension(); - - const abuseReportEl = await AbuseReportTestUtils.openReport(extension.id); - - ok( - !abuseReportEl._reasonsPanel.hidden, - "The list of abuse reasons is the currently visible" - ); - - let onceUpdated = AbuseReportTestUtils.promiseReportUpdated( - abuseReportEl, - "submit" - ); - await AbuseReportTestUtils.clickPanelButton(abuseReportEl._btnNext); - await onceUpdated; - - is(abuseReportEl.message, "", "The abuse report message is initially empty"); - - info("Test typing a message in the abuse report submit panel textarea"); - const typedMessage = "Description of the extension abuse report"; - - EventUtils.synthesizeComposition( - { - data: typedMessage, - type: "compositioncommit", - }, - abuseReportEl.ownerGlobal - ); - - is( - abuseReportEl.message, - typedMessage, - "Got the expected typed message in the abuse report" - ); - - const expectedDetail = { - addonId: extension.id, - }; - - const expectedReason = abuseReportEl.reason; - const expectedMessage = abuseReportEl.message; - - function handleSubmitRequest({ request, response }) { - response.setStatusLine(request.httpVersion, 200, "OK"); - response.setHeader("Content-Type", "application/json", false); - response.write("{}"); - } - - let reportSubmitted; - const onReportSubmitted = AbuseReportTestUtils.promiseReportSubmitHandled( - ({ data, request, response }) => { - reportSubmitted = JSON.parse(data); - handleSubmitRequest({ request, response }); - } - ); - - const onceReportClosed = - AbuseReportTestUtils.promiseReportClosed(abuseReportEl); - - const onMessageBarsCreated = AbuseReportTestUtils.promiseMessageBars(2); - - const onceSubmitEvent = BrowserTestUtils.waitForEvent( - abuseReportEl, - "abuse-report:submit" - ); - await AbuseReportTestUtils.clickPanelButton(abuseReportEl._btnSubmit); - const submitEvent = await onceSubmitEvent; - - const actualDetail = { - addonId: submitEvent.detail.addonId, - }; - Assert.deepEqual( - actualDetail, - expectedDetail, - "Got the expected detail in the abuse-report:submit event" - ); - - ok( - submitEvent.detail.report, - "Got a report object in the abuse-report:submit event detail" - ); - - // Verify that, when the "abuse-report:submit" has been sent, - // the abuse report panel has been hidden, the report has been - // submitted and the expected message bar is created in the - // HTML about:addons page. - info("Wait the report to be submitted to the api server"); - await onReportSubmitted; - info("Wait the report panel to be closed"); - await onceReportClosed; - - is( - reportSubmitted.addon, - ADDON_ID, - "Got the expected addon in the submitted report" - ); - is( - reportSubmitted.reason, - expectedReason, - "Got the expected reason in the submitted report" - ); - is( - reportSubmitted.message, - expectedMessage, - "Got the expected message in the submitted report" - ); - is( - reportSubmitted.report_entry_point, - REPORT_ENTRY_POINT, - "Got the expected report_entry_point in the submitted report" - ); - - info("Waiting the expected message bars to be created"); - const barDetails = await onMessageBarsCreated; - is(barDetails.length, 2, "Expect two message bars to have been created"); - is( - barDetails[0].definitionId, - "submitting", - "Got a submitting message bar as expected" - ); - is( - barDetails[1].definitionId, - "submitted", - "Got a submitted message bar as expected" - ); - - await extension.unload(); - await closeAboutAddons(); -}); - -// This helper does verify that the abuse report panel contains the expected -// suggestions when the selected reason requires it (and urls are being set -// on the links elements included in the suggestions when expected). -async function test_abusereport_suggestions(addonId) { - const addon = await AddonManager.getAddonByID(addonId); - - const abuseReportEl = await AbuseReportTestUtils.openReport(addonId); - - const { - _btnNext, - _btnGoBack, - _reasonsPanel, - _submitPanel, - _submitPanel: { _suggestions }, - } = abuseReportEl; - - for (const reason of AbuseReportTestUtils.getReasons(abuseReportEl)) { - const reasonInfo = AbuseReportTestUtils.getReasonInfo( - abuseReportEl, - reason - ); - - // TODO(Bug 1789718): Remove after the deprecated XPIProvider-based - // implementation is also removed. - const addonType = - addon.type === "sitepermission-deprecated" - ? "sitepermission" - : addon.type; - - if (reasonInfo.isReasonHidden(addonType)) { - continue; - } - - info(`Test suggestions for abuse reason "${reason}"`); - - // Select a reason with suggestions. - let radioEl = abuseReportEl.querySelector(`#abuse-reason-${reason}`); - ok(radioEl, `Found radio button for "${reason}"`); - radioEl.checked = true; - - // Make sure the element localization is completed before - // checking the content isn't empty. - await document.l10n.translateFragment(radioEl); - - // Verify each radio button has a non-empty localized string. - const localizedRadioContent = Array.from( - radioEl.closest("label").querySelectorAll("[data-l10n-id]") - ).filter(el => !el.hidden); - - for (let el of localizedRadioContent) { - isnot( - el.textContent, - "", - `Fluent string id '${el.getAttribute("data-l10n-id")}' missing` - ); - } - - // Switch to the submit form with the current reason radio selected. - let oncePanelUpdated = AbuseReportTestUtils.promiseReportUpdated( - abuseReportEl, - "submit" - ); - await AbuseReportTestUtils.clickPanelButton(_btnNext); - await oncePanelUpdated; - - const localizedSuggestionsContent = Array.from( - _suggestions.querySelectorAll("[data-l10n-id]") - ).filter(el => !el.hidden); - - is( - !_suggestions.hidden, - !!reasonInfo.hasSuggestions, - `Suggestions block has the expected visibility for "${reason}"` - ); - if (reasonInfo.hasSuggestions) { - ok( - !!localizedSuggestionsContent.length, - `Category suggestions should not be empty for "${reason}"` - ); - } else { - Assert.strictEqual( - localizedSuggestionsContent.length, - 0, - `Category suggestions should be empty for "${reason}"` - ); - } - - const extSupportLink = _suggestions.querySelector( - ".extension-support-link" - ); - if (extSupportLink) { - is( - extSupportLink.getAttribute("href"), - BASE_TEST_MANIFEST.homepage_url, - "Got the expected extension-support-url" - ); - } - - const learnMoreLinks = []; - learnMoreLinks.push( - ..._suggestions.querySelectorAll( - 'a[is="moz-support-link"], .abuse-policy-learnmore' - ) - ); - - if (learnMoreLinks.length) { - is( - _suggestions.querySelectorAll( - 'a[is="moz-support-link"]:not([support-page])' - ).length, - 0, - "Every SUMO link should point to a specific page" - ); - ok( - learnMoreLinks.every(el => el.getAttribute("target") === "_blank"), - "All the learn more links have target _blank" - ); - ok( - learnMoreLinks.every(el => el.hasAttribute("href")), - "All the learn more links have a url set" - ); - } - - oncePanelUpdated = AbuseReportTestUtils.promiseReportUpdated( - abuseReportEl, - "reasons" - ); - await AbuseReportTestUtils.clickPanelButton(_btnGoBack); - await oncePanelUpdated; - ok(!_reasonsPanel.hidden, "Reasons panel should be visible"); - ok(_submitPanel.hidden, "Submit panel should be hidden"); - } - - await closeAboutAddons(); -} - -add_task(async function test_abusereport_suggestions_extension() { - const EXT_ID = "test-extension-suggestions@mochi.test"; - const extension = await installTestExtension(EXT_ID); - await test_abusereport_suggestions(EXT_ID); - await extension.unload(); -}); - -add_task(async function test_abusereport_suggestions_theme() { - const THEME_ID = "theme@mochi.test"; - const theme = await installTestExtension(THEME_ID, "theme"); - await test_abusereport_suggestions(THEME_ID); - await theme.unload(); -}); - -// TODO(Bug 1789718): adapt to SitePermAddonProvider implementation. -add_task(async function test_abusereport_suggestions_sitepermission() { - const SITEPERM_ADDON_ID = "webmidi@mochi.test"; - const sitePermAddon = await installTestExtension( - SITEPERM_ADDON_ID, - "sitepermission-deprecated" - ); - await test_abusereport_suggestions(SITEPERM_ADDON_ID); - await sitePermAddon.unload(); -}); - -// This test case verifies the message bars created on other -// scenarios (e.g. report creation and submissions errors). -// -// TODO(Bug 1789718): adapt to SitePermAddonProvider implementation. -add_task(async function test_abusereport_messagebars() { - const EXT_ID = "test-extension-report@mochi.test"; - const EXT_ID2 = "test-extension-report-2@mochi.test"; - const THEME_ID = "test-theme-report@mochi.test"; - const SITEPERM_ADDON_ID = "webmidi-report@mochi.test"; - const extension = await installTestExtension(EXT_ID); - const extension2 = await installTestExtension(EXT_ID2); - const theme = await installTestExtension(THEME_ID, "theme"); - const sitePermAddon = await installTestExtension( - SITEPERM_ADDON_ID, - "sitepermission-deprecated" - ); - - async function assertMessageBars( - expectedMessageBarIds, - testSetup, - testMessageBarDetails - ) { - await openAboutAddons(); - const expectedLength = expectedMessageBarIds.length; - const onMessageBarsCreated = - AbuseReportTestUtils.promiseMessageBars(expectedLength); - // Reset the timestamp of the last report between tests. - AbuseReporter._lastReportTimestamp = null; - await testSetup(); - info(`Waiting for ${expectedLength} message-bars to be created`); - const barDetails = await onMessageBarsCreated; - Assert.deepEqual( - barDetails.map(d => d.definitionId), - expectedMessageBarIds, - "Got the expected message bars" - ); - if (testMessageBarDetails) { - await testMessageBarDetails(barDetails); - } - await closeAboutAddons(); - } - - function setTestRequestHandler(responseStatus, responseData) { - AbuseReportTestUtils.promiseReportSubmitHandled(({ request, response }) => { - response.setStatusLine(request.httpVersion, responseStatus, "Error"); - response.write(responseData); - }); - } - - await assertMessageBars(["ERROR_ADDON_NOTFOUND"], async () => { - info("Test message bars on addon not found"); - AbuseReportTestUtils.triggerNewReport( - "non-existend-addon-id@mochi.test", - REPORT_ENTRY_POINT - ); - }); - - await assertMessageBars(["submitting", "ERROR_RECENT_SUBMIT"], async () => { - info("Test message bars on recent submission"); - const promiseRendered = AbuseReportTestUtils.promiseReportRendered(); - AbuseReportTestUtils.triggerNewReport(EXT_ID, REPORT_ENTRY_POINT); - await promiseRendered; - AbuseReporter.updateLastReportTimestamp(); - AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); - }); - - await assertMessageBars(["submitting", "ERROR_ABORTED_SUBMIT"], async () => { - info("Test message bars on aborted submission"); - AbuseReportTestUtils.triggerNewReport(EXT_ID, REPORT_ENTRY_POINT); - await AbuseReportTestUtils.promiseReportRendered(); - const { _report } = AbuseReportTestUtils.getReportPanel(); - _report.abort(); - AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); - }); - - await assertMessageBars(["submitting", "ERROR_SERVER"], async () => { - info("Test message bars on server error"); - setTestRequestHandler(500); - AbuseReportTestUtils.triggerNewReport(EXT_ID, REPORT_ENTRY_POINT); - await AbuseReportTestUtils.promiseReportRendered(); - AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); - }); - - await assertMessageBars(["submitting", "ERROR_CLIENT"], async () => { - info("Test message bars on client error"); - setTestRequestHandler(400); - AbuseReportTestUtils.triggerNewReport(EXT_ID, REPORT_ENTRY_POINT); - await AbuseReportTestUtils.promiseReportRendered(); - AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); - }); - - await assertMessageBars(["submitting", "ERROR_UNKNOWN"], async () => { - info("Test message bars on unexpected status code"); - setTestRequestHandler(604); - AbuseReportTestUtils.triggerNewReport(EXT_ID, REPORT_ENTRY_POINT); - await AbuseReportTestUtils.promiseReportRendered(); - AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); - }); - - await assertMessageBars(["submitting", "ERROR_UNKNOWN"], async () => { - info("Test message bars on invalid json in the response data"); - setTestRequestHandler(200, ""); - AbuseReportTestUtils.triggerNewReport(EXT_ID, REPORT_ENTRY_POINT); - await AbuseReportTestUtils.promiseReportRendered(); - AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); - }); - - // Verify message bar on add-on without perm_can_uninstall. - await assertMessageBars( - ["submitting", "submitted-no-remove-action"], - async () => { - info("Test message bars on report submitted on an addon without remove"); - setTestRequestHandler(200, "{}"); - AbuseReportTestUtils.triggerNewReport(THEME_NO_UNINSTALL_ID, "menu"); - await AbuseReportTestUtils.promiseReportRendered(); - AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); - } - ); - - // Verify the 3 expected entry points: - // menu, toolbar_context_menu and uninstall - // (See https://addons-server.readthedocs.io/en/latest/topics/api/abuse.html). - await assertMessageBars(["submitting", "submitted"], async () => { - info("Test message bars on report opened from addon options menu"); - setTestRequestHandler(200, "{}"); - AbuseReportTestUtils.triggerNewReport(EXT_ID, "menu"); - await AbuseReportTestUtils.promiseReportRendered(); - AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); - }); - - for (const extId of [EXT_ID, THEME_ID]) { - await assertMessageBars( - ["submitting", "submitted"], - async () => { - info(`Test message bars on ${extId} reported from toolbar contextmenu`); - setTestRequestHandler(200, "{}"); - AbuseReportTestUtils.triggerNewReport(extId, "toolbar_context_menu"); - await AbuseReportTestUtils.promiseReportRendered(); - AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); - }, - ([, submittedDetails]) => { - const buttonsL10nId = Array.from( - submittedDetails.messagebar.querySelectorAll("button") - ).map(el => el.getAttribute("data-l10n-id")); - if (extId === THEME_ID) { - ok( - buttonsL10nId.every(id => id.endsWith("-theme")), - "submitted bar actions should use the Fluent id for themes" - ); - } else { - ok( - buttonsL10nId.every(id => id.endsWith("-extension")), - "submitted bar actions should use the Fluent id for extensions" - ); - } - } - ); - } - - for (const extId of [EXT_ID2, THEME_ID, SITEPERM_ADDON_ID]) { - const testFn = async () => { - info(`Test message bars on ${extId} reported opened from addon removal`); - setTestRequestHandler(200, "{}"); - AbuseReportTestUtils.triggerNewReport(extId, "uninstall"); - await AbuseReportTestUtils.promiseReportRendered(); - const addon = await AddonManager.getAddonByID(extId); - // Ensure that the test extension is pending uninstall as it would be - // when a user trigger this scenario on an actual addon uninstall. - await addon.uninstall(true); - AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); - }; - const assertMessageBarDetails = async ([, submittedDetails]) => - AbuseReportTestUtils.assertFluentStrings(submittedDetails.messagebar); - await assertMessageBars( - ["submitting", "submitted-and-removed"], - testFn, - assertMessageBarDetails - ); - } - - // Verify message bar on sitepermission add-on type. - await assertMessageBars( - ["submitting", "submitted"], - async () => { - info( - "Test message bars for report submitted on an sitepermission addon type" - ); - setTestRequestHandler(200, "{}"); - AbuseReportTestUtils.triggerNewReport(SITEPERM_ADDON_ID, "menu"); - await AbuseReportTestUtils.promiseReportRendered(); - AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); - }, - ([, submittedDetails]) => - AbuseReportTestUtils.assertFluentStrings(submittedDetails.messagebar) - ); - - await extension.unload(); - await extension2.unload(); - await theme.unload(); - await sitePermAddon.unload(); -}); - -add_task(async function test_abusereport_from_aboutaddons_menu() { - const EXT_ID = "test-report-from-aboutaddons-menu@mochi.test"; - const extension = await installTestExtension(EXT_ID); - - await openAboutAddons(); - - AbuseReportTestUtils.assertReportPanelHidden(); - - const addonCard = gManagerWindow.document.querySelector( - `addon-list addon-card[addon-id="${extension.id}"]` - ); - ok(addonCard, "Got the addon-card for the test extension"); - - const reportButton = addonCard.querySelector("[action=report]"); - ok(reportButton, "Got the report action for the test extension"); - - info("Click the report action and wait for the 'abuse-report:new' event"); - - let onceReportOpened = AbuseReportTestUtils.promiseReportOpened({ - addonId: extension.id, - reportEntryPoint: "menu", - }); - reportButton.click(); - const panelEl = await onceReportOpened; - - await AbuseReportTestUtils.closeReportPanel(panelEl); - - await closeAboutAddons(); - await extension.unload(); -}); - -add_task(async function test_abusereport_from_aboutaddons_remove() { - const EXT_ID = "test-report-from-aboutaddons-remove@mochi.test"; - - // Test on a theme addon to cover the report checkbox included in the - // uninstall dialog also on a theme. - const extension = await installTestExtension(EXT_ID, "theme"); - - await openAboutAddons("theme"); - - AbuseReportTestUtils.assertReportPanelHidden(); - - const addonCard = gManagerWindow.document.querySelector( - `addon-list addon-card[addon-id="${extension.id}"]` - ); - ok(addonCard, "Got the addon-card for the test theme extension"); - - const removeButton = addonCard.querySelector("[action=remove]"); - ok(removeButton, "Got the remove action for the test theme extension"); - - // Prepare the mocked prompt service. - const promptService = mockPromptService(); - promptService.confirmEx = createPromptConfirmEx({ - remove: true, - report: true, - }); - - info("Click the report action and wait for the 'abuse-report:new' event"); - - const onceReportOpened = AbuseReportTestUtils.promiseReportOpened({ - addonId: extension.id, - reportEntryPoint: "uninstall", - }); - removeButton.click(); - const panelEl = await onceReportOpened; - - await AbuseReportTestUtils.closeReportPanel(panelEl); - - await closeAboutAddons(); - await extension.unload(); -}); - -add_task(async function test_abusereport_from_browserAction_remove() { - const EXT_ID = "test-report-from-browseraction-remove@mochi.test"; - const xpiFile = AddonTestUtils.createTempWebExtensionFile({ - manifest: { - ...BASE_TEST_MANIFEST, - browser_action: { - default_area: "navbar", - }, - browser_specific_settings: { gecko: { id: EXT_ID } }, - }, - }); - const addon = await AddonManager.installTemporaryAddon(xpiFile); - - const buttonId = `${makeWidgetId(EXT_ID)}-browser-action`; - - async function promiseAnimationFrame() { - await new Promise(resolve => window.requestAnimationFrame(resolve)); - - let { tm } = Services; - return new Promise(resolve => tm.dispatchToMainThread(resolve)); - } - - async function reportFromContextMenuRemove() { - const menu = document.getElementById("toolbar-context-menu"); - const node = document.getElementById(CSS.escape(buttonId)); - const shown = BrowserTestUtils.waitForEvent( - menu, - "popupshown", - "Wair for contextmenu popup" - ); - - // Wait for an animation frame as we do for the other mochitest-browser - // tests related to the browserActions. - await promiseAnimationFrame(); - EventUtils.synthesizeMouseAtCenter(node, { type: "contextmenu" }); - await shown; - - info(`Clicking on "Remove Extension" context menu item`); - let removeExtension = menu.querySelector( - ".customize-context-removeExtension" - ); - removeExtension.click(); - - return menu; - } - - // Prepare the mocked prompt service. - const promptService = mockPromptService(); - promptService.confirmEx = createPromptConfirmEx({ - remove: true, - report: true, - }); - - await BrowserTestUtils.withNewTab("about:blank", async function () { - info(`Open browserAction context menu in toolbar context menu`); - let promiseMenu = reportFromContextMenuRemove(); - - // Wait about:addons to be loaded. - let browser = gBrowser.selectedBrowser; - await BrowserTestUtils.browserLoaded(browser); - - let onceReportOpened = AbuseReportTestUtils.promiseReportOpened({ - addonId: EXT_ID, - reportEntryPoint: "uninstall", - managerWindow: browser.contentWindow, - }); - - is( - browser.currentURI.spec, - "about:addons", - "about:addons tab currently selected" - ); - - let menu = await promiseMenu; - menu.hidePopup(); - - let panelEl = await onceReportOpened; - - await AbuseReportTestUtils.closeReportPanel(panelEl); - - let onceExtStarted = AddonTestUtils.promiseWebExtensionStartup(EXT_ID); - addon.cancelUninstall(); - await onceExtStarted; - - // Reload the tab to verify Bug 1559124 didn't regressed. - browser.contentWindow.location.reload(); - await BrowserTestUtils.browserLoaded(browser); - is( - browser.currentURI.spec, - "about:addons", - "about:addons tab currently selected" - ); - - onceReportOpened = AbuseReportTestUtils.promiseReportOpened({ - addonId: EXT_ID, - reportEntryPoint: "uninstall", - managerWindow: browser.contentWindow, - }); - - menu = await reportFromContextMenuRemove(); - info("Wait for the report panel"); - panelEl = await onceReportOpened; - - info("Wait for the report panel to be closed"); - await AbuseReportTestUtils.closeReportPanel(panelEl); - - menu.hidePopup(); - - onceExtStarted = AddonTestUtils.promiseWebExtensionStartup(EXT_ID); - addon.cancelUninstall(); - await onceExtStarted; - }); - - await addon.uninstall(); -}); - -/* - * Test report action hidden on non-supported extension types. - */ -add_task(async function test_report_action_hidden_on_builtin_addons() { - await openAboutAddons("theme"); - await AbuseReportTestUtils.assertReportActionHidden( - gManagerWindow, - DEFAULT_BUILTIN_THEME_ID - ); - await closeAboutAddons(); -}); - -add_task(async function test_report_action_hidden_on_system_addons() { - await openAboutAddons("extension"); - await AbuseReportTestUtils.assertReportActionHidden( - gManagerWindow, - EXT_SYSTEM_ADDON_ID - ); - await closeAboutAddons(); -}); - -add_task(async function test_report_action_hidden_on_dictionary_addons() { - await openAboutAddons("dictionary"); - await AbuseReportTestUtils.assertReportActionHidden( - gManagerWindow, - EXT_DICTIONARY_ADDON_ID - ); - await closeAboutAddons(); -}); - -add_task(async function test_report_action_hidden_on_langpack_addons() { - await openAboutAddons("locale"); - await AbuseReportTestUtils.assertReportActionHidden( - gManagerWindow, - EXT_LANGPACK_ADDON_ID - ); - await closeAboutAddons(); -}); - -// This test verifies that triggering a report that would be immediately -// cancelled (e.g. because abuse reports for that extension type are not -// supported) the abuse report is being hidden as expected. -add_task(async function test_report_hidden_on_report_unsupported_addontype() { - await openAboutAddons(); - - let onceCreateReportFailed = AbuseReportTestUtils.promiseMessageBars(1); - - AbuseReportTestUtils.triggerNewReport(EXT_UNSUPPORTED_TYPE_ADDON_ID, "menu"); - - await onceCreateReportFailed; - - ok(!AbuseReporter.getOpenDialog(), "report dialog should not be open"); - - await closeAboutAddons(); -}); - -/* - * Test regression fixes. - */ - -add_task(async function test_no_broken_suggestion_on_missing_supportURL() { - const EXT_ID = "test-no-author@mochi.test"; - const extension = await installTestExtension(EXT_ID, "extension", { - homepage_url: undefined, - }); - - const abuseReportEl = await AbuseReportTestUtils.openReport(EXT_ID); - - info("Select broken as the abuse reason"); - abuseReportEl.querySelector("#abuse-reason-broken").checked = true; - - let oncePanelUpdated = AbuseReportTestUtils.promiseReportUpdated( - abuseReportEl, - "submit" - ); - await AbuseReportTestUtils.clickPanelButton(abuseReportEl._btnNext); - await oncePanelUpdated; - - const suggestionEl = abuseReportEl.querySelector( - "abuse-report-reason-suggestions" - ); - is(suggestionEl.reason, "broken", "Got the expected suggestion element"); - ok(suggestionEl.hidden, "suggestion element should be empty"); - - await closeAboutAddons(); - await extension.unload(); -}); - -// This test verify that the abuse report panel is opening the -// author link using a null triggeringPrincipal. -add_task(async function test_abusereport_open_author_url() { - const abuseReportEl = await AbuseReportTestUtils.openReport( - EXT_WITH_PRIVILEGED_URL_ID - ); - - const authorLink = abuseReportEl._linkAddonAuthor; - ok(authorLink, "Got the author link element"); - is( - authorLink.href, - "about:config", - "Got a privileged url in the link element" - ); - - SimpleTest.waitForExplicitFinish(); - let waitForConsole = new Promise(resolve => { - SimpleTest.monitorConsole(resolve, [ - { - message: - // eslint-disable-next-line max-len - /Security Error: Content at moz-nullprincipal:{.*} may not load or link to about:config/, - }, - ]); - }); - - let tabSwitched = BrowserTestUtils.waitForEvent(gBrowser, "TabSwitchDone"); - authorLink.click(); - await tabSwitched; - - is( - gBrowser.selectedBrowser.currentURI.spec, - "about:blank", - "Got about:blank loaded in the new tab" - ); - - SimpleTest.endMonitorConsole(); - await waitForConsole; - - BrowserTestUtils.removeTab(gBrowser.selectedTab); - await closeAboutAddons(); -}); - -add_task(async function test_no_report_checkbox_for_unsupported_addon_types() { - async function test_report_checkbox_hidden(addon) { - await openAboutAddons(addon.type); - - const addonCard = gManagerWindow.document.querySelector( - `addon-list addon-card[addon-id="${addon.id}"]` - ); - ok(addonCard, "Got the addon-card for the test extension"); - - const removeButton = addonCard.querySelector("[action=remove]"); - ok(removeButton, "Got the remove action for the test extension"); - - // Prepare the mocked prompt service. - const promptService = mockPromptService(); - promptService.confirmEx = createPromptConfirmEx({ - remove: true, - report: false, - expectCheckboxHidden: true, - }); - - info("Click the report action and wait for the addon to be removed"); - const promiseCardRemoved = BrowserTestUtils.waitForEvent( - addonCard.closest("addon-list"), - "remove" - ); - removeButton.click(); - await promiseCardRemoved; - - await closeAboutAddons(); - } - - const reportNotSupportedAddons = [ - { - id: "fake-langpack-to-remove@mochi.test", - name: "This is a fake langpack", - version: "1.1", - type: "locale", - }, - { - id: "fake-dictionary-to-remove@mochi.test", - name: "This is a fake dictionary", - version: "1.1", - type: "dictionary", - }, - ]; - - AbuseReportTestUtils.createMockAddons(reportNotSupportedAddons); - - for (const { id } of reportNotSupportedAddons) { - const addon = await AddonManager.getAddonByID(id); - await test_report_checkbox_hidden(addon); - } -}); - -add_task(async function test_author_hidden_when_missing() { - const EXT_ID = "test-no-author@mochi.test"; - const extension = await installTestExtension(EXT_ID, "extension", { - author: undefined, - }); - - const abuseReportEl = await AbuseReportTestUtils.openReport(EXT_ID); - - const addon = await AddonManager.getAddonByID(EXT_ID); - - ok(!addon.creator, "addon.creator should not be undefined"); - ok( - abuseReportEl._addonAuthorContainer.hidden, - "author container should be hidden" - ); - - await closeAboutAddons(); - await extension.unload(); -}); - -// Verify addon.siteOrigin is used as a fallback when homepage_url/developer.url -// or support url are missing. -// -// TODO(Bug 1789718): adapt to SitePermAddonProvider implementation. -add_task(async function test_siteperm_siteorigin_fallback() { - const SITEPERM_ADDON_ID = "webmidi-site-origin@mochi.test"; - const sitePermAddon = await installTestExtension( - SITEPERM_ADDON_ID, - "sitepermission-deprecated", - { - homepage_url: undefined, - } - ); - - const abuseReportEl = await AbuseReportTestUtils.openReport( - SITEPERM_ADDON_ID - ); - const addon = await AddonManager.getAddonByID(SITEPERM_ADDON_ID); - - ok(addon.siteOrigin, "addon.siteOrigin should not be undefined"); - ok(!addon.supportURL, "addon.supportURL should not be set"); - ok(!addon.homepageURL, "addon.homepageURL should not be set"); - is( - abuseReportEl.supportURL, - addon.siteOrigin, - "Got the expected support_url" - ); - - await closeAboutAddons(); - await sitePermAddon.unload(); -}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_abuse_report_dialog.js b/toolkit/mozapps/extensions/test/browser/browser_html_abuse_report_dialog.js deleted file mode 100644 index 1efb28add3..0000000000 --- a/toolkit/mozapps/extensions/test/browser/browser_html_abuse_report_dialog.js +++ /dev/null @@ -1,185 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -/* eslint max-len: ["error", 80] */ - -loadTestSubscript("head_abuse_report.js"); - -add_setup(async function () { - // Make sure the integrated abuse report panel is the one enabled - // while this test file runs (instead of the AMO hosted form). - // NOTE: behaviors expected when amoFormEnabled is true are tested - // in the separate browser_amo_abuse_report.js test file. - await SpecialPowers.pushPrefEnv({ - set: [["extensions.abuseReport.amoFormEnabled", false]], - }); - await AbuseReportTestUtils.setup(); -}); - -/** - * Test tasks specific to the abuse report opened in its own dialog window. - */ - -add_task(async function test_close_icon_button_hidden_when_dialog() { - const addonId = "addon-to-report@mochi.test"; - const extension = await installTestExtension(addonId); - - const reportDialog = await AbuseReporter.openDialog( - addonId, - "menu", - gBrowser.selectedBrowser - ); - await AbuseReportTestUtils.promiseReportDialogRendered(); - - const panelEl = await reportDialog.promiseReportPanel; - - let promiseClosedWindow = waitClosedWindow(); - - EventUtils.synthesizeKey("VK_RETURN", {}, panelEl.ownerGlobal); - AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); - - await promiseClosedWindow; - ok( - await reportDialog.promiseReport, - "expect the report to not be cancelled by pressing enter" - ); - - await extension.unload(); -}); - -add_task(async function test_report_triggered_when_report_dialog_is_open() { - const addonId = "addon-to-report@mochi.test"; - const extension = await installTestExtension(addonId); - - const reportDialog = await AbuseReporter.openDialog( - addonId, - "menu", - gBrowser.selectedBrowser - ); - await AbuseReportTestUtils.promiseReportDialogRendered(); - - let promiseClosedWindow = waitClosedWindow(); - - const reportDialog2 = await AbuseReporter.openDialog( - addonId, - "menu", - gBrowser.selectedBrowser - ); - - await promiseClosedWindow; - - // Trigger the report submit and check that the second report is - // resolved as expected. - await AbuseReportTestUtils.promiseReportDialogRendered(); - - ok( - !reportDialog.window || reportDialog.window.closed, - "expect the first dialog to be closed" - ); - ok(!!reportDialog2.window, "expect the second dialog to be open"); - - is( - reportDialog2.window, - AbuseReportTestUtils.getReportDialog(), - "Got a report dialog as expected" - ); - - AbuseReportTestUtils.triggerSubmit("fake-reason", "fake-message"); - - // promiseReport is resolved to undefined if the report has been - // cancelled, otherwise it is resolved to a report object. - ok( - !(await reportDialog.promiseReport), - "expect the first report to be cancelled" - ); - ok( - !!(await reportDialog2.promiseReport), - "expect the second report to be resolved" - ); - - await extension.unload(); -}); - -add_task(async function test_report_dialog_window_closed_by_user() { - const addonId = "addon-to-report@mochi.test"; - const extension = await installTestExtension(addonId); - - const reportDialog = await AbuseReporter.openDialog( - addonId, - "menu", - gBrowser.selectedBrowser - ); - await AbuseReportTestUtils.promiseReportDialogRendered(); - - let promiseClosedWindow = waitClosedWindow(); - - reportDialog.close(); - - await promiseClosedWindow; - - ok( - !(await reportDialog.promiseReport), - "expect promiseReport to be resolved as user cancelled" - ); - - await extension.unload(); -}); - -add_task(async function test_amo_details_for_not_installed_addon() { - const addonId = "not-installed-addon@mochi.test"; - const fakeAMODetails = { - name: "fake name", - current_version: { version: "1.0" }, - type: "extension", - icon_url: "http://test.addons.org/asserts/fake-icon-url.png", - homepage: "http://fake.url/homepage", - support_url: "http://fake.url/support", - authors: [ - { name: "author1", url: "http://fake.url/author1" }, - { name: "author2", url: "http://fake.url/author2" }, - ], - is_recommended: true, - }; - - AbuseReportTestUtils.amoAddonDetailsMap.set(addonId, fakeAMODetails); - registerCleanupFunction(() => - AbuseReportTestUtils.amoAddonDetailsMap.clear() - ); - - const reportDialog = await AbuseReporter.openDialog( - addonId, - "menu", - gBrowser.selectedBrowser - ); - - const reportEl = await reportDialog.promiseReportPanel; - - // Assert that the panel has been able to retrieve from AMO - // all the addon details needed to render the panel correctly. - is(reportEl.addonId, addonId, "Got the expected addonId"); - is(reportEl.addonName, fakeAMODetails.name, "Got the expected addon name"); - is(reportEl.addonType, fakeAMODetails.type, "Got the expected addon type"); - is( - reportEl.authorName, - fakeAMODetails.authors[0].name, - "Got the first author name as expected" - ); - is( - reportEl.authorURL, - fakeAMODetails.authors[0].url, - "Got the first author url as expected" - ); - is(reportEl.iconURL, fakeAMODetails.icon_url, "Got the expected icon url"); - is( - reportEl.supportURL, - fakeAMODetails.support_url, - "Got the expected support url" - ); - is( - reportEl.homepageURL, - fakeAMODetails.homepage, - "Got the expected homepage url" - ); - - reportDialog.close(); -}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_detail_permissions.js b/toolkit/mozapps/extensions/test/browser/browser_html_detail_permissions.js index 32543f3bc9..130375020d 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_html_detail_permissions.js +++ b/toolkit/mozapps/extensions/test/browser/browser_html_detail_permissions.js @@ -419,7 +419,11 @@ async function runTest(options) { } } -async function testPermissionsView({ manifestV3enabled, manifest_version }) { +async function testPermissionsView({ + manifestV3enabled, + manifest_version, + expectGranted, +}) { await SpecialPowers.pushPrefEnv({ set: [["extensions.manifestV3.enabled", manifestV3enabled]], }); @@ -438,7 +442,16 @@ async function testPermissionsView({ manifestV3enabled, manifest_version }) { extension: extensions["addon1@mochi.test"], permissions: ["<all_urls>", "tabs", "webNavigation"], }); - } else { + } + if (manifest_version >= 3 && expectGranted) { + await runTest({ + extension: extensions["addon1@mochi.test"], + permissions: ["tabs", "webNavigation"], + optional_permissions: ["<all_urls>"], + optional_enabled: ["<all_urls>"], + }); + } + if (manifest_version >= 3 && !expectGranted) { await runTest({ extension: extensions["addon1@mochi.test"], permissions: ["tabs", "webNavigation"], @@ -544,8 +557,28 @@ add_task(async function testPermissionsView_MV2_manifestV3enabled() { await testPermissionsView({ manifestV3enabled: true, manifest_version: 2 }); }); +add_task(async function testPermissionsView_MV3_noInstallPrompt() { + await SpecialPowers.pushPrefEnv({ + set: [["extensions.originControls.grantByDefault", false]], + }); + await testPermissionsView({ + manifestV3enabled: true, + manifest_version: 3, + expectGranted: false, + }); + await SpecialPowers.popPrefEnv(); +}); + add_task(async function testPermissionsView_MV3() { - await testPermissionsView({ manifestV3enabled: true, manifest_version: 3 }); + await SpecialPowers.pushPrefEnv({ + set: [["extensions.originControls.grantByDefault", true]], + }); + await testPermissionsView({ + manifestV3enabled: true, + manifest_version: 3, + expectGranted: true, + }); + await SpecialPowers.popPrefEnv(); }); add_task(async function testPermissionsViewStates() { @@ -623,7 +656,10 @@ add_task(async function testPermissionsViewStates() { add_task(async function testAllUrlsNotGrantedUnconditionally_MV3() { await SpecialPowers.pushPrefEnv({ - set: [["extensions.manifestV3.enabled", true]], + set: [ + ["extensions.manifestV3.enabled", true], + ["extensions.originControls.grantByDefault", false], + ], }); const extension = ExtensionTestUtils.loadExtension({ diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js b/toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js index 76e7f2b255..8930d6dede 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js +++ b/toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js @@ -1260,7 +1260,7 @@ add_task(async function testGoBackButtonIsDisabledWhenHistoryIsEmpty() { // When we have a fresh new tab, `about:addons` is opened in it. let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, null); // Simulate a click on "Manage extension" from a context menu. - let win = await BrowserOpenAddonsMgr(viewID); + let win = await BrowserAddonUI.openAddonsMgr(viewID); await assertBackButtonIsDisabled(win); BrowserTestUtils.removeTab(tab); @@ -1288,7 +1288,7 @@ add_task(async function testGoBackButtonIsDisabledWhenHistoryIsEmptyInNewTab() { true ); // Simulate a click on "Manage extension" from a context menu. - let win = await BrowserOpenAddonsMgr(viewID); + let win = await BrowserAddonUI.openAddonsMgr(viewID); let addonsTab = await addonsTabLoaded; await assertBackButtonIsDisabled(win); @@ -1309,7 +1309,7 @@ add_task(async function testGoBackButtonIsDisabledAfterBrowserBackButton() { // When we have a fresh new tab, `about:addons` is opened in it. let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, null); // Simulate a click on "Manage extension" from a context menu. - let win = await BrowserOpenAddonsMgr(viewID); + let win = await BrowserAddonUI.openAddonsMgr(viewID); await assertBackButtonIsDisabled(win); // Navigate to the extensions list. diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_discover_view.js b/toolkit/mozapps/extensions/test/browser/browser_html_discover_view.js index 30cb45dc60..fdb0fabfd4 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_html_discover_view.js +++ b/toolkit/mozapps/extensions/test/browser/browser_html_discover_view.js @@ -653,11 +653,16 @@ add_task(async function checkDiscopaneNotice() { "moz-message-bar.discopane-notice" ); ok(messageBar, "Recommended notice should exist in extensions view"); + is( + messageBar.getAttribute("role"), + "alert", + "Recommended notice is an alert" + ); await switchToDiscoView(win); messageBar = win.document.querySelector("moz-message-bar.discopane-notice"); ok(messageBar, "Recommended notice should exist in disco view"); - messageBar.closeButtonEl.click(); + messageBar.closeButton.click(); messageBar = win.document.querySelector("moz-message-bar.discopane-notice"); ok(!messageBar, "Recommended notice should not exist in disco view"); await switchToNonDiscoView(win); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_message_bar.js b/toolkit/mozapps/extensions/test/browser/browser_html_message_bar.js deleted file mode 100644 index b60baf8799..0000000000 --- a/toolkit/mozapps/extensions/test/browser/browser_html_message_bar.js +++ /dev/null @@ -1,185 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/* eslint max-len: ["error", 80] */ - -let htmlAboutAddonsWindow; - -const HTML_NS = "http://www.w3.org/1999/xhtml"; - -function clickElement(el) { - el.dispatchEvent(new CustomEvent("click")); -} - -function createMessageBar(messageBarStack, { attrs, children, onclose } = {}) { - const win = messageBarStack.ownerGlobal; - const messageBar = win.document.createElementNS(HTML_NS, "message-bar"); - if (attrs) { - for (const [k, v] of Object.entries(attrs)) { - messageBar.setAttribute(k, v); - } - } - if (children) { - if (Array.isArray(children)) { - messageBar.append(...children); - } else { - messageBar.append(children); - } - } - messageBar.addEventListener("message-bar:close", onclose, { once: true }); - messageBarStack.append(messageBar); - return messageBar; -} - -add_setup(async function () { - htmlAboutAddonsWindow = await loadInitialView("extension"); - registerCleanupFunction(() => closeView(htmlAboutAddonsWindow)); -}); - -add_task(async function test_message_bar_stack() { - const win = htmlAboutAddonsWindow; - - let messageBarStack = win.document.getElementById("abuse-reports-messages"); - - ok(messageBarStack, "Got a message-bar-stack in HTML about:addons page"); - - is( - messageBarStack.maxMessageBarCount, - 3, - "Got the expected max-message-bar-count property" - ); - - is( - messageBarStack.childElementCount, - 0, - "message-bar-stack is initially empty" - ); -}); - -add_task(async function test_create_message_bar_create_and_onclose() { - const win = htmlAboutAddonsWindow; - const messageBarStack = win.document.getElementById("abuse-reports-messages"); - - let messageEl = win.document.createElementNS(HTML_NS, "span"); - messageEl.textContent = "A message bar text"; - let buttonEl = win.document.createElementNS(HTML_NS, "button"); - buttonEl.textContent = "An action button"; - - let messageBar; - let onceMessageBarClosed = new Promise(resolve => { - messageBar = createMessageBar(messageBarStack, { - children: [messageEl, buttonEl], - onclose: resolve, - }); - }); - - is( - messageBarStack.childElementCount, - 1, - "message-bar-stack has a child element" - ); - is( - messageBarStack.firstElementChild, - messageBar, - "newly created message-bar added as message-bar-stack child element" - ); - - const slot = messageBar.shadowRoot.querySelector("slot"); - is( - slot.assignedNodes()[0], - messageEl, - "Got the expected span element assigned to the message-bar slot" - ); - is( - slot.assignedNodes()[1], - buttonEl, - "Got the expected button element assigned to the message-bar slot" - ); - - let dismissed = BrowserTestUtils.waitForEvent( - messageBar, - "message-bar:user-dismissed" - ); - info("Click the close icon on the newly created message-bar"); - clickElement(messageBar.closeButton); - await dismissed; - - info("Expect the onclose function to be called"); - await onceMessageBarClosed; - - is( - messageBarStack.childElementCount, - 0, - "message-bar-stack has no child elements" - ); -}); - -add_task(async function test_max_message_bar_count() { - const win = htmlAboutAddonsWindow; - const messageBarStack = win.document.getElementById("abuse-reports-messages"); - - info("Create a new message-bar"); - let messageElement = document.createElementNS(HTML_NS, "span"); - messageElement = "message bar label"; - - let onceMessageBarClosed = new Promise(resolve => { - createMessageBar(messageBarStack, { - children: messageElement, - onclose: resolve, - }); - }); - - is( - messageBarStack.childElementCount, - 1, - "message-bar-stack has the expected number of children" - ); - - info("Create 3 more message bars"); - const allBarsPromises = []; - for (let i = 2; i <= 4; i++) { - allBarsPromises.push( - new Promise(resolve => { - createMessageBar(messageBarStack, { - attrs: { dismissable: "" }, - children: [messageElement, i], - onclose: resolve, - }); - }) - ); - } - - info("Expect first message-bar to closed automatically"); - await onceMessageBarClosed; - - is( - messageBarStack.childElementCount, - 3, - "message-bar-stack has the expected number of children" - ); - - info("Click on close icon for the second message-bar"); - clickElement(messageBarStack.firstElementChild.closeButton); - - info("Expect the second message-bar to be closed"); - await allBarsPromises[0]; - - is( - messageBarStack.childElementCount, - 2, - "message-bar-stack has the expected number of children" - ); - - info("Clear the entire message-bar-stack content"); - messageBarStack.textContent = ""; - - info("Expect all the created message-bar to be closed automatically"); - await Promise.all(allBarsPromises); - - is( - messageBarStack.childElementCount, - 0, - "message-bar-stack has no child elements" - ); -}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_options_ui_dark_theme.js b/toolkit/mozapps/extensions/test/browser/browser_html_options_ui_dark_theme.js new file mode 100644 index 0000000000..9261fa0a7e --- /dev/null +++ b/toolkit/mozapps/extensions/test/browser/browser_html_options_ui_dark_theme.js @@ -0,0 +1,173 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +AddonTestUtils.initMochitest(this); + +const LIGHT_SCHEME_BG = "rgb(255, 255, 255)"; +const LIGHT_SCHEME_FG = "rgb(0, 0, 0)"; + +// "browser.display.background_color.dark" pref value ("#1C1B22") maps to: +const DARK_SCHEME_BG = "rgb(28, 27, 34)"; +const DARK_SCHEME_FG = "rgb(251, 251, 254)"; + +async function getColorsForOptionsUI({ browser_style, open_in_tab }) { + let extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + manifest: { + options_ui: { + browser_style, + page: "options.html", + open_in_tab, + }, + }, + background() { + browser.test.onMessage.addListener(msg => { + browser.test.assertEq("openOptionsPage", msg, "expect openOptionsPage"); + browser.runtime.openOptionsPage(); + }); + }, + files: { + "options.html": `<style>:root { color-scheme: dark light; }</style> + <script src="options.js"></script>`, + "options.js": () => { + window.onload = () => { + browser.test.sendMessage("options_ui_opened"); + }; + }, + }, + }); + + await extension.startup(); + extension.sendMessage("openOptionsPage"); + await extension.awaitMessage("options_ui_opened"); + + const tab = gBrowser.selectedTab; + let optionsBrowser; + if (open_in_tab) { + optionsBrowser = tab.linkedBrowser; + is( + optionsBrowser.currentURI.spec, + `moz-extension://${extension.uuid}/options.html`, + "With open_in_tab=true, should open options.html in tab" + ); + } else { + // When not opening in a new tab, the inline options page is used. + is( + tab.linkedBrowser.currentURI.spec, + "about:addons", + "Without open_in_tab, should open about:addons" + ); + optionsBrowser = tab.linkedBrowser.contentDocument.getElementById( + "addon-inline-options" + ); + is( + optionsBrowser.currentURI.spec, + `moz-extension://${extension.uuid}/options.html`, + "Found options.html in inline options browser" + ); + } + + let colors = await SpecialPowers.spawn(optionsBrowser, [], () => { + let style = content.getComputedStyle(content.document.body); + // Note: cannot use style.backgroundColor because it defaults to + // "transparent" (aka rgba(0, 0, 0, 0)) which is meaningless. + // So we have to use windowUtils.canvasBackgroundColor instead. + return { + bgColor: content.windowUtils.canvasBackgroundColor, + fgColor: style.color, + }; + }); + + if (colors.bgColor === "rgba(0, 0, 0, 0)") { + // windowUtils.canvasBackgroundColor may still report a "transparent" + // background color when the options page is rendered inline in a <browser> + // at about:addons. In that case, the background color of the container + // element (i.e. the <browser>) is used to render the contents. + Assert.ok(!open_in_tab, "Background only transparent without open_in_tab"); + let style = optionsBrowser.ownerGlobal.getComputedStyle(optionsBrowser); + colors.bgColor = style.backgroundColor; + } + + BrowserTestUtils.removeTab(tab); + + await extension.unload(); + return colors; +} + +add_setup(async () => { + // The test calls openOptionsPage, which may end up re-using an existing blank + // tab. Upon closing that, the browser window of the test would close and the + // test would get stuck. To avoid that, make sure that there is a dummy tab + // around that keeps the window open. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "data:,"); + registerCleanupFunction(() => { + BrowserTestUtils.removeTab(tab); + }); +}); + +add_task(async function options_ui_open_in_tab_light() { + await SpecialPowers.pushPrefEnv({ set: [["ui.systemUsesDarkTheme", 0]] }); + // Note: browser_style:true should be no-op when open_in_tab:true. + // Therefore the result should be equivalent to the color of a normal web + // page, instead of options_ui_inline_light. + Assert.deepEqual( + await getColorsForOptionsUI({ browser_style: true, open_in_tab: true }), + { bgColor: LIGHT_SCHEME_BG, fgColor: LIGHT_SCHEME_FG } + ); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function options_ui_open_in_tab_dark() { + await SpecialPowers.pushPrefEnv({ set: [["ui.systemUsesDarkTheme", 1]] }); + // Note: browser_style:true should be no-op when open_in_tab:true. + // Therefore the result should be equivalent to the color of a normal web + // page, instead of options_ui_inline_dark. + Assert.deepEqual( + await getColorsForOptionsUI({ browser_style: true, open_in_tab: true }), + { bgColor: DARK_SCHEME_BG, fgColor: DARK_SCHEME_FG } + ); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function options_ui_light() { + await SpecialPowers.pushPrefEnv({ set: [["ui.systemUsesDarkTheme", 0]] }); + Assert.deepEqual( + await getColorsForOptionsUI({ browser_style: false, open_in_tab: false }), + { bgColor: LIGHT_SCHEME_BG, fgColor: LIGHT_SCHEME_FG } + ); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function options_ui_dark() { + await SpecialPowers.pushPrefEnv({ set: [["ui.systemUsesDarkTheme", 1]] }); + Assert.deepEqual( + await getColorsForOptionsUI({ browser_style: false, open_in_tab: false }), + { bgColor: DARK_SCHEME_BG, fgColor: DARK_SCHEME_FG } + ); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function options_ui_browser_style_true_light() { + await SpecialPowers.pushPrefEnv({ set: [["ui.systemUsesDarkTheme", 0]] }); + Assert.deepEqual( + await getColorsForOptionsUI({ browser_style: true, open_in_tab: false }), + // rgb(34, 36, 38) = color: #222426 from extension.css + { bgColor: LIGHT_SCHEME_BG, fgColor: "rgb(34, 36, 38)" } + ); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function options_ui_browser_style_true_dark() { + await SpecialPowers.pushPrefEnv({ set: [["ui.systemUsesDarkTheme", 1]] }); + Assert.deepEqual( + await getColorsForOptionsUI({ browser_style: true, open_in_tab: false }), + { bgColor: DARK_SCHEME_BG, fgColor: DARK_SCHEME_FG } + ); + await SpecialPowers.popPrefEnv(); +}); diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_scroll_restoration.js b/toolkit/mozapps/extensions/test/browser/browser_html_scroll_restoration.js index e4d88bc19a..76471007ec 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_html_scroll_restoration.js +++ b/toolkit/mozapps/extensions/test/browser/browser_html_scroll_restoration.js @@ -91,11 +91,15 @@ async function waitForStableLayout(win) { } function isLayoutStable(win) { - // <message-bar> elements may affect the layout of a page, and therefore we - // should check whether its embedded style sheet has finished loading. - for (let bar of win.document.querySelectorAll("message-bar")) { - // Check for the existence of a CSS property from message-bar.css. - if (!win.getComputedStyle(bar).getPropertyValue("--message-bar-icon-url")) { + // <moz-message-bar> elements may affect the layout of a page, and therefore + // we should check whether its embedded style sheet has finished loading. + for (let bar of win.document.querySelectorAll("moz-message-bar")) { + // Check for the existence of a CSS property from moz-message-bar.css. + if ( + !win + .getComputedStyle(bar) + .getPropertyValue("--message-bar-background-color") + ) { return false; } } diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_abuse_report.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_abuse_report.js deleted file mode 100644 index b9ea0f6a93..0000000000 --- a/toolkit/mozapps/extensions/test/browser/browser_webapi_abuse_report.js +++ /dev/null @@ -1,375 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -/* eslint max-len: ["error", 80] */ - -loadTestSubscript("head_abuse_report.js"); - -const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`; -const TELEMETRY_EVENTS_FILTERS = { - category: "addonsManager", - method: "report", -}; -const REPORT_PROP_NAMES = [ - "addon", - "addon_signature", - "reason", - "message", - "report_entry_point", -]; - -function getObjectProps(obj, propNames) { - const res = {}; - for (const k of propNames) { - res[k] = obj[k]; - } - return res; -} - -async function assertSubmittedReport(expectedReportProps) { - let reportSubmitted; - const onReportSubmitted = AbuseReportTestUtils.promiseReportSubmitHandled( - ({ data, request, response }) => { - reportSubmitted = JSON.parse(data); - handleSubmitRequest({ request, response }); - } - ); - - let panelEl = await AbuseReportTestUtils.promiseReportDialogRendered(); - - let promiseWinClosed = waitClosedWindow(); - let promisePanelUpdated = AbuseReportTestUtils.promiseReportUpdated( - panelEl, - "submit" - ); - panelEl._form.elements.reason.value = expectedReportProps.reason; - AbuseReportTestUtils.clickPanelButton(panelEl._btnNext); - await promisePanelUpdated; - - panelEl._form.elements.message.value = expectedReportProps.message; - // Reset the timestamp of the last report between tests. - AbuseReporter._lastReportTimestamp = null; - AbuseReportTestUtils.clickPanelButton(panelEl._btnSubmit); - await Promise.all([onReportSubmitted, promiseWinClosed]); - - ok(!panelEl.ownerGlobal, "Report dialog window is closed"); - Assert.deepEqual( - getObjectProps(reportSubmitted, REPORT_PROP_NAMES), - expectedReportProps, - "Got the expected report data submitted" - ); -} - -add_setup(async function () { - await AbuseReportTestUtils.setup(); - - await SpecialPowers.pushPrefEnv({ - set: [ - ["extensions.webapi.testing", true], - ["extensions.abuseReport.amWebAPI.enabled", true], - // Make sure the integrated abuse report panel is the one enabled - // while this test file runs (instead of the AMO hosted form). - // NOTE: behaviors expected when amoFormEnabled is true are tested - // in the separate browser_amo_abuse_report.js test file. - ["extensions.abuseReport.amoFormEnabled", false], - ], - }); -}); - -add_task(async function test_report_installed_addon_cancelled() { - Services.telemetry.clearEvents(); - - await BrowserTestUtils.withNewTab(TESTPAGE, async browser => { - const extension = await installTestExtension(ADDON_ID); - - let reportEnabled = await SpecialPowers.spawn(browser, [], () => { - return content.navigator.mozAddonManager.abuseReportPanelEnabled; - }); - - is(reportEnabled, true, "Expect abuseReportPanelEnabled to be true"); - - info("Test reportAbuse result on user cancelled report"); - - let promiseNewWindow = waitForNewWindow(); - let promiseWebAPIResult = SpecialPowers.spawn( - browser, - [ADDON_ID], - addonId => content.navigator.mozAddonManager.reportAbuse(addonId) - ); - - let win = await promiseNewWindow; - is(win, AbuseReportTestUtils.getReportDialog(), "Got the report dialog"); - - let panelEl = await AbuseReportTestUtils.promiseReportDialogRendered(); - - let promiseWinClosed = waitClosedWindow(); - AbuseReportTestUtils.clickPanelButton(panelEl._btnCancel); - let reportResult = await promiseWebAPIResult; - is( - reportResult, - false, - "Expect reportAbuse to resolve to false on user cancelled report" - ); - await promiseWinClosed; - ok(!panelEl.ownerGlobal, "Report dialog window is closed"); - - await extension.unload(); - }); - - // Expect no telemetry events collected for user cancelled reports. - TelemetryTestUtils.assertEvents([], TELEMETRY_EVENTS_FILTERS); -}); - -add_task(async function test_report_installed_addon_submitted() { - Services.telemetry.clearEvents(); - - await BrowserTestUtils.withNewTab(TESTPAGE, async browser => { - const extension = await installTestExtension(ADDON_ID); - - let promiseNewWindow = waitForNewWindow(); - let promiseWebAPIResult = SpecialPowers.spawn(browser, [ADDON_ID], id => - content.navigator.mozAddonManager.reportAbuse(id) - ); - let win = await promiseNewWindow; - is(win, AbuseReportTestUtils.getReportDialog(), "Got the report dialog"); - - await assertSubmittedReport({ - addon: ADDON_ID, - addon_signature: "missing", - message: "fake report message", - reason: "unwanted", - report_entry_point: "amo", - }); - - let reportResult = await promiseWebAPIResult; - is( - reportResult, - true, - "Expect reportAbuse to resolve to false on user cancelled report" - ); - - await extension.unload(); - }); - - TelemetryTestUtils.assertEvents( - [ - { - object: "amo", - value: ADDON_ID, - extra: { addon_type: "extension" }, - }, - ], - TELEMETRY_EVENTS_FILTERS - ); -}); - -add_task(async function test_report_unknown_not_installed_addon() { - const addonId = "unknown-addon@mochi.test"; - Services.telemetry.clearEvents(); - - await BrowserTestUtils.withNewTab(TESTPAGE, async browser => { - let promiseWebAPIResult = SpecialPowers.spawn(browser, [addonId], id => - content.navigator.mozAddonManager.reportAbuse(id).catch(err => { - return { name: err.name, message: err.message }; - }) - ); - - await Assert.deepEqual( - await promiseWebAPIResult, - { name: "Error", message: "Error creating abuse report" }, - "Got the expected rejected error on reporting unknown addon" - ); - - ok(!AbuseReportTestUtils.getReportDialog(), "No report dialog is open"); - }); - - TelemetryTestUtils.assertEvents( - [ - { - object: "amo", - value: addonId, - extra: { error_type: "ERROR_AMODETAILS_NOTFOUND" }, - }, - { - object: "amo", - value: addonId, - extra: { error_type: "ERROR_ADDON_NOTFOUND" }, - }, - ], - TELEMETRY_EVENTS_FILTERS - ); -}); - -add_task(async function test_report_not_installed_addon() { - const addonId = "not-installed-addon@mochi.test"; - Services.telemetry.clearEvents(); - - await BrowserTestUtils.withNewTab(TESTPAGE, async browser => { - const fakeAMODetails = { - name: "fake name", - current_version: { version: "1.0" }, - type: "extension", - icon_url: "http://test.addons.org/asserts/fake-icon-url.png", - homepage: "http://fake.url/homepage", - authors: [{ name: "author1", url: "http://fake.url/author1" }], - is_recommended: false, - }; - - AbuseReportTestUtils.amoAddonDetailsMap.set(addonId, fakeAMODetails); - registerCleanupFunction(() => - AbuseReportTestUtils.amoAddonDetailsMap.clear() - ); - - let promiseNewWindow = waitForNewWindow(); - - let promiseWebAPIResult = SpecialPowers.spawn(browser, [addonId], id => - content.navigator.mozAddonManager.reportAbuse(id) - ); - let win = await promiseNewWindow; - is(win, AbuseReportTestUtils.getReportDialog(), "Got the report dialog"); - - await assertSubmittedReport({ - addon: addonId, - addon_signature: "unknown", - message: "fake report message", - reason: "other", - report_entry_point: "amo", - }); - - let reportResult = await promiseWebAPIResult; - is( - reportResult, - true, - "Expect reportAbuse to resolve to true on submitted report" - ); - }); - - TelemetryTestUtils.assertEvents( - [ - { - object: "amo", - value: addonId, - extra: { addon_type: "extension" }, - }, - ], - TELEMETRY_EVENTS_FILTERS - ); -}); - -add_task(async function test_amo_report_on_report_already_inprogress() { - const extension = await installTestExtension(ADDON_ID); - const reportDialog = await AbuseReporter.openDialog( - ADDON_ID, - "menu", - gBrowser.selectedBrowser - ); - await AbuseReportTestUtils.promiseReportDialogRendered(); - ok(reportDialog.window, "Got an open report dialog"); - - let promiseWinClosed = waitClosedWindow(); - - await BrowserTestUtils.withNewTab(TESTPAGE, async browser => { - const promiseAMOResult = SpecialPowers.spawn(browser, [ADDON_ID], id => - content.navigator.mozAddonManager.reportAbuse(id) - ); - - await promiseWinClosed; - ok(reportDialog.window.closed, "previous report dialog should be closed"); - - is( - await reportDialog.promiseAMOResult, - undefined, - "old report cancelled after AMO called mozAddonManager.reportAbuse" - ); - - const panelEl = await AbuseReportTestUtils.promiseReportDialogRendered(); - - const { report } = AbuseReportTestUtils.getReportDialogParams(); - Assert.deepEqual( - { - reportEntryPoint: report.reportEntryPoint, - addonId: report.addon.id, - }, - { - reportEntryPoint: "amo", - addonId: ADDON_ID, - }, - "Got the expected report from the opened report dialog" - ); - - promiseWinClosed = waitClosedWindow(); - AbuseReportTestUtils.clickPanelButton(panelEl._btnCancel); - await promiseWinClosed; - - is( - await promiseAMOResult, - false, - "AMO report request resolved to false on cancel button clicked" - ); - }); - - await extension.unload(); -}); - -add_task(async function test_reject_on_unsupported_addon_types() { - const addonId = "not-supported-addon-type@mochi.test"; - - await BrowserTestUtils.withNewTab(TESTPAGE, async browser => { - const fakeAMODetails = { - name: "fake name", - current_version: { version: "1.0" }, - type: "fake-unsupported-addon-type", - }; - - AbuseReportTestUtils.amoAddonDetailsMap.set(addonId, fakeAMODetails); - registerCleanupFunction(() => - AbuseReportTestUtils.amoAddonDetailsMap.clear() - ); - - let webAPIResult = await SpecialPowers.spawn(browser, [addonId], id => - content.navigator.mozAddonManager.reportAbuse(id).then( - res => ({ gotRejection: false, result: res }), - err => ({ gotRejection: true, message: err.message }) - ) - ); - - Assert.deepEqual( - webAPIResult, - { gotRejection: true, message: "Error creating abuse report" }, - "Got the expected rejection from mozAddonManager.reportAbuse" - ); - }); -}); - -add_task(async function test_report_on_disabled_webapi() { - await SpecialPowers.pushPrefEnv({ - set: [["extensions.abuseReport.amWebAPI.enabled", false]], - }); - - await BrowserTestUtils.withNewTab(TESTPAGE, async browser => { - let reportEnabled = await SpecialPowers.spawn(browser, [], () => { - return content.navigator.mozAddonManager.abuseReportPanelEnabled; - }); - - is(reportEnabled, false, "Expect abuseReportPanelEnabled to be false"); - - info("Test reportAbuse result on report webAPI disabled"); - - let promiseWebAPIResult = SpecialPowers.spawn( - browser, - ["an-addon@mochi.test"], - addonId => - content.navigator.mozAddonManager.reportAbuse(addonId).catch(err => { - return { name: err.name, message: err.message }; - }) - ); - - Assert.deepEqual( - await promiseWebAPIResult, - { name: "Error", message: "amWebAPI reportAbuse not supported" }, - "Got the expected rejected error" - ); - }); - - await SpecialPowers.popPrefEnv(); -}); diff --git a/toolkit/mozapps/extensions/test/browser/head_abuse_report.js b/toolkit/mozapps/extensions/test/browser/head_abuse_report.js index 78c9206e0a..173f6ab7ea 100644 --- a/toolkit/mozapps/extensions/test/browser/head_abuse_report.js +++ b/toolkit/mozapps/extensions/test/browser/head_abuse_report.js @@ -3,35 +3,16 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* eslint max-len: ["error", 80] */ -/* exported installTestExtension, addCommonAbuseReportTestTasks, - * createPromptConfirmEx, DEFAULT_BUILTIN_THEME_ID, - * gManagerWindow, handleSubmitRequest, makeWidgetId, - * waitForNewWindow, waitClosedWindow, AbuseReporter, - * AbuseReporterTestUtils, AddonTestUtils +/* exported AbuseReportTestUtils, openAboutAddons, closeAboutAddons, + * gManagerWindow */ /* global MockProvider, loadInitialView, closeView */ -const { AbuseReporter } = ChromeUtils.importESModule( - "resource://gre/modules/AbuseReporter.sys.mjs" -); const { AddonTestUtils } = ChromeUtils.importESModule( "resource://testing-common/AddonTestUtils.sys.mjs" ); -const { ExtensionCommon } = ChromeUtils.importESModule( - "resource://gre/modules/ExtensionCommon.sys.mjs" -); -const { makeWidgetId } = ExtensionCommon; - -const ADDON_ID = "test-extension-to-report@mochi.test"; -const REPORT_ENTRY_POINT = "menu"; -const BASE_TEST_MANIFEST = { - name: "Fake extension to report", - author: "Fake author", - homepage_url: "https://fake.extension.url/", -}; -const DEFAULT_BUILTIN_THEME_ID = "default-theme@mozilla.org"; const EXT_DICTIONARY_ADDON_ID = "fake-dictionary@mochi.test"; const EXT_LANGPACK_ADDON_ID = "fake-langpack@mochi.test"; const EXT_WITH_PRIVILEGED_URL_ID = "ext-with-privileged-url@mochi.test"; @@ -54,110 +35,6 @@ async function closeAboutAddons() { } } -function waitForNewWindow() { - return new Promise(resolve => { - let listener = win => { - Services.obs.removeObserver(listener, "toplevel-window-ready"); - resolve(win); - }; - - Services.obs.addObserver(listener, "toplevel-window-ready"); - }); -} - -function waitClosedWindow(win) { - return new Promise(resolve => { - function onWindowClosed() { - if (win && !win.closed) { - // If a specific window reference has been passed, then check - // that the window is closed before resolving the promise. - return; - } - Services.obs.removeObserver(onWindowClosed, "xul-window-destroyed"); - resolve(); - } - Services.obs.addObserver(onWindowClosed, "xul-window-destroyed"); - }); -} - -async function installTestExtension( - id = ADDON_ID, - type = "extension", - manifest = {} -) { - let additionalProps = { - icons: { - 32: "test-icon.png", - }, - }; - - switch (type) { - case "theme": - additionalProps = { - ...additionalProps, - theme: { - colors: { - frame: "#a14040", - tab_background_text: "#fac96e", - }, - }, - }; - break; - - // TODO(Bug 1789718): Remove after the deprecated XPIProvider-based - // implementation is also removed. - case "sitepermission-deprecated": - additionalProps = { - name: "WebMIDI test addon for https://mochi.test", - install_origins: ["https://mochi.test"], - site_permissions: ["midi"], - }; - break; - case "extension": - break; - default: - throw new Error(`Unexpected addon type: ${type}`); - } - - const extensionOpts = { - manifest: { - ...BASE_TEST_MANIFEST, - ...additionalProps, - ...manifest, - browser_specific_settings: { gecko: { id } }, - }, - useAddonManager: "temporary", - }; - - // TODO(Bug 1789718): Remove after the deprecated XPIProvider-based - // implementation is also removed. - if (type === "sitepermission-deprecated") { - const xpi = AddonTestUtils.createTempWebExtensionFile(extensionOpts); - const addon = await AddonManager.installTemporaryAddon(xpi); - // The extension object that ExtensionTestUtils.loadExtension returns for - // mochitest is pretty tight to the Extension class, and so for now this - // returns a more minimal `extension` test object which only provides the - // `unload` method. - // - // For the purpose of the abuse reports tests that are using this helper - // this should be already enough. - return { - addon, - unload: () => addon.uninstall(), - }; - } - - const extension = ExtensionTestUtils.loadExtension(extensionOpts); - await extension.startup(); - return extension; -} - -function handleSubmitRequest({ request, response }) { - response.setStatusLine(request.httpVersion, 200, "OK"); - response.setHeader("Content-Type", "application/json", false); - response.write("{}"); -} - const AbuseReportTestUtils = { _mockProvider: null, _mockServer: null, @@ -166,228 +43,14 @@ const AbuseReportTestUtils = { // Mock addon details API endpoint. amoAddonDetailsMap: new Map(), - // Setup the test environment by setting the expected prefs and - // initializing MockProvider and the mock AMO server. + // Setup the test environment by setting the expected prefs and initializing + // MockProvider. async setup() { - // Enable html about:addons and the abuse reporting and - // set the api endpoints url to the mock service. await SpecialPowers.pushPrefEnv({ - set: [ - ["extensions.abuseReport.enabled", true], - ["extensions.abuseReport.url", "http://test.addons.org/api/report/"], - [ - "extensions.abuseReport.amoDetailsURL", - "http://test.addons.org/api/addons/addon/", - ], - ], + set: [["extensions.abuseReport.enabled", true]], }); this._setupMockProvider(); - this._setupMockServer(); - }, - - // Returns the currently open abuse report dialog window (if any). - getReportDialog() { - return Services.ww.getWindowByName("addons-abuse-report-dialog"); - }, - - // Returns the parameters related to the report dialog (if any). - getReportDialogParams() { - const win = this.getReportDialog(); - return win && win.arguments[0] && win.arguments[0].wrappedJSObject; - }, - - // Returns a reference to the addon-abuse-report element from the currently - // open abuse report. - getReportPanel() { - const win = this.getReportDialog(); - ok(win, "Got an abuse report dialog open"); - return win && win.document.querySelector("addon-abuse-report"); - }, - - // Returns the list of abuse report reasons. - getReasons(abuseReportEl) { - return Object.keys(abuseReportEl.ownerGlobal.ABUSE_REPORT_REASONS); - }, - - // Returns the info related to a given abuse report reason. - getReasonInfo(abuseReportEl, reason) { - return abuseReportEl.ownerGlobal.ABUSE_REPORT_REASONS[reason]; - }, - - async promiseReportOpened({ addonId, reportEntryPoint }) { - let abuseReportEl; - - if (!this.getReportDialog()) { - info("Wait for the report dialog window"); - const dialog = await waitForNewWindow(); - is(dialog, this.getReportDialog(), "Report dialog opened"); - } - - info("Wait for the abuse report panel render"); - abuseReportEl = await AbuseReportTestUtils.promiseReportDialogRendered(); - - ok(abuseReportEl, "Got an abuse report panel"); - is( - abuseReportEl.addon && abuseReportEl.addon.id, - addonId, - "Abuse Report panel rendered for the expected addonId" - ); - is( - abuseReportEl._report && abuseReportEl._report.reportEntryPoint, - reportEntryPoint, - "Abuse Report panel rendered for the expected reportEntryPoint" - ); - - return abuseReportEl; - }, - - // Return a promise resolved when the currently open report panel - // is closed. - // Also asserts that a specific report panel element has been closed, - // if one has been provided through the optional panel parameter. - async promiseReportClosed(panel) { - const win = panel ? panel.ownerGlobal : this.getReportDialog(); - if (!win || win.closed) { - throw Error("Expected report dialog not found or already closed"); - } - - await waitClosedWindow(win); - // Assert that the panel has been closed (if the caller has passed it). - if (panel) { - ok(!panel.ownerGlobal, "abuse report dialog closed"); - } - }, - - // Returns a promise resolved when the report panel has been rendered - // (rejects is there is no dialog currently open). - async promiseReportDialogRendered() { - const params = this.getReportDialogParams(); - if (!params) { - throw new Error("abuse report dialog not found"); - } - return params.promiseReportPanel; - }, - - // Given a `requestHandler` function, an HTTP server handler function - // to use to handle a report submit request received by the mock AMO server), - // returns a promise resolved when the mock AMO server has received and - // handled the report submit request. - async promiseReportSubmitHandled(requestHandler) { - if (typeof requestHandler !== "function") { - throw new Error("requestHandler should be a function"); - } - return new Promise((resolve, reject) => { - this._abuseRequestHandlers.unshift({ resolve, reject, requestHandler }); - }); - }, - - // Return a promise resolved to the abuse report panel element, - // once its rendering is completed. - // If abuseReportEl is undefined, it looks for the currently opened - // report panel. - async promiseReportRendered(abuseReportEl) { - let el = abuseReportEl; - - if (!el) { - const win = this.getReportDialog(); - if (!win) { - await waitForNewWindow(); - } - - el = await this.promiseReportDialogRendered(); - ok(el, "Got an abuse report panel"); - } - - return el._radioCheckedReason - ? el - : BrowserTestUtils.waitForEvent( - el, - "abuse-report:updated", - "Wait the abuse report panel to be rendered" - ).then(() => el); - }, - - // A promise resolved when the given abuse report panel element - // has been rendered. If a panel name ("reasons" or "submit") is - // passed as a second parameter, it also asserts that the panel is - // updated to the expected view mode. - async promiseReportUpdated(abuseReportEl, panel) { - const evt = await BrowserTestUtils.waitForEvent( - abuseReportEl, - "abuse-report:updated", - "Wait abuse report panel update" - ); - - if (panel) { - is(evt.detail.panel, panel, `Got a "${panel}" update event`); - - const el = abuseReportEl; - switch (evt.detail.panel) { - case "reasons": - ok(!el._reasonsPanel.hidden, "Reasons panel should be visible"); - ok(el._submitPanel.hidden, "Submit panel should be hidden"); - break; - case "submit": - ok(el._reasonsPanel.hidden, "Reasons panel should be hidden"); - ok(!el._submitPanel.hidden, "Submit panel should be visible"); - break; - } - } - }, - - // Returns a promise resolved once the expected number of abuse report - // message bars have been created. - promiseMessageBars(expectedMessageBarCount) { - return new Promise(resolve => { - const details = []; - function listener(evt) { - details.push(evt.detail); - if (details.length >= expectedMessageBarCount) { - cleanup(); - resolve(details); - } - } - function cleanup() { - if (gManagerWindow) { - gManagerWindow.document.removeEventListener( - "abuse-report:new-message-bar", - listener - ); - } - } - gManagerWindow.document.addEventListener( - "abuse-report:new-message-bar", - listener - ); - }); - }, - - async assertFluentStrings(containerEl) { - // Make sure all localized elements have defined Fluent strings. - let localizedEls = Array.from( - containerEl.querySelectorAll("[data-l10n-id]") - ); - if (containerEl.getAttribute("data-l10n-id")) { - localizedEls.push(containerEl); - } - ok(localizedEls.length, "Got localized elements"); - for (let el of localizedEls) { - const l10nId = el.getAttribute("data-l10n-id"); - const l10nAttrs = el.getAttribute("data-l10n-attrs"); - if (!l10nAttrs) { - await TestUtils.waitForCondition( - () => el.textContent !== "", - `Element with Fluent id '${l10nId}' should not be empty` - ); - } else { - await TestUtils.waitForCondition( - () => el.message !== "", - `Message attribute of the element with Fluent id '${l10nId}' - should not be empty` - ); - } - } }, // Assert that the report action visibility on the addon card @@ -419,68 +82,6 @@ const AbuseReportTestUtils = { return this.assertReportActionVisibility(gManagerWindow, extId, true); }, - // Assert that the report panel is hidden (or closed if the report - // panel is opened in its own dialog window). - async assertReportPanelHidden() { - const win = this.getReportDialog(); - ok(!win, "Abuse Report dialog should be initially hidden"); - }, - - createMockAddons(mockProviderAddons) { - this._mockProvider.createAddons(mockProviderAddons); - }, - - async clickPanelButton(buttonEl, { label = undefined } = {}) { - info(`Clicking the '${buttonEl.textContent.trim() || label}' button`); - // NOTE: ideally this should synthesize the mouse event, - // we call the click method to prevent intermittent timeouts - // due to the mouse event not received by the target element. - buttonEl.click(); - }, - - triggerNewReport(addonId, reportEntryPoint) { - gManagerWindow.openAbuseReport({ addonId, reportEntryPoint }); - }, - - triggerSubmit(reason, message) { - const reportEl = - this.getReportDialog().document.querySelector("addon-abuse-report"); - reportEl._form.elements.message.value = message; - reportEl._form.elements.reason.value = reason; - reportEl.submit(); - }, - - async openReport(addonId, reportEntryPoint = REPORT_ENTRY_POINT) { - // Close the current about:addons window if it has been leaved open from - // a previous test case failure. - if (gManagerWindow) { - await closeAboutAddons(); - } - - await openAboutAddons(); - - let promiseReportPanel = waitForNewWindow().then(() => - this.promiseReportDialogRendered() - ); - - this.triggerNewReport(addonId, reportEntryPoint); - - const panelEl = await promiseReportPanel; - await this.promiseReportRendered(panelEl); - is(panelEl.addonId, addonId, `Got Abuse Report panel for ${addonId}`); - - return panelEl; - }, - - async closeReportPanel(panelEl) { - const onceReportClosed = AbuseReportTestUtils.promiseReportClosed(panelEl); - - info("Cancel report and wait the dialog to be closed"); - panelEl.dispatchEvent(new CustomEvent("abuse-report:cancel")); - - await onceReportClosed; - }, - // Internal helper methods. _setupMockProvider() { @@ -529,87 +130,4 @@ const AbuseReportTestUtils = { }, ]); }, - - _setupMockServer() { - if (this._mockServer) { - return; - } - - // Init test report api server. - const server = AddonTestUtils.createHttpServer({ - hosts: ["test.addons.org"], - }); - this._mockServer = server; - - server.registerPathHandler("/api/report/", (request, response) => { - const stream = request.bodyInputStream; - const buffer = NetUtil.readInputStream(stream, stream.available()); - const data = new TextDecoder().decode(buffer); - const promisedHandler = this._abuseRequestHandlers.pop(); - if (promisedHandler) { - const { requestHandler, resolve, reject } = promisedHandler; - try { - requestHandler({ data, request, response }); - resolve(); - } catch (err) { - ok(false, `Unexpected requestHandler error ${err} ${err.stack}\n`); - reject(err); - } - } else { - ok(false, `Unexpected request: ${request.path} ${data}`); - } - }); - - server.registerPrefixHandler("/api/addons/addon/", (request, response) => { - const addonId = request.path.split("/").pop(); - if (!this.amoAddonDetailsMap.has(addonId)) { - response.setStatusLine(request.httpVersion, 404, "Not Found"); - response.write(JSON.stringify({ detail: "Not found." })); - } else { - response.setStatusLine(request.httpVersion, 200, "Success"); - response.write(JSON.stringify(this.amoAddonDetailsMap.get(addonId))); - } - }); - server.registerPathHandler( - "/assets/fake-icon-url.png", - (request, response) => { - response.setStatusLine(request.httpVersion, 200, "Success"); - response.write(""); - response.finish(); - } - ); - }, }; - -function createPromptConfirmEx({ - remove = false, - report = false, - expectCheckboxHidden = false, -} = {}) { - return (...args) => { - const checkboxState = args.pop(); - const checkboxMessage = args.pop(); - is( - checkboxState && checkboxState.value, - false, - "checkboxState should be initially false" - ); - if (expectCheckboxHidden) { - ok( - !checkboxMessage, - "Should not have a checkboxMessage in promptService.confirmEx call" - ); - } else { - ok( - checkboxMessage, - "Got a checkboxMessage in promptService.confirmEx call" - ); - } - - // Report checkbox selected. - checkboxState.value = report; - - // Remove accepted. - return remove ? 0 : 1; - }; -} diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_AbuseReporter.js b/toolkit/mozapps/extensions/test/xpcshell/test_AbuseReporter.js index c94ef29e31..4fe0821c70 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_AbuseReporter.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_AbuseReporter.js @@ -2,88 +2,21 @@ * http://creativecommons.org/publicdomain/zero/1.0/ */ -const { AbuseReporter, AbuseReportError } = ChromeUtils.importESModule( +const { AbuseReporter } = ChromeUtils.importESModule( "resource://gre/modules/AbuseReporter.sys.mjs" ); const { ClientID } = ChromeUtils.importESModule( "resource://gre/modules/ClientID.sys.mjs" ); -const { TelemetryController } = ChromeUtils.importESModule( - "resource://gre/modules/TelemetryController.sys.mjs" -); -const { TelemetryTestUtils } = ChromeUtils.importESModule( - "resource://testing-common/TelemetryTestUtils.sys.mjs" -); const APPNAME = "XPCShell"; const APPVERSION = "1"; const ADDON_ID = "test-addon@tests.mozilla.org"; -const ADDON_ID2 = "test-addon2@tests.mozilla.org"; const FAKE_INSTALL_INFO = { source: "fake-Install:Source", method: "fake:install method", }; -const PREF_REQUIRED_LOCALE = "intl.locale.requested"; -const REPORT_OPTIONS = { reportEntryPoint: "menu" }; -const TELEMETRY_EVENTS_FILTERS = { - category: "addonsManager", - method: "report", -}; - -const FAKE_AMO_DETAILS = { - name: { - "en-US": "fake name", - "it-IT": "fake it-IT name", - }, - current_version: { version: "1.0" }, - type: "extension", - is_recommended: true, -}; - -createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "49"); - -const server = createHttpServer({ hosts: ["test.addons.org"] }); - -// Mock abuse report API endpoint. -let apiRequestHandler; -server.registerPathHandler("/api/report/", (request, response) => { - const stream = request.bodyInputStream; - const buffer = NetUtil.readInputStream(stream, stream.available()); - const data = new TextDecoder().decode(buffer); - apiRequestHandler({ data, request, response }); -}); - -// Mock addon details API endpoint. -const amoAddonDetailsMap = new Map(); -server.registerPrefixHandler("/api/addons/addon/", (request, response) => { - const addonId = request.path.split("/").pop(); - if (!amoAddonDetailsMap.has(addonId)) { - response.setStatusLine(request.httpVersion, 404, "Not Found"); - response.write(JSON.stringify({ detail: "Not found." })); - } else { - response.setStatusLine(request.httpVersion, 200, "Success"); - response.write(JSON.stringify(amoAddonDetailsMap.get(addonId))); - } -}); - -function getProperties(obj, propNames) { - return propNames.reduce((acc, el) => { - acc[el] = obj[el]; - return acc; - }, {}); -} - -function handleSubmitRequest({ request, response }) { - response.setStatusLine(request.httpVersion, 200, "OK"); - response.setHeader("Content-Type", "application/json", false); - response.write("{}"); -} - -function clearAbuseReportState() { - // Clear the timestamp of the last submission. - AbuseReporter._lastReportTimestamp = null; -} async function installTestExtension(overrideOptions = {}) { const extOptions = { @@ -104,34 +37,6 @@ async function installTestExtension(overrideOptions = {}) { return { extension, addon }; } -async function assertRejectsAbuseReportError(promise, errorType, errorInfo) { - let error; - - await Assert.rejects( - promise, - err => { - // Log the actual error to make investigating test failures easier. - Cu.reportError(err); - error = err; - return err instanceof AbuseReportError; - }, - `Got an AbuseReportError` - ); - - equal(error.errorType, errorType, "Got the expected errorType"); - equal(error.errorInfo, errorInfo, "Got the expected errorInfo"); - ok( - error.message.includes(errorType), - "errorType should be included in the error message" - ); - if (errorInfo) { - ok( - error.message.includes(errorInfo), - "errorInfo should be included in the error message" - ); - } -} - async function assertBaseReportData({ reportData, addon }) { // Report properties related to addon metadata. equal(reportData.addon, ADDON_ID, "Got expected 'addon'"); @@ -191,42 +96,10 @@ async function assertBaseReportData({ reportData, addon }) { ); } -add_task(async function test_setup() { - Services.prefs.setCharPref( - "extensions.abuseReport.url", - "http://test.addons.org/api/report/" - ); - - Services.prefs.setCharPref( - "extensions.abuseReport.amoDetailsURL", - "http://test.addons.org/api/addons/addon" - ); +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "49"); +add_setup(async () => { await promiseStartupManager(); - // Telemetry test setup needed to ensure that the builtin events are defined - // and they can be collected and verified. - await TelemetryController.testSetup(); - - // This is actually only needed on Android, because it does not properly support unified telemetry - // and so, if not enabled explicitly here, it would make these tests to fail when running on a - // non-Nightly build. - const oldCanRecordBase = Services.telemetry.canRecordBase; - Services.telemetry.canRecordBase = true; - registerCleanupFunction(() => { - Services.telemetry.canRecordBase = oldCanRecordBase; - }); - - // Register a fake it-IT locale (used to test localized AMO details in some - // of the test case defined in this test file). - L10nRegistry.getInstance().registerSources([ - L10nFileSource.createMock( - "mock", - "app", - ["it-IT", "fr-FR"], - "resource://fake/locales/{locale}", - [] - ), - ]); }); add_task(async function test_addon_report_data() { @@ -260,37 +133,6 @@ add_task(async function test_addon_report_data() { await extension3.unload(); }); -add_task(async function test_report_on_not_installed_addon() { - Services.telemetry.clearEvents(); - - // Make sure that the AMO addons details API endpoint is going to - // return a 404 status for the not installed addon. - amoAddonDetailsMap.delete(ADDON_ID); - - await assertRejectsAbuseReportError( - AbuseReporter.createAbuseReport(ADDON_ID, REPORT_OPTIONS), - "ERROR_ADDON_NOTFOUND" - ); - - TelemetryTestUtils.assertEvents( - [ - { - object: REPORT_OPTIONS.reportEntryPoint, - value: ADDON_ID, - extra: { error_type: "ERROR_AMODETAILS_NOTFOUND" }, - }, - { - object: REPORT_OPTIONS.reportEntryPoint, - value: ADDON_ID, - extra: { error_type: "ERROR_ADDON_NOTFOUND" }, - }, - ], - TELEMETRY_EVENTS_FILTERS - ); - - Services.telemetry.clearEvents(); -}); - // This tests verifies how the addon installTelemetryInfo values are being // normalized into the addon_install_source and addon_install_method // expected by the API endpoint. @@ -379,526 +221,3 @@ add_task(async function test_normalized_addon_install_source_and_method() { await assertAddonInstallMethod(test, expect); } }); - -add_task(async function test_report_create_and_submit() { - Services.telemetry.clearEvents(); - - // Override the test api server request handler, to be able to - // intercept the submittions to the test api server. - let reportSubmitted; - apiRequestHandler = ({ data, request, response }) => { - reportSubmitted = JSON.parse(data); - handleSubmitRequest({ request, response }); - }; - - const { addon, extension } = await installTestExtension(); - - const reportEntryPoint = "menu"; - const report = await AbuseReporter.createAbuseReport(ADDON_ID, { - reportEntryPoint, - }); - - equal(report.addon, addon, "Got the expected addon property"); - equal( - report.reportEntryPoint, - reportEntryPoint, - "Got the expected reportEntryPoint" - ); - - const baseReportData = await AbuseReporter.getReportData(addon); - const reportProperties = { - message: "test message", - reason: "test-reason", - }; - - info("Submitting report"); - report.setMessage(reportProperties.message); - report.setReason(reportProperties.reason); - await report.submit(); - - const expectedEntries = Object.entries({ - report_entry_point: reportEntryPoint, - ...baseReportData, - ...reportProperties, - }); - - for (const [expectedKey, expectedValue] of expectedEntries) { - equal( - reportSubmitted[expectedKey], - expectedValue, - `Got the expected submitted value for "${expectedKey}"` - ); - } - - TelemetryTestUtils.assertEvents( - [ - { - object: reportEntryPoint, - value: ADDON_ID, - extra: { addon_type: "extension" }, - }, - ], - TELEMETRY_EVENTS_FILTERS - ); - - await extension.unload(); -}); - -add_task(async function test_error_recent_submit() { - Services.telemetry.clearEvents(); - clearAbuseReportState(); - - let reportSubmitted; - apiRequestHandler = ({ data, request, response }) => { - reportSubmitted = JSON.parse(data); - handleSubmitRequest({ request, response }); - }; - - const { extension } = await installTestExtension(); - const report = await AbuseReporter.createAbuseReport(ADDON_ID, { - reportEntryPoint: "uninstall", - }); - - const { extension: extension2 } = await installTestExtension({ - manifest: { - browser_specific_settings: { gecko: { id: ADDON_ID2 } }, - name: "Test Extension2", - }, - }); - const report2 = await AbuseReporter.createAbuseReport( - ADDON_ID2, - REPORT_OPTIONS - ); - - // Submit the two reports in fast sequence. - report.setReason("reason1"); - report2.setReason("reason2"); - await report.submit(); - await assertRejectsAbuseReportError(report2.submit(), "ERROR_RECENT_SUBMIT"); - equal( - reportSubmitted.reason, - "reason1", - "Server only received the data from the first submission" - ); - - TelemetryTestUtils.assertEvents( - [ - { - object: "uninstall", - value: ADDON_ID, - extra: { addon_type: "extension" }, - }, - { - object: REPORT_OPTIONS.reportEntryPoint, - value: ADDON_ID2, - extra: { - addon_type: "extension", - error_type: "ERROR_RECENT_SUBMIT", - }, - }, - ], - TELEMETRY_EVENTS_FILTERS - ); - - await extension.unload(); - await extension2.unload(); -}); - -add_task(async function test_submission_server_error() { - const { extension } = await installTestExtension(); - - async function testErrorCode({ - responseStatus, - responseText = "", - expectedErrorType, - expectedErrorInfo, - expectRequest = true, - }) { - info( - `Test expected AbuseReportError on response status "${responseStatus}"` - ); - Services.telemetry.clearEvents(); - clearAbuseReportState(); - - let requestReceived = false; - apiRequestHandler = ({ request, response }) => { - requestReceived = true; - response.setStatusLine(request.httpVersion, responseStatus, "Error"); - response.write(responseText); - }; - - const report = await AbuseReporter.createAbuseReport( - ADDON_ID, - REPORT_OPTIONS - ); - report.setReason("a-reason"); - const promiseSubmit = report.submit(); - if (typeof expectedErrorType === "string") { - // Assert a specific AbuseReportError errorType. - await assertRejectsAbuseReportError( - promiseSubmit, - expectedErrorType, - expectedErrorInfo - ); - } else { - // Assert on a given Error class. - await Assert.rejects(promiseSubmit, expectedErrorType); - } - equal( - requestReceived, - expectRequest, - `${expectRequest ? "" : "Not "}received a request as expected` - ); - - TelemetryTestUtils.assertEvents( - [ - { - object: REPORT_OPTIONS.reportEntryPoint, - value: ADDON_ID, - extra: { - addon_type: "extension", - error_type: - typeof expectedErrorType === "string" - ? expectedErrorType - : "ERROR_UNKNOWN", - }, - }, - ], - TELEMETRY_EVENTS_FILTERS - ); - } - - await testErrorCode({ - responseStatus: 500, - responseText: "A server error", - expectedErrorType: "ERROR_SERVER", - expectedErrorInfo: JSON.stringify({ - status: 500, - responseText: "A server error", - }), - }); - await testErrorCode({ - responseStatus: 404, - responseText: "Not found error", - expectedErrorType: "ERROR_CLIENT", - expectedErrorInfo: JSON.stringify({ - status: 404, - responseText: "Not found error", - }), - }); - // Test response with unexpected status code. - await testErrorCode({ - responseStatus: 604, - responseText: "An unexpected status code", - expectedErrorType: "ERROR_UNKNOWN", - expectedErrorInfo: JSON.stringify({ - status: 604, - responseText: "An unexpected status code", - }), - }); - // Test response status 200 with invalid json data. - await testErrorCode({ - responseStatus: 200, - expectedErrorType: /SyntaxError: JSON.parse/, - }); - - // Test on invalid url. - Services.prefs.setCharPref( - "extensions.abuseReport.url", - "invalid-protocol://abuse-report" - ); - await testErrorCode({ - expectedErrorType: "ERROR_NETWORK", - expectRequest: false, - }); - - await extension.unload(); -}); - -add_task(async function set_test_abusereport_url() { - Services.prefs.setCharPref( - "extensions.abuseReport.url", - "http://test.addons.org/api/report/" - ); -}); - -add_task(async function test_submission_aborting() { - Services.telemetry.clearEvents(); - clearAbuseReportState(); - - const { extension } = await installTestExtension(); - - // override the api request handler with one that is never going to reply. - let receivedRequestsCount = 0; - let resolvePendingResponses; - const waitToReply = new Promise( - resolve => (resolvePendingResponses = resolve) - ); - - const onRequestReceived = new Promise(resolve => { - apiRequestHandler = ({ request, response }) => { - response.processAsync(); - response.setStatusLine(request.httpVersion, 200, "OK"); - receivedRequestsCount++; - resolve(); - - // Keep the request pending until resolvePendingResponses have been - // called. - waitToReply.then(() => { - response.finish(); - }); - }; - }); - - const report = await AbuseReporter.createAbuseReport( - ADDON_ID, - REPORT_OPTIONS - ); - report.setReason("a-reason"); - const promiseResult = report.submit(); - - await onRequestReceived; - - Assert.greater( - receivedRequestsCount, - 0, - "Got the expected number of requests" - ); - Assert.strictEqual( - await Promise.race([promiseResult, Promise.resolve("pending")]), - "pending", - "Submission fetch request should still be pending" - ); - - report.abort(); - - await assertRejectsAbuseReportError(promiseResult, "ERROR_ABORTED_SUBMIT"); - - TelemetryTestUtils.assertEvents( - [ - { - object: REPORT_OPTIONS.reportEntryPoint, - value: ADDON_ID, - extra: { addon_type: "extension", error_type: "ERROR_ABORTED_SUBMIT" }, - }, - ], - TELEMETRY_EVENTS_FILTERS - ); - - await extension.unload(); - - // Unblock pending requests on the server request handler side, so that the - // test file can shutdown (otherwise the test run will be stuck after this - // task completed). - resolvePendingResponses(); -}); - -add_task(async function test_truncated_string_properties() { - const generateString = len => new Array(len).fill("a").join(""); - - const LONG_STRINGS_ADDON_ID = "addon-with-long-strings-props@mochi.test"; - const { extension } = await installTestExtension({ - manifest: { - name: generateString(400), - description: generateString(400), - browser_specific_settings: { gecko: { id: LONG_STRINGS_ADDON_ID } }, - }, - }); - - // Override the test api server request handler, to be able to - // intercept the properties actually submitted. - let reportSubmitted; - apiRequestHandler = ({ data, request, response }) => { - reportSubmitted = JSON.parse(data); - handleSubmitRequest({ request, response }); - }; - - const report = await AbuseReporter.createAbuseReport( - LONG_STRINGS_ADDON_ID, - REPORT_OPTIONS - ); - - report.setMessage("fake-message"); - report.setReason("fake-reason"); - await report.submit(); - - const expected = { - addon_name: generateString(255), - addon_summary: generateString(255), - }; - - Assert.deepEqual( - { - addon_name: reportSubmitted.addon_name, - addon_summary: reportSubmitted.addon_summary, - }, - expected, - "Got the long strings truncated as expected" - ); - - await extension.unload(); -}); - -add_task(async function test_report_recommended() { - const NON_RECOMMENDED_ADDON_ID = "non-recommended-addon@mochi.test"; - const RECOMMENDED_ADDON_ID = "recommended-addon@mochi.test"; - - const now = Date.now(); - const not_before = new Date(now - 3600000).toISOString(); - const not_after = new Date(now + 3600000).toISOString(); - - const { extension: nonRecommended } = await installTestExtension({ - manifest: { - name: "Fake non recommended addon", - browser_specific_settings: { gecko: { id: NON_RECOMMENDED_ADDON_ID } }, - }, - }); - - const { extension: recommended } = await installTestExtension({ - manifest: { - name: "Fake recommended addon", - browser_specific_settings: { gecko: { id: RECOMMENDED_ADDON_ID } }, - }, - files: { - "mozilla-recommendation.json": { - addon_id: RECOMMENDED_ADDON_ID, - states: ["recommended"], - validity: { not_before, not_after }, - }, - }, - }); - - // Override the test api server request handler, to be able to - // intercept the properties actually submitted. - let reportSubmitted; - apiRequestHandler = ({ data, request, response }) => { - reportSubmitted = JSON.parse(data); - handleSubmitRequest({ request, response }); - }; - - async function checkReportedSignature(addonId, expectedAddonSignature) { - clearAbuseReportState(); - const report = await AbuseReporter.createAbuseReport( - addonId, - REPORT_OPTIONS - ); - report.setMessage("fake-message"); - report.setReason("fake-reason"); - await report.submit(); - equal( - reportSubmitted.addon_signature, - expectedAddonSignature, - `Got the expected addon_signature for ${addonId}` - ); - } - - await checkReportedSignature(NON_RECOMMENDED_ADDON_ID, "signed"); - await checkReportedSignature(RECOMMENDED_ADDON_ID, "curated"); - - await nonRecommended.unload(); - await recommended.unload(); -}); - -add_task(async function test_query_amo_details() { - async function assertReportOnAMODetails({ addonId, expectedReport } = {}) { - // Clear last report timestamp and any telemetry event recorded so far. - clearAbuseReportState(); - Services.telemetry.clearEvents(); - - const report = await AbuseReporter.createAbuseReport(addonId, { - reportEntryPoint: "menu", - }); - - let reportSubmitted; - apiRequestHandler = ({ data, request, response }) => { - reportSubmitted = JSON.parse(data); - handleSubmitRequest({ request, response }); - }; - - report.setMessage("fake message"); - report.setReason("reason1"); - await report.submit(); - - Assert.deepEqual( - expectedReport, - getProperties(reportSubmitted, Object.keys(expectedReport)), - "Got the expected report properties" - ); - - // Telemetry recorded for the successfully submitted report. - TelemetryTestUtils.assertEvents( - [ - { - object: "menu", - value: addonId, - extra: { addon_type: FAKE_AMO_DETAILS.type }, - }, - ], - TELEMETRY_EVENTS_FILTERS - ); - - clearAbuseReportState(); - } - - // Add the expected AMO addons details. - const addonId = "not-installed-addon@mochi.test"; - amoAddonDetailsMap.set(addonId, FAKE_AMO_DETAILS); - - // Test on the default en-US locale. - Services.prefs.setCharPref(PREF_REQUIRED_LOCALE, "en-US"); - let locale = Services.locale.appLocaleAsBCP47; - equal(locale, "en-US", "Got the expected app locale set"); - - let expectedReport = { - addon: addonId, - addon_name: FAKE_AMO_DETAILS.name[locale], - addon_version: FAKE_AMO_DETAILS.current_version.version, - addon_install_source: "not_installed", - addon_install_method: null, - addon_signature: "curated", - }; - - await assertReportOnAMODetails({ addonId, expectedReport }); - - // Test with a non-default locale also available in the AMO details. - Services.prefs.setCharPref(PREF_REQUIRED_LOCALE, "it-IT"); - locale = Services.locale.appLocaleAsBCP47; - equal(locale, "it-IT", "Got the expected app locale set"); - - expectedReport = { - ...expectedReport, - addon_name: FAKE_AMO_DETAILS.name[locale], - }; - await assertReportOnAMODetails({ addonId, expectedReport }); - - // Test with a non-default locale not available in the AMO details. - Services.prefs.setCharPref(PREF_REQUIRED_LOCALE, "fr-FR"); - locale = Services.locale.appLocaleAsBCP47; - equal(locale, "fr-FR", "Got the expected app locale set"); - - expectedReport = { - ...expectedReport, - // Fallbacks on en-US for non available locales. - addon_name: FAKE_AMO_DETAILS.name["en-US"], - }; - await assertReportOnAMODetails({ addonId, expectedReport }); - - Services.prefs.clearUserPref(PREF_REQUIRED_LOCALE); - - amoAddonDetailsMap.clear(); -}); - -add_task(async function test_statictheme_normalized_into_type_theme() { - const themeId = "not-installed-statictheme@mochi.test"; - amoAddonDetailsMap.set(themeId, { - ...FAKE_AMO_DETAILS, - type: "statictheme", - }); - - const report = await AbuseReporter.createAbuseReport(themeId, REPORT_OPTIONS); - - equal(report.addon.id, themeId, "Got a report for the expected theme id"); - equal(report.addon.type, "theme", "Got the expected addon type"); - - amoAddonDetailsMap.clear(); -}); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_dictionary_webextension.js b/toolkit/mozapps/extensions/test/xpcshell/test_dictionary_webextension.js index 1f52c8a8bc..bcb777e135 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_dictionary_webextension.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_dictionary_webextension.js @@ -10,7 +10,7 @@ XPCOMUtils.defineLazyServiceGetter( "mozISpellCheckingEngine" ); -add_task(async function setup() { +add_setup(async function setup() { createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "61", "61"); // Initialize the URLPreloader so that we can load the built-in @@ -18,6 +18,12 @@ add_task(async function setup() { AddonTestUtils.initializeURLPreloader(); await promiseStartupManager(); + // Sanity check, builtin dictionaries should be registered. + Assert.deepEqual( + spellCheck.getDictionaryList(), + ["en-US"], + "Expect en-US builtin dictionary to be registered" + ); // Starts collecting the Addon Manager Telemetry events. AddonTestUtils.hookAMTelemetryEvents(); @@ -218,6 +224,13 @@ add_task( const WORD = "Flehgragh"; add_task(async function test_registration() { + // Sanity check, builtin dictionaries should be registered. + Assert.deepEqual( + spellCheck.getDictionaryList(), + ["en-US"], + "Expect en-US builtin dictionary to be registered" + ); + spellCheck.dictionaries = ["en-US"]; ok(!spellCheck.check(WORD), "Word should not pass check before add-on loads"); diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js index 913c802609..3551b2c8dd 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js @@ -1,5 +1,14 @@ +AddonTestUtils.overrideCertDB(); +AddonTestUtils.usePrivilegedSignatures = id => id.startsWith("privileged-"); + +// Some tests in this test file can't run on android build because +// theme addon type isn't supported. +const skipOnAndroid = () => ({ + skip_if: () => AppConstants.platform === "android", +}); + let profileDir; -add_task(async function setup() { +add_setup(async function setup() { profileDir = gProfD.clone(); profileDir.append("extensions"); @@ -19,7 +28,7 @@ const IMPLICIT_ID_ID = "{46607a7b-1b2a-40ce-9afe-91cda52c46a6}"; // webext-implicit-id.xpi has a minimal manifest with no // applications or browser_specific_settings, so its id comes // from its signature, which should be the ID constant defined below. -add_task(async function test_implicit_id() { +add_task(skipOnAndroid(), async function test_implicit_id() { let addon = await promiseAddonByID(IMPLICIT_ID_ID); equal(addon, null, "Add-on is not installed"); @@ -34,7 +43,7 @@ add_task(async function test_implicit_id() { // We should also be able to install webext-implicit-id.xpi temporarily // and it should look just like the regular install (ie, the ID should // come from the signature) -add_task(async function test_implicit_id_temp() { +add_task(skipOnAndroid(), async function test_implicit_id_temp() { let addon = await promiseAddonByID(IMPLICIT_ID_ID); equal(addon, null, "Add-on is not installed"); @@ -602,6 +611,187 @@ add_task(async function test_permissions_prompt() { await IOUtils.remove(xpi.path); }); +// Check normalized optional origins. +add_task(async function test_normalized_optional_origins() { + const assertAddonWrapperPermissionsProperties = async ( + manifest, + expected + ) => { + let xpi = ExtensionTestCommon.generateXPI({ manifest }); + + let install = await AddonManager.getInstallForFile(xpi); + + let perminfo; + let installPromptCompletedDeferred = Promise.withResolvers(); + let installPromptShownDeferred = Promise.withResolvers(); + install.promptHandler = info => { + perminfo = info; + installPromptShownDeferred.resolve(); + return installPromptCompletedDeferred.promise; + }; + + const promiseInstalled = install.install(); + info("Wait for the install prompt"); + await installPromptShownDeferred.promise; + + equal( + await promiseAddonByID(perminfo.addon.id), + null, + "Extension should not be installed yet" + ); + notEqual(perminfo, undefined, "Permission handler was invoked"); + notEqual(perminfo.addon, null, "Permission info includes the new addon"); + equal( + perminfo.addon.isPrivileged, + expected.isPrivileged, + `Expect the addon to be ${expected.isPrivileged ? "" : "non-"}privileged` + ); + let addon = perminfo.addon; + deepEqual( + addon.userPermissions, + expected.userPermissions, + "userPermissions are correct" + ); + deepEqual( + addon.optionalPermissions, + expected.optionalPermissions, + "optionalPermissions are correct" + ); + info("Assert normalized optional origins on non-installed extension"); + deepEqual( + addon.optionalOriginsNormalized, + expected.optionalOriginsNormalized, + "optionalOriginsNormalized are correct" + ); + + installPromptCompletedDeferred.resolve(); + await promiseInstalled; + addon = await promiseAddonByID(perminfo.addon.id); + notEqual(addon, null, "Extension was installed successfully"); + + info("Assert normalized optional origins again on installed extension"); + deepEqual( + addon.optionalOriginsNormalized, + expected.optionalOriginsNormalized, + "Normalized origin permissions are correct" + ); + + await addon.uninstall(); + await IOUtils.remove(xpi.path); + }; + + info( + "Test normalized optional origins on non-privileged ManifestV2 extension" + ); + const manifestV2 = { + name: "permissions test mv2", + manifest_version: 2, + // Include a permission that would trigger an install time prompt. + permissions: ["tabs"], + // Include optional origins to be normalized. + // NOTE: we expect the normalized origins to be deduplicated. + optional_permissions: [ + "http://*.example.com/", + "http://*.example.com/somepath/", + "*://example.org/*", + "*://example.org/another-path/*", + "file://*/*", + ], + }; + + await assertAddonWrapperPermissionsProperties(manifestV2, { + isPrivileged: false, + userPermissions: { permissions: manifestV2.permissions, origins: [] }, + optionalPermissions: { + permissions: [], + origins: manifestV2.optional_permissions, + }, + optionalOriginsNormalized: [ + "http://*.example.com/*", + "*://example.org/*", + "file://*/*", + ], + }); + + info( + "Test normalized optional origins on non-privileged ManifestV3 extension" + ); + const manifestV3 = { + name: "permissions test mv3", + manifest_version: 3, + // Include a permission that would trigger an install time prompt. + permissions: ["tabs"], + + // Content Scripts match patterns are also expected to be part + // of the optional host permissions (and to be deduplicated). + content_scripts: [ + { + matches: ["*://*/*"], + js: ["script.js"], + }, + { + matches: ["*://example.org/*"], + js: ["script2.js"], + }, + ], + + // Include optional origins to be normalized. + host_permissions: [ + "http://*.example.com/", + "*://example.org/*", + "file://*/*", + ], + }; + + await assertAddonWrapperPermissionsProperties(manifestV3, { + isPrivileged: false, + userPermissions: { permissions: manifestV3.permissions, origins: [] }, + optionalPermissions: { + permissions: [], + origins: [...manifestV3.host_permissions, "*://*/*"], + }, + optionalOriginsNormalized: [ + "http://*.example.com/*", + "*://example.org/*", + "file://*/*", + "*://*/*", + ], + }); + + info("Test normalized optional origins on privileged ManifestV2 extension"); + const manifestV2Privileged = { + name: "permissions test privileged mv2", + manifest_version: 2, + browser_specific_settings: { gecko: { id: "privileged-mv2@ext" } }, + // Include a permission that would trigger an install time prompt. + permissions: ["tabs", "mozillaAddons"], + optional_permissions: [ + "http://*.example.com/", + "*://example.org/*", + "file://*/*", + "resource://gre/", + ], + }; + + await assertAddonWrapperPermissionsProperties(manifestV2Privileged, { + isPrivileged: true, + userPermissions: { + permissions: manifestV2Privileged.permissions, + origins: [], + }, + optionalPermissions: { + permissions: [], + origins: manifestV2Privileged.optional_permissions, + }, + optionalOriginsNormalized: [ + "http://*.example.com/*", + "*://example.org/*", + "file://*/*", + "resource://gre/*", + ], + }); +}); + // Check permissions prompt cancellation add_task(async function test_permissions_prompt_cancel() { const manifest = { diff --git a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.toml b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.toml index e8d3807ab9..a342fad700 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.toml +++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.toml @@ -1,5 +1,4 @@ [DEFAULT] -skip-if = ["os == 'android'"] tags = "addons" head = "head_addons.js" firefox-appdir = "browser" @@ -11,252 +10,374 @@ support-files = [ "../xpinstall/amosigned-sha1only.xpi", ] +# TODO: Most tests are skipped on Android but we should re-enable them, +# cf. Bug 1872867. + ["test_AMBrowserExtensionsImport.js"] +skip-if = ["os == 'android'"] ["test_AbuseReporter.js"] +skip-if = ["os == 'android'"] ["test_AddonRepository.js"] +skip-if = ["os == 'android'"] ["test_AddonRepository_appIsShuttingDown.js"] +skip-if = ["os == 'android'"] ["test_AddonRepository_cache.js"] +skip-if = ["os == 'android'"] ["test_AddonRepository_cache_locale.js"] +skip-if = ["os == 'android'"] ["test_AddonRepository_langpacks.js"] +skip-if = ["os == 'android'"] ["test_AddonRepository_paging.js"] +skip-if = ["os == 'android'"] ["test_ProductAddonChecker.js"] +skip-if = ["os == 'android'"] ["test_ProductAddonChecker_signatures.js"] head = "head_addons.js head_cert_handling.js" +skip-if = ["os == 'android'"] ["test_QuarantinedDomains_AMRemoteSettings.js"] head = "head_addons.js head_amremotesettings.js ../../../../components/extensions/test/xpcshell/head_telemetry.js" +skip-if = ["os == 'android'"] ["test_QuarantinedDomains_AddonWrapper.js"] +skip-if = ["os == 'android'"] ["test_XPIStates.js"] -skip-if = ["condprof"] # Bug 1769184 - by design for now +skip-if = [ + "condprof", # Bug 1769184 - by design for now + "os == 'android'", +] ["test_XPIcancel.js"] +skip-if = ["os == 'android'"] ["test_addonStartup.js"] +skip-if = ["os == 'android'"] ["test_addon_manager_telemetry_events.js"] +skip-if = ["os == 'android'"] ["test_amo_stats_telemetry.js"] +skip-if = ["os == 'android'"] ["test_aom_startup.js"] +skip-if = ["os == 'android'"] ["test_bad_json.js"] +skip-if = ["os == 'android'"] ["test_badschema.js"] +skip-if = ["os == 'android'"] ["test_builtin_location.js"] +skip-if = ["os == 'android'"] ["test_cacheflush.js"] +skip-if = ["os == 'android'"] ["test_childprocess.js"] head = "" +skip-if = ["os == 'android'"] ["test_colorways_builtin_theme_upgrades.js"] -skip-if = ["appname == 'thunderbird'"] # Bug 1809438 - No colorways in Thunderbird +skip-if = [ + "appname == 'thunderbird'", # Bug 1809438 - No colorways in Thunderbird + "os == 'android'", +] ["test_cookies.js"] +skip-if = ["os == 'android'"] ["test_corrupt.js"] +skip-if = ["os == 'android'"] ["test_crash_annotation_quoting.js"] +skip-if = ["os == 'android'"] ["test_db_path.js"] head = "" +skip-if = ["os == 'android'"] ["test_delay_update_webextension.js"] tags = "webextensions" +skip-if = ["os == 'android'"] ["test_dependencies.js"] +skip-if = ["os == 'android'"] ["test_dictionary_webextension.js"] +skip-if = ["os == 'android'"] ["test_distribution.js"] +skip-if = ["os == 'android'"] ["test_distribution_langpack.js"] +skip-if = ["os == 'android'"] ["test_embedderDisabled.js"] +skip-if = ["os == 'android'"] ["test_error.js"] -skip-if = ["os == 'win'"] # Bug 1508482 +skip-if = [ + "os == 'win'", # Bug 1508482 + "os == 'android'", +] ["test_ext_management.js"] tags = "webextensions" +skip-if = ["os == 'android'"] ["test_general.js"] +skip-if = ["os == 'android'"] ["test_getInstallSourceFromHost.js"] +skip-if = ["os == 'android'"] ["test_gmpProvider.js"] -skip-if = ["appname != 'firefox'"] +skip-if = [ + "appname != 'firefox'", + "os == 'android'", +] ["test_harness.js"] +skip-if = ["os == 'android'"] ["test_hidden.js"] +skip-if = ["os == 'android'"] ["test_install.js"] +skip-if = ["os == 'android'"] ["test_installOrigins.js"] +skip-if = ["os == 'android'"] ["test_install_cancel.js"] +skip-if = ["os == 'android'"] ["test_install_file_change.js"] +skip-if = ["os == 'android'"] ["test_install_icons.js"] +skip-if = ["os == 'android'"] ["test_installtrigger_deprecation.js"] head = "head_addons.js head_amremotesettings.js" +skip-if = ["os == 'android'"] ["test_installtrigger_schemes.js"] +skip-if = ["os == 'android'"] ["test_isDebuggable.js"] +skip-if = ["os == 'android'"] ["test_isReady.js"] +skip-if = ["os == 'android'"] ["test_loadManifest_isPrivileged.js"] +skip-if = ["os == 'android'"] ["test_locale.js"] +skip-if = ["os == 'android'"] ["test_moved_extension_metadata.js"] skip-if = ["true"] # bug 1777900 ["test_no_addons.js"] +skip-if = ["os == 'android'"] ["test_nodisable_hidden.js"] +skip-if = ["os == 'android'"] ["test_onPropertyChanged_appDisabled.js"] head = "head_addons.js head_compat.js" -skip-if = ["tsan"] # Times out, bug 1674773 +skip-if = [ + "tsan", # Times out, bug 1674773 + "os == 'android'", +] ["test_permissions.js"] +skip-if = ["os == 'android'"] ["test_permissions_prefs.js"] +skip-if = ["os == 'android'"] ["test_pref_properties.js"] +skip-if = ["os == 'android'"] ["test_provider_markSafe.js"] +skip-if = ["os == 'android'"] ["test_provider_shutdown.js"] +skip-if = ["os == 'android'"] ["test_provider_unsafe_access_shutdown.js"] +skip-if = ["os == 'android'"] ["test_provider_unsafe_access_startup.js"] +skip-if = ["os == 'android'"] ["test_proxies.js"] -skip-if = ["require_signing"] +skip-if = [ + "require_signing", + "os == 'android'", +] ["test_recommendations.js"] -skip-if = ["require_signing"] +skip-if = [ + "require_signing", + "os == 'android'", +] ["test_registerchrome.js"] +skip-if = ["os == 'android'"] ["test_registry.js"] run-if = ["os == 'win'"] +skip-if = ["os == 'android'"] ["test_reinstall_disabled_addon.js"] +skip-if = ["os == 'android'"] ["test_reload.js"] -skip-if = ["os == 'win'"] # There's a problem removing a temp file without manually clearing the cache on Windows +skip-if = [ + "os == 'win'", # There's a problem removing a temp file without manually clearing the cache on Windows + "os == 'android'", +] tags = "webextensions" ["test_remote_pref_telemetry.js"] +skip-if = ["os == 'android'"] ["test_safemode.js"] +skip-if = ["os == 'android'"] ["test_schema_change.js"] +skip-if = ["os == 'android'"] ["test_seen.js"] +skip-if = ["os == 'android'"] ["test_shutdown.js"] +skip-if = ["os == 'android'"] ["test_shutdown_barriers.js"] +skip-if = ["os == 'android'"] ["test_shutdown_early.js"] -skip-if = ["condprof"] # Bug 1769184 - by design for now +skip-if = [ + "condprof", # Bug 1769184 - by design for now + "os == 'android'", +] ["test_sideload_scopes.js"] head = "head_addons.js head_sideload.js" skip-if = [ "os == 'linux'", # Bug 1613268 "condprof", # Bug 1769184 - by design for now + "os == 'android'", ] ["test_sideloads.js"] +skip-if = ["os == 'android'"] ["test_sideloads_after_rebuild.js"] -skip-if = ["condprof"] # Bug 1769184 - by design for now +skip-if = [ + "condprof", # Bug 1769184 - by design for now + "os == 'android'", +] head = "head_addons.js head_sideload.js" ["test_signed_inject.js"] skip-if = ["true"] # Bug 1394122 ["test_signed_install.js"] +skip-if = ["os == 'android'"] ["test_signed_langpack.js"] +skip-if = ["os == 'android'"] ["test_signed_long.js"] +skip-if = ["os == 'android'"] ["test_signed_updatepref.js"] skip-if = [ "require_signing", "!allow_legacy_extensions", + "os == 'android'", ] ["test_signed_verify.js"] ["test_sitePermsAddonProvider.js"] -skip-if = ["appname == 'thunderbird'"] # Disabled in extensions.manifest +skip-if = [ + "appname == 'thunderbird'", # Disabled in extensions.manifest + "os == 'android'", +] ["test_startup.js"] head = "head_addons.js head_sideload.js" skip-if = [ "os == 'linux'", # Bug 1613268 "condprof", # Bug 1769184 - by design for now + "os == 'android'", ] ["test_startup_enable.js"] +skip-if = ["os == 'android'"] ["test_startup_isPrivileged.js"] +skip-if = ["os == 'android'"] ["test_startup_scan.js"] head = "head_addons.js head_sideload.js" +skip-if = ["os == 'android'"] ["test_strictcompatibility.js"] head = "head_addons.js head_compat.js" +skip-if = ["os == 'android'"] ["test_syncGUID.js"] +skip-if = ["os == 'android'"] ["test_system_allowed.js"] head = "head_addons.js head_system_addons.js" +skip-if = ["os == 'android'"] ["test_system_delay_update.js"] head = "head_addons.js head_system_addons.js" +skip-if = ["os == 'android'"] ["test_system_profile_location.js"] +skip-if = ["os == 'android'"] ["test_system_repository.js"] head = "head_addons.js head_system_addons.js" +skip-if = ["os == 'android'"] ["test_system_reset.js"] head = "head_addons.js head_system_addons.js" +skip-if = ["os == 'android'"] ["test_system_update_blank.js"] head = "head_addons.js head_system_addons.js" +skip-if = ["os == 'android'"] ["test_system_update_checkSizeHash.js"] head = "head_addons.js head_system_addons.js" +skip-if = ["os == 'android'"] ["test_system_update_custom.js"] head = "head_addons.js head_system_addons.js" +skip-if = ["os == 'android'"] ["test_system_update_empty.js"] head = "head_addons.js head_system_addons.js" @@ -264,104 +385,148 @@ skip-if = ["true"] # Failing intermittently due to a race condition in the test, ["test_system_update_enterprisepolicy.js"] head = "head_addons.js head_system_addons.js" +skip-if = ["os == 'android'"] ["test_system_update_fail.js"] head = "head_addons.js head_system_addons.js" +skip-if = ["os == 'android'"] ["test_system_update_installTelemetryInfo.js"] head = "head_addons.js head_system_addons.js" +skip-if = ["os == 'android'"] ["test_system_update_newset.js"] head = "head_addons.js head_system_addons.js" +skip-if = ["os == 'android'"] ["test_system_update_overlapping.js"] head = "head_addons.js head_system_addons.js" +skip-if = ["os == 'android'"] ["test_system_update_uninstall_check.js"] head = "head_addons.js head_system_addons.js" +skip-if = ["os == 'android'"] ["test_system_update_upgrades.js"] head = "head_addons.js head_system_addons.js" +skip-if = ["os == 'android'"] ["test_system_upgrades.js"] -skip-if = ["condprof"] # Bug 1769184 - by design for now +skip-if = [ + "condprof", # Bug 1769184 - by design for now + "os == 'android'", +] head = "head_addons.js head_system_addons.js" ["test_systemaddomstartupprefs.js"] -skip-if = ["condprof"] # Bug 1769184 - by design for now +skip-if = [ + "condprof", # Bug 1769184 - by design for now + "os == 'android'", +] head = "head_addons.js head_system_addons.js" ["test_temporary.js"] -skip-if = ["os == 'win'"] # Bug 1469904 +skip-if = [ + "os == 'win'", # Bug 1469904 + "os == 'android'", +] tags = "webextensions" ["test_trash_directory.js"] run-if = ["os == 'win'"] +skip-if = ["os == 'android'"] ["test_types.js"] ["test_undouninstall.js"] -skip-if = ["os == 'win'"] # Bug 1358846 +skip-if = [ + "os == 'win'", # Bug 1358846 + "os == 'android'", +] ["test_update.js"] +skip-if = ["os == 'android'"] ["test_updateCancel.js"] +skip-if = ["os == 'android'"] ["test_update_addontype.js"] +skip-if = ["os == 'android'"] ["test_update_compatmode.js"] head = "head_addons.js head_compat.js" +skip-if = ["os == 'android'"] ["test_update_ignorecompat.js"] skip-if = ["true"] # Bug 676922 Bug 1437697 ["test_update_isPrivileged.js"] -skip-if = ["condprof"] # Bug 1769184 - by design for now +skip-if = [ + "condprof", # Bug 1769184 - by design for now + "os == 'android'", +] ["test_update_noSystemAddonUpdate.js"] head = "head_addons.js head_system_addons.js" +skip-if = ["os == 'android'"] ["test_update_strictcompat.js"] head = "head_addons.js head_compat.js" +skip-if = ["os == 'android'"] ["test_update_theme.js"] +skip-if = ["os == 'android'"] ["test_update_webextensions.js"] tags = "webextensions" +skip-if = ["os == 'android'"] ["test_updatecheck.js"] +skip-if = ["os == 'android'"] ["test_updatecheck_errors.js"] +skip-if = ["os == 'android'"] ["test_updatecheck_json.js"] +skip-if = ["os == 'android'"] ["test_updateid.js"] +skip-if = ["os == 'android'"] ["test_updateversion.js"] +skip-if = ["os == 'android'"] ["test_upgrade.js"] head = "head_addons.js head_compat.js" run-sequentially = "Uses global XCurProcD dir." +skip-if = ["os == 'android'"] ["test_upgrade_incompatible.js"] +skip-if = ["os == 'android'"] ["test_webextension.js"] tags = "webextensions" +skip-if = ["os == 'android'"] ["test_webextension_events.js"] tags = "webextensions" +skip-if = ["os == 'android'"] ["test_webextension_icons.js"] tags = "webextensions" +skip-if = ["os == 'android'"] ["test_webextension_install.js"] tags = "webextensions" ["test_webextension_install_syntax_error.js"] tags = "webextensions" +skip-if = ["os == 'android'"] ["test_webextension_langpack.js"] tags = "webextensions" +skip-if = ["os == 'android'"] ["test_webextension_theme.js"] tags = "webextensions" +skip-if = ["os == 'android'"] diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_required_useractivation.js b/toolkit/mozapps/extensions/test/xpinstall/browser_required_useractivation.js index 6c8894699d..b74cff859f 100644 --- a/toolkit/mozapps/extensions/test/xpinstall/browser_required_useractivation.js +++ b/toolkit/mozapps/extensions/test/xpinstall/browser_required_useractivation.js @@ -17,6 +17,7 @@ async function runTestCase(spawnArgs, spawnFn, { expectInstall, clickLink }) { set: [ // Make use the user activation requirements is enabled while running this test. ["xpinstall.userActivation.required", true], + ["dom.security.https_first", false], ], }); await BrowserTestUtils.withNewTab(TESTROOT, async browser => { diff --git a/toolkit/mozapps/installer/package-name.mk b/toolkit/mozapps/installer/package-name.mk index 36239fe79f..c7be519725 100644 --- a/toolkit/mozapps/installer/package-name.mk +++ b/toolkit/mozapps/installer/package-name.mk @@ -110,6 +110,9 @@ GTEST_PACKAGE = $(PKG_BASENAME).gtest.tests.tar.gz # `.xpt` artifacts: for use in artifact builds. XPT_ARTIFACTS_ARCHIVE_BASENAME = $(PKG_BASENAME).xpt_artifacts +ifeq (Darwin, $(OS_ARCH)) +UPDATE_FRAMEWORK_ARTIFACTS_ARCHIVE_BASENAME = $(PKG_BASENAME).update_framework_artifacts +endif # Darwin ifneq (,$(wildcard $(DIST)/bin/application.ini)) BUILDID = $(shell $(PYTHON3) $(MOZILLA_DIR)/config/printconfigsetting.py $(DIST)/bin/application.ini App BuildID) diff --git a/toolkit/mozapps/installer/packager.mk b/toolkit/mozapps/installer/packager.mk index 6d36d51375..f0572af6d8 100644 --- a/toolkit/mozapps/installer/packager.mk +++ b/toolkit/mozapps/installer/packager.mk @@ -109,9 +109,17 @@ endif # Darwin ifndef MOZ_ARTIFACT_BUILDS @echo 'Generating XPT artifacts archive ($(XPT_ARTIFACTS_ARCHIVE_BASENAME).zip)' $(call py_action,zip $(XPT_ARTIFACTS_ARCHIVE_BASENAME).zip,-C $(topobjdir)/config/makefiles/xpidl '$(ABS_DIST)/$(PKG_PATH)$(XPT_ARTIFACTS_ARCHIVE_BASENAME).zip' '*.xpt') +ifeq (Darwin, $(OS_ARCH)) + @echo 'Generating update-related macOS framework artifacts archive ($(UPDATE_FRAMEWORK_ARTIFACTS_ARCHIVE_BASENAME).zip)' + $(call py_action,zip $(UPDATE_FRAMEWORK_ARTIFACTS_ARCHIVE_BASENAME).zip,-C '$(ABS_DIST)/update_framework_artifacts' '$(ABS_DIST)/$(PKG_PATH)$(UPDATE_FRAMEWORK_ARTIFACTS_ARCHIVE_BASENAME).zip' '*.framework') +endif # Darwin else @echo 'Packaging existing XPT artifacts from artifact build into archive ($(XPT_ARTIFACTS_ARCHIVE_BASENAME).zip)' $(call py_action,zip $(XPT_ARTIFACTS_ARCHIVE_BASENAME).zip,-C $(ABS_DIST)/xpt_artifacts '$(ABS_DIST)/$(PKG_PATH)$(XPT_ARTIFACTS_ARCHIVE_BASENAME).zip' '*.xpt') +ifeq (Darwin, $(OS_ARCH)) + @echo 'Packaging existing update-related macOS framework artifacts from artifact build into archive ($(UPDATE_FRAMEWORK_ARTIFACTS_ARCHIVE_BASENAME).zip)' + $(call py_action,zip $(UPDATE_FRAMEWORK_ARTIFACTS_ARCHIVE_BASENAME).zip,-C $(ABS_DIST)/update_framework_artifacts '$(ABS_DIST)/$(PKG_PATH)$(UPDATE_FRAMEWORK_ARTIFACTS_ARCHIVE_BASENAME).zip' '*.framework') +endif # Darwin endif # MOZ_ARTIFACT_BUILDS prepare-package: stage-package @@ -119,6 +127,7 @@ prepare-package: stage-package make-package-internal: prepare-package make-sourcestamp-file @echo 'Compressing...' $(call MAKE_PACKAGE,$(DIST)) + echo $(PACKAGE) > $(ABS_DIST)/package_name.txt make-package: FORCE $(MAKE) make-package-internal diff --git a/toolkit/mozapps/installer/upload-files.mk b/toolkit/mozapps/installer/upload-files.mk index 6e1283e238..817f782772 100644 --- a/toolkit/mozapps/installer/upload-files.mk +++ b/toolkit/mozapps/installer/upload-files.mk @@ -394,6 +394,10 @@ endif # Upload `.xpt` artifacts for use in artifact builds. UPLOAD_FILES += $(call QUOTED_WILDCARD,$(DIST)/$(PKG_PATH)$(XPT_ARTIFACTS_ARCHIVE_BASENAME).zip) +# Upload update-related macOS framework artifacts for use in artifact builds. +ifeq ($(OS_ARCH),Darwin) +UPLOAD_FILES += $(call QUOTED_WILDCARD,$(DIST)/$(PKG_PATH)$(UPDATE_FRAMEWORK_ARTIFACTS_ARCHIVE_BASENAME).zip) +endif # Darwin ifndef MOZ_PKG_SRCDIR MOZ_PKG_SRCDIR = $(topsrcdir) diff --git a/toolkit/mozapps/macos-frameworks/ChannelPrefs-localbuild/Makefile.in b/toolkit/mozapps/macos-frameworks/ChannelPrefs-localbuild/Makefile.in new file mode 100644 index 0000000000..5dcb0821cb --- /dev/null +++ b/toolkit/mozapps/macos-frameworks/ChannelPrefs-localbuild/Makefile.in @@ -0,0 +1,25 @@ +# vim:set ts=8 sw=8 sts=8 noet: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +include $(topsrcdir)/config/rules.mk + +# In a compile build, the moz.build stanzas produce a binary named +# `ChannelPrefs-localbuild`. We need to produce +# `dist/update_framework_artifacts/ChannelPrefs-localbuild.framework/ChannelPrefs` +# for consumption by artifact builds. +# +# In an artifact build, we already have upstream artifacts in +# `dist/update_framework_artifacts/ChannelPrefs-localbuild.framework`. + +ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT)) +libs:: +ifneq (,$(COMPILE_ENVIRONMENT)) + rm -rf $(FINAL_TARGET)/ChannelPrefs-localbuild.framework + $(NSINSTALL) -D $(FINAL_TARGET)/ChannelPrefs-localbuild.framework + cp $(FINAL_TARGET)/ChannelPrefs-localbuild ChannelPrefs + $(NSINSTALL) ChannelPrefs $(FINAL_TARGET)/ChannelPrefs-localbuild.framework +endif # COMPILE_ENVIRONMENT + $(NSINSTALL) $(srcdir)/../ChannelPrefs/Info.plist $(FINAL_TARGET)/ChannelPrefs-localbuild.framework/Resources +endif # MOZ_WIDGET_TOOLKIT diff --git a/toolkit/mozapps/macos-frameworks/ChannelPrefs-localbuild/moz.build b/toolkit/mozapps/macos-frameworks/ChannelPrefs-localbuild/moz.build new file mode 100644 index 0000000000..ff91833ad3 --- /dev/null +++ b/toolkit/mozapps/macos-frameworks/ChannelPrefs-localbuild/moz.build @@ -0,0 +1,21 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "Application Update") + +Framework("ChannelPrefs-localbuild") +FINAL_TARGET = "dist/update_framework_artifacts" + +DEFINES["MOZ_UPDATE_CHANNEL_OVERRIDE"] = "default" + +UNIFIED_SOURCES += [ + "../ChannelPrefs/ChannelPrefs.mm", +] + +OS_LIBS += [ + "-framework Foundation", +] diff --git a/toolkit/mozapps/macos-frameworks/ChannelPrefs/ChannelPrefs.mm b/toolkit/mozapps/macos-frameworks/ChannelPrefs/ChannelPrefs.mm index f437bb857d..0428bbe8ab 100644 --- a/toolkit/mozapps/macos-frameworks/ChannelPrefs/ChannelPrefs.mm +++ b/toolkit/mozapps/macos-frameworks/ChannelPrefs/ChannelPrefs.mm @@ -6,7 +6,13 @@ #include "mozilla/HelperMacros.h" +#ifdef MOZ_UPDATE_CHANNEL_OVERRIDE +# define CHANNEL MOZ_UPDATE_CHANNEL_OVERRIDE +#else +# define CHANNEL MOZ_UPDATE_CHANNEL +#endif + NSString* ChannelPrefsGetChannel() { - return [NSString stringWithCString:MOZ_STRINGIFY(MOZ_UPDATE_CHANNEL) + return [NSString stringWithCString:MOZ_STRINGIFY(CHANNEL) encoding:NSUTF8StringEncoding]; } diff --git a/toolkit/mozapps/macos-frameworks/ChannelPrefs/Makefile.in b/toolkit/mozapps/macos-frameworks/ChannelPrefs/Makefile.in index 43552a771e..7f3bfd1cc5 100644 --- a/toolkit/mozapps/macos-frameworks/ChannelPrefs/Makefile.in +++ b/toolkit/mozapps/macos-frameworks/ChannelPrefs/Makefile.in @@ -5,10 +5,23 @@ include $(topsrcdir)/config/rules.mk +# In a compile build, the moz.build stanzas produce a binary named +# `ChannelPrefs`. We need to produce +# `dist/bin/ChannelPrefs.framework/ChannelPrefs` for consumption by the +# build. +# +# In an artifact build, we copy upstream artifacts from +# `dist/update_framework_artifacts/ChannelPrefs-localbuild.framework` + ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT)) libs:: - rm -rf $(DIST)/bin/ChannelPrefs.framework - - $(NSINSTALL) $(DIST)/bin/ChannelPrefs $(DIST)/bin/ChannelPrefs.framework - $(NSINSTALL) $(srcdir)/Info.plist $(DIST)/bin/ChannelPrefs.framework/Resources -endif + rm -rf $(FINAL_TARGET)/ChannelPrefs.framework + $(NSINSTALL) -D $(FINAL_TARGET)/ChannelPrefs.framework +ifneq (,$(COMPILE_ENVIRONMENT)) + $(NSINSTALL) $(FINAL_TARGET)/ChannelPrefs $(FINAL_TARGET)/ChannelPrefs.framework +endif # COMPILE_ENVIRONMENT +ifneq (,$(MOZ_ARTIFACT_BUILDS)) + $(NSINSTALL) $(DIST)/update_framework_artifacts/ChannelPrefs-localbuild.framework/ChannelPrefs $(FINAL_TARGET)/ChannelPrefs.framework +endif # MOZ_ARTIFACT_BUILDS + $(NSINSTALL) $(srcdir)/Info.plist $(FINAL_TARGET)/ChannelPrefs.framework/Resources +endif # MOZ_WIDGET_TOOLKIT diff --git a/toolkit/mozapps/macos-frameworks/moz.build b/toolkit/mozapps/macos-frameworks/moz.build index b1a315500c..f7ad2d314f 100644 --- a/toolkit/mozapps/macos-frameworks/moz.build +++ b/toolkit/mozapps/macos-frameworks/moz.build @@ -11,6 +11,7 @@ FINAL_LIBRARY = "xul" DIRS += [ "ChannelPrefs", + "ChannelPrefs-localbuild", ] EXPORTS += [ diff --git a/toolkit/mozapps/update/BackgroundUpdate.sys.mjs b/toolkit/mozapps/update/BackgroundUpdate.sys.mjs index 3022c9ffde..1435c0fa2b 100644 --- a/toolkit/mozapps/update/BackgroundUpdate.sys.mjs +++ b/toolkit/mozapps/update/BackgroundUpdate.sys.mjs @@ -69,18 +69,6 @@ function taskNameVersion(taskVersion) { return 2; } -async function deleteTasksInRange(installedVersion, currentVersion) { - for ( - let taskVersion = installedVersion; - taskVersion <= currentVersion; - taskVersion++ - ) { - await lazy.TaskScheduler.deleteTask(this.taskId, { - nameVersion: taskNameVersion(taskVersion), - }); - } -} - export var BackgroundUpdate = { QueryInterface: ChromeUtils.generateQI([ "nsINamed", @@ -102,6 +90,24 @@ export var BackgroundUpdate = { return taskId; }, + async deleteTasksInRange(installedVersion, currentVersion) { + for ( + let taskVersion = installedVersion; + taskVersion <= currentVersion; + taskVersion++ + ) { + try { + await lazy.TaskScheduler.deleteTask(this.taskId, { + nameVersion: taskNameVersion(taskVersion), + }); + } catch (e) { + lazy.log.error( + `deleteTasksInRange: Error deleting task ${taskVersion}: ${e}` + ); + } + } + }, + /** * Whether this installation has an App and a GRE omnijar. * @@ -583,7 +589,10 @@ export var BackgroundUpdate = { TASK_INSTALLED_VERSION_PREF, TASK_DEF_CURRENT_VERSION ); - await deleteTasksInRange(installedVersion, TASK_DEF_CURRENT_VERSION); + await this.deleteTasksInRange( + installedVersion, + TASK_DEF_CURRENT_VERSION + ); lazy.log.debug( `${SLUG}: witnessed falling (enabled -> disabled) edge; deleted task ${this.taskId}.` ); @@ -616,7 +625,10 @@ export var BackgroundUpdate = { TASK_INSTALLED_VERSION_PREF, TASK_DEF_CURRENT_VERSION ); - await deleteTasksInRange(installedVersion, TASK_DEF_CURRENT_VERSION); + await this.deleteTasksInRange( + installedVersion, + TASK_DEF_CURRENT_VERSION + ); } catch (e) { lazy.log.error(`${SLUG}: Error removing old task: ${e}`); } diff --git a/toolkit/mozapps/update/UpdateService.sys.mjs b/toolkit/mozapps/update/UpdateService.sys.mjs index 34bf1b9e19..21ecb3db48 100644 --- a/toolkit/mozapps/update/UpdateService.sys.mjs +++ b/toolkit/mozapps/update/UpdateService.sys.mjs @@ -2082,69 +2082,10 @@ function pollForStagingEnd() { lazy.setTimeout(pollingFn, pollingIntervalMs); } -/** - * Update Patch - * @param patch - * A <patch> element to initialize this object with - * @throws if patch has a size of 0 - * @constructor - */ -function UpdatePatch(patch) { - this._properties = {}; - this.errorCode = 0; - this.finalURL = null; - this.state = STATE_NONE; - - for (let i = 0; i < patch.attributes.length; ++i) { - var attr = patch.attributes.item(i); - // If an undefined value is saved to the xml file it will be a string when - // it is read from the xml file. - if (attr.value == "undefined") { - continue; - } - switch (attr.name) { - case "xmlns": - // Don't save the XML namespace. - break; - case "selected": - this.selected = attr.value == "true"; - break; - case "size": - if (0 == parseInt(attr.value)) { - LOG("UpdatePatch:init - 0-sized patch!"); - throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE); - } - this[attr.name] = attr.value; - break; - case "errorCode": - if (attr.value) { - let val = parseInt(attr.value); - // This will evaluate to false if the value is 0 but that's ok since - // this.errorCode is set to the default of 0 above. - if (val) { - this.errorCode = val; - } - } - break; - case "finalURL": - case "state": - case "type": - case "URL": - this[attr.name] = attr.value; - break; - default: - if (!this._attrNames.includes(attr.name)) { - // Set nsIPropertyBag properties that were read from the xml file. - this.setProperty(attr.name, attr.value); - } - break; - } - } -} -UpdatePatch.prototype = { +class UpdatePatch { // nsIUpdatePatch attribute names used to prevent nsIWritablePropertyBag from // over writing nsIUpdatePatch attributes. - _attrNames: [ + _attrNames = [ "errorCode", "finalURL", "selected", @@ -2152,12 +2093,71 @@ UpdatePatch.prototype = { "state", "type", "URL", - ], + ]; + + /** + * @param patch + * A <patch> element to initialize this object with + * @throws if patch has a size of 0 + * @constructor + */ + constructor(patch) { + this._properties = {}; + this.errorCode = 0; + this.finalURL = null; + this.state = STATE_NONE; + + for (let i = 0; i < patch.attributes.length; ++i) { + var attr = patch.attributes.item(i); + // If an undefined value is saved to the xml file it will be a string when + // it is read from the xml file. + if (attr.value == "undefined") { + continue; + } + switch (attr.name) { + case "xmlns": + // Don't save the XML namespace. + break; + case "selected": + this.selected = attr.value == "true"; + break; + case "size": + if (0 == parseInt(attr.value)) { + LOG("UpdatePatch:init - 0-sized patch!"); + throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE); + } + this[attr.name] = attr.value; + break; + case "errorCode": + if (attr.value) { + let val = parseInt(attr.value); + // This will evaluate to false if the value is 0 but that's ok since + // this.errorCode is set to the default of 0 above. + if (val) { + this.errorCode = val; + } + } + break; + case "finalURL": + case "state": + case "type": + case "URL": + this[attr.name] = attr.value; + break; + default: + if (!this._attrNames.includes(attr.name)) { + // Set nsIPropertyBag properties that were read from the xml file. + this.setProperty(attr.name, attr.value); + } + break; + } + } + } /** * See nsIUpdateService.idl */ - serialize: function UpdatePatch_serialize(updates) { + serialize(updates) { var patch = updates.createElementNS(URI_UPDATE_NS, "patch"); patch.setAttribute("size", this.size); patch.setAttribute("type", this.type); @@ -2185,12 +2185,12 @@ UpdatePatch.prototype = { } } return patch; - }, + } /** * See nsIWritablePropertyBag.idl */ - setProperty: function UpdatePatch_setProperty(name, value) { + setProperty(name, value) { if (this._attrNames.includes(name)) { throw Components.Exception( "Illegal value '" + @@ -2201,12 +2201,12 @@ UpdatePatch.prototype = { ); } this._properties[name] = { data: value, present: true }; - }, + } /** * See nsIWritablePropertyBag.idl */ - deleteProperty: function UpdatePatch_deleteProperty(name) { + deleteProperty(name) { if (this._attrNames.includes(name)) { throw Components.Exception( "Illegal value '" + @@ -2221,7 +2221,7 @@ UpdatePatch.prototype = { } else { throw Components.Exception("", Cr.NS_ERROR_FAILURE); } - }, + } /** * See nsIPropertyBag.idl @@ -2231,7 +2231,7 @@ UpdatePatch.prototype = { */ get enumerator() { return this.enumerate(); - }, + } *enumerate() { // An nsISupportsInterfacePointer is used so creating an array using @@ -2239,7 +2239,7 @@ UpdatePatch.prototype = { let ip = Cc["@mozilla.org/supports-interface-pointer;1"].createInstance( Ci.nsISupportsInterfacePointer ); - let qi = ChromeUtils.generateQI(["nsIProperty"]); + let qi = ChromeUtils.generateQI([Ci.nsIProperty]); for (let [name, value] of Object.entries(this._properties)) { if (value.present && !this._attrNames.includes(name)) { // The nsIPropertyBag enumerator returns a nsISimpleEnumerator whose @@ -2251,7 +2251,7 @@ UpdatePatch.prototype = { yield ip.data.QueryInterface(Ci.nsIProperty); } } - }, + } /** * See nsIPropertyBag.idl @@ -2259,7 +2259,7 @@ UpdatePatch.prototype = { * Note: returns null instead of throwing when the property doesn't exist to * simplify code and to silence warnings in debug builds. */ - getProperty: function UpdatePatch_getProperty(name) { + getProperty(name) { if (this._attrNames.includes(name)) { throw Components.Exception( "Illegal value '" + @@ -2273,161 +2273,19 @@ UpdatePatch.prototype = { return this._properties[name].data; } return null; - }, - - QueryInterface: ChromeUtils.generateQI([ - "nsIUpdatePatch", - "nsIPropertyBag", - "nsIWritablePropertyBag", - ]), -}; - -/** - * Update - * Implements nsIUpdate - * @param update - * An <update> element to initialize this object with - * @throws if the update contains no patches - * @constructor - */ -function Update(update) { - this._patches = []; - this._properties = {}; - this.isCompleteUpdate = false; - this.channel = "default"; - this.promptWaitTime = Services.prefs.getIntPref( - PREF_APP_UPDATE_PROMPTWAITTIME, - 43200 - ); - this.unsupported = false; - - // Null <update>, assume this is a message container and do no - // further initialization - if (!update) { - return; - } - - for (let i = 0; i < update.childNodes.length; ++i) { - let patchElement = update.childNodes.item(i); - if ( - patchElement.nodeType != patchElement.ELEMENT_NODE || - patchElement.localName != "patch" - ) { - continue; - } - - let patch; - try { - patch = new UpdatePatch(patchElement); - } catch (e) { - continue; - } - this._patches.push(patch); - } - - if (!this._patches.length && !update.hasAttribute("unsupported")) { - throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE); - } - - // Set the installDate value with the current time. If the update has an - // installDate attribute this will be replaced with that value if it doesn't - // equal 0. - this.installDate = new Date().getTime(); - this.patchCount = this._patches.length; - - for (let i = 0; i < update.attributes.length; ++i) { - let attr = update.attributes.item(i); - if (attr.name == "xmlns" || attr.value == "undefined") { - // Don't save the XML namespace or undefined values. - // If an undefined value is saved to the xml file it will be a string when - // it is read from the xml file. - continue; - } else if (attr.name == "detailsURL") { - this.detailsURL = attr.value; - } else if (attr.name == "installDate" && attr.value) { - let val = parseInt(attr.value); - if (val) { - this.installDate = val; - } - } else if (attr.name == "errorCode" && attr.value) { - let val = parseInt(attr.value); - if (val) { - // Set the value of |_errorCode| instead of |errorCode| since - // selectedPatch won't be available at this point and normally the - // nsIUpdatePatch will provide the errorCode. - this._errorCode = val; - } - } else if (attr.name == "isCompleteUpdate") { - this.isCompleteUpdate = attr.value == "true"; - } else if (attr.name == "promptWaitTime") { - if (!isNaN(attr.value)) { - this.promptWaitTime = parseInt(attr.value); - } - } else if (attr.name == "unsupported") { - this.unsupported = attr.value == "true"; - } else { - switch (attr.name) { - case "appVersion": - case "buildID": - case "channel": - case "displayVersion": - case "elevationFailure": - case "name": - case "previousAppVersion": - case "serviceURL": - case "statusText": - case "type": - this[attr.name] = attr.value; - break; - default: - if (!this._attrNames.includes(attr.name)) { - // Set nsIPropertyBag properties that were read from the xml file. - this.setProperty(attr.name, attr.value); - } - break; - } - } } - if (!this.previousAppVersion) { - this.previousAppVersion = Services.appinfo.version; - } - - if (!this.elevationFailure) { - this.elevationFailure = false; - } - - if (!this.detailsURL) { - try { - // Try using a default details URL supplied by the distribution - // if the update XML does not supply one. - this.detailsURL = Services.urlFormatter.formatURLPref( - PREF_APP_UPDATE_URL_DETAILS - ); - } catch (e) { - this.detailsURL = ""; - } - } - - if (!this.displayVersion) { - this.displayVersion = this.appVersion; - } - - if (!this.name) { - // When the update doesn't provide a name fallback to using - // "<App Name> <Update App Version>" - let brandBundle = Services.strings.createBundle(URI_BRAND_PROPERTIES); - let appName = brandBundle.GetStringFromName("brandShortName"); - this.name = lazy.gUpdateBundle.formatStringFromName("updateName", [ - appName, - this.displayVersion, - ]); - } + QueryInterface = ChromeUtils.generateQI([ + Ci.nsIUpdatePatch, + Ci.nsIPropertyBag, + Ci.nsIWritablePropertyBag, + ]); } -Update.prototype = { + +class Update { // nsIUpdate attribute names used to prevent nsIWritablePropertyBag from over // writing nsIUpdate attributes. - _attrNames: [ + _attrNames = [ "appVersion", "buildID", "channel", @@ -2445,14 +2303,156 @@ Update.prototype = { "statusText", "type", "unsupported", - ], + ]; + + /** + * Implements nsIUpdate + * @param update + * An <update> element to initialize this object with + * @throws if the update contains no patches + * @constructor + */ + constructor(update) { + this._patches = []; + this._properties = {}; + this.isCompleteUpdate = false; + this.channel = "default"; + this.promptWaitTime = Services.prefs.getIntPref( + PREF_APP_UPDATE_PROMPTWAITTIME, + 43200 + ); + this.unsupported = false; + + // Null <update>, assume this is a message container and do no + // further initialization + if (!update) { + return; + } + + for (let i = 0; i < update.childNodes.length; ++i) { + let patchElement = update.childNodes.item(i); + if ( + patchElement.nodeType != patchElement.ELEMENT_NODE || + patchElement.localName != "patch" + ) { + continue; + } + + let patch; + try { + patch = new UpdatePatch(patchElement); + } catch (e) { + continue; + } + this._patches.push(patch); + } + + if (!this._patches.length && !update.hasAttribute("unsupported")) { + throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE); + } + + // Set the installDate value with the current time. If the update has an + // installDate attribute this will be replaced with that value if it doesn't + // equal 0. + this.installDate = new Date().getTime(); + this.patchCount = this._patches.length; + + for (let i = 0; i < update.attributes.length; ++i) { + let attr = update.attributes.item(i); + if (attr.name == "xmlns" || attr.value == "undefined") { + // Don't save the XML namespace or undefined values. + // If an undefined value is saved to the xml file it will be a string when + // it is read from the xml file. + continue; + } else if (attr.name == "detailsURL") { + this.detailsURL = attr.value; + } else if (attr.name == "installDate" && attr.value) { + let val = parseInt(attr.value); + if (val) { + this.installDate = val; + } + } else if (attr.name == "errorCode" && attr.value) { + let val = parseInt(attr.value); + if (val) { + // Set the value of |_errorCode| instead of |errorCode| since + // selectedPatch won't be available at this point and normally the + // nsIUpdatePatch will provide the errorCode. + this._errorCode = val; + } + } else if (attr.name == "isCompleteUpdate") { + this.isCompleteUpdate = attr.value == "true"; + } else if (attr.name == "promptWaitTime") { + if (!isNaN(attr.value)) { + this.promptWaitTime = parseInt(attr.value); + } + } else if (attr.name == "unsupported") { + this.unsupported = attr.value == "true"; + } else { + switch (attr.name) { + case "appVersion": + case "buildID": + case "channel": + case "displayVersion": + case "elevationFailure": + case "name": + case "previousAppVersion": + case "serviceURL": + case "statusText": + case "type": + this[attr.name] = attr.value; + break; + default: + if (!this._attrNames.includes(attr.name)) { + // Set nsIPropertyBag properties that were read from the xml file. + this.setProperty(attr.name, attr.value); + } + break; + } + } + } + + if (!this.previousAppVersion) { + this.previousAppVersion = Services.appinfo.version; + } + + if (!this.elevationFailure) { + this.elevationFailure = false; + } + + if (!this.detailsURL) { + try { + // Try using a default details URL supplied by the distribution + // if the update XML does not supply one. + this.detailsURL = Services.urlFormatter.formatURLPref( + PREF_APP_UPDATE_URL_DETAILS + ); + } catch (e) { + this.detailsURL = ""; + } + } + + if (!this.displayVersion) { + this.displayVersion = this.appVersion; + } + + if (!this.name) { + // When the update doesn't provide a name fallback to using + // "<App Name> <Update App Version>" + let brandBundle = Services.strings.createBundle(URI_BRAND_PROPERTIES); + let appName = brandBundle.GetStringFromName("brandShortName"); + this.name = lazy.gUpdateBundle.formatStringFromName("updateName", [ + appName, + this.displayVersion, + ]); + } + } /** * See nsIUpdateService.idl */ - getPatchAt: function Update_getPatchAt(index) { + getPatchAt(index) { return this._patches[index]; - }, + } /** * See nsIUpdateService.idl @@ -2462,19 +2462,19 @@ Update.prototype = { * active updates from the update manager for some reason but still have * the update.status file to work with. */ - _state: "", + _state = ""; get state() { if (this.selectedPatch) { return this.selectedPatch.state; } return this._state; - }, + } set state(state) { if (this.selectedPatch) { this.selectedPatch.state = state; } this._state = state; - }, + } /** * See nsIUpdateService.idl @@ -2484,19 +2484,19 @@ Update.prototype = { * active updates from the update manager for some reason but still have * the update.status file to work with. */ - _errorCode: 0, + _errorCode = 0; get errorCode() { if (this.selectedPatch) { return this.selectedPatch.errorCode; } return this._errorCode; - }, + } set errorCode(errorCode) { if (this.selectedPatch) { this.selectedPatch.errorCode = errorCode; } this._errorCode = errorCode; - }, + } /** * See nsIUpdateService.idl @@ -2508,12 +2508,12 @@ Update.prototype = { } } return null; - }, + } /** * See nsIUpdateService.idl */ - serialize: function Update_serialize(updates) { + serialize(updates) { // If appVersion isn't defined just return null. This happens when cleaning // up invalid updates (e.g. incorrect channel). if (!this.appVersion) { @@ -2555,12 +2555,12 @@ Update.prototype = { updates.documentElement.appendChild(update); return update; - }, + } /** * See nsIWritablePropertyBag.idl */ - setProperty: function Update_setProperty(name, value) { + setProperty(name, value) { if (this._attrNames.includes(name)) { throw Components.Exception( "Illegal value '" + @@ -2571,12 +2571,12 @@ Update.prototype = { ); } this._properties[name] = { data: value, present: true }; - }, + } /** * See nsIWritablePropertyBag.idl */ - deleteProperty: function Update_deleteProperty(name) { + deleteProperty(name) { if (this._attrNames.includes(name)) { throw Components.Exception( "Illegal value '" + @@ -2591,7 +2591,7 @@ Update.prototype = { } else { throw Components.Exception("", Cr.NS_ERROR_FAILURE); } - }, + } /** * See nsIPropertyBag.idl @@ -2601,7 +2601,7 @@ Update.prototype = { */ get enumerator() { return this.enumerate(); - }, + } *enumerate() { // An nsISupportsInterfacePointer is used so creating an array using @@ -2609,7 +2609,7 @@ Update.prototype = { let ip = Cc["@mozilla.org/supports-interface-pointer;1"].createInstance( Ci.nsISupportsInterfacePointer ); - let qi = ChromeUtils.generateQI(["nsIProperty"]); + let qi = ChromeUtils.generateQI([Ci.nsIProperty]); for (let [name, value] of Object.entries(this._properties)) { if (value.present && !this._attrNames.includes(name)) { // The nsIPropertyBag enumerator returns a nsISimpleEnumerator whose @@ -2621,14 +2621,14 @@ Update.prototype = { yield ip.data.QueryInterface(Ci.nsIProperty); } } - }, + } /** * See nsIPropertyBag.idl * Note: returns null instead of throwing when the property doesn't exist to * simplify code and to silence warnings in debug builds. */ - getProperty: function Update_getProperty(name) { + getProperty(name) { if (this._attrNames.includes(name)) { throw Components.Exception( "Illegal value '" + @@ -2642,60 +2642,60 @@ Update.prototype = { return this._properties[name].data; } return null; - }, - - QueryInterface: ChromeUtils.generateQI([ - "nsIUpdate", - "nsIPropertyBag", - "nsIWritablePropertyBag", - ]), -}; - -/** - * UpdateService - * A Service for managing the discovery and installation of software updates. - * @constructor - */ -export function UpdateService() { - LOG("Creating UpdateService"); - // The observor notification to shut down the service must be before - // profile-before-change since nsIUpdateManager uses profile-before-change - // to shutdown and write the update xml files. - Services.obs.addObserver(this, "quit-application"); - lazy.UpdateLog.addConfigChangeListener(() => { - this._logStatus(); - }); + } - this._logStatus(); + QueryInterface = ChromeUtils.generateQI([ + Ci.nsIUpdate, + Ci.nsIPropertyBag, + Ci.nsIWritablePropertyBag, + ]); } -UpdateService.prototype = { +export class UpdateService { /** * The downloader we are using to download updates. There is only ever one of * these. */ - _downloader: null, + _downloader = null; /** * Whether or not the service registered the "online" observer. */ - _registeredOnlineObserver: false, + _registeredOnlineObserver = false; /** * The current number of consecutive socket errors */ - _consecutiveSocketErrors: 0, + _consecutiveSocketErrors = 0; /** * A timer used to retry socket errors */ - _retryTimer: null, + _retryTimer = null; /** * Whether or not a background update check was initiated by the * application update timer notification. */ - _isNotify: true, + _isNotify = true; + + /** + * UpdateService + * A Service for managing the discovery and installation of software updates. + * @constructor + */ + constructor() { + LOG("Creating UpdateService"); + // The observor notification to shut down the service must be before + // profile-before-change since nsIUpdateManager uses profile-before-change + // to shutdown and write the update xml files. + Services.obs.addObserver(this, "quit-application"); + lazy.UpdateLog.addConfigChangeListener(() => { + this._logStatus(); + }); + + this._logStatus(); + } /** * Handle Observer Service notifications @@ -2706,7 +2706,7 @@ UpdateService.prototype = { * @param data * Additional data */ - observe: async function AUS_observe(subject, topic, data) { + async observe(subject, topic, data) { switch (topic) { case "post-update-processing": // This pref was not cleared out of profiles after it stopped being used @@ -2790,7 +2790,7 @@ UpdateService.prototype = { } break; } - }, + } /** * The following needs to happen during the post-update-processing @@ -2808,7 +2808,7 @@ UpdateService.prototype = { * notify the user of install success. */ /* eslint-disable-next-line complexity */ - _postUpdateProcessing: async function AUS__postUpdateProcessing() { + async _postUpdateProcessing() { if (this.disabled) { // This function is a point when we can potentially enter the update // system, even with update disabled. Make sure that we do not continue @@ -3178,13 +3178,13 @@ UpdateService.prototype = { // Something went wrong with the patch application process. await handleFallbackToCompleteUpdate(); } - }, + } /** * Register an observer when the network comes online, so we can short-circuit * the app.update.interval when there isn't connectivity */ - _registerOnlineObserver: function AUS__registerOnlineObserver() { + _registerOnlineObserver() { if (this._registeredOnlineObserver) { LOG( "UpdateService:_registerOnlineObserver - observer already registered" @@ -3199,12 +3199,12 @@ UpdateService.prototype = { Services.obs.addObserver(this, "network:offline-status-changed"); this._registeredOnlineObserver = true; - }, + } /** * Called from the network:offline-status-changed observer. */ - _offlineStatusChanged: async function AUS__offlineStatusChanged(status) { + async _offlineStatusChanged(status) { if (status !== "online") { return; } @@ -3219,12 +3219,12 @@ UpdateService.prototype = { // the background checker is contained in notify await this._attemptResume(); - }, + } /** * See nsIUpdateService.idl */ - onCheckComplete: async function AUS_onCheckComplete(result) { + async onCheckComplete(result) { if (result.succeeded) { await this._selectAndInstallUpdate(result.updates); return; @@ -3307,12 +3307,12 @@ UpdateService.prototype = { ); AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_GENERAL_ERROR_SILENT); } - }, + } /** * Called when a connection should be resumed */ - _attemptResume: async function AUS_attemptResume() { + async _attemptResume() { LOG("UpdateService:_attemptResume"); // If a download is in progress and we aren't already downloading it, then // resume it. @@ -3348,23 +3348,23 @@ UpdateService.prototype = { let check = lazy.CheckSvc.checkForUpdates(lazy.CheckSvc.BACKGROUND_CHECK); await this.onCheckComplete(await check.result); })(); - }, + } /** * Notified when a timer fires * @param _timer * The timer that fired */ - notify: function AUS_notify(_timer) { + notify(_timer) { this._checkForBackgroundUpdates(true); - }, + } /** * See nsIUpdateService.idl */ - checkForBackgroundUpdates: function AUS_checkForBackgroundUpdates() { + checkForBackgroundUpdates() { return this._checkForBackgroundUpdates(false); - }, + } // The suffix used for background update check telemetry histogram ID's. get _pingSuffix() { @@ -3376,7 +3376,7 @@ UpdateService.prototype = { return AUSTLMY.SUBSEQUENT; } return this._isNotify ? AUSTLMY.NOTIFY : AUSTLMY.EXTERNAL; - }, + } /** * Checks for updates in the background. @@ -3384,9 +3384,7 @@ UpdateService.prototype = { * Whether or not a background update check was initiated by the * application update timer notification. */ - _checkForBackgroundUpdates: function AUS__checkForBackgroundUpdates( - isNotify - ) { + _checkForBackgroundUpdates(isNotify) { if (!this.disabled && AppConstants.NIGHTLY_BUILD) { // Scalar ID: update.suppress_prompts AUSTLMY.pingSuppressPrompts(); @@ -3584,7 +3582,7 @@ UpdateService.prototype = { })(); return true; - }, + } /** * Determine the update from the specified updates that should be offered. @@ -3594,7 +3592,7 @@ UpdateService.prototype = { * An array of available nsIUpdate items * @return The nsIUpdate to offer. */ - selectUpdate: function AUS_selectUpdate(updates) { + selectUpdate(updates) { if (!updates.length) { AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_UPDATE_FOUND); return null; @@ -3753,7 +3751,7 @@ UpdateService.prototype = { } return update; - }, + } /** * Determine which of the specified updates should be installed and begin the @@ -3761,7 +3759,7 @@ UpdateService.prototype = { * @param updates * An array of available updates */ - _selectAndInstallUpdate: async function AUS__selectAndInstallUpdate(updates) { + async _selectAndInstallUpdate(updates) { // Return early if there's an active update. The user is already aware and // is downloading or performed some user action to prevent notification. if (lazy.UM.downloadingUpdate) { @@ -3842,14 +3840,14 @@ UpdateService.prototype = { cleanupDownloadingUpdate(); } AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_DOWNLOAD_UPDATE); - }, + } /** * See nsIUpdateService.idl */ get isAppBaseDirWritable() { return isAppBaseDirWritable(); - }, + } get disabledForTesting() { return ( @@ -3858,7 +3856,7 @@ UpdateService.prototype = { lazy.RemoteAgent.running) && Services.prefs.getBoolPref(PREF_APP_UPDATE_DISABLEDFORTESTING, false) ); - }, + } /** * See nsIUpdateService.idl @@ -3869,7 +3867,7 @@ UpdateService.prototype = { this.disabledForTesting || Services.sysinfo.getProperty("isPackagedApp") ); - }, + } /** * See nsIUpdateService.idl @@ -3878,7 +3876,7 @@ UpdateService.prototype = { return ( Services.policies && !Services.policies.isAllowed("autoAppUpdateChecking") ); - }, + } /** * See nsIUpdateService.idl @@ -3912,7 +3910,7 @@ UpdateService.prototype = { LOG("UpdateService.canUsuallyCheckForUpdates - able to check for updates"); return true; - }, + } /** * See nsIUpdateService.idl @@ -3941,21 +3939,21 @@ UpdateService.prototype = { LOG("UpdateService.canCheckForUpdates - able to check for updates"); return true; - }, + } /** * See nsIUpdateService.idl */ get elevationRequired() { return getElevationRequired(); - }, + } /** * See nsIUpdateService.idl */ get canUsuallyApplyUpdates() { return getCanApplyUpdates(); - }, + } /** * See nsIUpdateService.idl @@ -3966,42 +3964,42 @@ UpdateService.prototype = { hasUpdateMutex() && !isOtherInstanceRunning() ); - }, + } /** * See nsIUpdateService.idl */ get canUsuallyStageUpdates() { return getCanStageUpdates(false); - }, + } /** * See nsIUpdateService.idl */ get canStageUpdates() { return getCanStageUpdates(); - }, + } /** * See nsIUpdateService.idl */ get canUsuallyUseBits() { return getCanUseBits(false) == "CanUseBits"; - }, + } /** * See nsIUpdateService.idl */ get canUseBits() { return getCanUseBits() == "CanUseBits"; - }, + } /** * See nsIUpdateService.idl */ get isOtherInstanceHandlingUpdates() { return !hasUpdateMutex() || isOtherInstanceRunning(); - }, + } /** * A set of download listeners to be notified by this._downloader when it @@ -4010,12 +4008,12 @@ UpdateService.prototype = { * These are stored on the UpdateService rather than on the Downloader, * because they ought to persist across multiple Downloader instances. */ - _downloadListeners: new Set(), + _downloadListeners = new Set(); /** * See nsIUpdateService.idl */ - addDownloadListener: function AUS_addDownloadListener(listener) { + addDownloadListener(listener) { let oldSize = this._downloadListeners.size; this._downloadListeners.add(listener); @@ -4030,12 +4028,12 @@ UpdateService.prototype = { if (this._downloader) { this._downloader.onDownloadListenerAdded(); } - }, + } /** * See nsIUpdateService.idl */ - removeDownloadListener: function AUS_removeDownloadListener(listener) { + removeDownloadListener(listener) { let elementRemoved = this._downloadListeners.delete(listener); if (!elementRemoved) { @@ -4049,29 +4047,29 @@ UpdateService.prototype = { if (this._downloader) { this._downloader.onDownloadListenerRemoved(); } - }, + } /** * Returns a boolean indicating whether there are any download listeners */ get hasDownloadListeners() { return !!this._downloadListeners.length; - }, + } /* * Calls the provided function once with each download listener that is * currently registered. */ - forEachDownloadListener: function AUS_forEachDownloadListener(fn) { + forEachDownloadListener(fn) { // Make a shallow copy in case listeners remove themselves. let listeners = new Set(this._downloadListeners); listeners.forEach(fn); - }, + } /** * See nsIUpdateService.idl */ - downloadUpdate: async function AUS_downloadUpdate(update) { + async downloadUpdate(update) { if (!update) { throw Components.Exception("", Cr.NS_ERROR_NULL_POINTER); } @@ -4133,12 +4131,12 @@ UpdateService.prototype = { } this._downloader = new Downloader(this); return this._downloader.downloadUpdate(update); - }, + } /** * See nsIUpdateService.idl */ - stopDownload: async function AUS_stopDownload() { + async stopDownload() { if (this.isDownloading) { await this._downloader.cancel(); } else if (this._retryTimer) { @@ -4154,7 +4152,7 @@ UpdateService.prototype = { await this._downloader.cleanup(); } this._downloader = null; - }, + } /** * Note that this is different from checking if `currentState` is @@ -4163,9 +4161,9 @@ UpdateService.prototype = { */ get isDownloading() { return this._downloader && this._downloader.isBusy; - }, + } - _logStatus: function AUS__logStatus() { + _logStatus() { if (!lazy.UpdateLog.enabled) { return; } @@ -4211,21 +4209,21 @@ UpdateService.prototype = { } } LOG("End of UpdateService status"); - }, + } /** * See nsIUpdateService.idl */ get onlyDownloadUpdatesThisSession() { return gOnlyDownloadUpdatesThisSession; - }, + } /** * See nsIUpdateService.idl */ set onlyDownloadUpdatesThisSession(newValue) { gOnlyDownloadUpdatesThisSession = newValue; - }, + } /** * See nsIUpdateService.idl @@ -4244,129 +4242,129 @@ UpdateService.prototype = { return "STATE_SWAP"; } return `[unknown update state: ${state}]`; - }, + } /** * See nsIUpdateService.idl */ get currentState() { return gUpdateState; - }, + } /** * See nsIUpdateService.idl */ get stateTransition() { return gStateTransitionPromise.promise; - }, - - classID: UPDATESERVICE_CID, - - QueryInterface: ChromeUtils.generateQI([ - "nsIApplicationUpdateService", - "nsITimerCallback", - "nsIObserver", - ]), -}; - -/** - * A service to manage active and past updates. - * @constructor - */ -export function UpdateManager() { - // Load the active-update.xml file to see if there is an active update. - let activeUpdates = this._loadXMLFileIntoArray(FILE_ACTIVE_UPDATE_XML); - if (activeUpdates.length) { - // Set the active update directly on the var used to cache the value. - this._readyUpdate = activeUpdates[0]; - if (activeUpdates.length >= 2) { - this._downloadingUpdate = activeUpdates[1]; - } - let status = readStatusFile(getReadyUpdateDir()); - LOG(`UpdateManager:UpdateManager - status = "${status}"`); - // This check is performed here since UpdateService:_postUpdateProcessing - // won't be called when there isn't an update.status file. - if (status == STATE_NONE) { - // Under some edgecases such as Windows system restore the - // active-update.xml will contain a pending update without the status - // file. To recover from this situation clean the updates dir and move - // the active update to the update history. - LOG( - "UpdateManager:UpdateManager - Found update data with no status " + - "file. Cleaning up..." - ); - this._readyUpdate.state = STATE_FAILED; - this._readyUpdate.errorCode = ERR_UPDATE_STATE_NONE; - this._readyUpdate.statusText = - lazy.gUpdateBundle.GetStringFromName("statusFailed"); - let newStatus = STATE_FAILED + ": " + ERR_UPDATE_STATE_NONE; - pingStateAndStatusCodes(this._readyUpdate, true, newStatus); - this.addUpdateToHistory(this._readyUpdate); - this._readyUpdate = null; - this.saveUpdates(); - cleanUpReadyUpdateDir(); - cleanUpDownloadingUpdateDir(); - } else if (status == STATE_DOWNLOADING) { - // The first update we read out of activeUpdates may not be the ready - // update, it may be the downloading update. - if (this._downloadingUpdate) { - // If the first update we read is a downloading update, it's - // unexpected to have read another active update. That would seem to - // indicate that we were downloading two updates at once, which we don't - // do. - LOG( - "UpdateManager:UpdateManager - Warning: Found and discarded a " + - "second downloading update." - ); - } - this._downloadingUpdate = this._readyUpdate; - this._readyUpdate = null; - } } - LOG( - "UpdateManager:UpdateManager - Initialized downloadingUpdate to " + - this._downloadingUpdate - ); - if (this._downloadingUpdate) { - LOG( - "UpdateManager:UpdateManager - Initialized downloadingUpdate state to " + - this._downloadingUpdate.state - ); - } - LOG( - "UpdateManager:UpdateManager - Initialized readyUpdate to " + - this._readyUpdate - ); - if (this._readyUpdate) { - LOG( - "UpdateManager:UpdateManager - Initialized readyUpdate state to " + - this._readyUpdate.state - ); - } + classID = UPDATESERVICE_CID; + + QueryInterface = ChromeUtils.generateQI([ + Ci.nsIApplicationUpdateService, + Ci.nsITimerCallback, + Ci.nsIObserver, + ]); } -UpdateManager.prototype = { +export class UpdateManager { /** * The nsIUpdate object for the update that has been downloaded. */ - _readyUpdate: null, + _readyUpdate = null; /** * The nsIUpdate object for the update currently being downloaded. */ - _downloadingUpdate: null, + _downloadingUpdate = null; /** * Whether the update history stored in _updates has changed since it was * loaded. */ - _updatesDirty: false, + _updatesDirty = false; + + /** + * A service to manage active and past updates. + * @constructor + */ + constructor() { + // Load the active-update.xml file to see if there is an active update. + let activeUpdates = this._loadXMLFileIntoArray(FILE_ACTIVE_UPDATE_XML); + if (activeUpdates.length) { + // Set the active update directly on the var used to cache the value. + this._readyUpdate = activeUpdates[0]; + if (activeUpdates.length >= 2) { + this._downloadingUpdate = activeUpdates[1]; + } + let status = readStatusFile(getReadyUpdateDir()); + LOG(`UpdateManager:UpdateManager - status = "${status}"`); + // This check is performed here since UpdateService:_postUpdateProcessing + // won't be called when there isn't an update.status file. + if (status == STATE_NONE) { + // Under some edgecases such as Windows system restore the + // active-update.xml will contain a pending update without the status + // file. To recover from this situation clean the updates dir and move + // the active update to the update history. + LOG( + "UpdateManager:UpdateManager - Found update data with no status " + + "file. Cleaning up..." + ); + this._readyUpdate.state = STATE_FAILED; + this._readyUpdate.errorCode = ERR_UPDATE_STATE_NONE; + this._readyUpdate.statusText = + lazy.gUpdateBundle.GetStringFromName("statusFailed"); + let newStatus = STATE_FAILED + ": " + ERR_UPDATE_STATE_NONE; + pingStateAndStatusCodes(this._readyUpdate, true, newStatus); + this.addUpdateToHistory(this._readyUpdate); + this._readyUpdate = null; + this.saveUpdates(); + cleanUpReadyUpdateDir(); + cleanUpDownloadingUpdateDir(); + } else if (status == STATE_DOWNLOADING) { + // The first update we read out of activeUpdates may not be the ready + // update, it may be the downloading update. + if (this._downloadingUpdate) { + // If the first update we read is a downloading update, it's + // unexpected to have read another active update. That would seem to + // indicate that we were downloading two updates at once, which we don't + // do. + LOG( + "UpdateManager:UpdateManager - Warning: Found and discarded a " + + "second downloading update." + ); + } + this._downloadingUpdate = this._readyUpdate; + this._readyUpdate = null; + } + } + + LOG( + "UpdateManager:UpdateManager - Initialized downloadingUpdate to " + + this._downloadingUpdate + ); + if (this._downloadingUpdate) { + LOG( + "UpdateManager:UpdateManager - Initialized downloadingUpdate state to " + + this._downloadingUpdate.state + ); + } + LOG( + "UpdateManager:UpdateManager - Initialized readyUpdate to " + + this._readyUpdate + ); + if (this._readyUpdate) { + LOG( + "UpdateManager:UpdateManager - Initialized readyUpdate state to " + + this._readyUpdate.state + ); + } + } /** * See nsIObserver.idl */ - observe: function UM_observe(subject, topic, data) { + observe(subject, topic, data) { // Hack to be able to run and cleanup tests by reloading the update data. if (topic == "um-reload-update-data") { if (!Cu.isInAutomation) { @@ -4431,7 +4429,7 @@ UpdateManager.prototype = { ); } } - }, + } /** * Loads an updates.xml formatted file into an array of nsIUpdate items. @@ -4439,7 +4437,7 @@ UpdateManager.prototype = { * The file name in the updates directory to load. * @return The array of nsIUpdate items held in the file. */ - _loadXMLFileIntoArray: function UM__loadXMLFileIntoArray(fileName) { + _loadXMLFileIntoArray(fileName) { let updates = []; let file = getUpdateFile([fileName]); if (!file.exists()) { @@ -4528,7 +4526,7 @@ UpdateManager.prototype = { } } return updates; - }, + } /** * Loads the update history from the updates.xml file into a cache. @@ -4538,41 +4536,41 @@ UpdateManager.prototype = { this._updatesCache = this._loadXMLFileIntoArray(FILE_UPDATES_XML); } return this._updatesCache; - }, + } /** * See nsIUpdateService.idl */ - getUpdateAt: function UM_getUpdateAt(aIndex) { + getUpdateAt(aIndex) { return this._getUpdates()[aIndex]; - }, + } /** * See nsIUpdateService.idl */ getUpdateCount() { return this._getUpdates().length; - }, + } /** * See nsIUpdateService.idl */ get readyUpdate() { return this._readyUpdate; - }, + } set readyUpdate(aUpdate) { this._readyUpdate = aUpdate; - }, + } /** * See nsIUpdateService.idl */ get downloadingUpdate() { return this._downloadingUpdate; - }, + } set downloadingUpdate(aUpdate) { this._downloadingUpdate = aUpdate; - }, + } /** * See nsIUpdateService.idl @@ -4583,7 +4581,7 @@ UpdateManager.prototype = { updates.unshift(aUpdate); // Limit the update history to 10 updates. updates.splice(10); - }, + } /** * Serializes an array of updates to an XML file or removes the file if the @@ -4594,10 +4592,7 @@ UpdateManager.prototype = { * The file name in the updates directory to write to. * @return true on success, false on error */ - _writeUpdatesToXMLFile: async function UM__writeUpdatesToXMLFile( - updates, - fileName - ) { + async _writeUpdatesToXMLFile(updates, fileName) { let file; try { file = getUpdateFile([fileName]); @@ -4656,14 +4651,14 @@ UpdateManager.prototype = { return false; } return true; - }, + } - _updatesXMLSaver: null, - _updatesXMLSaverCallback: null, + _updatesXMLSaver = null; + _updatesXMLSaverCallback = null; /** * See nsIUpdateService.idl */ - saveUpdates: function UM_saveUpdates() { + saveUpdates() { if (!this._updatesXMLSaver) { this._updatesXMLSaverCallback = () => this._updatesXMLSaver.finalize(); @@ -4680,13 +4675,13 @@ UpdateManager.prototype = { } this._updatesXMLSaver.arm(); - }, + } /** * Saves the active-updates.xml and updates.xml when the updates history has * been modified files. */ - _saveUpdatesXML: function UM__saveUpdatesXML() { + _saveUpdatesXML() { // This mechanism for how we store the updates might seem a bit odd, since, // if only one update is stored, we don't know if it's the ready update or // the downloading update. However, we can determine which it is by reading @@ -4718,12 +4713,12 @@ UpdateManager.prototype = { ); } return Promise.all(promises); - }, + } /** * See nsIUpdateService.idl */ - refreshUpdateStatus: async function UM_refreshUpdateStatus() { + async refreshUpdateStatus() { try { LOG("UpdateManager:refreshUpdateStatus - Staging done."); @@ -4855,12 +4850,12 @@ UpdateManager.prototype = { transitionState(Ci.nsIApplicationUpdateService.STATE_IDLE); } } - }, + } /** * See nsIUpdateService.idl */ - elevationOptedIn: function UM_elevationOptedIn() { + elevationOptedIn() { // The user has been been made aware that the update requires elevation. let update = this._readyUpdate; if (!update) { @@ -4884,30 +4879,30 @@ UpdateManager.prototype = { } else { LOG("UpdateManager:elevationOptedIn - Not in pending-elevate state."); } - }, + } /** * See nsIUpdateService.idl */ - cleanupDownloadingUpdate: function UM_cleanupDownloadingUpdate() { + cleanupDownloadingUpdate() { LOG( "UpdateManager:cleanupDownloadingUpdate - cleaning up downloading update." ); cleanupDownloadingUpdate(); - }, + } /** * See nsIUpdateService.idl */ - cleanupReadyUpdate: function UM_cleanupReadyUpdate() { + cleanupReadyUpdate() { LOG("UpdateManager:cleanupReadyUpdate - cleaning up ready update."); cleanupReadyUpdate(); - }, + } /** * See nsIUpdateService.idl */ - doInstallCleanup: async function UM_doInstallCleanup() { + async doInstallCleanup() { LOG("UpdateManager:doInstallCleanup - cleaning up"); let completionPromises = []; @@ -4944,12 +4939,12 @@ UpdateManager.prototype = { } return Promise.allSettled(completionPromises); - }, + } /** * See nsIUpdateService.idl */ - doUninstallCleanup: async function UM_doUninstallCleanup() { + async doUninstallCleanup() { LOG("UpdateManager:doUninstallCleanup - cleaning up."); let completionPromises = []; @@ -4965,11 +4960,14 @@ UpdateManager.prototype = { ); return Promise.allSettled(completionPromises); - }, + } - classID: Components.ID("{093C2356-4843-4C65-8709-D7DBCBBE7DFB}"), - QueryInterface: ChromeUtils.generateQI(["nsIUpdateManager", "nsIObserver"]), -}; + classID = Components.ID("{093C2356-4843-4C65-8709-D7DBCBBE7DFB}"); + QueryInterface = ChromeUtils.generateQI([ + Ci.nsIUpdateManager, + Ci.nsIObserver, + ]); +} /** * CheckerService @@ -5199,7 +5197,7 @@ export class CheckerService { return { id: checkId, result: this.#updateCheckData[requestKey].promise, - QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheck"]), + QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateCheck]), }; } @@ -5212,10 +5210,10 @@ export class CheckerService { succeeded: false, request: null, updates: [], - QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheckResult"]), + QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateCheckResult]), }) ), - QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheck"]), + QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateCheck]), }; } @@ -5370,7 +5368,7 @@ export class CheckerService { succeeded: true, request, updates, - QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheckResult"]), + QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateCheckResult]), }); } @@ -5408,7 +5406,7 @@ export class CheckerService { succeeded: false, request, updates: [update], - QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheckResult"]), + QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateCheckResult]), }); } @@ -5523,51 +5521,38 @@ export class CheckerService { } classID = Components.ID("{898CDC9B-E43F-422F-9CC4-2F6291B415A3}"); - QueryInterface = ChromeUtils.generateQI(["nsIUpdateChecker"]); + QueryInterface = ChromeUtils.generateQI([Ci.nsIUpdateChecker]); } -/** - * Manages the download of updates - * @param background - * Whether or not this downloader is operating in background - * update mode. - * @param updateService - * The update service that created this downloader. - * @constructor - */ -function Downloader(updateService) { - LOG("Creating Downloader"); - this.updateService = updateService; -} -Downloader.prototype = { +class Downloader { /** * The nsIUpdatePatch that we are downloading */ - _patch: null, + _patch = null; /** * The nsIUpdate that we are downloading */ - _update: null, + _update = null; /** * The nsIRequest object handling the download. */ - _request: null, + _request = null; /** * Whether or not the update being downloaded is a complete replacement of * the user's existing installation or a patch representing the difference * between the new version and the previous version. */ - isCompleteUpdate: null, + isCompleteUpdate = null; /** * We get the nsIRequest from nsIBITS asynchronously. When downloadUpdate has * been called, but this._request is not yet valid, _pendingRequest will be * a promise that will resolve when this._request has been set. */ - _pendingRequest: null, + _pendingRequest = null; /** * When using BITS, cancel actions happen asynchronously. This variable @@ -5577,7 +5562,7 @@ Downloader.prototype = { * resolved promise will remain stored in this variable to prevent cancel * from being called twice (which, for BITS, is an error). */ - _cancelPromise: null, + _cancelPromise = null; /** * BITS receives progress notifications slowly, unless a user is watching. @@ -5591,14 +5576,14 @@ Downloader.prototype = { * we don't know if we need to start Active mode when _pendingRequest * resolves. */ - _bitsActiveNotifications: false, + _bitsActiveNotifications = false; /** * This is a function that when called will stop the update process from * waiting for language pack updates. This is for safety to ensure that a * problem in the add-ons manager doesn't delay updates by much. */ - _langPackTimeout: null, + _langPackTimeout = null; /** * If gOnlyDownloadUpdatesThisSession is true, we prevent the update process @@ -5606,7 +5591,21 @@ Downloader.prototype = { * pretend that it hasn't in order to keep the current update in the * "downloading" state. */ - _pretendingDownloadIsNotDone: false, + _pretendingDownloadIsNotDone = false; + + /** + * Manages the download of updates + * @param background + * Whether or not this downloader is operating in background + * update mode. + * @param updateService + * The update service that created this downloader. + * @constructor + */ + constructor(updateService) { + LOG("Creating Downloader"); + this.updateService = updateService; + } /** * Cancels the active download. @@ -5615,7 +5614,7 @@ Downloader.prototype = { * an nsIIncrementalDownload, this will stop the download, but leaves the * data around to allow the transfer to be resumed later. */ - cancel: async function Downloader_cancel(cancelError) { + async cancel(cancelError) { LOG("Downloader: cancel"); if (cancelError === undefined) { cancelError = Cr.NS_BINDING_ABORTED; @@ -5664,13 +5663,13 @@ Downloader.prototype = { this._request.cancel(cancelError); } } - }, + } /** * Verify the downloaded file. We assume that the download is complete at * this point. */ - _verifyDownload: function Downloader__verifyDownload() { + _verifyDownload() { LOG("Downloader:_verifyDownload called"); if (!this._request) { AUSTLMY.pingDownloadCode( @@ -5695,7 +5694,7 @@ Downloader.prototype = { LOG("Downloader:_verifyDownload downloaded size == expected size."); return true; - }, + } /** * Select the patch to use given the current state of updateDir and the given @@ -5704,7 +5703,7 @@ Downloader.prototype = { * A nsIUpdate object to select a patch from * @return A nsIUpdatePatch object to download */ - _selectPatch: function Downloader__selectPatch(update) { + _selectPatch(update) { // Given an update to download, we will always try to download the patch // for a partial update over the patch for a full update. @@ -5809,7 +5808,7 @@ Downloader.prototype = { lazy.UM.downloadingUpdate = update; return selectedPatch; - }, + } /** * Whether or not the user wants to be notified that an update is being @@ -5820,35 +5819,30 @@ Downloader.prototype = { PREF_APP_UPDATE_NOTIFYDURINGDOWNLOAD, false ); - }, - - _notifyDownloadStatusObservers: - function Downloader_notifyDownloadStatusObservers() { - if (this._notifyDuringDownload) { - let status = this.updateService.isDownloading ? "downloading" : "idle"; - Services.obs.notifyObservers( - this._update, - "update-downloading", - status - ); - } - }, + } + + _notifyDownloadStatusObservers() { + if (this._notifyDuringDownload) { + let status = this.updateService.isDownloading ? "downloading" : "idle"; + Services.obs.notifyObservers(this._update, "update-downloading", status); + } + } /** * Whether or not we are currently downloading something. */ get isBusy() { return this._request != null || this._pendingRequest != null; - }, + } get usingBits() { return this._pendingRequest != null || this._request instanceof BitsRequest; - }, + } /** * Returns true if the specified patch can be downloaded with BITS. */ - _canUseBits: function Downloader__canUseBits(patch) { + _canUseBits(patch) { if (getCanUseBits() != "CanUseBits") { // This will have printed its own logging. No need to print more. return false; @@ -5863,13 +5857,13 @@ Downloader.prototype = { } LOG("Downloader:_canUseBits - Patch is able to use BITS download"); return true; - }, + } /** * Instruct the add-ons manager to start downloading language pack updates in * preparation for the current update. */ - _startLangPackUpdates: function Downloader__startLangPackUpdates() { + _startLangPackUpdates() { if (!Services.prefs.getBoolPref(PREF_APP_UPDATE_LANGPACK_ENABLED, false)) { return; } @@ -5912,14 +5906,14 @@ Downloader.prototype = { update, Promise.race([langPackPromise, timeoutPromise]) ); - }, + } /** * Download and stage the given update. * @param update * A nsIUpdate object to download a patch for. Cannot be null. */ - downloadUpdate: async function Downloader_downloadUpdate(update) { + async downloadUpdate(update) { LOG("UpdateService:downloadUpdate"); if (!update) { AUSTLMY.pingDownloadCode(undefined, AUSTLMY.DWNLD_ERR_NO_UPDATE); @@ -6190,116 +6184,114 @@ Downloader.prototype = { this._notifyDownloadStatusObservers(); return true; - }, + } /** * This is run when a download listener is added. */ - onDownloadListenerAdded: function Downloader_onDownloadListenerAdded() { + onDownloadListenerAdded() { // Increase the status update frequency when someone starts listening this._maybeStartActiveNotifications(); - }, + } /** * This is run when a download listener is removed. */ - onDownloadListenerRemoved: function Downloader_onDownloadListenerRemoved() { + onDownloadListenerRemoved() { // Decrease the status update frequency when no one is listening if (!this.hasDownloadListeners) { this._maybeStopActiveNotifications(); } - }, + } get hasDownloadListeners() { return this.updateService.hasDownloadListeners; - }, + } /** * This speeds up BITS progress notifications in response to a user watching * the notifications. */ - _maybeStartActiveNotifications: - async function Downloader__maybeStartActiveNotifications() { - if ( - this.usingBits && - !this._bitsActiveNotifications && - this.hasDownloadListeners && + async _maybeStartActiveNotifications() { + if ( + this.usingBits && + !this._bitsActiveNotifications && + this.hasDownloadListeners && + this._request + ) { + LOG( + "Downloader:_maybeStartActiveNotifications - Starting active " + + "notifications" + ); + this._bitsActiveNotifications = true; + await Promise.all([ this._request - ) { - LOG( - "Downloader:_maybeStartActiveNotifications - Starting active " + - "notifications" - ); - this._bitsActiveNotifications = true; - await Promise.all([ - this._request - .setNoProgressTimeout(BITS_ACTIVE_NO_PROGRESS_TIMEOUT_SECS) - .catch(error => { - LOG( - "Downloader:_maybeStartActiveNotifications - Failed to set " + - "no progress timeout. Error: " + - error - ); - }), - this._request - .changeMonitorInterval(BITS_ACTIVE_POLL_RATE_MS) - .catch(error => { - LOG( - "Downloader:_maybeStartActiveNotifications - Failed to increase " + - "status update frequency. Error: " + - error - ); - }), - ]); - } - }, + .setNoProgressTimeout(BITS_ACTIVE_NO_PROGRESS_TIMEOUT_SECS) + .catch(error => { + LOG( + "Downloader:_maybeStartActiveNotifications - Failed to set " + + "no progress timeout. Error: " + + error + ); + }), + this._request + .changeMonitorInterval(BITS_ACTIVE_POLL_RATE_MS) + .catch(error => { + LOG( + "Downloader:_maybeStartActiveNotifications - Failed to increase " + + "status update frequency. Error: " + + error + ); + }), + ]); + } + } /** * This slows down BITS progress notifications in response to a user no longer * watching the notifications. */ - _maybeStopActiveNotifications: - async function Downloader__maybeStopActiveNotifications() { - if ( - this.usingBits && - this._bitsActiveNotifications && - !this.hasDownloadListeners && + async _maybeStopActiveNotifications() { + if ( + this.usingBits && + this._bitsActiveNotifications && + !this.hasDownloadListeners && + this._request + ) { + LOG( + "Downloader:_maybeStopActiveNotifications - Stopping active " + + "notifications" + ); + this._bitsActiveNotifications = false; + await Promise.all([ this._request - ) { - LOG( - "Downloader:_maybeStopActiveNotifications - Stopping active " + - "notifications" - ); - this._bitsActiveNotifications = false; - await Promise.all([ - this._request - .setNoProgressTimeout(BITS_IDLE_NO_PROGRESS_TIMEOUT_SECS) - .catch(error => { - LOG( - "Downloader:_maybeStopActiveNotifications - Failed to set " + - "no progress timeout: " + - error - ); - }), - this._request - .changeMonitorInterval(BITS_IDLE_POLL_RATE_MS) - .catch(error => { - LOG( - "Downloader:_maybeStopActiveNotifications - Failed to decrease " + - "status update frequency: " + - error - ); - }), - ]); - } - }, + .setNoProgressTimeout(BITS_IDLE_NO_PROGRESS_TIMEOUT_SECS) + .catch(error => { + LOG( + "Downloader:_maybeStopActiveNotifications - Failed to set " + + "no progress timeout: " + + error + ); + }), + this._request + .changeMonitorInterval(BITS_IDLE_POLL_RATE_MS) + .catch(error => { + LOG( + "Downloader:_maybeStopActiveNotifications - Failed to decrease " + + "status update frequency: " + + error + ); + }), + ]); + } + } /** * When the async request begins * @param request * The nsIRequest object for the transfer */ - onStartRequest: function Downloader_onStartRequest(request) { + onStartRequest(request) { if (this.usingBits) { LOG("Downloader:onStartRequest"); } else { @@ -6319,7 +6311,7 @@ Downloader.prototype = { this.updateService.forEachDownloadListener(listener => { listener.onStartRequest(request); }); - }, + } /** * When new data has been downloaded @@ -6330,7 +6322,7 @@ Downloader.prototype = { * @param maxProgress * The total number of bytes that must be transferred */ - onProgress: function Downloader_onProgress(request, progress, maxProgress) { + onProgress(request, progress, maxProgress) { LOG("Downloader:onProgress - progress: " + progress + "/" + maxProgress); if (progress > this._patch.size) { @@ -6372,7 +6364,7 @@ Downloader.prototype = { } }); this.updateService._consecutiveSocketErrors = 0; - }, + } /** * When we have new status text @@ -6383,7 +6375,7 @@ Downloader.prototype = { * @param statusText * Human readable version of |status| */ - onStatus: function Downloader_onStatus(request, status, statusText) { + onStatus(request, status, statusText) { LOG( "Downloader:onStatus - status: " + status + ", statusText: " + statusText ); @@ -6393,7 +6385,7 @@ Downloader.prototype = { listener.onStatus(request, status, statusText); } }); - }, + } /** * When data transfer ceases @@ -6403,7 +6395,7 @@ Downloader.prototype = { * Status code containing the reason for the cessation. */ /* eslint-disable-next-line complexity */ - onStopRequest: async function Downloader_onStopRequest(request, status) { + async onStopRequest(request, status) { if (gOnlyDownloadUpdatesThisSession) { LOG( "Downloader:onStopRequest - End of update download detected and " + @@ -6936,25 +6928,25 @@ Downloader.prototype = { // Prevent leaking the update object (bug 454964) this._update = null; } - }, + } /** * This function should be called when shutting down so that resources get * freed properly. */ - cleanup: async function Downloader_cleanup() { + async cleanup() { if (this.usingBits) { if (this._pendingRequest) { await this._pendingRequest; } this._request.shutdown(); } - }, + } /** * See nsIInterfaceRequestor.idl */ - getInterface: function Downloader_getInterface(iid) { + getInterface(iid) { // The network request may require proxy authentication, so provide the // default nsIAuthPrompt if requested. if (iid.equals(Ci.nsIAuthPrompt)) { @@ -6963,14 +6955,14 @@ Downloader.prototype = { return prompt.QueryInterface(iid); } throw Components.Exception("", Cr.NS_NOINTERFACE); - }, + } - QueryInterface: ChromeUtils.generateQI([ - "nsIRequestObserver", - "nsIProgressEventSink", - "nsIInterfaceRequestor", - ]), -}; + QueryInterface = ChromeUtils.generateQI([ + Ci.nsIRequestObserver, + Ci.nsIProgressEventSink, + Ci.nsIInterfaceRequestor, + ]); +} // On macOS, all browser windows can be closed without Firefox exiting. If it // is left in this state for a while and an update is pending, we should restart diff --git a/toolkit/mozapps/update/common/readstrings.cpp b/toolkit/mozapps/update/common/readstrings.cpp index 28dc8ea6ff..4d09c9d9a4 100644 --- a/toolkit/mozapps/update/common/readstrings.cpp +++ b/toolkit/mozapps/update/common/readstrings.cpp @@ -144,7 +144,7 @@ int ReadStrings(const NS_tchar* path, const char* keyList, size_t flen = size_t(len); - char* fileContents = new char[flen + 1]; + mozilla::UniquePtr<char[]> fileContents(new char[flen + 1]); if (!fileContents) { return READ_STRINGS_MEM_ERROR; } @@ -154,16 +154,15 @@ int ReadStrings(const NS_tchar* path, const char* keyList, return READ_ERROR; } - size_t rd = fread(fileContents, sizeof(char), flen, fp); + size_t rd = fread(fileContents.get(), sizeof(char), flen, fp); if (rd != flen) { return READ_ERROR; } fileContents[flen] = '\0'; - int result = ReadStringsFromBuffer(fileContents, keyList, numStrings, results, - section); - delete[] fileContents; + int result = ReadStringsFromBuffer(fileContents.get(), keyList, numStrings, + results, section); return result; } diff --git a/toolkit/mozapps/update/docs/MaintenanceServiceTests.rst b/toolkit/mozapps/update/docs/MaintenanceServiceTests.rst index b954b572f8..65259c94d9 100644 --- a/toolkit/mozapps/update/docs/MaintenanceServiceTests.rst +++ b/toolkit/mozapps/update/docs/MaintenanceServiceTests.rst @@ -47,11 +47,11 @@ into the registry. [HKEY_LOCAL_MACHINE\SOFTWARE\Mozilla\MaintenanceService\3932ecacee736d366d6436db0f55bce4] [HKEY_LOCAL_MACHINE\SOFTWARE\Mozilla\MaintenanceService\3932ecacee736d366d6436db0f55bce4\0] - "issuer"="DigiCert SHA2 Assured ID Code Signing CA" + "issuer"="DigiCert Trusted G4 Code Signing RSA4096 SHA384 2021 CA1" "name"="Mozilla Corporation" [HKEY_LOCAL_MACHINE\SOFTWARE\Mozilla\MaintenanceService\3932ecacee736d366d6436db0f55bce4\1] - "issuer"="DigiCert Assured ID Code Signing CA-1" + "issuer"="DigiCert SHA2 Assured ID Code Signing CA" "name"="Mozilla Corporation" [HKEY_LOCAL_MACHINE\SOFTWARE\Mozilla\MaintenanceService\3932ecacee736d366d6436db0f55bce4\2] diff --git a/toolkit/mozapps/update/nsUpdateService.manifest b/toolkit/mozapps/update/nsUpdateService.manifest index a8d2534b1e..6db9b00eb3 100644 --- a/toolkit/mozapps/update/nsUpdateService.manifest +++ b/toolkit/mozapps/update/nsUpdateService.manifest @@ -1 +1 @@ -category update-timer nsUpdateService @mozilla.org/updates/update-service;1,getService,background-update-timer,app.update.interval,43200,86400 +category update-timer nsUpdateService @mozilla.org/updates/update-service;1,getService,background-update-timer,app.update.interval,21600,86400 diff --git a/toolkit/mozapps/update/updater/Makefile.in b/toolkit/mozapps/update/updater/Makefile.in index ec3ad9773a..f93a374428 100644 --- a/toolkit/mozapps/update/updater/Makefile.in +++ b/toolkit/mozapps/update/updater/Makefile.in @@ -5,6 +5,12 @@ # For changes here, also consider ./updater-xpcshell/Makefile.in +# In a compile build, the moz.build stanzas produce the binary named +# `UpdateSettings`; we just need it in the correct place. +# +# In an artifact build, we copy upstream artifacts from +# `dist/update_framework_artifacts/UpdateSettings-localbuild.framework` + ifndef MOZ_WINCONSOLE ifdef MOZ_DEBUG MOZ_WINCONSOLE = 1 @@ -26,6 +32,11 @@ libs:: $(NSINSTALL) -D $(DIST)/bin/updater.app/Contents/MacOS $(NSINSTALL) $(DIST)/bin/org.mozilla.updater $(DIST)/bin/updater.app/Contents/MacOS $(NSINSTALL) -D $(DIST)/bin/updater.app/Contents/Frameworks +ifneq (,$(COMPILE_ENVIRONMENT)) $(NSINSTALL) $(DIST)/bin/UpdateSettings $(DIST)/bin/updater.app/Contents/Frameworks/UpdateSettings.framework +endif # COMPILE_ENVIRONMENT +ifneq (,$(MOZ_ARTIFACT_BUILDS)) + $(NSINSTALL) $(DIST)/update_framework_artifacts/UpdateSettings-localbuild.framework/UpdateSettings $(DIST)/bin/updater.app/Contents/Frameworks/UpdateSettings.framework +endif # MOZ_ARTIFACT_BUILDS $(NSINSTALL) $(srcdir)/macos-frameworks/UpdateSettings/Info.plist $(DIST)/bin/updater.app/Contents/Frameworks/UpdateSettings.framework/Resources endif diff --git a/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings-localbuild/Makefile.in b/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings-localbuild/Makefile.in new file mode 100644 index 0000000000..2798eecd64 --- /dev/null +++ b/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings-localbuild/Makefile.in @@ -0,0 +1,25 @@ +# vim:set ts=8 sw=8 sts=8 noet: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +include $(topsrcdir)/config/rules.mk + +# In a compile build, the moz.build stanzas produce a binary named +# `UpdateSettings-localbuild`. We need to produce +# `dist/update_framework_artifacts/UpdateSettings-localbuild.framework/UpdateSettings` +# for consumption by artifact builds. +# +# In an artifact build, we already have upstream artifacts in +# `dist/update_framework_artifacts/UpdateSettings-localbuild.framework`. + +ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT)) +libs:: +ifneq (,$(COMPILE_ENVIRONMENT)) + rm -rf $(FINAL_TARGET)/UpdateSettings-localbuild.framework + $(NSINSTALL) -D $(FINAL_TARGET)/UpdateSettings-localbuild.framework + cp $(FINAL_TARGET)/UpdateSettings-localbuild UpdateSettings + $(NSINSTALL) UpdateSettings $(FINAL_TARGET)/UpdateSettings-localbuild.framework +endif # COMPILE_ENVIRONMENT + $(NSINSTALL) $(srcdir)/../UpdateSettings/Info.plist $(FINAL_TARGET)/UpdateSettings-localbuild.framework/Resources +endif # MOZ_WIDGET_TOOLKIT diff --git a/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings-localbuild/moz.build b/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings-localbuild/moz.build new file mode 100644 index 0000000000..ca3a0c9f1c --- /dev/null +++ b/toolkit/mozapps/update/updater/macos-frameworks/UpdateSettings-localbuild/moz.build @@ -0,0 +1,18 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Framework("UpdateSettings-localbuild") +FINAL_TARGET = "dist/update_framework_artifacts" + +DEFINES["ACCEPTED_MAR_CHANNEL_IDS"] = '""' + +UNIFIED_SOURCES += [ + "../UpdateSettings/UpdateSettings.mm", +] + +OS_LIBS += [ + "-framework Foundation", +] diff --git a/toolkit/mozapps/update/updater/macos-frameworks/moz.build b/toolkit/mozapps/update/updater/macos-frameworks/moz.build index 19fa11942e..cb49b680c0 100644 --- a/toolkit/mozapps/update/updater/macos-frameworks/moz.build +++ b/toolkit/mozapps/update/updater/macos-frameworks/moz.build @@ -7,7 +7,12 @@ with Files("**"): BUG_COMPONENT = ("Toolkit", "Application Update") -DIRS += ["UpdateSettings", "UpdateSettings-xpcshell", "UpdateSettings-WrongChannel"] +DIRS += [ + "UpdateSettings", + "UpdateSettings-localbuild", + "UpdateSettings-WrongChannel", + "UpdateSettings-xpcshell", +] EXPORTS += [ "UpdateSettingsUtil.h", diff --git a/toolkit/mozapps/update/updater/updater.cpp b/toolkit/mozapps/update/updater/updater.cpp index 084945dc44..81702d74bc 100644 --- a/toolkit/mozapps/update/updater/updater.cpp +++ b/toolkit/mozapps/update/updater/updater.cpp @@ -2992,7 +2992,7 @@ int NS_main(int argc, NS_tchar** argv) { #ifdef MOZ_VERIFY_MAR_SIGNATURE int rv = PopulategMARStrings(); if (rv == OK) { - printf("Channels Allowed: %s\n", gMARStrings.MARChannelID.get()); + printf("Channels Allowed: '%s'\n", gMARStrings.MARChannelID.get()); return 0; } printf("Error: %d\n", rv); |