/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
 * Contains elements of the Content Analysis UI, which are integrated into
 * various browser behaviors (uploading, downloading, printing, etc) that
 * require content analysis to be done.
 * The content analysis itself is done by the clients of this script, who
 * use nsIContentAnalysis to talk to the external CA system.
 */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const lazy = {};

XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "gContentAnalysis",
  "@mozilla.org/contentanalysis;1",
  Ci.nsIContentAnalysis
);

ChromeUtils.defineESModuleGetters(lazy, {
  clearTimeout: "resource://gre/modules/Timer.sys.mjs",
  PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs",
  setTimeout: "resource://gre/modules/Timer.sys.mjs",
});

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "silentNotifications",
  "browser.contentanalysis.silent_notifications",
  false
);

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "agentName",
  "browser.contentanalysis.agent_name",
  "A DLP agent"
);

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "showBlockedResult",
  "browser.contentanalysis.show_blocked_result",
  true
);

/**
 * A class that groups browsing contexts by their top-level one.
 * This is necessary because if there may be a subframe that
 * is showing a "DLP request busy" dialog when another subframe
 * (other the outer frame) wants to show one. This class makes it
 * convenient to find if another frame with the same top browsing
 * context is currently showing a dialog, and also to find if there
 * are any pending dialogs to show when one closes.
 */
class MapByTopBrowsingContext {
  #map;
  constructor() {
    this.#map = new Map();
  }
  /**
   * Gets any existing data associated with the browsing context
   *
   * @param {BrowsingContext} aBrowsingContext the browsing context to search for
   * @returns {object | undefined} the existing data, or `undefined` if there is none
   */
  getEntry(aBrowsingContext) {
    const topEntry = this.#map.get(aBrowsingContext.top);
    if (!topEntry) {
      return undefined;
    }
    return topEntry.get(aBrowsingContext);
  }
  /**
   * Returns whether the browsing context has any data associated with it
   *
   * @param {BrowsingContext} aBrowsingContext the browsing context to search for
   * @returns {boolean} Whether the browsing context has any associated data
   */
  hasEntry(aBrowsingContext) {
    const topEntry = this.#map.get(aBrowsingContext.top);
    if (!topEntry) {
      return false;
    }
    return topEntry.has(aBrowsingContext);
  }
  /**
   * Whether the tab containing the browsing context has a dialog
   * currently showing
   *
   * @param {BrowsingContext} aBrowsingContext the browsing context to search for
   * @returns {boolean} whether the tab has a dialog currently showing
   */
  hasEntryDisplayingNotification(aBrowsingContext) {
    const topEntry = this.#map.get(aBrowsingContext.top);
    if (!topEntry) {
      return false;
    }
    for (const otherEntry in topEntry.values()) {
      if (otherEntry.notification?.dialogBrowsingContext) {
        return true;
      }
    }
    return false;
  }
  /**
   * Gets another browsing context in the same tab that has pending "DLP busy" dialog
   * info to show, if any.
   *
   * @param {BrowsingContext} aBrowsingContext the browsing context to search for
   * @returns {BrowsingContext} Another browsing context in the same tab that has pending "DLP busy" dialog info, or `undefined` if there aren't any.
   */
  getBrowsingContextWithPendingNotification(aBrowsingContext) {
    const topEntry = this.#map.get(aBrowsingContext.top);
    if (!topEntry) {
      return undefined;
    }
    if (aBrowsingContext.top.isDiscarded) {
      // The top-level tab has already been closed, so remove
      // the top-level entry and return there are no pending dialogs.
      this.#map.delete(aBrowsingContext.top);
      return undefined;
    }
    for (const otherContext in topEntry.keys()) {
      if (
        topEntry.get(otherContext).notification?.dialogBrowsingContextArgs &&
        otherContext !== aBrowsingContext
      ) {
        return otherContext;
      }
    }
    return undefined;
  }
  /**
   * Deletes the entry for the browsing context, if any
   *
   * @param {BrowsingContext} aBrowsingContext the browsing context to delete
   * @returns {boolean} Whether an entry was deleted or not
   */
  deleteEntry(aBrowsingContext) {
    const topEntry = this.#map.get(aBrowsingContext.top);
    if (!topEntry) {
      return false;
    }
    const toReturn = topEntry.delete(aBrowsingContext);
    if (!topEntry.size || aBrowsingContext.top.isDiscarded) {
      // Either the inner Map is now empty, or the whole tab
      // has been closed. Either way, remove the top-level entry.
      this.#map.delete(aBrowsingContext.top);
    }
    return toReturn;
  }
  /**
   * Sets the associated data for the browsing context
   *
   * @param {BrowsingContext} aBrowsingContext the browsing context to set the data for
   * @param {object} aValue the data to associated with the browsing context
   * @returns {MapByTopBrowsingContext} this
   */
  setEntry(aBrowsingContext, aValue) {
    if (!aValue.request) {
      console.error(
        "MapByTopBrowsingContext.setEntry() called with a value without a request!"
      );
    }
    let topEntry = this.#map.get(aBrowsingContext.top);
    if (!topEntry) {
      topEntry = new Map();
      this.#map.set(aBrowsingContext.top, topEntry);
    }
    topEntry.set(aBrowsingContext, aValue);
    return this;
  }

