chromium/chrome/browser/resources/downloads/dangerous_download_interstitial.ts

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview 'downloads-dangerous-download-interstitial' is the interstitial
 * that allows bypassing a download warning (keeping a file flagged as
 * dangerous). A 'success' indicates the warning interstitial was confirmed and
 * the dangerous file was downloaded.
 */
import 'chrome://resources/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/cr_elements/cr_icon/cr_icon.js';
import 'chrome://resources/cr_elements/cr_radio_button/cr_radio_button.js';
import 'chrome://resources/cr_elements/cr_radio_group/cr_radio_group.js';
import 'chrome://resources/cr_elements/cr_shared_vars.css.js';

import {getInstance as getAnnouncerInstance} from 'chrome://resources/cr_elements/cr_a11y_announcer/cr_a11y_announcer.js';
import type {CrButtonElement} from 'chrome://resources/cr_elements/cr_button/cr_button.js';
import {assert} from 'chrome://resources/js/assert.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js';

import {BrowserProxy} from './browser_proxy.js';
import {getCss} from './dangerous_download_interstitial.css.js';
import {getHtml} from './dangerous_download_interstitial.html.js';
import type {PageHandlerInterface} from './downloads.mojom-webui.js';
import {DangerousDownloadInterstitialSurveyOptions as surveyOptions} from './downloads.mojom-webui.js';

export interface DownloadsDangerousDownloadInterstitialElement {
  $: {
    dialog: HTMLDialogElement,
    continueAnywayButton: CrButtonElement,
    backToSafetyButton: CrButtonElement,
  };
}

export class DownloadsDangerousDownloadInterstitialElement extends
    CrLitElement {
  static get is() {
    return 'downloads-dangerous-download-interstitial';
  }

  static override get styles() {
    return getCss();
  }

  override render() {
    return getHtml.bind(this)();
  }

  static override get properties() {
    return {
      bypassPromptItemId: {type: String},
      hideSurveyAndDownloadButton_: {type: Boolean},
      selectedRadioOption_: {type: String},
      trustSiteLine: {type: String},
      trustSiteLineAccessibleText: {type: String},
    };
  }

  bypassPromptItemId: string = '';
  trustSiteLine: string = '';
  trustSiteLineAccessibleText: string = '';

  private boundKeydown_: ((e: KeyboardEvent) => void)|null = null;
  protected hideSurveyAndDownloadButton_: boolean = true;
  protected selectedRadioOption_?: string;

  private mojoHandler_: PageHandlerInterface|null = null;

  override firstUpdated() {
    this.mojoHandler_ = BrowserProxy.getInstance().handler;
  }

  override connectedCallback() {
    super.connectedCallback();
    this.$.dialog.showModal();
    this.$.dialog.focus();
    this.disableEscapeKey_();
  }

  override disconnectedCallback() {
    super.disconnectedCallback();
    this.removeKeydownListener_();
  }

  getSurveyResponse(): surveyOptions {
    const surveyResponse = this.$.dialog.returnValue;
    switch (surveyResponse) {
      case 'CreatedFile':
        return surveyOptions.kCreatedFile;
      case 'TrustSite':
        return surveyOptions.kTrustSite;
      case 'AcceptRisk':
        return surveyOptions.kAcceptRisk;
      default:
        return surveyOptions.kNoResponse;
    }
  }

  private disableEscapeKey_() {
    this.boundKeydown_ = this.boundKeydown_ || this.onKeydown_.bind(this);
    this.addEventListener('keydown', this.boundKeydown_);
    // Sometimes <body> is key event's target and in that case the event
    // will bypass dialog. We should consume those events too in order to
    // modally. This prevents cancelling the interstitial via keyboard events.
    document.body.addEventListener('keydown', this.boundKeydown_);
  }

  protected onBackToSafetyClick_() {
    this.$.dialog.close();
    assert(!this.$.dialog.open);
    this.dispatchEvent(
        new CustomEvent('cancel', {bubbles: true, composed: true}));
  }

  protected onContinueAnywayClick_() {
    const continueAnywayButton = this.$.continueAnywayButton;
    assert(!!continueAnywayButton);
    continueAnywayButton.setAttribute('disabled', 'true');

    const backToSafetyButton = this.$.backToSafetyButton;
    assert(!!backToSafetyButton);
    backToSafetyButton.focus();
    this.hideSurveyAndDownloadButton_ = false;

    assert(this.bypassPromptItemId !== '');
    assert(!!this.mojoHandler_);
    this.mojoHandler_.recordOpenSurveyOnDangerousInterstitial(
        this.bypassPromptItemId);
  }

  protected onDownloadClick_() {
    getAnnouncerInstance().announce(
        loadTimeData.getString('screenreaderSavedDangerous'));

    this.$.dialog.close(this.selectedRadioOption_ || '');
    assert(!this.$.dialog.open);
    this.dispatchEvent(
        new CustomEvent('close', {bubbles: true, composed: true}));
  }

  private onKeydown_(e: KeyboardEvent) {
    if (e.key === 'Escape') {
      e.preventDefault();
    }
  }

  private removeKeydownListener_() {
    if (!this.boundKeydown_) {
      return;
    }

    this.removeEventListener('keydown', this.boundKeydown_);
    document.body.removeEventListener('keydown', this.boundKeydown_);
    this.boundKeydown_ = null;
  }

  protected onSelectedRadioOptionChanged_(e: CustomEvent<{value: string}>) {
    this.selectedRadioOption_ = e.detail.value;
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'downloads-dangerous-download-interstitial':
        DownloadsDangerousDownloadInterstitialElement;
  }
}

customElements.define(
    DownloadsDangerousDownloadInterstitialElement.is,
    DownloadsDangerousDownloadInterstitialElement);