1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5// TODO(jhawkins): Use hidden instead of showInline* and display:none.
6
7/**
8 * The type of the download object. The definition is based on
9 * chrome/browser/ui/webui/downloads_dom_handler.cc:CreateDownloadItemValue()
10 * @typedef {{by_ext_id: (string|undefined),
11 *            by_ext_name: (string|undefined),
12 *            danger_type: (string|undefined),
13 *            date_string: string,
14 *            file_externally_removed: boolean,
15 *            file_name: string,
16 *            file_path: string,
17 *            file_url: string,
18 *            id: string,
19 *            last_reason_text: (string|undefined),
20 *            otr: boolean,
21 *            percent: (number|undefined),
22 *            progress_status_text: (string|undefined),
23 *            received: (number|undefined),
24 *            resume: boolean,
25 *            retry: boolean,
26 *            since_string: string,
27 *            started: number,
28 *            state: string,
29 *            total: number,
30 *            url: string}}
31 */
32var BackendDownloadObject;
33
34/**
35 * Sets the display style of a node.
36 * @param {!Element} node The target element to show or hide.
37 * @param {boolean} isShow Should the target element be visible.
38 */
39function showInline(node, isShow) {
40  node.style.display = isShow ? 'inline' : 'none';
41}
42
43/**
44 * Sets the display style of a node.
45 * @param {!Element} node The target element to show or hide.
46 * @param {boolean} isShow Should the target element be visible.
47 */
48function showInlineBlock(node, isShow) {
49  node.style.display = isShow ? 'inline-block' : 'none';
50}
51
52/**
53 * Creates a link with a specified onclick handler and content.
54 * @param {function()} onclick The onclick handler.
55 * @param {string} value The link text.
56 * @return {!Element} The created link element.
57 */
58function createLink(onclick, value) {
59  var link = document.createElement('a');
60  link.onclick = onclick;
61  link.href = '#';
62  link.textContent = value;
63  link.oncontextmenu = function() { return false; };
64  return link;
65}
66
67/**
68 * Creates a button with a specified onclick handler and content.
69 * @param {function()} onclick The onclick handler.
70 * @param {string} value The button text.
71 * @return {Element} The created button.
72 */
73function createButton(onclick, value) {
74  var button = document.createElement('input');
75  button.type = 'button';
76  button.value = value;
77  button.onclick = onclick;
78  return button;
79}
80
81///////////////////////////////////////////////////////////////////////////////
82// Downloads
83/**
84 * Class to hold all the information about the visible downloads.
85 * @constructor
86 */
87function Downloads() {
88  /**
89   * @type {!Object.<string, Download>}
90   * @private
91   */
92  this.downloads_ = {};
93  this.node_ = $('downloads-display');
94  this.summary_ = $('downloads-summary-text');
95  this.searchText_ = '';
96
97  // Keep track of the dates of the newest and oldest downloads so that we
98  // know where to insert them.
99  this.newestTime_ = -1;
100
101  // Icon load request queue.
102  this.iconLoadQueue_ = [];
103  this.isIconLoading_ = false;
104
105  this.progressForeground1_ = new Image();
106  this.progressForeground1_.src =
107    'chrome://theme/IDR_DOWNLOAD_PROGRESS_FOREGROUND_32@1x';
108  this.progressForeground2_ = new Image();
109  this.progressForeground2_.src =
110    'chrome://theme/IDR_DOWNLOAD_PROGRESS_FOREGROUND_32@2x';
111
112  window.addEventListener('keydown', this.onKeyDown_.bind(this));
113}
114
115/**
116 * Called when a download has been updated or added.
117 * @param {BackendDownloadObject} download A backend download object
118 */
119Downloads.prototype.updated = function(download) {
120  var id = download.id;
121  if (!!this.downloads_[id]) {
122    this.downloads_[id].update(download);
123  } else {
124    this.downloads_[id] = new Download(download);
125    // We get downloads in display order, so we don't have to worry about
126    // maintaining correct order - we can assume that any downloads not in
127    // display order are new ones and so we can add them to the top of the
128    // list.
129    if (download.started > this.newestTime_) {
130      this.node_.insertBefore(this.downloads_[id].node, this.node_.firstChild);
131      this.newestTime_ = download.started;
132    } else {
133      this.node_.appendChild(this.downloads_[id].node);
134    }
135  }
136  // Download.prototype.update may change its nodeSince_ and nodeDate_, so
137  // update all the date displays.
138  // TODO(benjhayden) Only do this if its nodeSince_ or nodeDate_ actually did
139  // change since this may touch 150 elements and Downloads.prototype.updated
140  // may be called 150 times.
141  this.updateDateDisplay_();
142};
143
144/**
145 * Set our display search text.
146 * @param {string} searchText The string we're searching for.
147 */
148Downloads.prototype.setSearchText = function(searchText) {
149  this.searchText_ = searchText;
150};
151
152/**
153 * Update the summary block above the results
154 */
155Downloads.prototype.updateSummary = function() {
156  if (this.searchText_) {
157    this.summary_.textContent = loadTimeData.getStringF('searchresultsfor',
158                                                        this.searchText_);
159  } else {
160    this.summary_.textContent = '';
161  }
162
163  var hasDownloads = false;
164  for (var i in this.downloads_) {
165    hasDownloads = true;
166    break;
167  }
168};
169
170/**
171 * Returns the number of downloads in the model. Used by tests.
172 * @return {number} Returns the number of downloads shown on the page.
173 */
174Downloads.prototype.size = function() {
175  return Object.keys(this.downloads_).length;
176};
177
178/**
179 * Update the date visibility in our nodes so that no date is
180 * repeated.
181 * @private
182 */
183Downloads.prototype.updateDateDisplay_ = function() {
184  var dateContainers = document.getElementsByClassName('date-container');
185  var displayed = {};
186  for (var i = 0, container; container = dateContainers[i]; i++) {
187    var dateString = container.getElementsByClassName('date')[0].innerHTML;
188    if (!!displayed[dateString]) {
189      container.style.display = 'none';
190    } else {
191      displayed[dateString] = true;
192      container.style.display = 'block';
193    }
194  }
195};
196
197/**
198 * Remove a download.
199 * @param {string} id The id of the download to remove.
200 */
201Downloads.prototype.remove = function(id) {
202  this.node_.removeChild(this.downloads_[id].node);
203  delete this.downloads_[id];
204  this.updateDateDisplay_();
205};
206
207/**
208 * Clear all downloads and reset us back to a null state.
209 */
210Downloads.prototype.clear = function() {
211  for (var id in this.downloads_) {
212    this.downloads_[id].clear();
213    this.remove(id);
214  }
215};
216
217/**
218 * Schedule icon load.
219 * @param {HTMLImageElement} elem Image element that should contain the icon.
220 * @param {string} iconURL URL to the icon.
221 */
222Downloads.prototype.scheduleIconLoad = function(elem, iconURL) {
223  var self = this;
224
225  // Sends request to the next icon in the queue and schedules
226  // call to itself when the icon is loaded.
227  function loadNext() {
228    self.isIconLoading_ = true;
229    while (self.iconLoadQueue_.length > 0) {
230      var request = self.iconLoadQueue_.shift();
231      var oldSrc = request.element.src;
232      request.element.onabort = request.element.onerror =
233          request.element.onload = loadNext;
234      request.element.src = request.url;
235      if (oldSrc != request.element.src)
236        return;
237    }
238    self.isIconLoading_ = false;
239  }
240
241  // Create new request
242  var loadRequest = {element: elem, url: iconURL};
243  this.iconLoadQueue_.push(loadRequest);
244
245  // Start loading if none scheduled yet
246  if (!this.isIconLoading_)
247    loadNext();
248};
249
250/**
251 * Returns whether the displayed list needs to be updated or not.
252 * @param {Array} downloads Array of download nodes.
253 * @return {boolean} Returns true if the displayed list is to be updated.
254 */
255Downloads.prototype.isUpdateNeeded = function(downloads) {
256  var size = 0;
257  for (var i in this.downloads_)
258    size++;
259  if (size != downloads.length)
260    return true;
261  // Since there are the same number of items in the incoming list as
262  // |this.downloads_|, there won't be any removed downloads without some
263  // downloads having been inserted.  So check only for new downloads in
264  // deciding whether to update.
265  for (var i = 0; i < downloads.length; i++) {
266    if (!this.downloads_[downloads[i].id])
267      return true;
268  }
269  return false;
270};
271
272/**
273 * Handles shortcut keys.
274 * @param {Event} evt The keyboard event.
275 * @private
276 */
277Downloads.prototype.onKeyDown_ = function(evt) {
278  var keyEvt = /** @type {KeyboardEvent} */(evt);
279  if (keyEvt.keyCode == 67 && keyEvt.altKey) {  // alt + c.
280    clearAll();
281    keyEvt.preventDefault();
282  }
283};
284
285///////////////////////////////////////////////////////////////////////////////
286// Download
287/**
288 * A download and the DOM representation for that download.
289 * @param {BackendDownloadObject} download A backend download object
290 * @constructor
291 */
292function Download(download) {
293  // Create DOM
294  this.node = createElementWithClassName(
295      'div', 'download' + (download.otr ? ' otr' : ''));
296
297  // Dates
298  this.dateContainer_ = createElementWithClassName('div', 'date-container');
299  this.node.appendChild(this.dateContainer_);
300
301  this.nodeSince_ = createElementWithClassName('div', 'since');
302  this.nodeDate_ = createElementWithClassName('div', 'date');
303  this.dateContainer_.appendChild(this.nodeSince_);
304  this.dateContainer_.appendChild(this.nodeDate_);
305
306  // Container for all 'safe download' UI.
307  this.safe_ = createElementWithClassName('div', 'safe');
308  this.safe_.ondragstart = this.drag_.bind(this);
309  this.node.appendChild(this.safe_);
310
311  if (download.state != Download.States.COMPLETE) {
312    this.nodeProgressBackground_ =
313        createElementWithClassName('div', 'progress background');
314    this.safe_.appendChild(this.nodeProgressBackground_);
315
316    this.nodeProgressForeground_ =
317        createElementWithClassName('canvas', 'progress');
318    this.nodeProgressForeground_.width = Download.Progress.width;
319    this.nodeProgressForeground_.height = Download.Progress.height;
320    this.canvasProgress_ = this.nodeProgressForeground_.getContext('2d');
321
322    this.safe_.appendChild(this.nodeProgressForeground_);
323  }
324
325  this.nodeImg_ = createElementWithClassName('img', 'icon');
326  this.nodeImg_.alt = '';
327  this.safe_.appendChild(this.nodeImg_);
328
329  // FileLink is used for completed downloads, otherwise we show FileName.
330  this.nodeTitleArea_ = createElementWithClassName('div', 'title-area');
331  this.safe_.appendChild(this.nodeTitleArea_);
332
333  this.nodeFileLink_ = createLink(this.openFile_.bind(this), '');
334  this.nodeFileLink_.className = 'name';
335  this.nodeFileLink_.style.display = 'none';
336  this.nodeTitleArea_.appendChild(this.nodeFileLink_);
337
338  this.nodeFileName_ = createElementWithClassName('span', 'name');
339  this.nodeFileName_.style.display = 'none';
340  this.nodeTitleArea_.appendChild(this.nodeFileName_);
341
342  this.nodeStatus_ = createElementWithClassName('span', 'status');
343  this.nodeTitleArea_.appendChild(this.nodeStatus_);
344
345  var nodeURLDiv = createElementWithClassName('div', 'url-container');
346  this.safe_.appendChild(nodeURLDiv);
347
348  this.nodeURL_ = createElementWithClassName('a', 'src-url');
349  this.nodeURL_.target = '_blank';
350  nodeURLDiv.appendChild(this.nodeURL_);
351
352  // Controls.
353  this.nodeControls_ = createElementWithClassName('div', 'controls');
354  this.safe_.appendChild(this.nodeControls_);
355
356  // We don't need 'show in folder' in chromium os. See download_ui.cc and
357  // http://code.google.com/p/chromium-os/issues/detail?id=916.
358  if (loadTimeData.valueExists('control_showinfolder')) {
359    this.controlShow_ = createLink(this.show_.bind(this),
360        loadTimeData.getString('control_showinfolder'));
361    this.nodeControls_.appendChild(this.controlShow_);
362  } else {
363    this.controlShow_ = null;
364  }
365
366  this.controlRetry_ = document.createElement('a');
367  this.controlRetry_.download = '';
368  this.controlRetry_.textContent = loadTimeData.getString('control_retry');
369  this.nodeControls_.appendChild(this.controlRetry_);
370
371  // Pause/Resume are a toggle.
372  this.controlPause_ = createLink(this.pause_.bind(this),
373      loadTimeData.getString('control_pause'));
374  this.nodeControls_.appendChild(this.controlPause_);
375
376  this.controlResume_ = createLink(this.resume_.bind(this),
377      loadTimeData.getString('control_resume'));
378  this.nodeControls_.appendChild(this.controlResume_);
379
380  // Anchors <a> don't support the "disabled" property.
381  if (loadTimeData.getBoolean('allow_deleting_history')) {
382    this.controlRemove_ = createLink(this.remove_.bind(this),
383        loadTimeData.getString('control_removefromlist'));
384    this.controlRemove_.classList.add('control-remove-link');
385  } else {
386    this.controlRemove_ = document.createElement('span');
387    this.controlRemove_.classList.add('disabled-link');
388    var text = document.createTextNode(
389        loadTimeData.getString('control_removefromlist'));
390    this.controlRemove_.appendChild(text);
391  }
392  if (!loadTimeData.getBoolean('show_delete_history'))
393    this.controlRemove_.hidden = true;
394
395  this.nodeControls_.appendChild(this.controlRemove_);
396
397  this.controlCancel_ = createLink(this.cancel_.bind(this),
398      loadTimeData.getString('control_cancel'));
399  this.nodeControls_.appendChild(this.controlCancel_);
400
401  this.controlByExtension_ = document.createElement('span');
402  this.nodeControls_.appendChild(this.controlByExtension_);
403
404  // Container for 'unsafe download' UI.
405  this.danger_ = createElementWithClassName('div', 'show-dangerous');
406  this.node.appendChild(this.danger_);
407
408  this.dangerNodeImg_ = createElementWithClassName('img', 'icon');
409  this.dangerNodeImg_.alt = '';
410  this.danger_.appendChild(this.dangerNodeImg_);
411
412  this.dangerDesc_ = document.createElement('div');
413  this.danger_.appendChild(this.dangerDesc_);
414
415  // Buttons for the malicious case.
416  this.malwareNodeControls_ = createElementWithClassName('div', 'controls');
417  this.malwareSave_ = createLink(
418      this.saveDangerous_.bind(this),
419      loadTimeData.getString('danger_restore'));
420  this.malwareNodeControls_.appendChild(this.malwareSave_);
421  this.malwareDiscard_ = createLink(
422      this.discardDangerous_.bind(this),
423      loadTimeData.getString('control_removefromlist'));
424  this.malwareNodeControls_.appendChild(this.malwareDiscard_);
425  this.danger_.appendChild(this.malwareNodeControls_);
426
427  // Buttons for the dangerous but not malicious case.
428  this.dangerSave_ = createButton(
429      this.saveDangerous_.bind(this),
430      loadTimeData.getString('danger_save'));
431  this.danger_.appendChild(this.dangerSave_);
432
433  this.dangerDiscard_ = createButton(
434      this.discardDangerous_.bind(this),
435      loadTimeData.getString('danger_discard'));
436  this.danger_.appendChild(this.dangerDiscard_);
437
438  // Update member vars.
439  this.update(download);
440}
441
442/**
443 * The states a download can be in. These correspond to states defined in
444 * DownloadsDOMHandler::CreateDownloadItemValue
445 * @enum {string}
446 */
447Download.States = {
448  IN_PROGRESS: 'IN_PROGRESS',
449  CANCELLED: 'CANCELLED',
450  COMPLETE: 'COMPLETE',
451  PAUSED: 'PAUSED',
452  DANGEROUS: 'DANGEROUS',
453  INTERRUPTED: 'INTERRUPTED',
454};
455
456/**
457 * Explains why a download is in DANGEROUS state.
458 * @enum {string}
459 */
460Download.DangerType = {
461  NOT_DANGEROUS: 'NOT_DANGEROUS',
462  DANGEROUS_FILE: 'DANGEROUS_FILE',
463  DANGEROUS_URL: 'DANGEROUS_URL',
464  DANGEROUS_CONTENT: 'DANGEROUS_CONTENT',
465  UNCOMMON_CONTENT: 'UNCOMMON_CONTENT',
466  DANGEROUS_HOST: 'DANGEROUS_HOST',
467  POTENTIALLY_UNWANTED: 'POTENTIALLY_UNWANTED',
468};
469
470/**
471 * @param {number} a Some float.
472 * @param {number} b Some float.
473 * @param {number=} opt_pct Percent of min(a,b).
474 * @return {boolean} true if a is within opt_pct percent of b.
475 */
476function floatEq(a, b, opt_pct) {
477  return Math.abs(a - b) < (Math.min(a, b) * (opt_pct || 1.0) / 100.0);
478}
479
480/**
481 * Constants and "constants" for the progress meter.
482 */
483Download.Progress = {
484  START_ANGLE: -0.5 * Math.PI,
485  SIDE: 48,
486};
487
488/***/
489Download.Progress.HALF = Download.Progress.SIDE / 2;
490
491function computeDownloadProgress() {
492  if (floatEq(Download.Progress.scale, window.devicePixelRatio)) {
493    // Zooming in or out multiple times then typing Ctrl+0 resets the zoom level
494    // directly to 1x, which fires the matchMedia event multiple times.
495    return;
496  }
497  Download.Progress.scale = window.devicePixelRatio;
498  Download.Progress.width = Download.Progress.SIDE * Download.Progress.scale;
499  Download.Progress.height = Download.Progress.SIDE * Download.Progress.scale;
500  Download.Progress.radius = Download.Progress.HALF * Download.Progress.scale;
501  Download.Progress.centerX = Download.Progress.HALF * Download.Progress.scale;
502  Download.Progress.centerY = Download.Progress.HALF * Download.Progress.scale;
503}
504computeDownloadProgress();
505
506// Listens for when device-pixel-ratio changes between any zoom level.
507[0.3, 0.4, 0.6, 0.7, 0.8, 0.95, 1.05, 1.2, 1.4, 1.6, 1.9, 2.2, 2.7, 3.5, 4.5
508].forEach(function(scale) {
509  var media = '(-webkit-min-device-pixel-ratio:' + scale + ')';
510  window.matchMedia(media).addListener(computeDownloadProgress);
511});
512
513/**
514 * Updates the download to reflect new data.
515 * @param {BackendDownloadObject} download A backend download object
516 */
517Download.prototype.update = function(download) {
518  this.id_ = download.id;
519  this.filePath_ = download.file_path;
520  this.fileUrl_ = download.file_url;
521  this.fileName_ = download.file_name;
522  this.url_ = download.url;
523  this.state_ = download.state;
524  this.fileExternallyRemoved_ = download.file_externally_removed;
525  this.dangerType_ = download.danger_type;
526  this.lastReasonDescription_ = download.last_reason_text;
527  this.byExtensionId_ = download.by_ext_id;
528  this.byExtensionName_ = download.by_ext_name;
529
530  this.since_ = download.since_string;
531  this.date_ = download.date_string;
532
533  // See DownloadItem::PercentComplete
534  this.percent_ = Math.max(download.percent, 0);
535  this.progressStatusText_ = download.progress_status_text;
536  this.received_ = download.received;
537
538  if (this.state_ == Download.States.DANGEROUS) {
539    this.updateDangerousFile();
540  } else {
541    downloads.scheduleIconLoad(this.nodeImg_,
542                               'chrome://fileicon/' +
543                                   encodeURIComponent(this.filePath_) +
544                                   '?scale=' + window.devicePixelRatio + 'x');
545
546    if (this.state_ == Download.States.COMPLETE &&
547        !this.fileExternallyRemoved_) {
548      this.nodeFileLink_.textContent = this.fileName_;
549      this.nodeFileLink_.href = this.fileUrl_;
550      this.nodeFileLink_.oncontextmenu = null;
551    } else if (this.nodeFileName_.textContent != this.fileName_) {
552      this.nodeFileName_.textContent = this.fileName_;
553    }
554    if (this.state_ == Download.States.INTERRUPTED) {
555      this.nodeFileName_.classList.add('interrupted');
556    } else if (this.nodeFileName_.classList.contains('interrupted')) {
557      this.nodeFileName_.classList.remove('interrupted');
558    }
559
560    showInline(this.nodeFileLink_,
561               this.state_ == Download.States.COMPLETE &&
562                   !this.fileExternallyRemoved_);
563    // nodeFileName_ has to be inline-block to avoid the 'interaction' with
564    // nodeStatus_. If both are inline, it appears that their text contents
565    // are merged before the bidi algorithm is applied leading to an
566    // undesirable reordering. http://crbug.com/13216
567    showInlineBlock(this.nodeFileName_,
568                    this.state_ != Download.States.COMPLETE ||
569                        this.fileExternallyRemoved_);
570
571    if (this.state_ == Download.States.IN_PROGRESS) {
572      this.nodeProgressForeground_.style.display = 'block';
573      this.nodeProgressBackground_.style.display = 'block';
574      this.nodeProgressForeground_.width = Download.Progress.width;
575      this.nodeProgressForeground_.height = Download.Progress.height;
576
577      var foregroundImage = (window.devicePixelRatio < 2) ?
578        downloads.progressForeground1_ : downloads.progressForeground2_;
579
580      // Draw a pie-slice for the progress.
581      this.canvasProgress_.globalCompositeOperation = 'copy';
582      this.canvasProgress_.drawImage(
583          foregroundImage,
584          0, 0,  // sx, sy
585          foregroundImage.width,
586          foregroundImage.height,
587          0, 0,  // x, y
588          Download.Progress.width, Download.Progress.height);
589      this.canvasProgress_.globalCompositeOperation = 'destination-in';
590      this.canvasProgress_.beginPath();
591      this.canvasProgress_.moveTo(Download.Progress.centerX,
592                                  Download.Progress.centerY);
593
594      // Draw an arc CW for both RTL and LTR. http://crbug.com/13215
595      this.canvasProgress_.arc(Download.Progress.centerX,
596                               Download.Progress.centerY,
597                               Download.Progress.radius,
598                               Download.Progress.START_ANGLE,
599                               Download.Progress.START_ANGLE + Math.PI * 0.02 *
600                               Number(this.percent_),
601                               false);
602
603      this.canvasProgress_.lineTo(Download.Progress.centerX,
604                                  Download.Progress.centerY);
605      this.canvasProgress_.fill();
606      this.canvasProgress_.closePath();
607    } else if (this.nodeProgressBackground_) {
608      this.nodeProgressForeground_.style.display = 'none';
609      this.nodeProgressBackground_.style.display = 'none';
610    }
611
612    if (this.controlShow_) {
613      showInline(this.controlShow_,
614                 this.state_ == Download.States.COMPLETE &&
615                     !this.fileExternallyRemoved_);
616    }
617    showInline(this.controlRetry_, download.retry);
618    this.controlRetry_.href = this.url_;
619    showInline(this.controlPause_, this.state_ == Download.States.IN_PROGRESS);
620    showInline(this.controlResume_, download.resume);
621    var showCancel = this.state_ == Download.States.IN_PROGRESS ||
622                     this.state_ == Download.States.PAUSED;
623    showInline(this.controlCancel_, showCancel);
624    showInline(this.controlRemove_, !showCancel);
625
626    if (this.byExtensionId_ && this.byExtensionName_) {
627      // Format 'control_by_extension' with a link instead of plain text by
628      // splitting the formatted string into pieces.
629      var slug = 'XXXXX';
630      var formatted = loadTimeData.getStringF('control_by_extension', slug);
631      var slugIndex = formatted.indexOf(slug);
632      this.controlByExtension_.textContent = formatted.substr(0, slugIndex);
633      this.controlByExtensionLink_ = document.createElement('a');
634      this.controlByExtensionLink_.href =
635          'chrome://extensions#' + this.byExtensionId_;
636      this.controlByExtensionLink_.textContent = this.byExtensionName_;
637      this.controlByExtension_.appendChild(this.controlByExtensionLink_);
638      if (slugIndex < (formatted.length - slug.length))
639        this.controlByExtension_.appendChild(document.createTextNode(
640            formatted.substr(slugIndex + 1)));
641    }
642
643    this.nodeSince_.textContent = this.since_;
644    this.nodeDate_.textContent = this.date_;
645    // Don't unnecessarily update the url, as doing so will remove any
646    // text selection the user has started (http://crbug.com/44982).
647    if (this.nodeURL_.textContent != this.url_) {
648      this.nodeURL_.textContent = this.url_;
649      this.nodeURL_.href = this.url_;
650    }
651    this.nodeStatus_.textContent = this.getStatusText_();
652
653    this.danger_.style.display = 'none';
654    this.safe_.style.display = 'block';
655  }
656};
657
658/**
659 * Decorates the icons, strings, and buttons for a download to reflect the
660 * danger level of a file. Dangerous & malicious files are treated differently.
661 */
662Download.prototype.updateDangerousFile = function() {
663  switch (this.dangerType_) {
664    case Download.DangerType.DANGEROUS_FILE: {
665      this.dangerDesc_.textContent = loadTimeData.getStringF(
666          'danger_file_desc', this.fileName_);
667      break;
668    }
669    case Download.DangerType.DANGEROUS_URL: {
670      this.dangerDesc_.textContent = loadTimeData.getString('danger_url_desc');
671      break;
672    }
673    case Download.DangerType.DANGEROUS_CONTENT:  // Fall through.
674    case Download.DangerType.DANGEROUS_HOST: {
675      this.dangerDesc_.textContent = loadTimeData.getStringF(
676          'danger_content_desc', this.fileName_);
677      break;
678    }
679    case Download.DangerType.UNCOMMON_CONTENT: {
680      this.dangerDesc_.textContent = loadTimeData.getStringF(
681          'danger_uncommon_desc', this.fileName_);
682      break;
683    }
684    case Download.DangerType.POTENTIALLY_UNWANTED: {
685      this.dangerDesc_.textContent = loadTimeData.getStringF(
686          'danger_settings_desc', this.fileName_);
687      break;
688    }
689  }
690
691  if (this.dangerType_ == Download.DangerType.DANGEROUS_FILE) {
692    downloads.scheduleIconLoad(
693        this.dangerNodeImg_,
694        'chrome://theme/IDR_WARNING?scale=' + window.devicePixelRatio + 'x');
695  } else {
696    downloads.scheduleIconLoad(
697        this.dangerNodeImg_,
698        'chrome://theme/IDR_SAFEBROWSING_WARNING?scale=' +
699            window.devicePixelRatio + 'x');
700    this.dangerDesc_.className = 'malware-description';
701  }
702
703  if (this.dangerType_ == Download.DangerType.DANGEROUS_CONTENT ||
704      this.dangerType_ == Download.DangerType.DANGEROUS_HOST ||
705      this.dangerType_ == Download.DangerType.DANGEROUS_URL ||
706      this.dangerType_ == Download.DangerType.POTENTIALLY_UNWANTED) {
707    this.malwareNodeControls_.style.display = 'block';
708    this.dangerDiscard_.style.display = 'none';
709    this.dangerSave_.style.display = 'none';
710  } else {
711    this.malwareNodeControls_.style.display = 'none';
712    this.dangerDiscard_.style.display = 'inline';
713    this.dangerSave_.style.display = 'inline';
714  }
715
716  this.danger_.style.display = 'block';
717  this.safe_.style.display = 'none';
718};
719
720/**
721 * Removes applicable bits from the DOM in preparation for deletion.
722 */
723Download.prototype.clear = function() {
724  this.safe_.ondragstart = null;
725  this.nodeFileLink_.onclick = null;
726  if (this.controlShow_) {
727    this.controlShow_.onclick = null;
728  }
729  this.controlCancel_.onclick = null;
730  this.controlPause_.onclick = null;
731  this.controlResume_.onclick = null;
732  this.dangerDiscard_.onclick = null;
733  this.dangerSave_.onclick = null;
734  this.malwareDiscard_.onclick = null;
735  this.malwareSave_.onclick = null;
736
737  this.node.innerHTML = '';
738};
739
740/**
741 * @private
742 * @return {string} User-visible status update text.
743 */
744Download.prototype.getStatusText_ = function() {
745  switch (this.state_) {
746    case Download.States.IN_PROGRESS:
747      return this.progressStatusText_;
748    case Download.States.CANCELLED:
749      return loadTimeData.getString('status_cancelled');
750    case Download.States.PAUSED:
751      return loadTimeData.getString('status_paused');
752    case Download.States.DANGEROUS:
753      // danger_url_desc is also used by DANGEROUS_CONTENT.
754      var desc = this.dangerType_ == Download.DangerType.DANGEROUS_FILE ?
755          'danger_file_desc' : 'danger_url_desc';
756      return loadTimeData.getString(desc);
757    case Download.States.INTERRUPTED:
758      return this.lastReasonDescription_;
759    case Download.States.COMPLETE:
760      return this.fileExternallyRemoved_ ?
761          loadTimeData.getString('status_removed') : '';
762  }
763  assertNotReached();
764  return '';
765};
766
767/**
768 * Tells the backend to initiate a drag, allowing users to drag
769 * files from the download page and have them appear as native file
770 * drags.
771 * @return {boolean} Returns false to prevent the default action.
772 * @private
773 */
774Download.prototype.drag_ = function() {
775  chrome.send('drag', [this.id_.toString()]);
776  return false;
777};
778
779/**
780 * Tells the backend to open this file.
781 * @return {boolean} Returns false to prevent the default action.
782 * @private
783 */
784Download.prototype.openFile_ = function() {
785  chrome.send('openFile', [this.id_.toString()]);
786  return false;
787};
788
789/**
790 * Tells the backend that the user chose to save a dangerous file.
791 * @return {boolean} Returns false to prevent the default action.
792 * @private
793 */
794Download.prototype.saveDangerous_ = function() {
795  chrome.send('saveDangerous', [this.id_.toString()]);
796  return false;
797};
798
799/**
800 * Tells the backend that the user chose to discard a dangerous file.
801 * @return {boolean} Returns false to prevent the default action.
802 * @private
803 */
804Download.prototype.discardDangerous_ = function() {
805  chrome.send('discardDangerous', [this.id_.toString()]);
806  downloads.remove(this.id_);
807  return false;
808};
809
810/**
811 * Tells the backend to show the file in explorer.
812 * @return {boolean} Returns false to prevent the default action.
813 * @private
814 */
815Download.prototype.show_ = function() {
816  chrome.send('show', [this.id_.toString()]);
817  return false;
818};
819
820/**
821 * Tells the backend to pause this download.
822 * @return {boolean} Returns false to prevent the default action.
823 * @private
824 */
825Download.prototype.pause_ = function() {
826  chrome.send('pause', [this.id_.toString()]);
827  return false;
828};
829
830/**
831 * Tells the backend to resume this download.
832 * @return {boolean} Returns false to prevent the default action.
833 * @private
834 */
835Download.prototype.resume_ = function() {
836  chrome.send('resume', [this.id_.toString()]);
837  return false;
838};
839
840/**
841 * Tells the backend to remove this download from history and download shelf.
842 * @return {boolean} Returns false to prevent the default action.
843 * @private
844 */
845 Download.prototype.remove_ = function() {
846   if (loadTimeData.getBoolean('allow_deleting_history')) {
847    chrome.send('remove', [this.id_.toString()]);
848  }
849  return false;
850};
851
852/**
853 * Tells the backend to cancel this download.
854 * @return {boolean} Returns false to prevent the default action.
855 * @private
856 */
857Download.prototype.cancel_ = function() {
858  chrome.send('cancel', [this.id_.toString()]);
859  return false;
860};
861
862///////////////////////////////////////////////////////////////////////////////
863// Page:
864var downloads, resultsTimeout;
865
866// TODO(benjhayden): Rename Downloads to DownloadManager, downloads to
867// downloadManager or theDownloadManager or DownloadManager.get() to prevent
868// confusing Downloads with Download.
869
870/**
871 * The FIFO array that stores updates of download files to be appeared
872 * on the download page. It is guaranteed that the updates in this array
873 * are reflected to the download page in a FIFO order.
874*/
875var fifoResults;
876
877function load() {
878  chrome.send('onPageLoaded');
879  fifoResults = [];
880  downloads = new Downloads();
881  $('term').focus();
882  setSearch('');
883
884  var clearAllHolder = $('clear-all-holder');
885  var clearAllElement;
886  if (loadTimeData.getBoolean('allow_deleting_history')) {
887    clearAllElement = createLink(clearAll, loadTimeData.getString('clear_all'));
888    clearAllElement.classList.add('clear-all-link');
889    clearAllHolder.classList.remove('disabled-link');
890  } else {
891    clearAllElement = document.createTextNode(
892        loadTimeData.getString('clear_all'));
893    clearAllHolder.classList.add('disabled-link');
894  }
895  if (!loadTimeData.getBoolean('show_delete_history'))
896    clearAllHolder.hidden = true;
897
898  clearAllHolder.appendChild(clearAllElement);
899  clearAllElement.oncontextmenu = function() { return false; };
900
901  // TODO(jhawkins): Use a link-button here.
902  var openDownloadsFolderLink = $('open-downloads-folder');
903  openDownloadsFolderLink.onclick = function() {
904    chrome.send('openDownloadsFolder');
905  };
906  openDownloadsFolderLink.oncontextmenu = function() { return false; };
907
908  $('term').onsearch = function(e) {
909    setSearch($('term').value);
910  };
911}
912
913function setSearch(searchText) {
914  fifoResults.length = 0;
915  downloads.setSearchText(searchText);
916  searchText = searchText.toString().match(/(?:[^\s"]+|"[^"]*")+/g);
917  if (searchText) {
918    searchText = searchText.map(function(term) {
919      // strip quotes
920      return (term.match(/\s/) &&
921              term[0].match(/["']/) &&
922              term[term.length - 1] == term[0]) ?
923        term.substr(1, term.length - 2) : term;
924    });
925  } else {
926    searchText = [];
927  }
928  chrome.send('getDownloads', searchText);
929}
930
931function clearAll() {
932  if (!loadTimeData.getBoolean('allow_deleting_history'))
933    return;
934
935  fifoResults.length = 0;
936  downloads.clear();
937  downloads.setSearchText('');
938  chrome.send('clearAll');
939}
940
941///////////////////////////////////////////////////////////////////////////////
942// Chrome callbacks:
943/**
944 * Our history system calls this function with results from searches or when
945 * downloads are added or removed.
946 * @param {Array.<Object>} results List of updates.
947 */
948function downloadsList(results) {
949  if (downloads && downloads.isUpdateNeeded(results)) {
950    if (resultsTimeout)
951      clearTimeout(resultsTimeout);
952    fifoResults.length = 0;
953    downloads.clear();
954    downloadUpdated(results);
955  }
956  downloads.updateSummary();
957}
958
959/**
960 * When a download is updated (progress, state change), this is called.
961 * @param {Array.<Object>} results List of updates for the download process.
962 */
963function downloadUpdated(results) {
964  // Sometimes this can get called too early.
965  if (!downloads)
966    return;
967
968  fifoResults = fifoResults.concat(results);
969  tryDownloadUpdatedPeriodically();
970}
971
972/**
973 * Try to reflect as much updates as possible within 50ms.
974 * This function is scheduled again and again until all updates are reflected.
975 */
976function tryDownloadUpdatedPeriodically() {
977  var start = Date.now();
978  while (fifoResults.length) {
979    var result = fifoResults.shift();
980    downloads.updated(result);
981    // Do as much as we can in 50ms.
982    if (Date.now() - start > 50) {
983      clearTimeout(resultsTimeout);
984      resultsTimeout = setTimeout(tryDownloadUpdatedPeriodically, 5);
985      break;
986    }
987  }
988}
989
990// Add handlers to HTML elements.
991window.addEventListener('DOMContentLoaded', load);
992
993preventDefaultOnPoundLinkClicks();  // From util.js.
994