forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			887 lines
		
	
	
	
		
			29 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			887 lines
		
	
	
	
		
			29 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* -*- 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);
 | |
|   },
 | |
| };
 | 
