1/* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5/** 6 * Provides access to downloads from previous sessions on platforms that store 7 * them in a different location than session downloads. 8 * 9 * This module works with objects that are compatible with Download, while using 10 * the Places interfaces internally. Some of the Places objects may also be 11 * exposed to allow the consumers to integrate with history view commands. 12 */ 13 14"use strict"; 15 16var EXPORTED_SYMBOLS = ["DownloadHistory"]; 17 18const { DownloadList } = ChromeUtils.import( 19 "resource://gre/modules/DownloadList.jsm" 20); 21const { XPCOMUtils } = ChromeUtils.import( 22 "resource://gre/modules/XPCOMUtils.jsm" 23); 24 25XPCOMUtils.defineLazyModuleGetters(this, { 26 Downloads: "resource://gre/modules/Downloads.jsm", 27 FileUtils: "resource://gre/modules/FileUtils.jsm", 28 OS: "resource://gre/modules/osfile.jsm", 29 PlacesUtils: "resource://gre/modules/PlacesUtils.jsm", 30 Services: "resource://gre/modules/Services.jsm", 31}); 32 33// Places query used to retrieve all history downloads for the related list. 34const HISTORY_PLACES_QUERY = 35 "place:transition=" + 36 Ci.nsINavHistoryService.TRANSITION_DOWNLOAD + 37 "&sort=" + 38 Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING; 39 40const DESTINATIONFILEURI_ANNO = "downloads/destinationFileURI"; 41const METADATA_ANNO = "downloads/metaData"; 42 43const METADATA_STATE_FINISHED = 1; 44const METADATA_STATE_FAILED = 2; 45const METADATA_STATE_CANCELED = 3; 46const METADATA_STATE_PAUSED = 4; 47const METADATA_STATE_BLOCKED_PARENTAL = 6; 48const METADATA_STATE_DIRTY = 8; 49 50/** 51 * Provides methods to retrieve downloads from previous sessions and store 52 * downloads for future sessions. 53 */ 54var DownloadHistory = { 55 /** 56 * Retrieves the main DownloadHistoryList object which provides a unified view 57 * on downloads from both previous browsing sessions and this session. 58 * 59 * @param type 60 * Determines which type of downloads from this session should be 61 * included in the list. This is Downloads.PUBLIC by default, but can 62 * also be Downloads.PRIVATE or Downloads.ALL. 63 * @param maxHistoryResults 64 * Optional number that limits the amount of results the history query 65 * may return. 66 * 67 * @return {Promise} 68 * @resolves The requested DownloadHistoryList object. 69 * @rejects JavaScript exception. 70 */ 71 async getList({ type = Downloads.PUBLIC, maxHistoryResults } = {}) { 72 await DownloadCache.ensureInitialized(); 73 74 let key = `${type}|${maxHistoryResults ? maxHistoryResults : -1}`; 75 if (!this._listPromises[key]) { 76 this._listPromises[key] = Downloads.getList(type).then(list => { 77 // When the amount of history downloads is capped, we request the list in 78 // descending order, to make sure that the list can apply the limit. 79 let query = 80 HISTORY_PLACES_QUERY + 81 (maxHistoryResults ? "&maxResults=" + maxHistoryResults : ""); 82 return new DownloadHistoryList(list, query); 83 }); 84 } 85 86 return this._listPromises[key]; 87 }, 88 89 /** 90 * This object is populated with one key for each type of download list that 91 * can be returned by the getList method. The values are promises that resolve 92 * to DownloadHistoryList objects. 93 */ 94 _listPromises: {}, 95 96 async addDownloadToHistory(download) { 97 if ( 98 download.source.isPrivate || 99 !PlacesUtils.history.canAddURI(PlacesUtils.toURI(download.source.url)) 100 ) { 101 return; 102 } 103 104 await DownloadCache.addDownload(download); 105 106 await this._updateHistoryListData(download.source.url); 107 }, 108 109 /** 110 * Stores new detailed metadata for the given download in history. This is 111 * normally called after a download finishes, fails, or is canceled. 112 * 113 * Failed or canceled downloads with partial data are not stored as paused, 114 * because the information from the session download is required for resuming. 115 * 116 * @param download 117 * Download object whose metadata should be updated. If the object 118 * represents a private download, the call has no effect. 119 */ 120 async updateMetaData(download) { 121 if (download.source.isPrivate || !download.stopped) { 122 return; 123 } 124 125 let state = METADATA_STATE_CANCELED; 126 if (download.succeeded) { 127 state = METADATA_STATE_FINISHED; 128 } else if (download.error) { 129 if (download.error.becauseBlockedByParentalControls) { 130 state = METADATA_STATE_BLOCKED_PARENTAL; 131 } else if (download.error.becauseBlockedByReputationCheck) { 132 state = METADATA_STATE_DIRTY; 133 } else { 134 state = METADATA_STATE_FAILED; 135 } 136 } 137 138 let metaData = { state, endTime: download.endTime }; 139 if (download.succeeded) { 140 metaData.fileSize = download.target.size; 141 } 142 143 // The verdict may still be present even if the download succeeded. 144 if (download.error && download.error.reputationCheckVerdict) { 145 metaData.reputationCheckVerdict = download.error.reputationCheckVerdict; 146 } 147 148 // This should be executed before any async parts, to ensure the cache is 149 // updated before any notifications are activated. 150 await DownloadCache.setMetadata(download.source.url, metaData); 151 152 await this._updateHistoryListData(download.source.url); 153 }, 154 155 async _updateHistoryListData(sourceUrl) { 156 for (let key of Object.getOwnPropertyNames(this._listPromises)) { 157 let downloadHistoryList = await this._listPromises[key]; 158 downloadHistoryList.updateForMetaDataChange( 159 sourceUrl, 160 DownloadCache.get(sourceUrl) 161 ); 162 } 163 }, 164}; 165 166/** 167 * This cache exists: 168 * - in order to optimize the load of DownloadsHistoryList, when Places 169 * annotations for history downloads must be read. In fact, annotations are 170 * stored in a single table, and reading all of them at once is much more 171 * efficient than an individual query. 172 * - to avoid needing to do asynchronous reading of the database during download 173 * list updates, which are designed to be synchronous (to improve UI 174 * responsiveness). 175 * 176 * The cache is initialized the first time DownloadHistory.getList is called, or 177 * when data is added. 178 */ 179var DownloadCache = { 180 _data: new Map(), 181 _initializePromise: null, 182 183 /** 184 * Initializes the cache, loading the data from the places database. 185 * 186 * @return {Promise} Returns a promise that is resolved once the 187 * initialization is complete. 188 */ 189 ensureInitialized() { 190 if (this._initializePromise) { 191 return this._initializePromise; 192 } 193 this._initializePromise = (async () => { 194 PlacesUtils.history.addObserver(this, true); 195 196 let pageAnnos = await PlacesUtils.history.fetchAnnotatedPages([ 197 METADATA_ANNO, 198 DESTINATIONFILEURI_ANNO, 199 ]); 200 201 let metaDataPages = pageAnnos.get(METADATA_ANNO); 202 if (metaDataPages) { 203 for (let { uri, content } of metaDataPages) { 204 try { 205 this._data.set(uri.href, JSON.parse(content)); 206 } catch (ex) { 207 // Do nothing - JSON.parse could throw. 208 } 209 } 210 } 211 212 let destinationFilePages = pageAnnos.get(DESTINATIONFILEURI_ANNO); 213 if (destinationFilePages) { 214 for (let { uri, content } of destinationFilePages) { 215 let newData = this.get(uri.href); 216 newData.targetFileSpec = content; 217 this._data.set(uri.href, newData); 218 } 219 } 220 })(); 221 222 return this._initializePromise; 223 }, 224 225 /** 226 * This returns an object containing the meta data for the supplied URL. 227 * 228 * @param {String} url The url to get the meta data for. 229 * @return {Object|null} Returns an empty object if there is no meta data found, or 230 * an object containing the meta data. The meta data 231 * will look like: 232 * 233 * { targetFileSpec, state, endTime, fileSize, ... } 234 * 235 * The targetFileSpec property is the value of "downloads/destinationFileURI", 236 * while the other properties are taken from "downloads/metaData". Any of the 237 * properties may be missing from the object. 238 */ 239 get(url) { 240 return this._data.get(url) || {}; 241 }, 242 243 /** 244 * Adds a download to the cache and the places database. 245 * 246 * @param {Download} download The download to add to the database and cache. 247 */ 248 async addDownload(download) { 249 await this.ensureInitialized(); 250 251 let targetFile = new FileUtils.File(download.target.path); 252 let targetUri = Services.io.newFileURI(targetFile); 253 254 // This should be executed before any async parts, to ensure the cache is 255 // updated before any notifications are activated. 256 // Note: this intentionally overwrites any metadata as this is 257 // the start of a new download. 258 this._data.set(download.source.url, { targetFileSpec: targetUri.spec }); 259 260 let originalPageInfo = await PlacesUtils.history.fetch(download.source.url); 261 262 let pageInfo = await PlacesUtils.history.insert({ 263 url: download.source.url, 264 // In case we are downloading a file that does not correspond to a web 265 // page for which the title is present, we populate the otherwise empty 266 // history title with the name of the destination file, to allow it to be 267 // visible and searchable in history results. 268 title: 269 (originalPageInfo && originalPageInfo.title) || targetFile.leafName, 270 visits: [ 271 { 272 // The start time is always available when we reach this point. 273 date: download.startTime, 274 transition: PlacesUtils.history.TRANSITIONS.DOWNLOAD, 275 referrer: download.source.referrerInfo 276 ? download.source.referrerInfo.originalReferrer 277 : null, 278 }, 279 ], 280 }); 281 282 await PlacesUtils.history.update({ 283 annotations: new Map([["downloads/destinationFileURI", targetUri.spec]]), 284 // XXX Bug 1479445: We shouldn't have to supply both guid and url here, 285 // but currently we do. 286 guid: pageInfo.guid, 287 url: pageInfo.url, 288 }); 289 }, 290 291 /** 292 * Sets the metadata for a given url. If the cache already contains meta data 293 * for the given url, it will be overwritten (note: the targetFileSpec will be 294 * maintained). 295 * 296 * @param {String} url The url to set the meta data for. 297 * @param {Object} metadata The new metaData to save in the cache. 298 */ 299 async setMetadata(url, metadata) { 300 await this.ensureInitialized(); 301 302 // This should be executed before any async parts, to ensure the cache is 303 // updated before any notifications are activated. 304 let existingData = this.get(url); 305 let newData = { ...metadata }; 306 if ("targetFileSpec" in existingData) { 307 newData.targetFileSpec = existingData.targetFileSpec; 308 } 309 this._data.set(url, newData); 310 311 try { 312 await PlacesUtils.history.update({ 313 annotations: new Map([[METADATA_ANNO, JSON.stringify(metadata)]]), 314 url, 315 }); 316 } catch (ex) { 317 Cu.reportError(ex); 318 } 319 }, 320 321 QueryInterface: ChromeUtils.generateQI([ 322 Ci.nsINavHistoryObserver, 323 Ci.nsISupportsWeakReference, 324 ]), 325 326 // nsINavHistoryObserver 327 onDeleteURI(uri) { 328 this._data.delete(uri.spec); 329 }, 330 onClearHistory() { 331 this._data.clear(); 332 }, 333 onBeginUpdateBatch() {}, 334 onEndUpdateBatch() {}, 335 onTitleChanged() {}, 336 onFrecencyChanged() {}, 337 onManyFrecenciesChanged() {}, 338 onPageChanged() {}, 339 onDeleteVisits() {}, 340}; 341 342/** 343 * Represents a download from the browser history. This object implements part 344 * of the interface of the Download object. 345 * 346 * While Download objects are shared between the public DownloadList and all the 347 * DownloadHistoryList instances, multiple HistoryDownload objects referring to 348 * the same item can be created for different DownloadHistoryList instances. 349 * 350 * @param placesNode 351 * The Places node from which the history download should be initialized. 352 */ 353function HistoryDownload(placesNode) { 354 this.placesNode = placesNode; 355 356 // History downloads should get the referrer from Places (bug 829201). 357 this.source = { 358 url: placesNode.uri, 359 isPrivate: false, 360 }; 361 this.target = { 362 path: undefined, 363 exists: false, 364 size: undefined, 365 }; 366 367 // In case this download cannot obtain its end time from the Places metadata, 368 // use the time from the Places node, that is the start time of the download. 369 this.endTime = placesNode.time / 1000; 370} 371 372HistoryDownload.prototype = { 373 /** 374 * DownloadSlot containing this history download. 375 */ 376 slot: null, 377 378 /** 379 * Pushes information from Places metadata into this object. 380 */ 381 updateFromMetaData(metaData) { 382 try { 383 this.target.path = Cc["@mozilla.org/network/protocol;1?name=file"] 384 .getService(Ci.nsIFileProtocolHandler) 385 .getFileFromURLSpec(metaData.targetFileSpec).path; 386 } catch (ex) { 387 this.target.path = undefined; 388 } 389 390 if ("state" in metaData) { 391 this.succeeded = metaData.state == METADATA_STATE_FINISHED; 392 this.canceled = 393 metaData.state == METADATA_STATE_CANCELED || 394 metaData.state == METADATA_STATE_PAUSED; 395 this.endTime = metaData.endTime; 396 397 // Recreate partial error information from the state saved in history. 398 if (metaData.state == METADATA_STATE_FAILED) { 399 this.error = { message: "History download failed." }; 400 } else if (metaData.state == METADATA_STATE_BLOCKED_PARENTAL) { 401 this.error = { becauseBlockedByParentalControls: true }; 402 } else if (metaData.state == METADATA_STATE_DIRTY) { 403 this.error = { 404 becauseBlockedByReputationCheck: true, 405 reputationCheckVerdict: metaData.reputationCheckVerdict || "", 406 }; 407 } else { 408 this.error = null; 409 } 410 411 // Normal history downloads are assumed to exist until the user interface 412 // is refreshed, at which point these values may be updated. 413 this.target.exists = true; 414 this.target.size = metaData.fileSize; 415 } else { 416 // Metadata might be missing from a download that has started but hasn't 417 // stopped already. Normally, this state is overridden with the one from 418 // the corresponding in-progress session download. But if the browser is 419 // terminated abruptly and additionally the file with information about 420 // in-progress downloads is lost, we may end up using this state. We use 421 // the failed state to allow the download to be restarted. 422 // 423 // On the other hand, if the download is missing the target file 424 // annotation as well, it is just a very old one, and we can assume it 425 // succeeded. 426 this.succeeded = !this.target.path; 427 this.error = this.target.path ? { message: "Unstarted download." } : null; 428 this.canceled = false; 429 430 // These properties may be updated if the user interface is refreshed. 431 this.target.exists = false; 432 this.target.size = undefined; 433 } 434 }, 435 436 /** 437 * History downloads are never in progress. 438 */ 439 stopped: true, 440 441 /** 442 * No percentage indication is shown for history downloads. 443 */ 444 hasProgress: false, 445 446 /** 447 * History downloads cannot be restarted using their partial data, even if 448 * they are indicated as paused in their Places metadata. The only way is to 449 * use the information from a persisted session download, that will be shown 450 * instead of the history download. In case this session download is not 451 * available, we show the history download as canceled, not paused. 452 */ 453 hasPartialData: false, 454 455 /** 456 * This method may be called when deleting a history download. 457 */ 458 async finalize() {}, 459 460 /** 461 * This method mimicks the "refresh" method of session downloads. 462 */ 463 async refresh() { 464 try { 465 this.target.size = (await OS.File.stat(this.target.path)).size; 466 this.target.exists = true; 467 } catch (ex) { 468 // We keep the known file size from the metadata, if any. 469 this.target.exists = false; 470 } 471 472 this.slot.list._notifyAllViews("onDownloadChanged", this); 473 }, 474}; 475 476/** 477 * Represents one item in the list of public session and history downloads. 478 * 479 * The object may contain a session download, a history download, or both. When 480 * both a history and a session download are present, the session download gets 481 * priority and its information is accessed. 482 * 483 * @param list 484 * The DownloadHistoryList that owns this DownloadSlot object. 485 */ 486function DownloadSlot(list) { 487 this.list = list; 488} 489 490DownloadSlot.prototype = { 491 list: null, 492 493 /** 494 * Download object representing the session download contained in this slot. 495 */ 496 sessionDownload: null, 497 498 /** 499 * HistoryDownload object contained in this slot. 500 */ 501 get historyDownload() { 502 return this._historyDownload; 503 }, 504 set historyDownload(historyDownload) { 505 this._historyDownload = historyDownload; 506 if (historyDownload) { 507 historyDownload.slot = this; 508 } 509 }, 510 _historyDownload: null, 511 512 /** 513 * Returns the Download or HistoryDownload object for displaying information 514 * and executing commands in the user interface. 515 */ 516 get download() { 517 return this.sessionDownload || this.historyDownload; 518 }, 519}; 520 521/** 522 * Represents an ordered collection of DownloadSlot objects containing a merged 523 * view on session downloads and history downloads. Views on this list will 524 * receive notifications for changes to both types of downloads. 525 * 526 * Downloads in this list are sorted from oldest to newest, with all session 527 * downloads after all the history downloads. When a new history download is 528 * added and the list also contains session downloads, the insertBefore option 529 * of the onDownloadAdded notification refers to the first session download. 530 * 531 * The list of downloads cannot be modified using the DownloadList methods. 532 * 533 * @param publicList 534 * Underlying DownloadList containing public downloads. 535 * @param place 536 * Places query used to retrieve history downloads. 537 */ 538var DownloadHistoryList = function(publicList, place) { 539 DownloadList.call(this); 540 541 // While "this._slots" contains all the data in order, the other properties 542 // provide fast access for the most common operations. 543 this._slots = []; 544 this._slotsForUrl = new Map(); 545 this._slotForDownload = new WeakMap(); 546 547 // Start the asynchronous queries to retrieve history and session downloads. 548 publicList.addView(this).catch(Cu.reportError); 549 let query = {}, 550 options = {}; 551 PlacesUtils.history.queryStringToQuery(place, query, options); 552 553 // NB: The addObserver call sets our nsINavHistoryResultObserver.result. 554 let result = PlacesUtils.history.executeQuery(query.value, options.value); 555 result.addObserver(this); 556 557 // Our history result observer is long lived for fast shared views, so free 558 // the reference on shutdown to prevent leaks. 559 Services.obs.addObserver(() => { 560 this.result = null; 561 }, "quit-application-granted"); 562}; 563 564DownloadHistoryList.prototype = { 565 __proto__: DownloadList.prototype, 566 567 /** 568 * This is set when executing the Places query. 569 */ 570 get result() { 571 return this._result; 572 }, 573 set result(result) { 574 if (this._result == result) { 575 return; 576 } 577 578 if (this._result) { 579 this._result.removeObserver(this); 580 this._result.root.containerOpen = false; 581 } 582 583 this._result = result; 584 585 if (this._result) { 586 this._result.root.containerOpen = true; 587 } 588 }, 589 _result: null, 590 591 /** 592 * Updates the download history item when the meta data or destination file 593 * changes. 594 * 595 * @param {String} sourceUrl The sourceUrl which was updated. 596 * @param {Object} metaData The new meta data for the sourceUrl. 597 */ 598 updateForMetaDataChange(sourceUrl, metaData) { 599 let slotsForUrl = this._slotsForUrl.get(sourceUrl); 600 if (!slotsForUrl) { 601 return; 602 } 603 604 for (let slot of slotsForUrl) { 605 if (slot.sessionDownload) { 606 // The visible data doesn't change, so we don't have to notify views. 607 return; 608 } 609 slot.historyDownload.updateFromMetaData(metaData); 610 this._notifyAllViews("onDownloadChanged", slot.download); 611 } 612 }, 613 614 /** 615 * Index of the first slot that contains a session download. This is equal to 616 * the length of the list when there are no session downloads. 617 */ 618 _firstSessionSlotIndex: 0, 619 620 _insertSlot({ slot, index, slotsForUrl }) { 621 // Add the slot to the ordered array. 622 this._slots.splice(index, 0, slot); 623 this._downloads.splice(index, 0, slot.download); 624 if (!slot.sessionDownload) { 625 this._firstSessionSlotIndex++; 626 } 627 628 // Add the slot to the fast access maps. 629 slotsForUrl.add(slot); 630 this._slotsForUrl.set(slot.download.source.url, slotsForUrl); 631 632 // Add the associated view items. 633 this._notifyAllViews("onDownloadAdded", slot.download, { 634 insertBefore: this._downloads[index + 1], 635 }); 636 }, 637 638 _removeSlot({ slot, slotsForUrl }) { 639 // Remove the slot from the ordered array. 640 let index = this._slots.indexOf(slot); 641 this._slots.splice(index, 1); 642 this._downloads.splice(index, 1); 643 if (this._firstSessionSlotIndex > index) { 644 this._firstSessionSlotIndex--; 645 } 646 647 // Remove the slot from the fast access maps. 648 slotsForUrl.delete(slot); 649 if (slotsForUrl.size == 0) { 650 this._slotsForUrl.delete(slot.download.source.url); 651 } 652 653 // Remove the associated view items. 654 this._notifyAllViews("onDownloadRemoved", slot.download); 655 }, 656 657 /** 658 * Ensures that the information about a history download is stored in at least 659 * one slot, adding a new one at the end of the list if necessary. 660 * 661 * A reference to the same Places node will be stored in the HistoryDownload 662 * object for all the DownloadSlot objects associated with the source URL. 663 * 664 * @param placesNode 665 * The Places node that represents the history download. 666 */ 667 _insertPlacesNode(placesNode) { 668 let slotsForUrl = this._slotsForUrl.get(placesNode.uri) || new Set(); 669 670 // If there are existing slots associated with this URL, we only have to 671 // ensure that the Places node reference is kept updated in case the more 672 // recent Places notification contained a different node object. 673 if (slotsForUrl.size > 0) { 674 for (let slot of slotsForUrl) { 675 if (!slot.historyDownload) { 676 slot.historyDownload = new HistoryDownload(placesNode); 677 } else { 678 slot.historyDownload.placesNode = placesNode; 679 } 680 } 681 return; 682 } 683 684 // If there are no existing slots for this URL, we have to create a new one. 685 // Since the history download is visible in the slot, we also have to update 686 // the object using the Places metadata. 687 let historyDownload = new HistoryDownload(placesNode); 688 historyDownload.updateFromMetaData(DownloadCache.get(placesNode.uri)); 689 let slot = new DownloadSlot(this); 690 slot.historyDownload = historyDownload; 691 this._insertSlot({ slot, slotsForUrl, index: this._firstSessionSlotIndex }); 692 }, 693 694 // nsINavHistoryResultObserver 695 containerStateChanged(node, oldState, newState) { 696 this.invalidateContainer(node); 697 }, 698 699 // nsINavHistoryResultObserver 700 invalidateContainer(container) { 701 this._notifyAllViews("onDownloadBatchStarting"); 702 703 // Remove all the current slots containing only history downloads. 704 for (let index = this._slots.length - 1; index >= 0; index--) { 705 let slot = this._slots[index]; 706 if (slot.sessionDownload) { 707 // The visible data doesn't change, so we don't have to notify views. 708 slot.historyDownload = null; 709 } else { 710 let slotsForUrl = this._slotsForUrl.get(slot.download.source.url); 711 this._removeSlot({ slot, slotsForUrl }); 712 } 713 } 714 715 // Add new slots or reuse existing ones for history downloads. 716 for (let index = container.childCount - 1; index >= 0; --index) { 717 try { 718 this._insertPlacesNode(container.getChild(index)); 719 } catch (ex) { 720 Cu.reportError(ex); 721 } 722 } 723 724 this._notifyAllViews("onDownloadBatchEnded"); 725 }, 726 727 // nsINavHistoryResultObserver 728 nodeInserted(parent, placesNode) { 729 this._insertPlacesNode(placesNode); 730 }, 731 732 // nsINavHistoryResultObserver 733 nodeRemoved(parent, placesNode, aOldIndex) { 734 let slotsForUrl = this._slotsForUrl.get(placesNode.uri); 735 for (let slot of slotsForUrl) { 736 if (slot.sessionDownload) { 737 // The visible data doesn't change, so we don't have to notify views. 738 slot.historyDownload = null; 739 } else { 740 this._removeSlot({ slot, slotsForUrl }); 741 } 742 } 743 }, 744 745 // nsINavHistoryResultObserver 746 nodeIconChanged() {}, 747 nodeTitleChanged() {}, 748 nodeKeywordChanged() {}, 749 nodeDateAddedChanged() {}, 750 nodeLastModifiedChanged() {}, 751 nodeHistoryDetailsChanged() {}, 752 nodeTagsChanged() {}, 753 sortingChanged() {}, 754 nodeMoved() {}, 755 nodeURIChanged() {}, 756 batching() {}, 757 758 // DownloadList callback 759 onDownloadAdded(download) { 760 let url = download.source.url; 761 let slotsForUrl = this._slotsForUrl.get(url) || new Set(); 762 763 // For every source URL, there can be at most one slot containing a history 764 // download without an associated session download. If we find one, then we 765 // can reuse it for the current session download, although we have to move 766 // it together with the other session downloads. 767 let slot = [...slotsForUrl][0]; 768 if (slot && !slot.sessionDownload) { 769 // Remove the slot because we have to change its position. 770 this._removeSlot({ slot, slotsForUrl }); 771 } else { 772 slot = new DownloadSlot(this); 773 } 774 slot.sessionDownload = download; 775 this._insertSlot({ slot, slotsForUrl, index: this._slots.length }); 776 this._slotForDownload.set(download, slot); 777 }, 778 779 // DownloadList callback 780 onDownloadChanged(download) { 781 let slot = this._slotForDownload.get(download); 782 this._notifyAllViews("onDownloadChanged", slot.download); 783 }, 784 785 // DownloadList callback 786 onDownloadRemoved(download) { 787 let url = download.source.url; 788 let slotsForUrl = this._slotsForUrl.get(url); 789 let slot = this._slotForDownload.get(download); 790 this._removeSlot({ slot, slotsForUrl }); 791 792 this._slotForDownload.delete(download); 793 794 // If there was only one slot for this source URL and it also contained a 795 // history download, we should resurrect it in the correct area of the list. 796 if (slotsForUrl.size == 0 && slot.historyDownload) { 797 // We have one download slot containing both a session download and a 798 // history download, and we are now removing the session download. 799 // Previously, we did not use the Places metadata because it was obscured 800 // by the session download. Since this is no longer the case, we have to 801 // read the latest metadata before resurrecting the history download. 802 slot.historyDownload.updateFromMetaData(DownloadCache.get(url)); 803 slot.sessionDownload = null; 804 // Place the resurrected history slot after all the session slots. 805 this._insertSlot({ 806 slot, 807 slotsForUrl, 808 index: this._firstSessionSlotIndex, 809 }); 810 } 811 }, 812 813 // DownloadList 814 add() { 815 throw new Error("Not implemented."); 816 }, 817 818 // DownloadList 819 remove() { 820 throw new Error("Not implemented."); 821 }, 822 823 // DownloadList 824 removeFinished() { 825 throw new Error("Not implemented."); 826 }, 827}; 828