ContentAnalysis.sys.mjs (39677B)
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2 /* vim: set ts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 // @ts-check 7 8 /** 9 * Contains elements of the Content Analysis UI, which are integrated into 10 * various browser behaviors (uploading, downloading, printing, etc) that 11 * require content analysis to be done. 12 * The content analysis itself is done by the clients of this script, who 13 * use nsIContentAnalysis to talk to the external CA system. 14 */ 15 16 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 17 18 const lazy = {}; 19 let internalContentAnalysisService = undefined; 20 21 ChromeUtils.defineESModuleGetters(lazy, { 22 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", 23 clearTimeout: "resource://gre/modules/Timer.sys.mjs", 24 PanelMultiView: 25 "moz-src:///browser/components/customizableui/PanelMultiView.sys.mjs", 26 setTimeout: "resource://gre/modules/Timer.sys.mjs", 27 }); 28 29 XPCOMUtils.defineLazyPreferenceGetter( 30 lazy, 31 "silentNotifications", 32 "browser.contentanalysis.silent_notifications", 33 false 34 ); 35 36 XPCOMUtils.defineLazyPreferenceGetter( 37 lazy, 38 "agentName", 39 "browser.contentanalysis.agent_name", 40 "A DLP agent" 41 ); 42 43 XPCOMUtils.defineLazyPreferenceGetter( 44 lazy, 45 "showBlockedResult", 46 "browser.contentanalysis.show_blocked_result", 47 true 48 ); 49 50 export const ContentAnalysis = { 51 _SHOW_NOTIFICATIONS: true, 52 53 _SHOW_DIALOGS: false, 54 55 _SLOW_DLP_NOTIFICATION_BLOCKING_TIMEOUT_MS: 250, 56 57 _SLOW_DLP_NOTIFICATION_NONBLOCKING_TIMEOUT_MS: 3 * 1000, 58 59 _RESULT_NOTIFICATION_TIMEOUT_MS: 5 * 60 * 1000, // 5 min 60 61 _RESULT_NOTIFICATION_FAST_TIMEOUT_MS: 60 * 1000, // 1 min 62 63 PROMPTID_PREFIX: "ContentAnalysisSlowDialog-", 64 65 isInitialized: false, 66 67 /** 68 * @typedef {object} NotificationInfo - information about the busy dialog itself that is showing 69 * @property {*} [close] - Method to close the native notification 70 * @property {BrowsingContext} [dialogBrowsingContext] - browsing context where the 71 * confirm() dialog is shown 72 */ 73 74 /** 75 * @typedef {object} BusyDialogInfo - information about a busy dialog that is either showing or will 76 * will be shown after a delay. 77 * @property {string} userActionId - The userActionId of the request 78 * @property {Set<string>} requestTokenSet - The set of requestTokens associated with the userActionId 79 * @property {*} [timer] - Result of a setTimeout() call that can be used to cancel the showing of the busy 80 * dialog if it has not been displayed yet. 81 * @property {NotificationInfo} [notification] - Information about the busy dialog that is being shown. 82 */ 83 84 /** 85 * @type {Map<string, BusyDialogInfo>} 86 * 87 * Maps string UserActionId to info about the busy dialog. 88 */ 89 userActionToBusyDialogMap: new Map(), 90 91 /** 92 * @typedef {object} ResourceNameOrOperationType 93 * @property {string} [name] - the name of the resource 94 * @property {number} [operationType] - the type of operation 95 */ 96 97 /** 98 * @typedef {object} RequestInfo 99 * @property {CanonicalBrowsingContext?} browsingContext - browsing context where the request was sent from 100 * @property {ResourceNameOrOperationType} resourceNameOrOperationType - name of the operation 101 */ 102 103 /** 104 * @type {Map<string, RequestInfo>} 105 */ 106 requestTokenToRequestInfo: new Map(), 107 108 /** 109 * @type {Set<string>} 110 */ 111 warnDialogRequestTokens: new Set(), 112 113 /** 114 * The nsIContentAnalysis to use instead of lazy.gContentAnalysis. Should 115 * only be used for tests. 116 * 117 * @type {nsIContentAnalysis?} 118 */ 119 mockContentAnalysisForTest: undefined, 120 121 /** 122 * The nsIContentAnalysis to use. Nothing else in this file should 123 * use lazy.gContentAnalysis. 124 * 125 * @returns {nsIContentAnalysis} 126 */ 127 get contentAnalysis() { 128 if (this.mockContentAnalysisForTest) { 129 return this.mockContentAnalysisForTest; 130 } 131 if (!internalContentAnalysisService) { 132 internalContentAnalysisService = Cc[ 133 "@mozilla.org/contentanalysis;1" 134 ].getService(Ci.nsIContentAnalysis); 135 } 136 return internalContentAnalysisService; 137 }, 138 139 /** 140 * Sets the nsIContentAnalysis to use. Should only be used for tests. 141 * 142 * @param {nsIContentAnalysis?} contentAnalysis 143 */ 144 setMockContentAnalysisForTest(contentAnalysis) { 145 this.mockContentAnalysisForTest = contentAnalysis; 146 }, 147 148 /** 149 * Registers for various messages/events that will indicate the 150 * need for communicating something to the user. 151 * 152 * @param {Window} window - The window to monitor 153 */ 154 initialize(window) { 155 if (!this.contentAnalysis.isActive) { 156 this.uninitialize(); 157 return; 158 } 159 let doc = window.document; 160 if (!this.isInitialized) { 161 this.isInitialized = true; 162 this.initializeObservers(); 163 164 ChromeUtils.defineLazyGetter(this, "l10n", function () { 165 return new Localization( 166 ["branding/brand.ftl", "toolkit/contentanalysis/contentanalysis.ftl"], 167 true 168 ); 169 }); 170 } 171 172 // Do this even if initialized so the icon shows up on new windows, not just the 173 // first one. 174 for (let indicator of doc.getElementsByClassName( 175 "content-analysis-indicator" 176 )) { 177 doc.l10n.setAttributes(indicator, "content-analysis-indicator-tooltip", { 178 agentName: lazy.agentName, 179 }); 180 } 181 doc.documentElement.setAttribute("contentanalysisactive", "true"); 182 }, 183 184 async uninitialize() { 185 if (this.isInitialized) { 186 this.isInitialized = false; 187 this.requestTokenToRequestInfo.clear(); 188 this.userActionToBusyDialogMap.clear(); 189 this.uninitializeObservers(); 190 } 191 }, 192 193 /** 194 * Register UI for CA events. 195 */ 196 initializeObservers() { 197 Services.obs.addObserver(this, "dlp-request-made"); 198 Services.obs.addObserver(this, "dlp-response"); 199 Services.obs.addObserver(this, "quit-application"); 200 Services.obs.addObserver(this, "quit-application-granted"); 201 Services.obs.addObserver(this, "quit-application-requested"); 202 }, 203 204 /** 205 * Unregister UI for CA events. 206 */ 207 uninitializeObservers() { 208 Services.obs.removeObserver(this, "dlp-request-made"); 209 Services.obs.removeObserver(this, "dlp-response"); 210 Services.obs.removeObserver(this, "quit-application"); 211 Services.obs.removeObserver(this, "quit-application-granted"); 212 Services.obs.removeObserver(this, "quit-application-requested"); 213 }, 214 215 // nsIObserver 216 async observe(aSubj, aTopic, _aData) { 217 switch (aTopic) { 218 case "quit-application-requested": { 219 if (aSubj.data) { 220 // something has already cancelled the quit operation, 221 // so we don't need to do anything. 222 return; 223 } 224 let pendingRequestInfos = this._getAllSlowCARequestInfos(); 225 let requestDescriptions = Array.from( 226 pendingRequestInfos.flatMap(info => 227 info 228 ? [ 229 this._getResourceNameFromNameOrOperationType( 230 info.resourceNameOrOperationType 231 ), 232 ] 233 : [] 234 ) 235 ); 236 if (!requestDescriptions.length) { 237 return; 238 } 239 let messageBody = this.l10n.formatValueSync( 240 "contentanalysis-inprogress-quit-message" 241 ); 242 messageBody = messageBody + "\n\n"; 243 messageBody += requestDescriptions.join("\n"); 244 let buttonSelected = Services.prompt.confirmEx( 245 null, 246 this.l10n.formatValueSync("contentanalysis-inprogress-quit-title"), 247 messageBody, 248 Ci.nsIPromptService.BUTTON_POS_0 * 249 Ci.nsIPromptService.BUTTON_TITLE_IS_STRING + 250 Ci.nsIPromptService.BUTTON_POS_1 * 251 Ci.nsIPromptService.BUTTON_TITLE_CANCEL + 252 Ci.nsIPromptService.BUTTON_POS_0_DEFAULT, 253 this.l10n.formatValueSync( 254 "contentanalysis-inprogress-quit-yesbutton" 255 ), 256 null, 257 null, 258 null, 259 { value: false } 260 ); 261 if (buttonSelected === 1) { 262 // Cancel the quit operation 263 aSubj.data = true; 264 } else { 265 // Ideally we would wait until "quit-application" to cancel outstanding 266 // DLP requests, but the "DLP busy" or "DLP blocked" dialog can block the 267 // main thread, thus preventing the "quit-application" from being sent, 268 // which causes a shutdownhang. (bug 1899703) 269 this.contentAnalysis.cancelAllRequests(true); 270 } 271 break; 272 } 273 // Note that we do this in quit-application-granted instead of quit-application 274 // because otherwise we can get a shutdownhang if WARN dialogs are showing and 275 // the user quits via keyboard or the hamburger menu (bug 1959966) 276 case "quit-application-granted": { 277 // We're quitting, so respond false to all WARN dialogs. 278 let requestTokensToCancel = this.warnDialogRequestTokens; 279 // Clear this first so the handler showing the dialog will know not 280 // to call respondToWarnDialog() again. 281 this.warnDialogRequestTokens = new Set(); 282 for (let warnDialogRequestToken of requestTokensToCancel) { 283 this.contentAnalysis.respondToWarnDialog( 284 warnDialogRequestToken, 285 false 286 ); 287 } 288 break; 289 } 290 case "quit-application": { 291 this.uninitialize(); 292 break; 293 } 294 case "dlp-request-made": 295 { 296 const request = aSubj.QueryInterface(Ci.nsIContentAnalysisRequest); 297 if (!request) { 298 console.error( 299 "Showing in-browser Content Analysis notification but no request was passed" 300 ); 301 return; 302 } 303 let browsingContext = request.windowGlobalParent?.browsingContext; 304 if ( 305 !browsingContext && 306 request.operationTypeForDisplay !== 307 Ci.nsIContentAnalysisRequest.eDownload 308 ) { 309 throw new Error( 310 "Got dlp-request-made message but couldn't find a browsingContext!" 311 ); 312 } 313 314 // Start timer that, when it expires, 315 // presents a "slow CA check" message. 316 let resourceNameOrOperationType = 317 this._getResourceNameOrOperationTypeFromRequest(request, false); 318 this.requestTokenToRequestInfo.set(request.requestToken, { 319 browsingContext, 320 resourceNameOrOperationType, 321 }); 322 this._queueSlowCAMessage( 323 request, 324 resourceNameOrOperationType, 325 browsingContext 326 ); 327 } 328 break; 329 case "dlp-response": { 330 const response = aSubj.QueryInterface(Ci.nsIContentAnalysisResponse); 331 // Cancels timer or slow message UI, 332 // if present, and possibly presents the CA verdict. 333 if (!response) { 334 throw new Error( 335 "Got dlp-response message but no response object was passed" 336 ); 337 } 338 339 let windowAndResourceNameOrOperationType = 340 this.requestTokenToRequestInfo.get(response.requestToken); 341 if (!windowAndResourceNameOrOperationType) { 342 // We may get multiple responses, for example, if we are blocked or 343 // canceled after receiving our verdict because we were part of a 344 // multipart transaction. Just ignore that. 345 console.warn( 346 `Got dlp-response message with unknown token ${response.requestToken} | action: ${response.action}` 347 ); 348 return; 349 } 350 this.requestTokenToRequestInfo.delete(response.requestToken); 351 this._removeSlowCAMessage(response.userActionId, response.requestToken); 352 if ( 353 windowAndResourceNameOrOperationType.resourceNameOrOperationType 354 ?.operationType === Ci.nsIContentAnalysisRequest.eDownload 355 ) { 356 // Don't show warn/block/error dialogs for downloads; they're shown 357 // inside the downloads panel. 358 return; 359 } 360 const responseResult = 361 response?.action ?? Ci.nsIContentAnalysisResponse.eUnspecified; 362 // Don't show dialog if this is a cached response 363 if (!response?.isCachedResponse) { 364 await this._showCAResult( 365 windowAndResourceNameOrOperationType.resourceNameOrOperationType, 366 windowAndResourceNameOrOperationType.browsingContext, 367 response.requestToken, 368 response.userActionId, 369 responseResult, 370 response.isSyntheticResponse, 371 response.cancelError 372 ); 373 } 374 break; 375 } 376 } 377 }, 378 379 /** 380 * Shows the panel that indicates that DLP is active. 381 * 382 * @param {Element} element The toolbarbutton the user has clicked on 383 * @param {*} panelUI Maintains state for the main menu panel 384 */ 385 async showPanel(element, panelUI) { 386 element.ownerDocument.l10n.setAttributes( 387 lazy.PanelMultiView.getViewNode( 388 element.ownerDocument, 389 "content-analysis-panel-description" 390 ), 391 "content-analysis-panel-text-styled", 392 { agentName: lazy.agentName } 393 ); 394 panelUI.showSubView("content-analysis-panel", element); 395 }, 396 397 /** 398 * Closes a busy dialog 399 * 400 * @param {BusyDialogInfo?} caView - the busy dialog to close 401 */ 402 _disconnectFromView(caView) { 403 if (!caView) { 404 return; 405 } 406 if (caView.timer) { 407 lazy.clearTimeout(caView.timer); 408 } else if (caView.notification) { 409 if (caView.notification.close) { 410 // native notification 411 caView.notification.close(); 412 } else if (caView.notification.dialogBrowsingContext) { 413 // in-browser notification 414 let browser = 415 caView.notification.dialogBrowsingContext.top.embedderElement; 416 // If we're showing a dialog in the sidebar, the dialog is managed 417 // by the embedderElement. 418 let isSidebar = 419 browser?.ownerGlobal?.browsingContext?.embedderElement?.id == 420 "sidebar"; 421 if (isSidebar) { 422 browser = browser.ownerGlobal.browsingContext.embedderElement; 423 } 424 // browser will be null if the tab was closed 425 let win = browser?.ownerGlobal; 426 if (win) { 427 let dialogBox = win.gBrowser.getTabDialogBox(browser); 428 // Just close the dialog associated with this CA request. 429 dialogBox.getTabDialogManager().abortDialogs(dialog => { 430 return ( 431 dialog.promptID == this.PROMPTID_PREFIX + caView.userActionId 432 ); 433 }); 434 } 435 } else { 436 console.error( 437 "Unexpected content analysis notification - can't close it!" 438 ); 439 } 440 } 441 }, 442 443 /** 444 * Shows either a dialog or native notification or both, depending on the values of 445 * _SHOW_DIALOGS and _SHOW_NOTIFICATIONS. 446 * 447 * @param {string} aMessage - Message to show 448 * @param {CanonicalBrowsingContext?} aBrowsingContext - BrowsingContext to show the dialog in. If 449 * null, the top browsing context will be used for native notifications. 450 * @param {number} aTimeout - timeout for closing the native notification. 0 indicates it is 451 * not automatically closed. 452 * @returns {NotificationInfo?} - information about the native notification, if it has been shown. 453 */ 454 _showMessage(aMessage, aBrowsingContext, aTimeout = 0) { 455 if (this._SHOW_DIALOGS) { 456 Services.prompt.asyncAlert( 457 aBrowsingContext, 458 Ci.nsIPrompt.MODAL_TYPE_WINDOW, 459 this.l10n.formatValueSync("contentanalysis-alert-title"), 460 aMessage 461 ); 462 } 463 464 if (this._SHOW_NOTIFICATIONS) { 465 // Downloading as a "save as" operation does not provide a browsing context, 466 // so use the the top window in that case. 467 let topWindow = 468 aBrowsingContext?.topChromeWindow ?? 469 aBrowsingContext?.embedderWindowGlobal.browsingContext 470 .topChromeWindow ?? 471 lazy.BrowserWindowTracker.getTopWindow({ 472 allowFromInactiveWorkspace: true, 473 }); 474 if (!topWindow) { 475 console.error( 476 "Unable to get window to show Content Analysis notification for." 477 ); 478 return null; 479 } 480 const notification = new topWindow.Notification( 481 this.l10n.formatValueSync("contentanalysis-notification-title"), 482 { body: aMessage, silent: lazy.silentNotifications } 483 ); 484 485 if (aTimeout != 0) { 486 lazy.setTimeout(() => { 487 notification.close(); 488 }, aTimeout); 489 } 490 return notification; 491 } 492 493 return null; 494 }, 495 496 /** 497 * Whether the notification should block browser interaction. 498 * 499 * @param {nsIContentAnalysisRequest.AnalysisType} aAnalysisType The type of DLP analysis being done. 500 * @returns {boolean} 501 */ 502 _shouldShowBlockingNotification(aAnalysisType) { 503 return !( 504 aAnalysisType == Ci.nsIContentAnalysisRequest.eFileDownloaded || 505 aAnalysisType == Ci.nsIContentAnalysisRequest.ePrint 506 ); 507 }, 508 509 /** 510 * This function also transforms the nameOrOperationType so we won't have to 511 * look it up again. 512 * 513 * @param {ResourceNameOrOperationType} nameOrOperationType 514 * @returns {string} 515 */ 516 _getResourceNameFromNameOrOperationType(nameOrOperationType) { 517 if (!nameOrOperationType.name) { 518 let l10nId = undefined; 519 switch (nameOrOperationType.operationType) { 520 case Ci.nsIContentAnalysisRequest.eClipboard: 521 l10nId = "contentanalysis-operationtype-clipboard"; 522 break; 523 case Ci.nsIContentAnalysisRequest.eDroppedText: 524 l10nId = "contentanalysis-operationtype-dropped-text"; 525 break; 526 case Ci.nsIContentAnalysisRequest.eOperationPrint: 527 l10nId = "contentanalysis-operationtype-print"; 528 break; 529 } 530 if (!l10nId) { 531 console.error( 532 "Unknown operationTypeForDisplay: " + 533 nameOrOperationType.operationType 534 ); 535 return ""; 536 } 537 nameOrOperationType.name = this.l10n.formatValueSync(l10nId); 538 } 539 return nameOrOperationType.name; 540 }, 541 542 /** 543 * Gets a name or operation type from a request 544 * 545 * @param {nsIContentAnalysisRequest} aRequest The nsIContentAnalysisRequest 546 * @param {boolean} aStandalone Whether the message is going to be used on its own 547 * line. This is used to add more context to the message 548 * if a file is being uploaded or downloaded rather than 549 * just the name of the file. 550 * @returns {ResourceNameOrOperationType} 551 */ 552 _getResourceNameOrOperationTypeFromRequest(aRequest, aStandalone) { 553 /** 554 * @type {ResourceNameOrOperationType} 555 */ 556 let nameOrOperationType = { 557 operationType: aRequest.operationTypeForDisplay, 558 }; 559 if ( 560 aRequest.operationTypeForDisplay == Ci.nsIContentAnalysisRequest.eUpload 561 ) { 562 if (aStandalone) { 563 nameOrOperationType.name = this.l10n.formatValueSync( 564 "contentanalysis-upload-description", 565 { filename: aRequest.fileNameForDisplay } 566 ); 567 } else { 568 nameOrOperationType.name = aRequest.fileNameForDisplay; 569 } 570 } else if ( 571 aRequest.operationTypeForDisplay == Ci.nsIContentAnalysisRequest.eDownload 572 ) { 573 if (aStandalone) { 574 nameOrOperationType.name = this.l10n.formatValueSync( 575 "contentanalysis-download-description", 576 { filename: aRequest.fileNameForDisplay } 577 ); 578 } else { 579 nameOrOperationType.name = aRequest.fileNameForDisplay; 580 } 581 } 582 return nameOrOperationType; 583 }, 584 585 /** 586 * Sets up an "operation is in progress" dialog to be shown after a delay, 587 * unless one is already showing for this userActionId. 588 * 589 * @param {nsIContentAnalysisRequest} aRequest 590 * @param {ResourceNameOrOperationType} aResourceNameOrOperationType 591 * @param {CanonicalBrowsingContext?} aBrowsingContext 592 */ 593 _queueSlowCAMessage( 594 aRequest, 595 aResourceNameOrOperationType, 596 aBrowsingContext 597 ) { 598 let entry = this.userActionToBusyDialogMap.get(aRequest.userActionId); 599 if (entry) { 600 // Don't show busy dialog if another request is already doing so. 601 entry.requestTokenSet.add(aRequest.requestToken); 602 return; 603 } 604 605 const analysisType = aRequest.analysisType; 606 // For operations that block browser interaction, show the "slow content analysis" 607 // dialog faster 608 let slowTimeoutMs = this._shouldShowBlockingNotification(analysisType) 609 ? this._SLOW_DLP_NOTIFICATION_BLOCKING_TIMEOUT_MS 610 : this._SLOW_DLP_NOTIFICATION_NONBLOCKING_TIMEOUT_MS; 611 612 entry = { 613 requestTokenSet: new Set([aRequest.requestToken]), 614 userActionId: aRequest.userActionId, 615 }; 616 this.userActionToBusyDialogMap.set(aRequest.userActionId, entry); 617 entry.timer = lazy.setTimeout(() => { 618 entry.timer = null; 619 entry.notification = this._showSlowCAMessage( 620 analysisType, 621 aRequest, 622 this._getSlowDialogMessage( 623 aResourceNameOrOperationType, 624 aRequest.userActionRequestsCount 625 ), 626 aBrowsingContext 627 ); 628 }, slowTimeoutMs); 629 }, 630 631 /** 632 * Removes the Slow CA message, if it is showing 633 * 634 * @param {string} aUserActionId The user action ID to remove 635 * @param {string} aRequestToken The request token to remove 636 */ 637 _removeSlowCAMessage(aUserActionId, aRequestToken) { 638 let entry = this.userActionToBusyDialogMap.get(aUserActionId); 639 if (!entry) { 640 console.error( 641 `Couldn't find slow dialog for user action ${aUserActionId}` 642 ); 643 return; 644 } 645 if (!entry.requestTokenSet.delete(aRequestToken)) { 646 console.warn( 647 `Couldn't find request ${aRequestToken} in slow dialog object for user action ${aUserActionId}. Shutting down?` 648 ); 649 return; 650 } 651 if (entry.requestTokenSet.size) { 652 // Continue showing the busy dialog since other requests are still pending. 653 return; 654 } 655 this.userActionToBusyDialogMap.delete(aUserActionId); 656 this._disconnectFromView(entry); 657 }, 658 659 /** 660 * Gets all the requests that are still in progress. 661 * 662 * @returns {IteratorObject<RequestInfo>} Information about the requests that are still in progress 663 */ 664 _getAllSlowCARequestInfos() { 665 return this.userActionToBusyDialogMap 666 .values() 667 .flatMap(val => val.requestTokenSet) 668 .map(requestToken => this.requestTokenToRequestInfo.get(requestToken)); 669 }, 670 671 /** 672 * Show a message to the user to indicate that a CA request is taking 673 * a long time. 674 * 675 * @param {nsIContentAnalysisRequest.AnalysisType} aOperation The operation 676 * @param {nsIContentAnalysisRequest} aRequest The request that is taking a long time 677 * @param {string} aBodyMessage Message to show in the body of the alert 678 * @param {CanonicalBrowsingContext?} aBrowsingContext BrowsingContext to show the alert in 679 */ 680 _showSlowCAMessage(aOperation, aRequest, aBodyMessage, aBrowsingContext) { 681 if (!this._shouldShowBlockingNotification(aOperation)) { 682 return this._showMessage(aBodyMessage, aBrowsingContext); 683 } 684 685 if (!aRequest) { 686 throw new Error( 687 "Showing in-browser Content Analysis notification but no request was passed" 688 ); 689 } 690 691 return this._showSlowCABlockingMessage( 692 aBrowsingContext, 693 aRequest.userActionId, 694 aRequest.requestToken, 695 aBodyMessage 696 ); 697 }, 698 699 /** 700 * Gets the dialog message to show for the Slow CA dialog. 701 * 702 * @param {ResourceNameOrOperationType} aResourceNameOrOperationType 703 * @param {number} aNumRequests 704 * @returns {string} 705 */ 706 _getSlowDialogMessage(aResourceNameOrOperationType, aNumRequests) { 707 if (aResourceNameOrOperationType.name) { 708 let label = 709 aNumRequests > 1 710 ? "contentanalysis-slow-agent-dialog-body-file-and-more" 711 : "contentanalysis-slow-agent-dialog-body-file"; 712 713 return this.l10n.formatValueSync(label, { 714 agent: lazy.agentName, 715 filename: aResourceNameOrOperationType.name, 716 count: aNumRequests - 1, 717 }); 718 } 719 let l10nId = undefined; 720 switch (aResourceNameOrOperationType.operationType) { 721 case Ci.nsIContentAnalysisRequest.eClipboard: 722 l10nId = "contentanalysis-slow-agent-dialog-body-clipboard"; 723 break; 724 case Ci.nsIContentAnalysisRequest.eDroppedText: 725 l10nId = "contentanalysis-slow-agent-dialog-body-dropped-text"; 726 break; 727 case Ci.nsIContentAnalysisRequest.eOperationPrint: 728 l10nId = "contentanalysis-slow-agent-dialog-body-print"; 729 break; 730 } 731 if (!l10nId) { 732 console.error( 733 "Unknown operationTypeForDisplay: ", 734 aResourceNameOrOperationType 735 ); 736 return ""; 737 } 738 return this.l10n.formatValueSync(l10nId, { agent: lazy.agentName }); 739 }, 740 741 /** 742 * Gets the dialog message to show when the request has an error. 743 * 744 * @param {ResourceNameOrOperationType} aResourceNameOrOperationType 745 * @returns {string} 746 */ 747 _getErrorDialogMessage(aResourceNameOrOperationType) { 748 if (aResourceNameOrOperationType.name) { 749 return this.l10n.formatValueSync( 750 "contentanalysis-error-message-upload-file", 751 { filename: aResourceNameOrOperationType.name } 752 ); 753 } 754 let l10nId = undefined; 755 switch (aResourceNameOrOperationType.operationType) { 756 case Ci.nsIContentAnalysisRequest.eClipboard: 757 l10nId = "contentanalysis-error-message-clipboard"; 758 break; 759 case Ci.nsIContentAnalysisRequest.eDroppedText: 760 l10nId = "contentanalysis-error-message-dropped-text"; 761 break; 762 case Ci.nsIContentAnalysisRequest.eOperationPrint: 763 l10nId = "contentanalysis-error-message-print"; 764 break; 765 } 766 if (!l10nId) { 767 console.error( 768 "Unknown operationTypeForDisplay: ", 769 aResourceNameOrOperationType 770 ); 771 return ""; 772 } 773 return this.l10n.formatValueSync(l10nId); 774 }, 775 776 /** 777 * Show the Slow CA blocking dialog. 778 * 779 * @param {BrowsingContext} aBrowsingContext 780 * @param {string} aUserActionId 781 * @param {string} aRequestToken 782 * @param {string} aBodyMessage 783 * @returns {NotificationInfo} 784 */ 785 _showSlowCABlockingMessage( 786 aBrowsingContext, 787 aUserActionId, 788 aRequestToken, 789 aBodyMessage 790 ) { 791 // Note that TabDialogManager maintains a list of displaying dialogs, and so 792 // we can pop up multiple of these and the first one will keep displaying until 793 // it is closed, at which point the next one will display, etc. 794 let promise = Services.prompt.asyncConfirmEx( 795 aBrowsingContext, 796 Ci.nsIPromptService.MODAL_TYPE_TAB, 797 this.l10n.formatValueSync("contentanalysis-slow-agent-dialog-header"), 798 aBodyMessage, 799 Ci.nsIPromptService.BUTTON_POS_0 * 800 Ci.nsIPromptService.BUTTON_TITLE_CANCEL + 801 Ci.nsIPromptService.BUTTON_POS_1_DEFAULT + 802 Ci.nsIPromptService.SHOW_SPINNER, 803 null, 804 null, 805 null, 806 null, 807 false, 808 { promptID: this.PROMPTID_PREFIX + aUserActionId } 809 ); 810 promise 811 .catch(() => { 812 // need a catch clause to avoid an unhandled JS exception 813 // when we programmatically close the dialog or close the tab. 814 }) 815 .finally(() => { 816 // This is also called if the tab/window is closed while a request is 817 // in progress, in which case we need to cancel all related requests. 818 // 819 // If aUserActionId is still in userActionToBusyDialogMap, 820 // this means the dialog wasn't closed by _disconnectFromView(), 821 // so cancel the operation. 822 if (this.userActionToBusyDialogMap.has(aUserActionId)) { 823 this.contentAnalysis.cancelAllRequestsAssociatedWithUserAction( 824 aUserActionId 825 ); 826 } 827 // Do this after checking userActionToBusyDialogMap, since 828 // _removeSlowCAMessage() will remove the entry from 829 // userActionToBusyDialogMap. 830 if (this.requestTokenToRequestInfo.delete(aRequestToken)) { 831 // I think this is needed to clean up when the tab/window 832 // is closed. 833 this._removeSlowCAMessage(aUserActionId, aRequestToken); 834 } 835 }); 836 return { 837 dialogBrowsingContext: aBrowsingContext, 838 }; 839 }, 840 841 /** 842 * Show a message to the user to indicate the result of a CA request. 843 * 844 * @param {ResourceNameOrOperationType} aResourceNameOrOperationType 845 * @param {CanonicalBrowsingContext} aBrowsingContext 846 * @param {string} aRequestToken 847 * @param {string} aUserActionId 848 * @param {number} aCAResult 849 * @param {boolean} aIsSyntheticResponse 850 * @param {number} aRequestCancelError 851 * @returns {Promise<NotificationInfo?>} a notification object (if shown) 852 */ 853 async _showCAResult( 854 aResourceNameOrOperationType, 855 aBrowsingContext, 856 aRequestToken, 857 aUserActionId, 858 aCAResult, 859 aIsSyntheticResponse, 860 aRequestCancelError 861 ) { 862 let message = null; 863 let timeoutMs = 0; 864 865 switch (aCAResult) { 866 case Ci.nsIContentAnalysisResponse.eAllow: 867 // We don't need to show anything 868 return null; 869 case Ci.nsIContentAnalysisResponse.eReportOnly: 870 message = await this.l10n.formatValue( 871 "contentanalysis-genericresponse-message", 872 { 873 content: this._getResourceNameFromNameOrOperationType( 874 aResourceNameOrOperationType 875 ), 876 response: "REPORT_ONLY", 877 } 878 ); 879 timeoutMs = this._RESULT_NOTIFICATION_FAST_TIMEOUT_MS; 880 break; 881 case Ci.nsIContentAnalysisResponse.eWarn: { 882 let allow = false; 883 try { 884 this.warnDialogRequestTokens.add(aRequestToken); 885 const result = await Services.prompt.asyncConfirmEx( 886 aBrowsingContext, 887 Ci.nsIPromptService.MODAL_TYPE_TAB, 888 await this.l10n.formatValue("contentanalysis-warndialogtitle"), 889 await this._warnDialogText(aResourceNameOrOperationType), 890 Ci.nsIPromptService.BUTTON_POS_0 * 891 Ci.nsIPromptService.BUTTON_TITLE_IS_STRING + 892 Ci.nsIPromptService.BUTTON_POS_1 * 893 Ci.nsIPromptService.BUTTON_TITLE_IS_STRING + 894 Ci.nsIPromptService.BUTTON_POS_2_DEFAULT, 895 await this.l10n.formatValue( 896 "contentanalysis-warndialog-response-allow" 897 ), 898 await this.l10n.formatValue( 899 "contentanalysis-warndialog-response-deny" 900 ), 901 null, 902 null, 903 false 904 ); 905 allow = result.get("buttonNumClicked") === 0; 906 } catch { 907 // This can happen if the dialog is closed programmatically, for 908 // example if the tab is moved to a new window. 909 // In this case just pretend the user clicked deny, as this 910 // emulates the behavior of cancelling when 911 // the request is still active. 912 allow = false; 913 } 914 // Note that the shutdown code in the "quit-application" handler 915 // may have cleared out warnDialogRequestTokens and responded 916 // to the request already, so don't call respondToWarnDialog() 917 // if aRequestToken is not in warnDialogRequestTokens. 918 if (this.warnDialogRequestTokens.delete(aRequestToken)) { 919 this.contentAnalysis.respondToWarnDialog(aRequestToken, allow); 920 } 921 return null; 922 } 923 case Ci.nsIContentAnalysisResponse.eBlock: { 924 if (!aIsSyntheticResponse && !lazy.showBlockedResult) { 925 // Don't show anything 926 return null; 927 } 928 let titleId = undefined; 929 let body = undefined; 930 if (aResourceNameOrOperationType.name) { 931 titleId = 932 aResourceNameOrOperationType.operationType == 933 Ci.nsIContentAnalysisRequest.eUpload 934 ? "contentanalysis-block-dialog-title-upload-file" 935 : "contentanalysis-block-dialog-title-download-file"; 936 body = this.l10n.formatValueSync( 937 aResourceNameOrOperationType.operationType == 938 Ci.nsIContentAnalysisRequest.eUpload 939 ? "contentanalysis-block-dialog-body-upload-file" 940 : "contentanalysis-block-dialog-body-download-file", 941 { filename: aResourceNameOrOperationType.name } 942 ); 943 } else { 944 let bodyId = undefined; 945 let bodyHasContent = false; 946 switch (aResourceNameOrOperationType.operationType) { 947 case Ci.nsIContentAnalysisRequest.eClipboard: { 948 // Unlike the cases below, this can be shown when the DLP 949 // agent is not available. We use a different message for that. 950 const caInfo = await this.contentAnalysis.getDiagnosticInfo(); 951 titleId = "contentanalysis-block-dialog-title-clipboard"; 952 bodyId = caInfo.connectedToAgent 953 ? "contentanalysis-block-dialog-body-clipboard" 954 : "contentanalysis-no-agent-connected-message-content"; 955 bodyHasContent = true; 956 break; 957 } 958 case Ci.nsIContentAnalysisRequest.eDroppedText: 959 titleId = "contentanalysis-block-dialog-title-dropped-text"; 960 bodyId = "contentanalysis-block-dialog-body-dropped-text"; 961 break; 962 case Ci.nsIContentAnalysisRequest.eOperationPrint: 963 titleId = "contentanalysis-block-dialog-title-print"; 964 bodyId = "contentanalysis-block-dialog-body-print"; 965 break; 966 } 967 if (!titleId || !bodyId) { 968 console.error( 969 "Unknown operationTypeForDisplay: ", 970 aResourceNameOrOperationType 971 ); 972 return null; 973 } 974 if (bodyHasContent) { 975 body = this.l10n.formatValueSync(bodyId, { 976 agent: lazy.agentName, 977 content: "", 978 }); 979 } else { 980 body = this.l10n.formatValueSync(bodyId); 981 } 982 } 983 let alertBrowsingContext = aBrowsingContext; 984 if (aBrowsingContext.embedderElement?.getAttribute("printpreview")) { 985 // If we're in a print preview dialog, things are tricky. 986 // The window itself is about to close (because of the thrown NS_ERROR_CONTENT_BLOCKED), 987 // so using an async call would just immediately make the dialog disappear. (bug 1899714) 988 // Using a blocking version can cause a hang if the window is resizing while 989 // we show the dialog. (bug 1900798) 990 // So instead, try to find the browser that this print preview dialog is on top of 991 // and show the dialog there. 992 let printPreviewBrowser = aBrowsingContext.embedderElement; 993 let win = printPreviewBrowser.ownerGlobal; 994 for (let browser of win.gBrowser.browsers) { 995 if ( 996 win.PrintUtils.getPreviewBrowser(browser)?.browserId === 997 printPreviewBrowser.browserId 998 ) { 999 alertBrowsingContext = browser.browsingContext; 1000 break; 1001 } 1002 } 1003 } 1004 await Services.prompt.asyncAlert( 1005 alertBrowsingContext, 1006 Ci.nsIPromptService.MODAL_TYPE_TAB, 1007 this.l10n.formatValueSync(titleId), 1008 body 1009 ); 1010 return null; 1011 } 1012 case Ci.nsIContentAnalysisResponse.eUnspecified: 1013 message = await this.l10n.formatValue( 1014 "contentanalysis-unspecified-error-message-content", 1015 { 1016 agent: lazy.agentName, 1017 content: this._getErrorDialogMessage(aResourceNameOrOperationType), 1018 } 1019 ); 1020 timeoutMs = this._RESULT_NOTIFICATION_TIMEOUT_MS; 1021 break; 1022 case Ci.nsIContentAnalysisResponse.eCanceled: 1023 { 1024 let messageId; 1025 switch (aRequestCancelError) { 1026 case Ci.nsIContentAnalysisResponse.eUserInitiated: 1027 console.error( 1028 "Got unexpected cancel response with eUserInitiated" 1029 ); 1030 return null; 1031 case Ci.nsIContentAnalysisResponse.eOtherRequestInGroupCancelled: 1032 return null; 1033 case Ci.nsIContentAnalysisResponse.eNoAgent: 1034 messageId = "contentanalysis-no-agent-connected-message-content"; 1035 break; 1036 case Ci.nsIContentAnalysisResponse.eInvalidAgentSignature: 1037 messageId = 1038 "contentanalysis-invalid-agent-signature-message-content"; 1039 break; 1040 case Ci.nsIContentAnalysisResponse.eErrorOther: 1041 messageId = "contentanalysis-unspecified-error-message-content"; 1042 break; 1043 case Ci.nsIContentAnalysisResponse.eShutdown: 1044 // we're shutting down, no need to show a dialog 1045 return null; 1046 case Ci.nsIContentAnalysisResponse.eTimeout: 1047 // We only show this if the default action was to block. 1048 messageId = "contentanalysis-timeout-block-error-message-content"; 1049 break; 1050 default: 1051 console.error( 1052 "Unexpected CA cancelError value: " + aRequestCancelError 1053 ); 1054 messageId = "contentanalysis-unspecified-error-message-content"; 1055 break; 1056 } 1057 // We got an error with this request, so close any dialogs for any other request 1058 // with the same user action id and also remove their data so we don't show 1059 // any dialogs they might later try to show. 1060 const busyDialogInfo = 1061 this.userActionToBusyDialogMap.get(aUserActionId); 1062 if (busyDialogInfo) { 1063 busyDialogInfo.requestTokenSet.forEach(requestToken => { 1064 this.requestTokenToRequestInfo.delete(requestToken); 1065 this._removeSlowCAMessage(aUserActionId, requestToken); 1066 }); 1067 } 1068 message = await this.l10n.formatValue(messageId, { 1069 agent: lazy.agentName, 1070 content: this._getErrorDialogMessage(aResourceNameOrOperationType), 1071 contentName: this._getResourceNameFromNameOrOperationType( 1072 aResourceNameOrOperationType 1073 ), 1074 }); 1075 timeoutMs = this._RESULT_NOTIFICATION_TIMEOUT_MS; 1076 } 1077 break; 1078 default: 1079 throw new Error("Unexpected CA result value: " + aCAResult); 1080 } 1081 1082 if (!message) { 1083 console.error( 1084 "_showCAResult did not get a message populated for result value " + 1085 aCAResult 1086 ); 1087 return null; 1088 } 1089 1090 return this._showMessage(message, aBrowsingContext, timeoutMs); 1091 }, 1092 1093 /** 1094 * Returns the correct text for warn dialog contents. 1095 * 1096 * @param {ResourceNameOrOperationType} aResourceNameOrOperationType 1097 */ 1098 async _warnDialogText(aResourceNameOrOperationType) { 1099 const caInfo = await this.contentAnalysis.getDiagnosticInfo(); 1100 if (caInfo.connectedToAgent) { 1101 return await this.l10n.formatValue("contentanalysis-warndialogtext", { 1102 content: this._getResourceNameFromNameOrOperationType( 1103 aResourceNameOrOperationType 1104 ), 1105 }); 1106 } 1107 return await this.l10n.formatValue( 1108 "contentanalysis-no-agent-connected-message-content", 1109 { agent: lazy.agentName, content: "" } 1110 ); 1111 }, 1112 };