  getAllRequests() {
    let requests = [];
    this.#map.forEach(topEntry => {
      for (let entry of topEntry.values()) {
        requests.push(entry.request);
      }
    });
    return requests;
  }
}

export const ContentAnalysis = {
  _SHOW_NOTIFICATIONS: true,

  _SHOW_DIALOGS: false,

  _SLOW_DLP_NOTIFICATION_BLOCKING_TIMEOUT_MS: 250,

  _SLOW_DLP_NOTIFICATION_NONBLOCKING_TIMEOUT_MS: 3 * 1000,

  _RESULT_NOTIFICATION_TIMEOUT_MS: 5 * 60 * 1000, // 5 min

  _RESULT_NOTIFICATION_FAST_TIMEOUT_MS: 60 * 1000, // 1 min

  isInitialized: false,

  dlpBusyViewsByTopBrowsingContext: new MapByTopBrowsingContext(),

  requestTokenToRequestInfo: new Map(),

  /**
   * Registers for various messages/events that will indicate the
   * need for communicating something to the user.
   */
  initialize(doc) {
    if (!this.isInitialized) {
      this.isInitialized = true;
      this.initializeDownloadCA();

      ChromeUtils.defineLazyGetter(this, "l10n", function () {
        return new Localization(
          ["branding/brand.ftl", "toolkit/contentanalysis/contentanalysis.ftl"],
          true
        );
      });
    }

    // Do this even if initialized so the icon shows up on new windows, not just the
    // first one.
    if (lazy.gContentAnalysis.isActive) {
      doc.l10n.setAttributes(
        doc.getElementById("content-analysis-indicator"),
        "content-analysis-indicator-tooltip",
        { agentName: lazy.agentName }
      );
      doc.documentElement.setAttribute("contentanalysisactive", "true");
    }
  },

  async uninitialize() {
    if (this.isInitialized) {
      this.isInitialized = false;
      this.requestTokenToRequestInfo.clear();
    }
  },

  /**
   * Register UI for file download CA events.
   */
  async initializeDownloadCA() {
    Services.obs.addObserver(this, "dlp-request-made");
    Services.obs.addObserver(this, "dlp-response");
    Services.obs.addObserver(this, "quit-application");
    Services.obs.addObserver(this, "quit-application-requested");
  },

  // nsIObserver
  async observe(aSubj, aTopic, _aData) {
    switch (aTopic) {
      case "quit-application-requested": {
        let pendingRequests =
          this.dlpBusyViewsByTopBrowsingContext.getAllRequests();
        if (pendingRequests.length) {
          let messageBody = this.l10n.formatValueSync(
            "contentanalysis-inprogress-quit-message"
          );
          messageBody = messageBody + "\n\n";
          for (const pendingRequest of pendingRequests) {
            let name = this._getResourceNameFromNameOrOperationType(
              this._getResourceNameOrOperationTypeFromRequest(
                pendingRequest,
                true
              )
            );
            messageBody = messageBody + name + "\n";
          }
          let buttonSelected = Services.prompt.confirmEx(
            null,
            this.l10n.formatValueSync("contentanalysis-inprogress-quit-title"),
            messageBody,
            Ci.nsIPromptService.BUTTON_POS_0 *
              Ci.nsIPromptService.BUTTON_TITLE_IS_STRING +
              Ci.nsIPromptService.BUTTON_POS_1 *
                Ci.nsIPromptService.BUTTON_TITLE_CANCEL +
              Ci.nsIPromptService.BUTTON_POS_0_DEFAULT,
            this.l10n.formatValueSync(
              "contentanalysis-inprogress-quit-yesbutton"
            ),
            null,
            null,
            null,
            { value: 0 }
          );
          if (buttonSelected === 0) {
            lazy.gContentAnalysis.cancelAllRequests();
          } else {
            aSubj.data = true;
          }
        }
        break;
      }
      case "quit-application": {
        this.uninitialize();
        break;
      }
      case "dlp-request-made":
        {
          const request = aSubj.QueryInterface(Ci.nsIContentAnalysisRequest);
          if (!request) {
            console.error(
              "Showing in-browser Content Analysis notification but no request was passed"
            );
            return;
          }
          const analysisType = request.analysisType;
          // For operations that block browser interaction, show the "slow content analysis"
          // dialog faster
          let slowTimeoutMs = this._shouldShowBlockingNotification(analysisType)
            ? this._SLOW_DLP_NOTIFICATION_BLOCKING_TIMEOUT_MS
            : this._SLOW_DLP_NOTIFICATION_NONBLOCKING_TIMEOUT_MS;
          let browsingContext = request.windowGlobalParent?.browsingContext;
          if (!browsingContext) {
            throw new Error(
              "Got dlp-request-made message but couldn't find a browsingContext!"
            );
          }

          // Start timer that, when it expires,
          // presents a "slow CA check" message.
          // Note that there should only be one DLP request
          // at a time per browsingContext (since we block the UI and
          // the content process waits synchronously for the result).
          if (this.dlpBusyViewsByTopBrowsingContext.hasEntry(browsingContext)) {
            throw new Error(
              "Got dlp-request-made message for a browsingContext that already has a busy view!"
            );
          }
          let resourceNameOrOperationType =
            this._getResourceNameOrOperationTypeFromRequest(request, false);
          this.requestTokenToRequestInfo.set(request.requestToken, {
            browsingContext,
            resourceNameOrOperationType,
          });
          this.dlpBusyViewsByTopBrowsingContext.setEntry(browsingContext, {
            timer: lazy.setTimeout(() => {
              this.dlpBusyViewsByTopBrowsingContext.setEntry(browsingContext, {
                notification: this._showSlowCAMessage(
                  analysisType,
                  request,
                  resourceNameOrOperationType,
                  browsingContext
                ),
                request,
              });
            }, slowTimeoutMs),
            request,
          });
        }
        break;
      case "dlp-response": {
        const request = aSubj.QueryInterface(Ci.nsIContentAnalysisResponse);
        // Cancels timer or slow message UI,
        // if present, and possibly presents the CA verdict.
        if (!request) {
          throw new Error("Got dlp-response message but no request was passed");
        }

        let windowAndResourceNameOrOperationType =
          this.requestTokenToRequestInfo.get(request.requestToken);
        if (!windowAndResourceNameOrOperationType) {
          // Perhaps this was cancelled just before the response came in from the
          // DLP agent.
          console.warn(
            `Got dlp-response message with unknown token ${request.requestToken}`
          );
          return;
        }
        this.requestTokenToRequestInfo.delete(request.requestToken);
        let dlpBusyView = this.dlpBusyViewsByTopBrowsingContext.getEntry(
          windowAndResourceNameOrOperationType.browsingContext
        );
        if (dlpBusyView) {
          this._disconnectFromView(dlpBusyView);
          this.dlpBusyViewsByTopBrowsingContext.deleteEntry(
            windowAndResourceNameOrOperationType.browsingContext
          );
        }
        const responseResult =
          request?.action ?? Ci.nsIContentAnalysisResponse.eUnspecified;
        await this._showCAResult(
          windowAndResourceNameOrOperationType.resourceNameOrOperationType,
          windowAndResourceNameOrOperationType.browsingContext,
          request.requestToken,
          responseResult,
          request.cancelError
        );
        this._showAnotherPendingDialog(
          windowAndResourceNameOrOperationType.browsingContext
        );
        break;
      }
    }
  },

  async showPanel(element, panelUI) {
    element.ownerDocument.l10n.setAttributes(
      lazy.PanelMultiView.getViewNode(
        element.ownerDocument,
        "content-analysis-panel-description"
      ),
      "content-analysis-panel-text",
      { agentName: lazy.agentName }
    );
    panelUI.showSubView("content-analysis-panel", element);
  },

  _showAnotherPendingDialog(aBrowsingContext) {
    const otherBrowsingContext =
      this.dlpBusyViewsByTopBrowsingContext.getBrowsingContextWithPendingNotification(
        aBrowsingContext
      );
    if (otherBrowsingContext) {
      const args =
        this.dlpBusyViewsByTopBrowsingContext.getEntry(otherBrowsingContext);
      this.dlpBusyViewsByTopBrowsingContext.setEntry(otherBrowsingContext, {
        notification: this._showSlowCABlockingMessage(
          otherBrowsingContext,
          args.requestToken,
          args.resourceNameOrOperationType
        ),
        request: args.request,
      });
    }
  },

  _disconnectFromView(caView) {
    if (!caView) {
      return;
    }
    if (caView.timer) {
      lazy.clearTimeout(caView.timer);
    } else if (caView.notification) {
      if (caView.notification.close) {
        // native notification
        caView.notification.close();
      } else if (caView.notification.dialogBrowsingContext) {
        // in-browser notification
        let browser =
          caView.notification.dialogBrowsingContext.top.embedderElement;
        // browser will be null if the tab was closed
        let win = browser?.ownerGlobal;
        if (win) {
          let dialogBox = win.gBrowser.getTabDialogBox(browser);
          // Don't close any content-modal dialogs, because we could be doing
          // content analysis on something like a prompt() call.
          dialogBox.getTabDialogManager().abortDialogs();
        }
      } else {
        console.error(
          "Unexpected content analysis notification - can't close it!"
        );
      }
    }
  },

  _showMessage(aMessage, aBrowsingContext, aTimeout = 0) {
    if (this._SHOW_DIALOGS) {
      Services.prompt.asyncAlert(
        aBrowsingContext,
        Ci.nsIPrompt.MODAL_TYPE_WINDOW,
        this.l10n.formatValueSync("contentanalysis-alert-title"),
        aMessage
      );
    }

    if (this._SHOW_NOTIFICATIONS) {
      let topWindow =
        aBrowsingContext.topChromeWindow ??
        aBrowsingContext.embedderWindowGlobal.browsingContext.topChromeWindow;
      const notification = new topWindow.Notification(
        this.l10n.formatValueSync("contentanalysis-notification-title"),
        {
          body: aMessage,
          silent: lazy.silentNotifications,
        }
      );

      if (aTimeout != 0) {
        lazy.setTimeout(() => {
          notification.close();
        }, aTimeout);
      }
      return notification;
    }

    return null;
  },

  _shouldShowBlockingNotification(aAnalysisType) {
    return !(
      aAnalysisType == Ci.nsIContentAnalysisRequest.eFileDownloaded ||
      aAnalysisType == Ci.nsIContentAnalysisRequest.ePrint
    );
  },

  // This function also transforms the nameOrOperationType so we won't have to
  // look it up again.
  _getResourceNameFromNameOrOperationType(nameOrOperationType) {
    if (!nameOrOperationType.name) {
      let l10nId = undefined;
      switch (nameOrOperationType.operationType) {
        case Ci.nsIContentAnalysisRequest.eClipboard:
          l10nId = "contentanalysis-operationtype-clipboard";
          break;
        case Ci.nsIContentAnalysisRequest.eDroppedText:
          l10nId = "contentanalysis-operationtype-dropped-text";
          break;
        case Ci.nsIContentAnalysisRequest.eOperationPrint:
          l10nId = "contentanalysis-operationtype-print";
          break;
      }
      if (!l10nId) {
        console.error(
          "Unknown operationTypeForDisplay: " +
            nameOrOperationType.operationType
        );
        return "";
      }
      nameOrOperationType.name = this.l10n.formatValueSync(l10nId);
    }
    return nameOrOperationType.name;
  },

  /**
   * Gets a name or operation type from a request
   *
   * @param {object} aRequest The nsIContentAnalysisRequest
   * @param {boolean} aStandalone Whether the message is going to be used on its own
   *                              line. This is used to add more context to the message
   *                              if a file is being uploaded rather than just the name
   *                              of the file.
   * @returns {object} An object with either a name property that can be used as-is, or
   *                   an operationType property.
   */
  _getResourceNameOrOperationTypeFromRequest(aRequest, aStandalone) {
    if (
      aRequest.operationTypeForDisplay ==
      Ci.nsIContentAnalysisRequest.eCustomDisplayString
    ) {
      if (aStandalone) {
        return {
          name: this.l10n.formatValueSync(
            "contentanalysis-customdisplaystring-description",
            {
              filename: aRequest.operationDisplayString,
            }
          ),
        };
      }
      return { name: aRequest.operationDisplayString };
    }
    return { operationType: aRequest.operationTypeForDisplay };
  },

  /**
   * Show a message to the user to indicate that a CA request is taking
   * a long time.
   */
  _showSlowCAMessage(
    aOperation,
    aRequest,
    aResourceNameOrOperationType,
    aBrowsingContext
  ) {
    if (!this._shouldShowBlockingNotification(aOperation)) {
      return this._showMessage(
        this._getSlowDialogMessage(aResourceNameOrOperationType),
        aBrowsingContext
      );
    }

    if (!aRequest) {
      throw new Error(
        "Showing in-browser Content Analysis notification but no request was passed"
      );
    }

    if (
      this.dlpBusyViewsByTopBrowsingContext.hasEntryDisplayingNotification(
        aBrowsingContext
      )
    ) {
      // This tab already has a frame displaying a "DLP in progress" message, so we can't
      // show another one right now. Record the arguments we will need to show another
      // "DLP in progress" message when the existing message goes away.
      return {
        requestToken: aRequest.requestToken,
        dialogBrowsingContextArgs: {
          resourceNameOrOperationType: aResourceNameOrOperationType,
        },
      };
    }

    return this._showSlowCABlockingMessage(
      aBrowsingContext,
      aRequest.requestToken,
      aResourceNameOrOperationType
    );
  },

  _getSlowDialogMessage(aResourceNameOrOperationType) {
    if (aResourceNameOrOperationType.name) {
      return this.l10n.formatValueSync(
        "contentanalysis-slow-agent-dialog-body-file",
        {
          agent: lazy.agentName,
          filename: aResourceNameOrOperationType.name,
        }
      );
    }
    let l10nId = undefined;
    switch (aResourceNameOrOperationType.operationType) {
      case Ci.nsIContentAnalysisRequest.eClipboard:
        l10nId = "contentanalysis-slow-agent-dialog-body-clipboard";
        break;
      case Ci.nsIContentAnalysisRequest.eDroppedText:
        l10nId = "contentanalysis-slow-agent-dialog-body-dropped-text";
        break;
      case Ci.nsIContentAnalysisRequest.eOperationPrint:
        l10nId = "contentanalysis-slow-agent-dialog-body-print";
        break;
    }
    if (!l10nId) {
      console.error(
        "Unknown operationTypeForDisplay: ",
        aResourceNameOrOperationType
      );
      return "";
    }
    return this.l10n.formatValueSync(l10nId, {
      agent: lazy.agentName,
    });
  },

  _getErrorDialogMessage(aResourceNameOrOperationType) {
    if (aResourceNameOrOperationType.name) {
      return this.l10n.formatValueSync(
        "contentanalysis-error-message-upload-file",
        {
          filename: aResourceNameOrOperationType.name,
        }
      );
    }
    let l10nId = undefined;
    switch (aResourceNameOrOperationType.operationType) {
      case Ci.nsIContentAnalysisRequest.eClipboard:
        l10nId = "contentanalysis-error-message-clipboard";
        break;
      case Ci.nsIContentAnalysisRequest.eDroppedText:
        l10nId = "contentanalysis-error-message-dropped-text";
        break;
      case Ci.nsIContentAnalysisRequest.eOperationPrint:
        l10nId = "contentanalysis-error-message-print";
        break;
    }
    if (!l10nId) {
      console.error(
        "Unknown operationTypeForDisplay: ",
        aResourceNameOrOperationType
      );
      return "";
    }
    return this.l10n.formatValueSync(l10nId);
  },
  _showSlowCABlockingMessage(
    aBrowsingContext,
    aRequestToken,
    aResourceNameOrOperationType
  ) {
    let bodyMessage = this._getSlowDialogMessage(aResourceNameOrOperationType);
    let promise = Services.prompt.asyncConfirmEx(
      aBrowsingContext,
      Ci.nsIPromptService.MODAL_TYPE_TAB,
      this.l10n.formatValueSync("contentanalysis-slow-agent-dialog-header"),
      bodyMessage,
      Ci.nsIPromptService.BUTTON_POS_0 *
        Ci.nsIPromptService.BUTTON_TITLE_CANCEL +
        Ci.nsIPromptService.BUTTON_POS_1_DEFAULT +
        Ci.nsIPromptService.SHOW_SPINNER,
      null,
      null,
      null,
      null,
      false
    );
    promise
      .catch(() => {
        // need a catch clause to avoid an unhandled JS exception
        // when we programmatically close the dialog.
        // Since this only happens when we are programmatically closing
        // the dialog, no need to log the exception.
      })
      .finally(() => {
        // This is also be called if the tab/window is closed while a request is in progress,
        // in which case we need to cancel the request.
        if (this.requestTokenToRequestInfo.delete(aRequestToken)) {
          lazy.gContentAnalysis.cancelContentAnalysisRequest(aRequestToken);
          let dlpBusyView =
            this.dlpBusyViewsByTopBrowsingContext.getEntry(aBrowsingContext);
          if (dlpBusyView) {
            this._disconnectFromView(dlpBusyView);
            this.dlpBusyViewsByTopBrowsingContext.deleteEntry(aBrowsingContext);
          }
        }
      });
    return {
      requestToken: aRequestToken,
      dialogBrowsingContext: aBrowsingContext,
    };
  },

  /**
   * Show a message to the user to indicate the result of a CA request.
   *
   * @returns {object} a notification object (if shown)
   */
  async _showCAResult(
    aResourceNameOrOperationType,
    aBrowsingContext,
    aRequestToken,
    aCAResult,
    aRequestCancelError
  ) {
    let message = null;
    let timeoutMs = 0;

    switch (aCAResult) {
      case Ci.nsIContentAnalysisResponse.eAllow:
        // We don't need to show anything
        return null;
      case Ci.nsIContentAnalysisResponse.eReportOnly:
        message = await this.l10n.formatValue(
          "contentanalysis-genericresponse-message",
          {
            content: this._getResourceNameFromNameOrOperationType(
              aResourceNameOrOperationType
            ),
            response: "REPORT_ONLY",
          }
        );
        timeoutMs = this._RESULT_NOTIFICATION_FAST_TIMEOUT_MS;
        break;
      case Ci.nsIContentAnalysisResponse.eWarn: {
        const result = await Services.prompt.asyncConfirmEx(
          aBrowsingContext,
          Ci.nsIPromptService.MODAL_TYPE_TAB,
          await this.l10n.formatValue("contentanalysis-warndialogtitle"),
          await this.l10n.formatValue("contentanalysis-warndialogtext", {
            content: this._getResourceNameFromNameOrOperationType(
              aResourceNameOrOperationType
            ),
          }),
          Ci.nsIPromptService.BUTTON_POS_0 *
            Ci.nsIPromptService.BUTTON_TITLE_IS_STRING +
            Ci.nsIPromptService.BUTTON_POS_1 *
              Ci.nsIPromptService.BUTTON_TITLE_IS_STRING +
            Ci.nsIPromptService.BUTTON_POS_2_DEFAULT,
          await this.l10n.formatValue(
            "contentanalysis-warndialog-response-allow"
          ),
          await this.l10n.formatValue(
            "contentanalysis-warndialog-response-deny"
          ),
          null,
          null,
          {}
        );
        const allow = result.get("buttonNumClicked") === 0;
        lazy.gContentAnalysis.respondToWarnDialog(aRequestToken, allow);
        return null;
      }
      case Ci.nsIContentAnalysisResponse.eBlock: {
        if (!lazy.showBlockedResult) {
          // Don't show anything
          return null;
        }
        let titleId = undefined;
        let body = undefined;
        if (aResourceNameOrOperationType.name) {
          titleId = "contentanalysis-block-dialog-title-upload-file";
          body = this.l10n.formatValueSync(
            "contentanalysis-block-dialog-body-upload-file",
            {
              filename: aResourceNameOrOperationType.name,
            }
          );
        } else {
          let bodyId = undefined;
          switch (aResourceNameOrOperationType.operationType) {
            case Ci.nsIContentAnalysisRequest.eClipboard:
              titleId = "contentanalysis-block-dialog-title-clipboard";
              bodyId = "contentanalysis-block-dialog-body-clipboard";
              break;
            case Ci.nsIContentAnalysisRequest.eDroppedText:
              titleId = "contentanalysis-block-dialog-title-dropped-text";
              bodyId = "contentanalysis-block-dialog-body-dropped-text";
              break;
            case Ci.nsIContentAnalysisRequest.eOperationPrint:
              titleId = "contentanalysis-block-dialog-title-print";
              bodyId = "contentanalysis-block-dialog-body-print";
              break;
          }
          if (!titleId || !bodyId) {
            console.error(
              "Unknown operationTypeForDisplay: ",
              aResourceNameOrOperationType
            );
            return null;
          }
          body = this.l10n.formatValueSync(bodyId);
        }
        Services.prompt.alertBC(
          aBrowsingContext,
          Ci.nsIPromptService.MODAL_TYPE_TAB,
          this.l10n.formatValueSync(titleId),
          body
        );
        return null;
      }
      case Ci.nsIContentAnalysisResponse.eUnspecified:
        message = await this.l10n.formatValue(
          "contentanalysis-unspecified-error-message-content",
          {
            agent: lazy.agentName,
            content: this._getErrorDialogMessage(aResourceNameOrOperationType),
          }
        );
        timeoutMs = this._RESULT_NOTIFICATION_TIMEOUT_MS;
        break;
      case Ci.nsIContentAnalysisResponse.eCanceled:
        {
          let messageId;
          switch (aRequestCancelError) {
            case Ci.nsIContentAnalysisResponse.eUserInitiated:
              console.error(
                "Got unexpected cancel response with eUserInitiated"
              );
              return null;
            case Ci.nsIContentAnalysisResponse.eNoAgent:
              messageId = "contentanalysis-no-agent-connected-message-content";
              break;
            case Ci.nsIContentAnalysisResponse.eInvalidAgentSignature:
              messageId =
                "contentanalysis-invalid-agent-signature-message-content";
              break;
            case Ci.nsIContentAnalysisResponse.eErrorOther:
              messageId = "contentanalysis-unspecified-error-message-content";
              break;
            default:
              console.error(
                "Unexpected CA cancelError value: " + aRequestCancelError
              );
              messageId = "contentanalysis-unspecified-error-message-content";
              break;
          }
          message = await this.l10n.formatValue(messageId, {
            agent: lazy.agentName,
            content: this._getErrorDialogMessage(aResourceNameOrOperationType),
          });
          timeoutMs = this._RESULT_NOTIFICATION_TIMEOUT_MS;
        }
        break;
      default:
        throw new Error("Unexpected CA result value: " + aCAResult);
    }

    if (!message) {
      console.error(
        "_showCAResult did not get a message populated for result value " +
          aCAResult
      );
      return null;
    }

    return this._showMessage(message, aBrowsingContext, timeoutMs);
  },
};
