// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import './icons.html.js';
// <if expr="_google_chrome">
import './internal/icons.html.js';
// </if>
import 'chrome://resources/cr_elements/cr_action_menu/cr_action_menu.js';
import 'chrome://resources/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
import 'chrome://resources/cr_elements/cr_icons.css.js';
import 'chrome://resources/cr_elements/cr_hidden_style.css.js';
// <if expr="_google_chrome">
import 'chrome://resources/cr_elements/cr_link_row/cr_link_row.js';
// </if>
import 'chrome://resources/cr_elements/cr_progress/cr_progress.js';
import 'chrome://resources/cr_elements/icons.html.js';
import 'chrome://resources/cr_elements/cr_shared_vars.css.js';
import 'chrome://resources/js/action_link.js';
import 'chrome://resources/cr_elements/action_link.css.js';
import './strings.m.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import {getInstance as getAnnouncerInstance} from 'chrome://resources/cr_elements/cr_a11y_announcer/cr_a11y_announcer.js';
import type {CrActionMenuElement} from 'chrome://resources/cr_elements/cr_action_menu/cr_action_menu.js';
import type {CrIconButtonElement} from 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
import {getToastManager} from 'chrome://resources/cr_elements/cr_toast/cr_toast_manager.js';
import {FocusRowMixin} from 'chrome://resources/cr_elements/focus_row_mixin.js';
import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js';
import {assert, assertNotReached} from 'chrome://resources/js/assert.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {mojoString16ToString} from 'chrome://resources/js/mojo_type_util.js';
import {sanitizeInnerHtml} from 'chrome://resources/js/parse_html_subset.js';
import {htmlEscape} from 'chrome://resources/js/util.js';
import type {String16} from 'chrome://resources/mojo/mojo/public/mojom/base/string16.mojom-webui.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {BrowserProxy} from './browser_proxy.js';
import type {MojomData} from './data.js';
import type {PageHandlerInterface} from './downloads.mojom-webui.js';
import {DangerType, SafeBrowsingState, State, TailoredWarningType} from './downloads.mojom-webui.js';
import {IconLoaderImpl} from './icon_loader.js';
import {getTemplate} from './item.html.js';
export interface DownloadsItemElement {
$: {
'controlled-by': HTMLElement,
'file-icon': HTMLImageElement,
'file-link': HTMLAnchorElement,
'referrer-url': HTMLAnchorElement,
'url': HTMLAnchorElement,
};
}
const DownloadsItemElementBase = I18nMixin(FocusRowMixin(PolymerElement));
/**
* The UI pattern for displaying a download. Computed from DangerType and other
* properties of the download and user's profile.
*/
enum DisplayType {
NORMAL,
DANGEROUS,
SUSPICIOUS,
UNVERIFIED,
INSECURE,
ERROR,
}
export class DownloadsItemElement extends DownloadsItemElementBase {
static get is() {
return 'downloads-item';
}
static get template() {
return getTemplate();
}
static get properties() {
return {
data: Object,
completelyOnDisk_: {
computed: 'computeCompletelyOnDisk_(' +
'data.state, data.fileExternallyRemoved)',
type: Boolean,
value: true,
},
shouldLinkFilename_: {
computed: 'computeShouldLinkFilename_(' +
'data.dangerType, completelyOnDisk_)',
type: Boolean,
value: true,
},
hasShowInFolderLink_: {
computed: 'computeHasShowInFolderLink_(' +
'data.state, data.fileExternallyRemoved, data.dangerType)',
type: Boolean,
value: true,
},
controlledBy_: {
computed: 'computeControlledBy_(data.byExtId, data.byExtName)',
type: String,
value: '',
},
iconAriaLabel_: {
type: String,
computed: 'computeIconAriaLabel_(displayType_)',
},
isActive_: {
computed: 'computeIsActive_(data.fileExternallyRemoved, completelyOnDisk_)',
type: Boolean,
value: true,
},
isDangerous_: {
computed: 'computeIsDangerous_(data.state)',
type: Boolean,
value: false,
},
isMalware_: {
computed: 'computeIsMalware_(isDangerous_, data.dangerType)',
type: Boolean,
value: false,
},
isReviewable_: {
computed: 'computeIsReviewable_(data.isReviewable)',
type: Boolean,
value: false,
},
isInProgress_: {
computed: 'computeIsInProgress_(data.state)',
type: Boolean,
value: false,
},
pauseOrResumeText_: {
computed: 'computePauseOrResumeText_(isInProgress_, data.resume)',
type: String,
},
showCancel_: {
computed: 'computeShowCancel_(data.state)',
type: Boolean,
value: false,
},
showProgress_: {
computed: 'computeShowProgress_(showCancel_, data.percent)',
type: Boolean,
value: false,
},
showDeepScan_: {
computed: 'computeShowDeepScan_(data.state)',
type: Boolean,
value: false,
},
showOpenAnyway_: {
computed: 'computeShowOpenAnyway_(data.dangerType)',
type: Boolean,
value: false,
},
displayType_: {
computed: 'computeDisplayType_(data.isInsecure, data.state,' +
'data.dangerType, data.safeBrowsingState,' +
'data.hasSafeBrowsingVerdict)',
type: DisplayType,
value: DisplayType.NORMAL,
},
// <if expr="_google_chrome">
showEsbPromotion: {
type: Boolean,
value: false,
},
// </if>
useFileIcon_: Boolean,
showReferrerUrl_: {
type: Boolean,
value: () => loadTimeData.getBoolean('showReferrerUrl'),
},
};
}
static get observers() {
return [
// TODO(dbeam): this gets called way more when I observe data.byExtId
// and data.byExtName directly. Why?
'observeControlledBy_(controlledBy_)',
'observeDisplayType_(displayType_, isDangerous_, data.*)',
'restoreFocusAfterCancelIfNeeded_(data)',
];
}
data: MojomData;
// <if expr="_google_chrome">
showEsbPromotion: boolean;
// </if>
private mojoHandler_: PageHandlerInterface|null = null;
private controlledBy_: string;
private iconAriaLabel_: string;
private isActive_: boolean;
private isDangerous_: boolean;
private isReviewable_: boolean;
private isInProgress_: boolean;
private pauseOrResumeText_: string;
private showCancel_: boolean;
private showProgress_: boolean;
private showDeepScan_: boolean;
private showOpenAnyway_: boolean;
private useFileIcon_: boolean;
private restoreFocusAfterCancel_: boolean = false;
private displayType_: DisplayType;
private completelyOnDisk_: boolean;
override overrideCustomEquivalent: boolean;
constructor() {
super();
/** Used by FocusRowMixin. */
this.overrideCustomEquivalent = true;
}
override ready() {
super.ready();
this.setAttribute('role', 'row');
this.mojoHandler_ = BrowserProxy.getInstance().handler;
}
/** Overrides FocusRowMixin. */
override getCustomEquivalent(sampleElement: HTMLElement): HTMLElement|null {
if (sampleElement.getAttribute('focus-type') === 'cancel') {
return this.shadowRoot!.querySelector('[focus-type="retry"]');
}
if (sampleElement.getAttribute('focus-type') === 'retry') {
return this.shadowRoot!.querySelector('[focus-type="pauseOrResume"]');
}
return null;
}
getFileIcon(): HTMLImageElement {
return this.$['file-icon'];
}
getMoreActionsButton(): CrIconButtonElement|null {
const button =
this.shadowRoot!.querySelector<CrIconButtonElement>('#more-actions');
return button || null;
}
getMoreActionsMenu(): CrActionMenuElement {
const menu = this.shadowRoot!.querySelector<CrActionMenuElement>(
'#more-actions-menu');
assert(!!menu);
return menu;
}
/**
* @return A JS string of the display URL.
*/
private getDisplayUrlStr_(displayUrl: String16): string {
return mojoString16ToString(displayUrl);
}
private computeClass_(): string {
const classes = [];
if (this.isActive_) {
classes.push('is-active');
}
if (this.isDangerous_) {
classes.push('dangerous');
}
if (this.showProgress_) {
classes.push('show-progress');
}
return classes.join(' ');
}
private computeCompletelyOnDisk_(): boolean {
if (this.data.fileExternallyRemoved) {
return false;
}
switch (this.data.state) {
case State.kComplete:
return true;
case State.kInProgress:
case State.kCancelled:
case State.kPaused:
case State.kDangerous:
case State.kInterrupted:
case State.kInsecure:
case State.kAsyncScanning:
case State.kPromptForScanning:
case State.kPromptForLocalPasswordScanning:
return false;
default:
assertNotReached('Unhandled State encountered');
}
}
private computeShouldLinkFilename_(): boolean {
if (this.data === undefined) {
return false;
}
if (!this.completelyOnDisk_) {
return false;
}
switch (this.data.dangerType) {
case DangerType.kDeepScannedFailed:
return false;
case DangerType.kNoApplicableDangerType:
case DangerType.kDangerousFile:
case DangerType.kDangerousUrl:
case DangerType.kDangerousContent:
case DangerType.kCookieTheft:
case DangerType.kUncommonContent:
case DangerType.kDangerousHost:
case DangerType.kPotentiallyUnwanted:
case DangerType.kAsyncScanning:
case DangerType.kAsyncLocalPasswordScanning:
case DangerType.kBlockedPasswordProtected:
case DangerType.kBlockedTooLarge:
case DangerType.kSensitiveContentWarning:
case DangerType.kSensitiveContentBlock:
case DangerType.kDeepScannedSafe:
case DangerType.kDeepScannedOpenedDangerous:
case DangerType.kBlockedScanFailed:
return true;
default:
assertNotReached('Unhandled DangerType encountered');
}
}
private computeHasShowInFolderLink_(): boolean {
if (this.data === undefined) {
return false;
}
if (!this.computeCompletelyOnDisk_()) {
return false;
}
switch (this.data.dangerType) {
case DangerType.kDeepScannedFailed:
return false;
case DangerType.kNoApplicableDangerType:
case DangerType.kDangerousFile:
case DangerType.kDangerousUrl:
case DangerType.kDangerousContent:
case DangerType.kCookieTheft:
case DangerType.kUncommonContent:
case DangerType.kDangerousHost:
case DangerType.kPotentiallyUnwanted:
case DangerType.kAsyncScanning:
case DangerType.kAsyncLocalPasswordScanning:
case DangerType.kBlockedPasswordProtected:
case DangerType.kBlockedTooLarge:
case DangerType.kSensitiveContentWarning:
case DangerType.kSensitiveContentBlock:
case DangerType.kDeepScannedSafe:
case DangerType.kDeepScannedOpenedDangerous:
case DangerType.kBlockedScanFailed:
return true;
default:
assertNotReached('Unhandled DangerType encountered');
}
}
private computeControlledBy_(): string {
if (!this.data.byExtId || !this.data.byExtName) {
return '';
}
const url = `chrome://extensions/?id=${this.data.byExtId}`;
const name = this.data.byExtName;
return loadTimeData.getStringF('controlledByUrl', url, htmlEscape(name));
}
private computeDate_(): string {
assert(typeof this.data.hideDate === 'boolean');
if (this.data.hideDate) {
return '';
}
return this.data.sinceString || this.data.dateString;
}
private computeDescriptionVisible_(): boolean {
return this.computeDescription_() !== '';
}
private computeSecondLineVisible_(): boolean {
if (!this.data) {
return false;
}
switch (this.data.state) {
case State.kAsyncScanning:
return true;
case State.kInProgress:
case State.kCancelled:
case State.kComplete:
case State.kPaused:
case State.kDangerous:
case State.kInterrupted:
case State.kInsecure:
case State.kPromptForScanning:
case State.kPromptForLocalPasswordScanning:
return false;
default:
assertNotReached('Unhandled State encountered');
}
}
private isSuspiciousEnterpriseApVerdict_(
requestsApVerdicts: boolean, dangerType: DangerType): boolean {
switch (dangerType) {
case DangerType.kUncommonContent:
return requestsApVerdicts;
case DangerType.kSensitiveContentWarning:
return true;
case DangerType.kNoApplicableDangerType:
case DangerType.kDangerousFile:
case DangerType.kDangerousUrl:
case DangerType.kDangerousContent:
case DangerType.kCookieTheft:
case DangerType.kDangerousHost:
case DangerType.kPotentiallyUnwanted:
case DangerType.kAsyncScanning:
case DangerType.kAsyncLocalPasswordScanning:
case DangerType.kBlockedPasswordProtected:
case DangerType.kBlockedTooLarge:
case DangerType.kSensitiveContentBlock:
case DangerType.kDeepScannedFailed:
case DangerType.kDeepScannedSafe:
case DangerType.kDeepScannedOpenedDangerous:
case DangerType.kBlockedScanFailed:
return false;
default:
assertNotReached('Unhandled DangerType encountered');
}
}
private computeDisplayType_(): DisplayType {
// Most downloads are normal. If we don't have data, don't assume danger.
if (!this.data) {
return DisplayType.NORMAL;
}
if (this.data.isInsecure) {
return DisplayType.INSECURE;
}
switch (this.data.state) {
case State.kAsyncScanning:
case State.kPromptForScanning:
case State.kPromptForLocalPasswordScanning:
return DisplayType.SUSPICIOUS;
case State.kInsecure:
return DisplayType.INSECURE;
case State.kInProgress:
case State.kCancelled:
case State.kComplete:
case State.kPaused:
case State.kDangerous:
case State.kInterrupted:
break;
default:
assertNotReached('Unhandled State encountered');
}
// Enterprise AP verdicts.
if (this.isSuspiciousEnterpriseApVerdict_(
loadTimeData.getBoolean('requestsApVerdicts'),
this.data.dangerType)) {
return DisplayType.SUSPICIOUS;
}
switch (this.data.dangerType) {
// Mimics logic in download_ui_model.cc for downloads with danger_type
// DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE.
case DangerType.kDangerousFile:
return this.data.hasSafeBrowsingVerdict ? DisplayType.SUSPICIOUS :
DisplayType.UNVERIFIED;
case DangerType.kDangerousUrl:
case DangerType.kDangerousContent:
case DangerType.kCookieTheft:
case DangerType.kDangerousHost:
case DangerType.kPotentiallyUnwanted:
case DangerType.kDeepScannedOpenedDangerous:
return DisplayType.DANGEROUS;
case DangerType.kUncommonContent:
case DangerType.kDeepScannedFailed:
return DisplayType.SUSPICIOUS;
case DangerType.kNoApplicableDangerType:
case DangerType.kAsyncScanning:
case DangerType.kAsyncLocalPasswordScanning:
case DangerType.kSensitiveContentWarning:
case DangerType.kDeepScannedSafe:
case DangerType.kBlockedScanFailed:
return DisplayType.NORMAL;
case DangerType.kBlockedPasswordProtected:
case DangerType.kBlockedTooLarge:
case DangerType.kSensitiveContentBlock:
return DisplayType.ERROR;
default:
assertNotReached('Unhandled DangerType encountered');
}
}
private computeDeepScanControlText_(): string {
switch (this.data.state) {
case State.kPromptForScanning:
return loadTimeData.getString('controlDeepScan');
case State.kPromptForLocalPasswordScanning:
return loadTimeData.getString('controlLocalPasswordScan');
case State.kInProgress:
case State.kCancelled:
case State.kComplete:
case State.kPaused:
case State.kDangerous:
case State.kInterrupted:
case State.kInsecure:
case State.kAsyncScanning:
return '';
default:
assertNotReached('Unhandled State encountered');
}
}
private computeSaveDangerousLabel_(): string {
switch (this.displayType_) {
case DisplayType.DANGEROUS:
return this.i18n('controlKeepDangerous');
case DisplayType.SUSPICIOUS:
return this.i18n('controlKeepSuspicious');
case DisplayType.UNVERIFIED:
return this.i18n('controlKeepUnverified');
case DisplayType.INSECURE:
return this.i18n('controlKeepInsecure');
case DisplayType.NORMAL:
case DisplayType.ERROR:
return '';
default:
assertNotReached('Unhandled DisplayType encountered');
}
}
private computeDescription_(): string {
if (!this.data) {
return '';
}
const data = this.data;
switch (data.state) {
case State.kComplete:
switch (data.dangerType) {
case DangerType.kDeepScannedOpenedDangerous:
return loadTimeData.getString('deepScannedOpenedDangerousDesc');
case DangerType.kDeepScannedFailed:
return loadTimeData.getString('deepScannedFailedDesc');
case DangerType.kNoApplicableDangerType:
case DangerType.kDangerousFile:
case DangerType.kDangerousUrl:
case DangerType.kDangerousContent:
case DangerType.kCookieTheft:
case DangerType.kUncommonContent:
case DangerType.kDangerousHost:
case DangerType.kPotentiallyUnwanted:
case DangerType.kAsyncScanning:
case DangerType.kAsyncLocalPasswordScanning:
case DangerType.kBlockedPasswordProtected:
case DangerType.kBlockedTooLarge:
case DangerType.kSensitiveContentWarning:
case DangerType.kSensitiveContentBlock:
case DangerType.kDeepScannedSafe:
case DangerType.kBlockedScanFailed:
return '';
default:
assertNotReached('Unhandled DangerType encountered');
}
case State.kInsecure:
return loadTimeData.getString('insecureDownloadDesc');
case State.kDangerous:
switch (data.dangerType) {
case DangerType.kNoApplicableDangerType:
return '';
case DangerType.kDangerousFile:
return data.safeBrowsingState ===
SafeBrowsingState.kNoSafeBrowsing ?
loadTimeData.getString('noSafeBrowsingDesc') :
loadTimeData.getString('dangerFileDesc');
case DangerType.kDangerousUrl:
case DangerType.kDangerousContent:
case DangerType.kDangerousHost:
return loadTimeData.getString('dangerDownloadDesc');
case DangerType.kCookieTheft:
switch (data.tailoredWarningType) {
case TailoredWarningType.kCookieTheft:
return loadTimeData.getString('dangerDownloadCookieTheft');
case TailoredWarningType.kCookieTheftWithAccountInfo:
return data.accountEmail ?
loadTimeData.getStringF(
'dangerDownloadCookieTheftAndAccountDesc',
data.accountEmail) :
loadTimeData.getString('dangerDownloadCookieTheft');
case TailoredWarningType.kSuspiciousArchive:
case TailoredWarningType.kNoApplicableTailoredWarningType:
return loadTimeData.getString('dangerDownloadDesc');
default:
assertNotReached('Unhandled TailoredWarningType encountered');
}
case DangerType.kUncommonContent:
switch (data.tailoredWarningType) {
case TailoredWarningType.kSuspiciousArchive:
return loadTimeData.getString(
'dangerUncommonSuspiciousArchiveDesc');
case TailoredWarningType.kCookieTheft:
case TailoredWarningType.kCookieTheftWithAccountInfo:
case TailoredWarningType.kNoApplicableTailoredWarningType:
return loadTimeData.getString('dangerUncommonDesc');
default:
assertNotReached('Unhandled TailoredWarningType encountered');
}
case DangerType.kPotentiallyUnwanted:
return loadTimeData.getString('dangerSettingsDesc');
case DangerType.kAsyncScanning:
case DangerType.kAsyncLocalPasswordScanning:
case DangerType.kBlockedPasswordProtected:
case DangerType.kBlockedTooLarge:
return '';
case DangerType.kSensitiveContentWarning:
return loadTimeData.getString('sensitiveContentWarningDesc');
case DangerType.kSensitiveContentBlock:
case DangerType.kDeepScannedFailed:
case DangerType.kDeepScannedSafe:
case DangerType.kDeepScannedOpenedDangerous:
case DangerType.kBlockedScanFailed:
return '';
default:
assertNotReached('Unhandled DangerType encountered');
}
case State.kAsyncScanning:
return loadTimeData.getString('asyncScanningDownloadDesc');
case State.kPromptForScanning:
return loadTimeData.getString('promptForScanningDesc');
case State.kPromptForLocalPasswordScanning:
return loadTimeData.getString('promptForLocalPasswordScanningDesc');
case State.kInProgress:
case State.kPaused: // Fallthrough.
return data.progressStatusText;
case State.kInterrupted:
switch (data.dangerType) {
case DangerType.kNoApplicableDangerType:
case DangerType.kDangerousFile:
case DangerType.kDangerousUrl:
case DangerType.kDangerousContent:
case DangerType.kDangerousHost:
case DangerType.kCookieTheft:
case DangerType.kUncommonContent:
case DangerType.kPotentiallyUnwanted:
case DangerType.kAsyncScanning:
case DangerType.kAsyncLocalPasswordScanning:
return '';
case DangerType.kBlockedPasswordProtected:
return loadTimeData.getString('blockedPasswordProtectedDesc');
case DangerType.kBlockedTooLarge:
return loadTimeData.getString('blockedTooLargeDesc');
case DangerType.kSensitiveContentWarning:
return '';
case DangerType.kSensitiveContentBlock:
return loadTimeData.getString('sensitiveContentBlockedDesc');
case DangerType.kDeepScannedFailed:
case DangerType.kDeepScannedSafe:
case DangerType.kDeepScannedOpenedDangerous:
case DangerType.kBlockedScanFailed:
return '';
default:
assertNotReached('Unhandled DangerType encountered');
}
case State.kCancelled:
return '';
default:
assertNotReached('Unhandled State encountered');
}
}
private computeIconAriaHidden_(): string {
return (this.iconAriaLabel_ === '').toString();
}
private computeIconAriaLabel_(): string {
switch (this.displayType_) {
case DisplayType.DANGEROUS:
return this.i18n('accessibleLabelDangerous');
case DisplayType.INSECURE:
return this.i18n('accessibleLabelInsecure');
case DisplayType.UNVERIFIED:
return this.i18n('accessibleLabelUnverified');
case DisplayType.SUSPICIOUS:
return this.i18n('accessibleLabelSuspicious');
case DisplayType.NORMAL:
case DisplayType.ERROR:
return '';
default:
assertNotReached('Unhandled DisplayType encountered');
}
}
private iconAndDescriptionColor_(): string {
switch (this.displayType_) {
case DisplayType.DANGEROUS:
case DisplayType.ERROR:
return 'red';
case DisplayType.INSECURE:
case DisplayType.UNVERIFIED:
case DisplayType.SUSPICIOUS:
return 'grey';
case DisplayType.NORMAL:
return '';
default:
assertNotReached('Unhandled DisplayType encountered');
}
}
private computeIcon_(): string {
if (this.data) {
switch (this.displayType_) {
case DisplayType.DANGEROUS:
return 'downloads:dangerous';
case DisplayType.INSECURE:
case DisplayType.UNVERIFIED:
case DisplayType.SUSPICIOUS:
return 'cr:warning';
case DisplayType.ERROR:
return 'cr:error';
case DisplayType.NORMAL:
break;
default:
assertNotReached('Unhandled DisplayType encountered');
}
assert(this.displayType_ === DisplayType.NORMAL);
const dangerType = this.data.dangerType as DangerType;
if (this.isSuspiciousEnterpriseApVerdict_(
loadTimeData.getBoolean('requestsApVerdicts'), dangerType)) {
return 'cr:warning';
}
switch (dangerType) {
case DangerType.kDeepScannedFailed:
return 'cr:info';
case DangerType.kSensitiveContentBlock:
case DangerType.kBlockedTooLarge:
case DangerType.kBlockedPasswordProtected:
return 'cr:error';
case DangerType.kNoApplicableDangerType:
case DangerType.kDangerousFile:
case DangerType.kDangerousUrl:
case DangerType.kDangerousContent:
case DangerType.kCookieTheft:
case DangerType.kUncommonContent:
case DangerType.kDangerousHost:
case DangerType.kPotentiallyUnwanted:
case DangerType.kAsyncScanning:
case DangerType.kAsyncLocalPasswordScanning:
case DangerType.kSensitiveContentWarning:
case DangerType.kDeepScannedSafe:
case DangerType.kDeepScannedOpenedDangerous:
case DangerType.kBlockedScanFailed:
break;
default:
assertNotReached('Unhandled DangerType encountered');
}
switch (this.data.state) {
case State.kAsyncScanning:
case State.kPromptForScanning:
case State.kPromptForLocalPasswordScanning:
return 'cr:warning';
case State.kInProgress:
case State.kCancelled:
case State.kComplete:
case State.kPaused:
case State.kDangerous:
case State.kInterrupted:
case State.kInsecure:
break;
default:
assertNotReached('Unhandled State encountered');
}
}
if (this.isDangerous_) {
return 'downloads:dangerous';
}
if (!this.useFileIcon_) {
return 'cr:insert-drive-file';
}
return '';
}
private computeIconColor_(): string {
if (this.data) {
return this.iconAndDescriptionColor_();
}
if (this.isDangerous_) {
return 'red';
}
if (!this.useFileIcon_) {
return 'light-grey';
}
return '';
}
private computeIsActive_(): boolean {
if (this.data && this.data.fileExternallyRemoved) {
return false;
}
return this.completelyOnDisk_;
}
private computeIsDangerous_(): boolean {
switch (this.data.state) {
case State.kDangerous:
case State.kInsecure:
return true;
case State.kInProgress:
case State.kCancelled:
case State.kComplete:
case State.kPaused:
case State.kInterrupted:
case State.kAsyncScanning:
case State.kPromptForScanning:
case State.kPromptForLocalPasswordScanning:
return false;
default:
assertNotReached('Unhandled State encountered');
}
}
private computeIsInProgress_(): boolean {
switch (this.data.state) {
case State.kInProgress:
return true;
case State.kCancelled:
case State.kComplete:
case State.kPaused:
case State.kDangerous:
case State.kInterrupted:
case State.kInsecure:
case State.kAsyncScanning:
case State.kPromptForScanning:
case State.kPromptForLocalPasswordScanning:
return false;
default:
assertNotReached('Unhandled State encountered');
}
}
private computeIsMalware_(): boolean {
if (!this.isDangerous_) {
return false;
}
switch (this.data.dangerType) {
case DangerType.kDangerousUrl:
case DangerType.kDangerousContent:
case DangerType.kCookieTheft:
case DangerType.kDangerousHost:
case DangerType.kPotentiallyUnwanted:
return true;
case DangerType.kNoApplicableDangerType:
case DangerType.kDangerousFile:
case DangerType.kUncommonContent:
case DangerType.kAsyncScanning:
case DangerType.kAsyncLocalPasswordScanning:
case DangerType.kBlockedPasswordProtected:
case DangerType.kBlockedTooLarge:
case DangerType.kSensitiveContentWarning:
case DangerType.kSensitiveContentBlock:
case DangerType.kDeepScannedFailed:
case DangerType.kDeepScannedSafe:
case DangerType.kDeepScannedOpenedDangerous:
case DangerType.kBlockedScanFailed:
return false;
default:
assertNotReached('Unhandled DangerType encountered');
}
}
private computeIsReviewable_(): boolean {
return this.data.isReviewable;
}
private computePauseOrResumeText_(): string {
if (this.data === undefined) {
return '';
}
if (this.isInProgress_) {
return loadTimeData.getString('controlPause');
}
if (this.data.resume) {
return loadTimeData.getString('controlResume');
}
return '';
}
private computeShowRemove_(): boolean {
const canDelete = loadTimeData.getBoolean('allowDeletingHistory');
const hideRemove = this.isDangerous_ || this.showCancel_ || !canDelete;
return !hideRemove;
}
private computeRemoveStyle_(): string {
return this.computeShowRemove_() ? '' : 'visibility: hidden';
}
private computeShowControlsForDangerous_(): boolean {
return !this.isReviewable_ && this.isDangerous_;
}
private computeShowCancel_(): boolean {
if (!this.data) {
return false;
}
switch (this.data.state) {
case State.kInProgress:
case State.kPaused:
return true;
case State.kCancelled:
case State.kComplete:
case State.kDangerous:
case State.kInterrupted:
case State.kInsecure:
case State.kAsyncScanning:
case State.kPromptForScanning:
case State.kPromptForLocalPasswordScanning:
return false;
default:
assertNotReached('Unhandled State encountered');
}
}
private computeShowProgress_(): boolean {
if (!this.data) {
return false;
}
switch (this.data.state) {
case State.kInProgress:
case State.kCancelled:
case State.kComplete:
case State.kPaused:
case State.kDangerous:
case State.kInterrupted:
case State.kInsecure:
return this.showCancel_ && this.data.percent >= -1;
case State.kAsyncScanning:
return true;
case State.kPromptForScanning:
case State.kPromptForLocalPasswordScanning:
return false;
default:
assertNotReached('Unhandled State encountered');
}
}
private computeShowDeepScan_(): boolean {
switch (this.data.state) {
case State.kPromptForScanning:
case State.kPromptForLocalPasswordScanning:
return true;
case State.kInProgress:
case State.kCancelled:
case State.kComplete:
case State.kPaused:
case State.kDangerous:
case State.kInterrupted:
case State.kInsecure:
case State.kAsyncScanning:
return false;
default:
assertNotReached('Unhandled State encountered');
}
}
private computeShowOpenAnyway_(): boolean {
switch (this.data.dangerType) {
case DangerType.kDeepScannedFailed:
return true;
case DangerType.kNoApplicableDangerType:
case DangerType.kDangerousFile:
case DangerType.kDangerousUrl:
case DangerType.kDangerousContent:
case DangerType.kCookieTheft:
case DangerType.kUncommonContent:
case DangerType.kDangerousHost:
case DangerType.kPotentiallyUnwanted:
case DangerType.kAsyncScanning:
case DangerType.kAsyncLocalPasswordScanning:
case DangerType.kBlockedPasswordProtected:
case DangerType.kBlockedTooLarge:
case DangerType.kSensitiveContentWarning:
case DangerType.kSensitiveContentBlock:
case DangerType.kDeepScannedSafe:
case DangerType.kDeepScannedOpenedDangerous:
case DangerType.kBlockedScanFailed:
return false;
default:
assertNotReached('Unhandled DangerType encountered');
}
}
private computeShowActionMenu_(): boolean {
if (!this.data) {
return false;
}
// If any of these actions are available, the action menu must be shown
// because they don't have corresponding "quick actions".
return !!this.pauseOrResumeText_ || // pause-or-resume
this.computeShowControlsForDangerous_() || // save-dangerous
this.data.retry || // retry
this.showDeepScan_ || // deep-scan and bypass-deep-scan
this.showCancel_ || // cancel
this.showOpenAnyway_ || // open-anyway
this.isReviewable_; // review-dangerous
}
private computeShowCopyDownloadLink_(): boolean {
return !!(this.data && this.data.url);
}
private computeShowQuickRemove_(): boolean {
return this.isReviewable_ || this.computeShowRemove_() ||
this.computeShowControlsForDangerous_();
}
private computeShowQuickShow_(): boolean {
// Only show the quick "show in folder" button if the full action menu
// is hidden. If the action menu is shown, hide the quick "show in folder"
// button to save space since this action will have an entry in the menu
// anyway.
return this.computeHasShowInFolderLink_() && !this.computeShowActionMenu_();
}
private computeTag_(): string {
switch (this.data.state) {
case State.kCancelled:
return loadTimeData.getString('statusCancelled');
case State.kInterrupted:
return this.data.lastReasonText;
case State.kComplete:
return this.data.fileExternallyRemoved ?
loadTimeData.getString('statusRemoved') :
'';
case State.kInProgress:
case State.kPaused:
case State.kDangerous:
case State.kInsecure:
case State.kAsyncScanning:
case State.kPromptForScanning:
case State.kPromptForLocalPasswordScanning:
return '';
default:
assertNotReached('Unhandled State encountered');
}
}
private isIndeterminate_(): boolean {
if (this.data.percent === -1) {
return true;
}
switch (this.data.state) {
case State.kAsyncScanning:
return true;
case State.kInProgress:
case State.kCancelled:
case State.kComplete:
case State.kPaused:
case State.kDangerous:
case State.kInterrupted:
case State.kInsecure:
case State.kPromptForScanning:
case State.kPromptForLocalPasswordScanning:
return false;
default:
assertNotReached('Unhandled State encountered');
}
}
private observeControlledBy_() {
this.$['controlled-by'].innerHTML = sanitizeInnerHtml(this.controlledBy_);
if (this.controlledBy_) {
const link = this.shadowRoot!.querySelector('#controlled-by a');
link!.setAttribute('focus-row-control', '');
link!.setAttribute('focus-type', 'controlledBy');
}
}
private shouldShowReferrerUrl_(): boolean {
return loadTimeData.getBoolean('showReferrerUrl') &&
this.data.displayReferrerUrl.data.length > 0;
}
getReferrerUrlAnchorElement(): HTMLAnchorElement|null {
return this.$['referrer-url'].querySelector('a') || null;
}
private observeDisplayType_() {
const removeFileUrlLinks = () => {
this.$.url.removeAttribute('href');
this.$['file-link'].removeAttribute('href');
};
const updateReferrerUrlLinkHref = (hrefValue?: string) => {
const referrerUrlLink = this.getReferrerUrlAnchorElement();
if (!referrerUrlLink) {
// No <a> tag, nothing to do.
return;
}
if (!hrefValue) {
referrerUrlLink.removeAttribute('href');
return;
}
referrerUrlLink.setAttribute('href', hrefValue);
referrerUrlLink.setAttribute('focus-row-control', '');
referrerUrlLink.setAttribute('focus-type', 'referrerUrl');
referrerUrlLink.setAttribute('target', '_blank');
referrerUrlLink.setAttribute('rel', 'noopener');
};
if (!this.data) {
return;
}
// "else" case already handled by `shouldShowReferrerUrl_`.
if (this.data.displayReferrerUrl.data.length > 0) {
const referrerLine = loadTimeData.getStringF(
'referrerLine', mojoString16ToString(this.data.displayReferrerUrl));
this.$['referrer-url'].innerHTML = sanitizeInnerHtml(referrerLine);
}
// Returns whether to use the file icon, and additionally clears file url
// links if necessary.
const mayUseFileIcon = () => {
const use = this.displayType_ === DisplayType.NORMAL;
if (!use) {
removeFileUrlLinks();
updateReferrerUrlLinkHref();
}
return use;
};
this.useFileIcon_ = mayUseFileIcon();
if (!this.useFileIcon_) {
return;
}
// The file is not dangerous. Link the url if supplied.
if (this.data.url) {
this.$.url.href = this.data.url.url;
} else {
removeFileUrlLinks();
}
// The file is not dangerous. Link the referrer_url if supplied.
if (this.data.referrerUrl) {
updateReferrerUrlLinkHref(this.data.referrerUrl.url);
} else {
updateReferrerUrlLinkHref();
}
const path = this.data.filePath;
IconLoaderImpl.getInstance()
.loadIcon(this.$['file-icon'], path)
.then(success => {
if (path === this.data.filePath &&
this.data.state !== State.kAsyncScanning) {
// Check again if we may use the file icon, to avoid a race between
// loading the icon and determining the proper danger type.
this.useFileIcon_ = mayUseFileIcon() && success;
}
});
}
// <if expr="_google_chrome">
private onEsbPromotionClick_() {
assert(!!this.mojoHandler_);
this.mojoHandler_.openEsbSettings();
}
// </if>
private onCopyDownloadLinkClick_(e: Event) {
if (!this.data.url) {
return;
}
navigator.clipboard.writeText(this.data.url.url);
this.displayCopyToast_(e);
}
private onMoreActionsClick_() {
const button = this.getMoreActionsButton();
// The menu button is not always shown, but if this handler is invoked, then
// it must be.
assert(!!button);
this.getMoreActionsMenu().showAt(button);
}
// Handles the "x" remove button which can be different actions depending on
// the state of the download.
private onQuickRemoveClick_(e: Event) {
if (this.isReviewable_ || this.computeShowControlsForDangerous_()) {
this.onDiscardDangerousClick_(e);
return;
}
assert(this.computeShowRemove_());
this.onRemoveClick_(e);
}
private onCancelClick_() {
this.restoreFocusAfterCancel_ = true;
assert(!!this.mojoHandler_);
this.mojoHandler_.cancel(this.data.id);
getAnnouncerInstance().announce(
loadTimeData.getString('screenreaderCanceled'));
this.getMoreActionsMenu().close();
}
private onDiscardDangerousClick_(e: Event) {
assert(!!this.mojoHandler_);
this.mojoHandler_.discardDangerous(this.data.id);
this.displayRemovedToast_(/*canUndo=*/ false, e);
this.getMoreActionsMenu().close();
}
private onOpenNowClick_() {
this.mojoHandler_!.openDuringScanningRequiringGesture(this.data.id);
this.getMoreActionsMenu().close();
}
private onDeepScanClick_() {
this.mojoHandler_!.deepScan(this.data.id);
this.getMoreActionsMenu().close();
}
private onBypassDeepScanClick_() {
this.mojoHandler_!.bypassDeepScanRequiringGesture(this.data.id);
this.getMoreActionsMenu().close();
}
private onReviewDangerousClick_() {
this.mojoHandler_!.reviewDangerousRequiringGesture(this.data.id);
this.getMoreActionsMenu().close();
}
private onOpenAnywayClick_() {
this.mojoHandler_!.openFileRequiringGesture(this.data.id);
this.getMoreActionsMenu().close();
}
private onDragStart_(e: Event) {
e.preventDefault();
this.mojoHandler_!.drag(this.data.id);
}
private onFileLinkClick_(e: Event) {
e.preventDefault();
this.mojoHandler_!.openFileRequiringGesture(this.data.id);
}
private onUrlClick_() {
if (!this.data.url) {
return;
}
chrome.send(
'metricsHandler:recordAction', ['Downloads_OpenUrlOfDownloadedItem']);
}
private doPause_() {
assert(!!this.mojoHandler_);
this.mojoHandler_.pause(this.data.id);
getAnnouncerInstance().announce(
loadTimeData.getString('screenreaderPaused'));
}
private doResume_() {
assert(!!this.mojoHandler_);
this.mojoHandler_.resume(this.data.id);
getAnnouncerInstance().announce(
loadTimeData.getString('screenreaderResumed'));
}
private onPauseOrResumeClick_() {
if (this.isInProgress_) {
this.doPause_();
} else {
this.doResume_();
}
this.getMoreActionsMenu().close();
}
private displayCopyToast_(e: Event) {
if (!this.data.url) {
return;
}
const pieces = loadTimeData.getSubstitutedStringPieces(
loadTimeData.getString('toastCopiedDownloadLink'),
this.data.url.url) as unknown as
Array<{collapsible: boolean, value: string, arg: string}>;
pieces.forEach(p => {
p.collapsible = !!p.arg;
});
getToastManager().showForStringPieces(pieces, /*hideSlotted=*/ true);
e.stopPropagation();
e.preventDefault();
}
private displayRemovedToast_(canUndo: boolean, e: Event) {
const templateStringId =
(this.displayType_ === DisplayType.NORMAL && this.completelyOnDisk_) ?
'toastDeletedFromHistoryStillOnDevice' :
'toastDeletedFromHistory';
const pieces =
loadTimeData.getSubstitutedStringPieces(
loadTimeData.getString(templateStringId), this.data.fileName) as
unknown as Array<{collapsible: boolean, value: string, arg?: string}>;
pieces.forEach(p => {
// Make the file name collapsible.
p.collapsible = !!p.arg;
});
getToastManager().showForStringPieces(pieces, /*hideSlotted=*/ !canUndo);
// Stop propagating a click to the document to remove toast.
e.stopPropagation();
e.preventDefault();
}
private onRemoveClick_(e: Event) {
assert(!!this.mojoHandler_);
this.mojoHandler_.remove(this.data.id);
const canUndo = !this.data.isDangerous && !this.data.isInsecure;
this.displayRemovedToast_(canUndo, e);
this.getMoreActionsMenu().close();
}
private onRetryClick_() {
this.mojoHandler_!.retryDownload(this.data.id);
this.getMoreActionsMenu().close();
}
private notifySaveDangerousClick_() {
this.dispatchEvent(new CustomEvent('save-dangerous-click', {
bubbles: true,
composed: true,
detail: {id: this.data.id},
}));
}
private onSaveDangerousClick_() {
this.getMoreActionsMenu().close();
if (this.displayType_ === DisplayType.DANGEROUS) {
this.notifySaveDangerousClick_();
return;
}
// "Suspicious" types which show up in grey can be validated directly.
// This maps each such display type to its applicable screenreader
// announcement string id.
const SAVED_FROM_PAGE_TYPES_ANNOUNCEMENTS = new Map([
[DisplayType.SUSPICIOUS, 'screenreaderSavedSuspicious'],
[DisplayType.UNVERIFIED, 'screenreaderSavedUnverified'],
[DisplayType.INSECURE, 'screenreaderSavedInsecure'],
]);
assert(SAVED_FROM_PAGE_TYPES_ANNOUNCEMENTS.has(this.displayType_));
assert(!!this.mojoHandler_);
this.mojoHandler_.saveSuspiciousRequiringGesture(this.data.id);
const announcement = loadTimeData.getString(
SAVED_FROM_PAGE_TYPES_ANNOUNCEMENTS.get(this.displayType_) as string);
getAnnouncerInstance().announce(announcement);
}
private onShowClick_() {
this.mojoHandler_!.show(this.data.id);
this.getMoreActionsMenu().close();
}
private restoreFocusAfterCancelIfNeeded_() {
if (!this.restoreFocusAfterCancel_) {
return;
}
this.restoreFocusAfterCancel_ = false;
setTimeout(() => {
const element = this.getFocusRow().getFirstFocusable('retry');
if (element) {
(element as HTMLElement).focus();
}
});
}
}
declare global {
interface HTMLElementTagNameMap {
'downloads-item': DownloadsItemElement;
}
}
customElements.define(DownloadsItemElement.is, DownloadsItemElement);