|
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 /** |
|
8 * Provides functions to integrate with the host application, handling for |
|
9 * example the global prompts on shutdown. |
|
10 */ |
|
11 |
|
12 "use strict"; |
|
13 |
|
14 this.EXPORTED_SYMBOLS = [ |
|
15 "DownloadIntegration", |
|
16 ]; |
|
17 |
|
18 //////////////////////////////////////////////////////////////////////////////// |
|
19 //// Globals |
|
20 |
|
21 const Cc = Components.classes; |
|
22 const Ci = Components.interfaces; |
|
23 const Cu = Components.utils; |
|
24 const Cr = Components.results; |
|
25 |
|
26 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
27 |
|
28 XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", |
|
29 "resource://gre/modules/DeferredTask.jsm"); |
|
30 XPCOMUtils.defineLazyModuleGetter(this, "Downloads", |
|
31 "resource://gre/modules/Downloads.jsm"); |
|
32 XPCOMUtils.defineLazyModuleGetter(this, "DownloadStore", |
|
33 "resource://gre/modules/DownloadStore.jsm"); |
|
34 XPCOMUtils.defineLazyModuleGetter(this, "DownloadImport", |
|
35 "resource://gre/modules/DownloadImport.jsm"); |
|
36 XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper", |
|
37 "resource://gre/modules/DownloadUIHelper.jsm"); |
|
38 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", |
|
39 "resource://gre/modules/FileUtils.jsm"); |
|
40 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", |
|
41 "resource://gre/modules/NetUtil.jsm"); |
|
42 XPCOMUtils.defineLazyModuleGetter(this, "OS", |
|
43 "resource://gre/modules/osfile.jsm"); |
|
44 #ifdef MOZ_PLACES |
|
45 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", |
|
46 "resource://gre/modules/PlacesUtils.jsm"); |
|
47 #endif |
|
48 XPCOMUtils.defineLazyModuleGetter(this, "Promise", |
|
49 "resource://gre/modules/Promise.jsm"); |
|
50 XPCOMUtils.defineLazyModuleGetter(this, "Services", |
|
51 "resource://gre/modules/Services.jsm"); |
|
52 XPCOMUtils.defineLazyModuleGetter(this, "Task", |
|
53 "resource://gre/modules/Task.jsm"); |
|
54 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", |
|
55 "resource://gre/modules/NetUtil.jsm"); |
|
56 |
|
57 XPCOMUtils.defineLazyServiceGetter(this, "gDownloadPlatform", |
|
58 "@mozilla.org/toolkit/download-platform;1", |
|
59 "mozIDownloadPlatform"); |
|
60 XPCOMUtils.defineLazyServiceGetter(this, "gEnvironment", |
|
61 "@mozilla.org/process/environment;1", |
|
62 "nsIEnvironment"); |
|
63 XPCOMUtils.defineLazyServiceGetter(this, "gMIMEService", |
|
64 "@mozilla.org/mime;1", |
|
65 "nsIMIMEService"); |
|
66 XPCOMUtils.defineLazyServiceGetter(this, "gExternalProtocolService", |
|
67 "@mozilla.org/uriloader/external-protocol-service;1", |
|
68 "nsIExternalProtocolService"); |
|
69 |
|
70 XPCOMUtils.defineLazyGetter(this, "gParentalControlsService", function() { |
|
71 if ("@mozilla.org/parental-controls-service;1" in Cc) { |
|
72 return Cc["@mozilla.org/parental-controls-service;1"] |
|
73 .createInstance(Ci.nsIParentalControlsService); |
|
74 } |
|
75 return null; |
|
76 }); |
|
77 |
|
78 XPCOMUtils.defineLazyServiceGetter(this, "gApplicationReputationService", |
|
79 "@mozilla.org/downloads/application-reputation-service;1", |
|
80 Ci.nsIApplicationReputationService); |
|
81 |
|
82 XPCOMUtils.defineLazyServiceGetter(this, "volumeService", |
|
83 "@mozilla.org/telephony/volume-service;1", |
|
84 "nsIVolumeService"); |
|
85 |
|
86 const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer", |
|
87 "initWithCallback"); |
|
88 |
|
89 /** |
|
90 * Indicates the delay between a change to the downloads data and the related |
|
91 * save operation. This value is the result of a delicate trade-off, assuming |
|
92 * the host application uses the browser history instead of the download store |
|
93 * to save completed downloads. |
|
94 * |
|
95 * If a download takes less than this interval to complete (for example, saving |
|
96 * a page that is already displayed), then no input/output is triggered by the |
|
97 * download store except for an existence check, resulting in the best possible |
|
98 * efficiency. |
|
99 * |
|
100 * Conversely, if the browser is closed before this interval has passed, the |
|
101 * download will not be saved. This prevents it from being restored in the next |
|
102 * session, and if there is partial data associated with it, then the ".part" |
|
103 * file will not be deleted when the browser starts again. |
|
104 * |
|
105 * In all cases, for best efficiency, this value should be high enough that the |
|
106 * input/output for opening or closing the target file does not overlap with the |
|
107 * one for saving the list of downloads. |
|
108 */ |
|
109 const kSaveDelayMs = 1500; |
|
110 |
|
111 /** |
|
112 * This pref indicates if we have already imported (or attempted to import) |
|
113 * the downloads database from the previous SQLite storage. |
|
114 */ |
|
115 const kPrefImportedFromSqlite = "browser.download.importedFromSqlite"; |
|
116 |
|
117 /** |
|
118 * List of observers to listen against |
|
119 */ |
|
120 const kObserverTopics = [ |
|
121 "quit-application-requested", |
|
122 "offline-requested", |
|
123 "last-pb-context-exiting", |
|
124 "last-pb-context-exited", |
|
125 "sleep_notification", |
|
126 "suspend_process_notification", |
|
127 "wake_notification", |
|
128 "resume_process_notification", |
|
129 "network:offline-about-to-go-offline", |
|
130 "network:offline-status-changed", |
|
131 "xpcom-will-shutdown", |
|
132 ]; |
|
133 |
|
134 //////////////////////////////////////////////////////////////////////////////// |
|
135 //// DownloadIntegration |
|
136 |
|
137 /** |
|
138 * Provides functions to integrate with the host application, handling for |
|
139 * example the global prompts on shutdown. |
|
140 */ |
|
141 this.DownloadIntegration = { |
|
142 // For testing only |
|
143 _testMode: false, |
|
144 testPromptDownloads: 0, |
|
145 dontLoadList: false, |
|
146 dontLoadObservers: false, |
|
147 dontCheckParentalControls: false, |
|
148 shouldBlockInTest: false, |
|
149 #ifdef MOZ_URL_CLASSIFIER |
|
150 dontCheckApplicationReputation: false, |
|
151 #else |
|
152 dontCheckApplicationReputation: true, |
|
153 #endif |
|
154 shouldBlockInTestForApplicationReputation: false, |
|
155 dontOpenFileAndFolder: false, |
|
156 downloadDoneCalled: false, |
|
157 _deferTestOpenFile: null, |
|
158 _deferTestShowDir: null, |
|
159 _deferTestClearPrivateList: null, |
|
160 |
|
161 /** |
|
162 * Main DownloadStore object for loading and saving the list of persistent |
|
163 * downloads, or null if the download list was never requested and thus it |
|
164 * doesn't need to be persisted. |
|
165 */ |
|
166 _store: null, |
|
167 |
|
168 /** |
|
169 * Gets and sets test mode |
|
170 */ |
|
171 get testMode() this._testMode, |
|
172 set testMode(mode) { |
|
173 this._downloadsDirectory = null; |
|
174 return (this._testMode = mode); |
|
175 }, |
|
176 |
|
177 /** |
|
178 * Performs initialization of the list of persistent downloads, before its |
|
179 * first use by the host application. This function may be called only once |
|
180 * during the entire lifetime of the application. |
|
181 * |
|
182 * @param aList |
|
183 * DownloadList object to be populated with the download objects |
|
184 * serialized from the previous session. This list will be persisted |
|
185 * to disk during the session lifetime. |
|
186 * |
|
187 * @return {Promise} |
|
188 * @resolves When the list has been populated. |
|
189 * @rejects JavaScript exception. |
|
190 */ |
|
191 initializePublicDownloadList: function(aList) { |
|
192 return Task.spawn(function task_DI_initializePublicDownloadList() { |
|
193 if (this.dontLoadList) { |
|
194 // In tests, only register the history observer. This object is kept |
|
195 // alive by the history service, so we don't keep a reference to it. |
|
196 new DownloadHistoryObserver(aList); |
|
197 return; |
|
198 } |
|
199 |
|
200 if (this._store) { |
|
201 throw new Error("initializePublicDownloadList may be called only once."); |
|
202 } |
|
203 |
|
204 this._store = new DownloadStore(aList, OS.Path.join( |
|
205 OS.Constants.Path.profileDir, |
|
206 "downloads.json")); |
|
207 this._store.onsaveitem = this.shouldPersistDownload.bind(this); |
|
208 |
|
209 if (this._importedFromSqlite) { |
|
210 try { |
|
211 yield this._store.load(); |
|
212 } catch (ex) { |
|
213 Cu.reportError(ex); |
|
214 } |
|
215 } else { |
|
216 let sqliteDBpath = OS.Path.join(OS.Constants.Path.profileDir, |
|
217 "downloads.sqlite"); |
|
218 |
|
219 if (yield OS.File.exists(sqliteDBpath)) { |
|
220 let sqliteImport = new DownloadImport(aList, sqliteDBpath); |
|
221 yield sqliteImport.import(); |
|
222 |
|
223 let importCount = (yield aList.getAll()).length; |
|
224 if (importCount > 0) { |
|
225 try { |
|
226 yield this._store.save(); |
|
227 } catch (ex) { } |
|
228 } |
|
229 |
|
230 // No need to wait for the file removal. |
|
231 OS.File.remove(sqliteDBpath).then(null, Cu.reportError); |
|
232 } |
|
233 |
|
234 Services.prefs.setBoolPref(kPrefImportedFromSqlite, true); |
|
235 |
|
236 // Don't even report error here because this file is pre Firefox 3 |
|
237 // and most likely doesn't exist. |
|
238 OS.File.remove(OS.Path.join(OS.Constants.Path.profileDir, |
|
239 "downloads.rdf")); |
|
240 |
|
241 } |
|
242 |
|
243 // After the list of persistent downloads has been loaded, add the |
|
244 // DownloadAutoSaveView and the DownloadHistoryObserver (even if the load |
|
245 // operation failed). These objects are kept alive by the underlying |
|
246 // DownloadList and by the history service respectively. We wait for a |
|
247 // complete initialization of the view used for detecting changes to |
|
248 // downloads to be persisted, before other callers get a chance to modify |
|
249 // the list without being detected. |
|
250 yield new DownloadAutoSaveView(aList, this._store).initialize(); |
|
251 new DownloadHistoryObserver(aList); |
|
252 }.bind(this)); |
|
253 }, |
|
254 |
|
255 #ifdef MOZ_WIDGET_GONK |
|
256 /** |
|
257 * Finds the default download directory which can be either in the |
|
258 * internal storage or on the sdcard. |
|
259 * |
|
260 * @return {Promise} |
|
261 * @resolves The downloads directory string path. |
|
262 */ |
|
263 _getDefaultDownloadDirectory: function() { |
|
264 return Task.spawn(function() { |
|
265 let directoryPath; |
|
266 let win = Services.wm.getMostRecentWindow("navigator:browser"); |
|
267 let storages = win.navigator.getDeviceStorages("sdcard"); |
|
268 let preferredStorageName; |
|
269 // Use the first one or the default storage. |
|
270 storages.forEach((aStorage) => { |
|
271 if (aStorage.default || !preferredStorageName) { |
|
272 preferredStorageName = aStorage.storageName; |
|
273 } |
|
274 }); |
|
275 |
|
276 // Now get the path for this storage area. |
|
277 if (preferredStorageName) { |
|
278 let volume = volumeService.getVolumeByName(preferredStorageName); |
|
279 if (volume && |
|
280 volume.isMediaPresent && |
|
281 !volume.isMountLocked && |
|
282 !volume.isSharing) { |
|
283 directoryPath = OS.Path.join(volume.mountPoint, "downloads"); |
|
284 yield OS.File.makeDir(directoryPath, { ignoreExisting: true }); |
|
285 } |
|
286 } |
|
287 if (directoryPath) { |
|
288 throw new Task.Result(directoryPath); |
|
289 } else { |
|
290 throw new Components.Exception("No suitable storage for downloads.", |
|
291 Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH); |
|
292 } |
|
293 }); |
|
294 }, |
|
295 #endif |
|
296 |
|
297 /** |
|
298 * Determines if a Download object from the list of persistent downloads |
|
299 * should be saved into a file, so that it can be restored across sessions. |
|
300 * |
|
301 * This function allows filtering out downloads that the host application is |
|
302 * not interested in persisting across sessions, for example downloads that |
|
303 * finished successfully. |
|
304 * |
|
305 * @param aDownload |
|
306 * The Download object to be inspected. This is originally taken from |
|
307 * the global DownloadList object for downloads that were not started |
|
308 * from a private browsing window. The item may have been removed |
|
309 * from the list since the save operation started, though in this case |
|
310 * the save operation will be repeated later. |
|
311 * |
|
312 * @return True to save the download, false otherwise. |
|
313 */ |
|
314 shouldPersistDownload: function (aDownload) |
|
315 { |
|
316 // In the default implementation, we save all the downloads currently in |
|
317 // progress, as well as stopped downloads for which we retained partially |
|
318 // downloaded data. Stopped downloads for which we don't need to track the |
|
319 // presence of a ".part" file are only retained in the browser history. |
|
320 // On b2g, we keep a few days of history. |
|
321 #ifdef MOZ_B2G |
|
322 let maxTime = Date.now() - |
|
323 Services.prefs.getIntPref("dom.downloads.max_retention_days") * 24 * 60 * 60 * 1000; |
|
324 return (aDownload.startTime > maxTime) || |
|
325 aDownload.hasPartialData || |
|
326 !aDownload.stopped; |
|
327 #else |
|
328 return aDownload.hasPartialData || !aDownload.stopped; |
|
329 #endif |
|
330 }, |
|
331 |
|
332 /** |
|
333 * Returns the system downloads directory asynchronously. |
|
334 * |
|
335 * @return {Promise} |
|
336 * @resolves The downloads directory string path. |
|
337 */ |
|
338 getSystemDownloadsDirectory: function DI_getSystemDownloadsDirectory() { |
|
339 return Task.spawn(function() { |
|
340 if (this._downloadsDirectory) { |
|
341 // This explicitly makes this function a generator for Task.jsm. We |
|
342 // need this because calls to the "yield" operator below may be |
|
343 // preprocessed out on some platforms. |
|
344 yield undefined; |
|
345 throw new Task.Result(this._downloadsDirectory); |
|
346 } |
|
347 |
|
348 let directoryPath = null; |
|
349 #ifdef XP_MACOSX |
|
350 directoryPath = this._getDirectory("DfltDwnld"); |
|
351 #elifdef XP_WIN |
|
352 // For XP/2K, use My Documents/Downloads. Other version uses |
|
353 // the default Downloads directory. |
|
354 let version = parseFloat(Services.sysinfo.getProperty("version")); |
|
355 if (version < 6) { |
|
356 directoryPath = yield this._createDownloadsDirectory("Pers"); |
|
357 } else { |
|
358 directoryPath = this._getDirectory("DfltDwnld"); |
|
359 } |
|
360 #elifdef XP_UNIX |
|
361 #ifdef MOZ_WIDGET_ANDROID |
|
362 // Android doesn't have a $HOME directory, and by default we only have |
|
363 // write access to /data/data/org.mozilla.{$APP} and /sdcard |
|
364 directoryPath = gEnvironment.get("DOWNLOADS_DIRECTORY"); |
|
365 if (!directoryPath) { |
|
366 throw new Components.Exception("DOWNLOADS_DIRECTORY is not set.", |
|
367 Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH); |
|
368 } |
|
369 #elifdef MOZ_WIDGET_GONK |
|
370 directoryPath = this._getDefaultDownloadDirectory(); |
|
371 #else |
|
372 // For Linux, use XDG download dir, with a fallback to Home/Downloads |
|
373 // if the XDG user dirs are disabled. |
|
374 try { |
|
375 directoryPath = this._getDirectory("DfltDwnld"); |
|
376 } catch(e) { |
|
377 directoryPath = yield this._createDownloadsDirectory("Home"); |
|
378 } |
|
379 #endif |
|
380 #else |
|
381 directoryPath = yield this._createDownloadsDirectory("Home"); |
|
382 #endif |
|
383 this._downloadsDirectory = directoryPath; |
|
384 throw new Task.Result(this._downloadsDirectory); |
|
385 }.bind(this)); |
|
386 }, |
|
387 _downloadsDirectory: null, |
|
388 |
|
389 /** |
|
390 * Returns the user downloads directory asynchronously. |
|
391 * |
|
392 * @return {Promise} |
|
393 * @resolves The downloads directory string path. |
|
394 */ |
|
395 getPreferredDownloadsDirectory: function DI_getPreferredDownloadsDirectory() { |
|
396 return Task.spawn(function() { |
|
397 let directoryPath = null; |
|
398 #ifdef MOZ_WIDGET_GONK |
|
399 directoryPath = this._getDefaultDownloadDirectory(); |
|
400 #else |
|
401 let prefValue = 1; |
|
402 |
|
403 try { |
|
404 prefValue = Services.prefs.getIntPref("browser.download.folderList"); |
|
405 } catch(e) {} |
|
406 |
|
407 switch(prefValue) { |
|
408 case 0: // Desktop |
|
409 directoryPath = this._getDirectory("Desk"); |
|
410 break; |
|
411 case 1: // Downloads |
|
412 directoryPath = yield this.getSystemDownloadsDirectory(); |
|
413 break; |
|
414 case 2: // Custom |
|
415 try { |
|
416 let directory = Services.prefs.getComplexValue("browser.download.dir", |
|
417 Ci.nsIFile); |
|
418 directoryPath = directory.path; |
|
419 yield OS.File.makeDir(directoryPath, { ignoreExisting: true }); |
|
420 } catch(ex) { |
|
421 // Either the preference isn't set or the directory cannot be created. |
|
422 directoryPath = yield this.getSystemDownloadsDirectory(); |
|
423 } |
|
424 break; |
|
425 default: |
|
426 directoryPath = yield this.getSystemDownloadsDirectory(); |
|
427 } |
|
428 #endif |
|
429 throw new Task.Result(directoryPath); |
|
430 }.bind(this)); |
|
431 }, |
|
432 |
|
433 /** |
|
434 * Returns the temporary downloads directory asynchronously. |
|
435 * |
|
436 * @return {Promise} |
|
437 * @resolves The downloads directory string path. |
|
438 */ |
|
439 getTemporaryDownloadsDirectory: function DI_getTemporaryDownloadsDirectory() { |
|
440 return Task.spawn(function() { |
|
441 let directoryPath = null; |
|
442 #ifdef XP_MACOSX |
|
443 directoryPath = yield this.getPreferredDownloadsDirectory(); |
|
444 #elifdef MOZ_WIDGET_ANDROID |
|
445 directoryPath = yield this.getSystemDownloadsDirectory(); |
|
446 #elifdef MOZ_WIDGET_GONK |
|
447 directoryPath = yield this.getSystemDownloadsDirectory(); |
|
448 #else |
|
449 // For Metro mode on Windows 8, we want searchability for documents |
|
450 // that the user chose to open with an external application. |
|
451 if (Services.metro && Services.metro.immersive) { |
|
452 directoryPath = yield this.getSystemDownloadsDirectory(); |
|
453 } else { |
|
454 directoryPath = this._getDirectory("TmpD"); |
|
455 } |
|
456 #endif |
|
457 throw new Task.Result(directoryPath); |
|
458 }.bind(this)); |
|
459 }, |
|
460 |
|
461 /** |
|
462 * Checks to determine whether to block downloads for parental controls. |
|
463 * |
|
464 * aParam aDownload |
|
465 * The download object. |
|
466 * |
|
467 * @return {Promise} |
|
468 * @resolves The boolean indicates to block downloads or not. |
|
469 */ |
|
470 shouldBlockForParentalControls: function DI_shouldBlockForParentalControls(aDownload) { |
|
471 if (this.dontCheckParentalControls) { |
|
472 return Promise.resolve(this.shouldBlockInTest); |
|
473 } |
|
474 |
|
475 let isEnabled = gParentalControlsService && |
|
476 gParentalControlsService.parentalControlsEnabled; |
|
477 let shouldBlock = isEnabled && |
|
478 gParentalControlsService.blockFileDownloadsEnabled; |
|
479 |
|
480 // Log the event if required by parental controls settings. |
|
481 if (isEnabled && gParentalControlsService.loggingEnabled) { |
|
482 gParentalControlsService.log(gParentalControlsService.ePCLog_FileDownload, |
|
483 shouldBlock, |
|
484 NetUtil.newURI(aDownload.source.url), null); |
|
485 } |
|
486 |
|
487 return Promise.resolve(shouldBlock); |
|
488 }, |
|
489 |
|
490 /** |
|
491 * Checks to determine whether to block downloads because they might be |
|
492 * malware, based on application reputation checks. |
|
493 * |
|
494 * aParam aDownload |
|
495 * The download object. |
|
496 * |
|
497 * @return {Promise} |
|
498 * @resolves The boolean indicates to block downloads or not. |
|
499 */ |
|
500 shouldBlockForReputationCheck: function (aDownload) { |
|
501 if (this.dontCheckApplicationReputation) { |
|
502 return Promise.resolve(this.shouldBlockInTestForApplicationReputation); |
|
503 } |
|
504 let hash; |
|
505 let sigInfo; |
|
506 try { |
|
507 hash = aDownload.saver.getSha256Hash(); |
|
508 sigInfo = aDownload.saver.getSignatureInfo(); |
|
509 } catch (ex) { |
|
510 // Bail if DownloadSaver doesn't have a hash. |
|
511 return Promise.resolve(false); |
|
512 } |
|
513 if (!hash || !sigInfo) { |
|
514 return Promise.resolve(false); |
|
515 } |
|
516 let deferred = Promise.defer(); |
|
517 let aReferrer = null; |
|
518 if (aDownload.source.referrer) { |
|
519 aReferrer: NetUtil.newURI(aDownload.source.referrer); |
|
520 } |
|
521 gApplicationReputationService.queryReputation({ |
|
522 sourceURI: NetUtil.newURI(aDownload.source.url), |
|
523 referrerURI: aReferrer, |
|
524 fileSize: aDownload.currentBytes, |
|
525 sha256Hash: hash, |
|
526 signatureInfo: sigInfo }, |
|
527 function onComplete(aShouldBlock, aRv) { |
|
528 deferred.resolve(aShouldBlock); |
|
529 }); |
|
530 return deferred.promise; |
|
531 }, |
|
532 |
|
533 #ifdef XP_WIN |
|
534 /** |
|
535 * Checks whether downloaded files should be marked as coming from |
|
536 * Internet Zone. |
|
537 * |
|
538 * @return true if files should be marked |
|
539 */ |
|
540 _shouldSaveZoneInformation: function() { |
|
541 let key = Cc["@mozilla.org/windows-registry-key;1"] |
|
542 .createInstance(Ci.nsIWindowsRegKey); |
|
543 try { |
|
544 key.open(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, |
|
545 "Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\Attachments", |
|
546 Ci.nsIWindowsRegKey.ACCESS_QUERY_VALUE); |
|
547 try { |
|
548 return key.readIntValue("SaveZoneInformation") != 1; |
|
549 } finally { |
|
550 key.close(); |
|
551 } |
|
552 } catch (ex) { |
|
553 // If the key is not present, files should be marked by default. |
|
554 return true; |
|
555 } |
|
556 }, |
|
557 #endif |
|
558 |
|
559 /** |
|
560 * Performs platform-specific operations when a download is done. |
|
561 * |
|
562 * aParam aDownload |
|
563 * The Download object. |
|
564 * |
|
565 * @return {Promise} |
|
566 * @resolves When all the operations completed successfully. |
|
567 * @rejects JavaScript exception if any of the operations failed. |
|
568 */ |
|
569 downloadDone: function(aDownload) { |
|
570 return Task.spawn(function () { |
|
571 #ifdef XP_WIN |
|
572 // On Windows, we mark any file saved to the NTFS file system as coming |
|
573 // from the Internet security zone unless Group Policy disables the |
|
574 // feature. We do this by writing to the "Zone.Identifier" Alternate |
|
575 // Data Stream directly, because the Save method of the |
|
576 // IAttachmentExecute interface would trigger operations that may cause |
|
577 // the application to hang, or other performance issues. |
|
578 // The stream created in this way is forward-compatible with all the |
|
579 // current and future versions of Windows. |
|
580 if (this._shouldSaveZoneInformation()) { |
|
581 let zone; |
|
582 try { |
|
583 zone = gDownloadPlatform.mapUrlToZone(aDownload.source.url); |
|
584 } catch (e) { |
|
585 // Default to Internet Zone if mapUrlToZone failed for |
|
586 // whatever reason. |
|
587 zone = Ci.mozIDownloadPlatform.ZONE_INTERNET; |
|
588 } |
|
589 try { |
|
590 // Don't write zone IDs for Local, Intranet, or Trusted sites |
|
591 // to match Windows behavior. |
|
592 if (zone >= Ci.mozIDownloadPlatform.ZONE_INTERNET) { |
|
593 let streamPath = aDownload.target.path + ":Zone.Identifier"; |
|
594 let stream = yield OS.File.open(streamPath, { create: true }); |
|
595 try { |
|
596 yield stream.write(new TextEncoder().encode("[ZoneTransfer]\r\nZoneId=" + zone + "\r\n")); |
|
597 } finally { |
|
598 yield stream.close(); |
|
599 } |
|
600 } |
|
601 } catch (ex) { |
|
602 // If writing to the stream fails, we ignore the error and continue. |
|
603 // The Windows API error 123 (ERROR_INVALID_NAME) is expected to |
|
604 // occur when working on a file system that does not support |
|
605 // Alternate Data Streams, like FAT32, thus we don't report this |
|
606 // specific error. |
|
607 if (!(ex instanceof OS.File.Error) || ex.winLastError != 123) { |
|
608 Cu.reportError(ex); |
|
609 } |
|
610 } |
|
611 } |
|
612 #endif |
|
613 |
|
614 // Now that the file is completely downloaded, mark it |
|
615 // accessible by other users on this system, if the user's |
|
616 // global preferences so indicate. (On Unix, this applies the |
|
617 // umask. On Windows, currently does nothing.) |
|
618 // Errors should be reported, but are not fatal. |
|
619 try { |
|
620 yield OS.File.setPermissions(aDownload.target.path); |
|
621 } catch (ex) { |
|
622 Cu.reportError(ex); |
|
623 } |
|
624 |
|
625 gDownloadPlatform.downloadDone(NetUtil.newURI(aDownload.source.url), |
|
626 new FileUtils.File(aDownload.target.path), |
|
627 aDownload.contentType, |
|
628 aDownload.source.isPrivate); |
|
629 this.downloadDoneCalled = true; |
|
630 }.bind(this)); |
|
631 }, |
|
632 |
|
633 /* |
|
634 * Launches a file represented by the target of a download. This can |
|
635 * open the file with the default application for the target MIME type |
|
636 * or file extension, or with a custom application if |
|
637 * aDownload.launcherPath is set. |
|
638 * |
|
639 * @param aDownload |
|
640 * A Download object that contains the necessary information |
|
641 * to launch the file. The relevant properties are: the target |
|
642 * file, the contentType and the custom application chosen |
|
643 * to launch it. |
|
644 * |
|
645 * @return {Promise} |
|
646 * @resolves When the instruction to launch the file has been |
|
647 * successfully given to the operating system. Note that |
|
648 * the OS might still take a while until the file is actually |
|
649 * launched. |
|
650 * @rejects JavaScript exception if there was an error trying to launch |
|
651 * the file. |
|
652 */ |
|
653 launchDownload: function (aDownload) { |
|
654 let deferred = Task.spawn(function DI_launchDownload_task() { |
|
655 let file = new FileUtils.File(aDownload.target.path); |
|
656 |
|
657 #ifndef XP_WIN |
|
658 // Ask for confirmation if the file is executable, except on Windows where |
|
659 // the operating system will show the prompt based on the security zone. |
|
660 // We do this here, instead of letting the caller handle the prompt |
|
661 // separately in the user interface layer, for two reasons. The first is |
|
662 // because of its security nature, so that add-ons cannot forget to do |
|
663 // this check. The second is that the system-level security prompt would |
|
664 // be displayed at launch time in any case. |
|
665 if (file.isExecutable() && !this.dontOpenFileAndFolder) { |
|
666 // We don't anchor the prompt to a specific window intentionally, not |
|
667 // only because this is the same behavior as the system-level prompt, |
|
668 // but also because the most recently active window is the right choice |
|
669 // in basically all cases. |
|
670 let shouldLaunch = yield DownloadUIHelper.getPrompter() |
|
671 .confirmLaunchExecutable(file.path); |
|
672 if (!shouldLaunch) { |
|
673 return; |
|
674 } |
|
675 } |
|
676 #endif |
|
677 |
|
678 // In case of a double extension, like ".tar.gz", we only |
|
679 // consider the last one, because the MIME service cannot |
|
680 // handle multiple extensions. |
|
681 let fileExtension = null, mimeInfo = null; |
|
682 let match = file.leafName.match(/\.([^.]+)$/); |
|
683 if (match) { |
|
684 fileExtension = match[1]; |
|
685 } |
|
686 |
|
687 try { |
|
688 // The MIME service might throw if contentType == "" and it can't find |
|
689 // a MIME type for the given extension, so we'll treat this case as |
|
690 // an unknown mimetype. |
|
691 mimeInfo = gMIMEService.getFromTypeAndExtension(aDownload.contentType, |
|
692 fileExtension); |
|
693 } catch (e) { } |
|
694 |
|
695 if (aDownload.launcherPath) { |
|
696 if (!mimeInfo) { |
|
697 // This should not happen on normal circumstances because launcherPath |
|
698 // is only set when we had an instance of nsIMIMEInfo to retrieve |
|
699 // the custom application chosen by the user. |
|
700 throw new Error( |
|
701 "Unable to create nsIMIMEInfo to launch a custom application"); |
|
702 } |
|
703 |
|
704 // Custom application chosen |
|
705 let localHandlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"] |
|
706 .createInstance(Ci.nsILocalHandlerApp); |
|
707 localHandlerApp.executable = new FileUtils.File(aDownload.launcherPath); |
|
708 |
|
709 mimeInfo.preferredApplicationHandler = localHandlerApp; |
|
710 mimeInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp; |
|
711 |
|
712 // In test mode, allow the test to verify the nsIMIMEInfo instance. |
|
713 if (this.dontOpenFileAndFolder) { |
|
714 throw new Task.Result(mimeInfo); |
|
715 } |
|
716 |
|
717 mimeInfo.launchWithFile(file); |
|
718 return; |
|
719 } |
|
720 |
|
721 // No custom application chosen, let's launch the file with the default |
|
722 // handler. In test mode, we indicate this with a null value. |
|
723 if (this.dontOpenFileAndFolder) { |
|
724 throw new Task.Result(null); |
|
725 } |
|
726 |
|
727 // First let's try to launch it through the MIME service application |
|
728 // handler |
|
729 if (mimeInfo) { |
|
730 mimeInfo.preferredAction = Ci.nsIMIMEInfo.useSystemDefault; |
|
731 |
|
732 try { |
|
733 mimeInfo.launchWithFile(file); |
|
734 return; |
|
735 } catch (ex) { } |
|
736 } |
|
737 |
|
738 // If it didn't work or if there was no MIME info available, |
|
739 // let's try to directly launch the file. |
|
740 try { |
|
741 file.launch(); |
|
742 return; |
|
743 } catch (ex) { } |
|
744 |
|
745 // If our previous attempts failed, try sending it through |
|
746 // the system's external "file:" URL handler. |
|
747 gExternalProtocolService.loadUrl(NetUtil.newURI(file)); |
|
748 yield undefined; |
|
749 }.bind(this)); |
|
750 |
|
751 if (this.dontOpenFileAndFolder) { |
|
752 deferred.then((value) => { this._deferTestOpenFile.resolve(value); }, |
|
753 (error) => { this._deferTestOpenFile.reject(error); }); |
|
754 } |
|
755 |
|
756 return deferred; |
|
757 }, |
|
758 |
|
759 /* |
|
760 * Shows the containing folder of a file. |
|
761 * |
|
762 * @param aFilePath |
|
763 * The path to the file. |
|
764 * |
|
765 * @return {Promise} |
|
766 * @resolves When the instruction to open the containing folder has been |
|
767 * successfully given to the operating system. Note that |
|
768 * the OS might still take a while until the folder is actually |
|
769 * opened. |
|
770 * @rejects JavaScript exception if there was an error trying to open |
|
771 * the containing folder. |
|
772 */ |
|
773 showContainingDirectory: function (aFilePath) { |
|
774 let deferred = Task.spawn(function DI_showContainingDirectory_task() { |
|
775 let file = new FileUtils.File(aFilePath); |
|
776 |
|
777 if (this.dontOpenFileAndFolder) { |
|
778 return; |
|
779 } |
|
780 |
|
781 try { |
|
782 // Show the directory containing the file and select the file. |
|
783 file.reveal(); |
|
784 return; |
|
785 } catch (ex) { } |
|
786 |
|
787 // If reveal fails for some reason (e.g., it's not implemented on unix |
|
788 // or the file doesn't exist), try using the parent if we have it. |
|
789 let parent = file.parent; |
|
790 if (!parent) { |
|
791 throw new Error( |
|
792 "Unexpected reference to a top-level directory instead of a file"); |
|
793 } |
|
794 |
|
795 try { |
|
796 // Open the parent directory to show where the file should be. |
|
797 parent.launch(); |
|
798 return; |
|
799 } catch (ex) { } |
|
800 |
|
801 // If launch also fails (probably because it's not implemented), let |
|
802 // the OS handler try to open the parent. |
|
803 gExternalProtocolService.loadUrl(NetUtil.newURI(parent)); |
|
804 yield undefined; |
|
805 }.bind(this)); |
|
806 |
|
807 if (this.dontOpenFileAndFolder) { |
|
808 deferred.then((value) => { this._deferTestShowDir.resolve("success"); }, |
|
809 (error) => { |
|
810 // Ensure that _deferTestShowDir has at least one consumer |
|
811 // for the error, otherwise the error will be reported as |
|
812 // uncaught. |
|
813 this._deferTestShowDir.promise.then(null, function() {}); |
|
814 this._deferTestShowDir.reject(error); |
|
815 }); |
|
816 } |
|
817 |
|
818 return deferred; |
|
819 }, |
|
820 |
|
821 /** |
|
822 * Calls the directory service, create a downloads directory and returns an |
|
823 * nsIFile for the downloads directory. |
|
824 * |
|
825 * @return {Promise} |
|
826 * @resolves The directory string path. |
|
827 */ |
|
828 _createDownloadsDirectory: function DI_createDownloadsDirectory(aName) { |
|
829 // We read the name of the directory from the list of translated strings |
|
830 // that is kept by the UI helper module, even if this string is not strictly |
|
831 // displayed in the user interface. |
|
832 let directoryPath = OS.Path.join(this._getDirectory(aName), |
|
833 DownloadUIHelper.strings.downloadsFolder); |
|
834 |
|
835 // Create the Downloads folder and ignore if it already exists. |
|
836 return OS.File.makeDir(directoryPath, { ignoreExisting: true }). |
|
837 then(function() { |
|
838 return directoryPath; |
|
839 }); |
|
840 }, |
|
841 |
|
842 /** |
|
843 * Calls the directory service and returns an nsIFile for the requested |
|
844 * location name. |
|
845 * |
|
846 * @return The directory string path. |
|
847 */ |
|
848 _getDirectory: function DI_getDirectory(aName) { |
|
849 return Services.dirsvc.get(this.testMode ? "TmpD" : aName, Ci.nsIFile).path; |
|
850 }, |
|
851 |
|
852 /** |
|
853 * Register the downloads interruption observers. |
|
854 * |
|
855 * @param aList |
|
856 * The public or private downloads list. |
|
857 * @param aIsPrivate |
|
858 * True if the list is private, false otherwise. |
|
859 * |
|
860 * @return {Promise} |
|
861 * @resolves When the views and observers are added. |
|
862 */ |
|
863 addListObservers: function DI_addListObservers(aList, aIsPrivate) { |
|
864 if (this.dontLoadObservers) { |
|
865 return Promise.resolve(); |
|
866 } |
|
867 |
|
868 DownloadObserver.registerView(aList, aIsPrivate); |
|
869 if (!DownloadObserver.observersAdded) { |
|
870 DownloadObserver.observersAdded = true; |
|
871 for (let topic of kObserverTopics) { |
|
872 Services.obs.addObserver(DownloadObserver, topic, false); |
|
873 } |
|
874 } |
|
875 return Promise.resolve(); |
|
876 }, |
|
877 |
|
878 /** |
|
879 * Checks if we have already imported (or attempted to import) |
|
880 * the downloads database from the previous SQLite storage. |
|
881 * |
|
882 * @return boolean True if we the previous DB was imported. |
|
883 */ |
|
884 get _importedFromSqlite() { |
|
885 try { |
|
886 return Services.prefs.getBoolPref(kPrefImportedFromSqlite); |
|
887 } catch (ex) { |
|
888 return false; |
|
889 } |
|
890 }, |
|
891 }; |
|
892 |
|
893 //////////////////////////////////////////////////////////////////////////////// |
|
894 //// DownloadObserver |
|
895 |
|
896 this.DownloadObserver = { |
|
897 /** |
|
898 * Flag to determine if the observers have been added previously. |
|
899 */ |
|
900 observersAdded: false, |
|
901 |
|
902 /** |
|
903 * Timer used to delay restarting canceled downloads upon waking and returning |
|
904 * online. |
|
905 */ |
|
906 _wakeTimer: null, |
|
907 |
|
908 /** |
|
909 * Set that contains the in progress publics downloads. |
|
910 * It's kept updated when a public download is added, removed or changes its |
|
911 * properties. |
|
912 */ |
|
913 _publicInProgressDownloads: new Set(), |
|
914 |
|
915 /** |
|
916 * Set that contains the in progress private downloads. |
|
917 * It's kept updated when a private download is added, removed or changes its |
|
918 * properties. |
|
919 */ |
|
920 _privateInProgressDownloads: new Set(), |
|
921 |
|
922 /** |
|
923 * Set that contains the downloads that have been canceled when going offline |
|
924 * or to sleep. These are started again when returning online or waking. This |
|
925 * list is not persisted so when exiting and restarting, the downloads will not |
|
926 * be started again. |
|
927 */ |
|
928 _canceledOfflineDownloads: new Set(), |
|
929 |
|
930 /** |
|
931 * Registers a view that updates the corresponding downloads state set, based |
|
932 * on the aIsPrivate argument. The set is updated when a download is added, |
|
933 * removed or changes its properties. |
|
934 * |
|
935 * @param aList |
|
936 * The public or private downloads list. |
|
937 * @param aIsPrivate |
|
938 * True if the list is private, false otherwise. |
|
939 */ |
|
940 registerView: function DO_registerView(aList, aIsPrivate) { |
|
941 let downloadsSet = aIsPrivate ? this._privateInProgressDownloads |
|
942 : this._publicInProgressDownloads; |
|
943 let downloadsView = { |
|
944 onDownloadAdded: aDownload => { |
|
945 if (!aDownload.stopped) { |
|
946 downloadsSet.add(aDownload); |
|
947 } |
|
948 }, |
|
949 onDownloadChanged: aDownload => { |
|
950 if (aDownload.stopped) { |
|
951 downloadsSet.delete(aDownload); |
|
952 } else { |
|
953 downloadsSet.add(aDownload); |
|
954 } |
|
955 }, |
|
956 onDownloadRemoved: aDownload => { |
|
957 downloadsSet.delete(aDownload); |
|
958 // The download must also be removed from the canceled when offline set. |
|
959 this._canceledOfflineDownloads.delete(aDownload); |
|
960 } |
|
961 }; |
|
962 |
|
963 // We register the view asynchronously. |
|
964 aList.addView(downloadsView).then(null, Cu.reportError); |
|
965 }, |
|
966 |
|
967 /** |
|
968 * Wrapper that handles the test mode before calling the prompt that display |
|
969 * a warning message box that informs that there are active downloads, |
|
970 * and asks whether the user wants to cancel them or not. |
|
971 * |
|
972 * @param aCancel |
|
973 * The observer notification subject. |
|
974 * @param aDownloadsCount |
|
975 * The current downloads count. |
|
976 * @param aPrompter |
|
977 * The prompter object that shows the confirm dialog. |
|
978 * @param aPromptType |
|
979 * The type of prompt notification depending on the observer. |
|
980 */ |
|
981 _confirmCancelDownloads: function DO_confirmCancelDownload( |
|
982 aCancel, aDownloadsCount, aPrompter, aPromptType) { |
|
983 // If user has already dismissed the request, then do nothing. |
|
984 if ((aCancel instanceof Ci.nsISupportsPRBool) && aCancel.data) { |
|
985 return; |
|
986 } |
|
987 // Handle test mode |
|
988 if (DownloadIntegration.testMode) { |
|
989 DownloadIntegration.testPromptDownloads = aDownloadsCount; |
|
990 return; |
|
991 } |
|
992 |
|
993 aCancel.data = aPrompter.confirmCancelDownloads(aDownloadsCount, aPromptType); |
|
994 }, |
|
995 |
|
996 /** |
|
997 * Resume all downloads that were paused when going offline, used when waking |
|
998 * from sleep or returning from being offline. |
|
999 */ |
|
1000 _resumeOfflineDownloads: function DO_resumeOfflineDownloads() { |
|
1001 this._wakeTimer = null; |
|
1002 |
|
1003 for (let download of this._canceledOfflineDownloads) { |
|
1004 download.start(); |
|
1005 } |
|
1006 }, |
|
1007 |
|
1008 //////////////////////////////////////////////////////////////////////////// |
|
1009 //// nsIObserver |
|
1010 |
|
1011 observe: function DO_observe(aSubject, aTopic, aData) { |
|
1012 let downloadsCount; |
|
1013 let p = DownloadUIHelper.getPrompter(); |
|
1014 switch (aTopic) { |
|
1015 case "quit-application-requested": |
|
1016 downloadsCount = this._publicInProgressDownloads.size + |
|
1017 this._privateInProgressDownloads.size; |
|
1018 this._confirmCancelDownloads(aSubject, downloadsCount, p, p.ON_QUIT); |
|
1019 break; |
|
1020 case "offline-requested": |
|
1021 downloadsCount = this._publicInProgressDownloads.size + |
|
1022 this._privateInProgressDownloads.size; |
|
1023 this._confirmCancelDownloads(aSubject, downloadsCount, p, p.ON_OFFLINE); |
|
1024 break; |
|
1025 case "last-pb-context-exiting": |
|
1026 downloadsCount = this._privateInProgressDownloads.size; |
|
1027 this._confirmCancelDownloads(aSubject, downloadsCount, p, |
|
1028 p.ON_LEAVE_PRIVATE_BROWSING); |
|
1029 break; |
|
1030 case "last-pb-context-exited": |
|
1031 let deferred = Task.spawn(function() { |
|
1032 let list = yield Downloads.getList(Downloads.PRIVATE); |
|
1033 let downloads = yield list.getAll(); |
|
1034 |
|
1035 // We can remove the downloads and finalize them in parallel. |
|
1036 for (let download of downloads) { |
|
1037 list.remove(download).then(null, Cu.reportError); |
|
1038 download.finalize(true).then(null, Cu.reportError); |
|
1039 } |
|
1040 }); |
|
1041 // Handle test mode |
|
1042 if (DownloadIntegration.testMode) { |
|
1043 deferred.then((value) => { DownloadIntegration._deferTestClearPrivateList.resolve("success"); }, |
|
1044 (error) => { DownloadIntegration._deferTestClearPrivateList.reject(error); }); |
|
1045 } |
|
1046 break; |
|
1047 case "sleep_notification": |
|
1048 case "suspend_process_notification": |
|
1049 case "network:offline-about-to-go-offline": |
|
1050 for (let download of this._publicInProgressDownloads) { |
|
1051 download.cancel(); |
|
1052 this._canceledOfflineDownloads.add(download); |
|
1053 } |
|
1054 for (let download of this._privateInProgressDownloads) { |
|
1055 download.cancel(); |
|
1056 this._canceledOfflineDownloads.add(download); |
|
1057 } |
|
1058 break; |
|
1059 case "wake_notification": |
|
1060 case "resume_process_notification": |
|
1061 let wakeDelay = 10000; |
|
1062 try { |
|
1063 wakeDelay = Services.prefs.getIntPref("browser.download.manager.resumeOnWakeDelay"); |
|
1064 } catch(e) {} |
|
1065 |
|
1066 if (wakeDelay >= 0) { |
|
1067 this._wakeTimer = new Timer(this._resumeOfflineDownloads.bind(this), wakeDelay, |
|
1068 Ci.nsITimer.TYPE_ONE_SHOT); |
|
1069 } |
|
1070 break; |
|
1071 case "network:offline-status-changed": |
|
1072 if (aData == "online") { |
|
1073 this._resumeOfflineDownloads(); |
|
1074 } |
|
1075 break; |
|
1076 // We need to unregister observers explicitly before we reach the |
|
1077 // "xpcom-shutdown" phase, otherwise observers may be notified when some |
|
1078 // required services are not available anymore. We can't unregister |
|
1079 // observers on "quit-application", because this module is also loaded |
|
1080 // during "make package" automation, and the quit notification is not sent |
|
1081 // in that execution environment (bug 973637). |
|
1082 case "xpcom-will-shutdown": |
|
1083 for (let topic of kObserverTopics) { |
|
1084 Services.obs.removeObserver(this, topic); |
|
1085 } |
|
1086 break; |
|
1087 } |
|
1088 }, |
|
1089 |
|
1090 //////////////////////////////////////////////////////////////////////////// |
|
1091 //// nsISupports |
|
1092 |
|
1093 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]) |
|
1094 }; |
|
1095 |
|
1096 //////////////////////////////////////////////////////////////////////////////// |
|
1097 //// DownloadHistoryObserver |
|
1098 |
|
1099 #ifdef MOZ_PLACES |
|
1100 /** |
|
1101 * Registers a Places observer so that operations on download history are |
|
1102 * reflected on the provided list of downloads. |
|
1103 * |
|
1104 * You do not need to keep a reference to this object in order to keep it alive, |
|
1105 * because the history service already keeps a strong reference to it. |
|
1106 * |
|
1107 * @param aList |
|
1108 * DownloadList object linked to this observer. |
|
1109 */ |
|
1110 this.DownloadHistoryObserver = function (aList) |
|
1111 { |
|
1112 this._list = aList; |
|
1113 PlacesUtils.history.addObserver(this, false); |
|
1114 } |
|
1115 |
|
1116 this.DownloadHistoryObserver.prototype = { |
|
1117 /** |
|
1118 * DownloadList object linked to this observer. |
|
1119 */ |
|
1120 _list: null, |
|
1121 |
|
1122 //////////////////////////////////////////////////////////////////////////// |
|
1123 //// nsISupports |
|
1124 |
|
1125 QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver]), |
|
1126 |
|
1127 //////////////////////////////////////////////////////////////////////////// |
|
1128 //// nsINavHistoryObserver |
|
1129 |
|
1130 onDeleteURI: function DL_onDeleteURI(aURI, aGUID) { |
|
1131 this._list.removeFinished(download => aURI.equals(NetUtil.newURI( |
|
1132 download.source.url))); |
|
1133 }, |
|
1134 |
|
1135 onClearHistory: function DL_onClearHistory() { |
|
1136 this._list.removeFinished(); |
|
1137 }, |
|
1138 |
|
1139 onTitleChanged: function () {}, |
|
1140 onBeginUpdateBatch: function () {}, |
|
1141 onEndUpdateBatch: function () {}, |
|
1142 onVisit: function () {}, |
|
1143 onPageChanged: function () {}, |
|
1144 onDeleteVisits: function () {}, |
|
1145 }; |
|
1146 #else |
|
1147 /** |
|
1148 * Empty implementation when we have no Places support, for example on B2G. |
|
1149 */ |
|
1150 this.DownloadHistoryObserver = function (aList) {} |
|
1151 #endif |
|
1152 |
|
1153 //////////////////////////////////////////////////////////////////////////////// |
|
1154 //// DownloadAutoSaveView |
|
1155 |
|
1156 /** |
|
1157 * This view can be added to a DownloadList object to trigger a save operation |
|
1158 * in the given DownloadStore object when a relevant change occurs. You should |
|
1159 * call the "initialize" method in order to register the view and load the |
|
1160 * current state from disk. |
|
1161 * |
|
1162 * You do not need to keep a reference to this object in order to keep it alive, |
|
1163 * because the DownloadList object already keeps a strong reference to it. |
|
1164 * |
|
1165 * @param aList |
|
1166 * The DownloadList object on which the view should be registered. |
|
1167 * @param aStore |
|
1168 * The DownloadStore object used for saving. |
|
1169 */ |
|
1170 this.DownloadAutoSaveView = function (aList, aStore) |
|
1171 { |
|
1172 this._list = aList; |
|
1173 this._store = aStore; |
|
1174 this._downloadsMap = new Map(); |
|
1175 this._writer = new DeferredTask(() => this._store.save(), kSaveDelayMs); |
|
1176 } |
|
1177 |
|
1178 this.DownloadAutoSaveView.prototype = { |
|
1179 /** |
|
1180 * DownloadList object linked to this view. |
|
1181 */ |
|
1182 _list: null, |
|
1183 |
|
1184 /** |
|
1185 * The DownloadStore object used for saving. |
|
1186 */ |
|
1187 _store: null, |
|
1188 |
|
1189 /** |
|
1190 * True when the initial state of the downloads has been loaded. |
|
1191 */ |
|
1192 _initialized: false, |
|
1193 |
|
1194 /** |
|
1195 * Registers the view and loads the current state from disk. |
|
1196 * |
|
1197 * @return {Promise} |
|
1198 * @resolves When the view has been registered. |
|
1199 * @rejects JavaScript exception. |
|
1200 */ |
|
1201 initialize: function () |
|
1202 { |
|
1203 // We set _initialized to true after adding the view, so that |
|
1204 // onDownloadAdded doesn't cause a save to occur. |
|
1205 return this._list.addView(this).then(() => this._initialized = true); |
|
1206 }, |
|
1207 |
|
1208 /** |
|
1209 * This map contains only Download objects that should be saved to disk, and |
|
1210 * associates them with the result of their getSerializationHash function, for |
|
1211 * the purpose of detecting changes to the relevant properties. |
|
1212 */ |
|
1213 _downloadsMap: null, |
|
1214 |
|
1215 /** |
|
1216 * DeferredTask for the save operation. |
|
1217 */ |
|
1218 _writer: null, |
|
1219 |
|
1220 /** |
|
1221 * Called when the list of downloads changed, this triggers the asynchronous |
|
1222 * serialization of the list of downloads. |
|
1223 */ |
|
1224 saveSoon: function () |
|
1225 { |
|
1226 this._writer.arm(); |
|
1227 }, |
|
1228 |
|
1229 ////////////////////////////////////////////////////////////////////////////// |
|
1230 //// DownloadList view |
|
1231 |
|
1232 onDownloadAdded: function (aDownload) |
|
1233 { |
|
1234 if (DownloadIntegration.shouldPersistDownload(aDownload)) { |
|
1235 this._downloadsMap.set(aDownload, aDownload.getSerializationHash()); |
|
1236 if (this._initialized) { |
|
1237 this.saveSoon(); |
|
1238 } |
|
1239 } |
|
1240 }, |
|
1241 |
|
1242 onDownloadChanged: function (aDownload) |
|
1243 { |
|
1244 if (!DownloadIntegration.shouldPersistDownload(aDownload)) { |
|
1245 if (this._downloadsMap.has(aDownload)) { |
|
1246 this._downloadsMap.delete(aDownload); |
|
1247 this.saveSoon(); |
|
1248 } |
|
1249 return; |
|
1250 } |
|
1251 |
|
1252 let hash = aDownload.getSerializationHash(); |
|
1253 if (this._downloadsMap.get(aDownload) != hash) { |
|
1254 this._downloadsMap.set(aDownload, hash); |
|
1255 this.saveSoon(); |
|
1256 } |
|
1257 }, |
|
1258 |
|
1259 onDownloadRemoved: function (aDownload) |
|
1260 { |
|
1261 if (this._downloadsMap.has(aDownload)) { |
|
1262 this._downloadsMap.delete(aDownload); |
|
1263 this.saveSoon(); |
|
1264 } |
|
1265 }, |
|
1266 }; |