diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /dom/downloads/DownloadsAPI.js | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'dom/downloads/DownloadsAPI.js')
-rw-r--r-- | dom/downloads/DownloadsAPI.js | 517 |
1 files changed, 517 insertions, 0 deletions
diff --git a/dom/downloads/DownloadsAPI.js b/dom/downloads/DownloadsAPI.js new file mode 100644 index 000000000..8294e2a3e --- /dev/null +++ b/dom/downloads/DownloadsAPI.js @@ -0,0 +1,517 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const Cr = Components.results; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/DOMRequestHelper.jsm"); +Cu.import("resource://gre/modules/DownloadsIPC.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "cpmm", + "@mozilla.org/childprocessmessagemanager;1", + "nsIMessageSender"); +XPCOMUtils.defineLazyServiceGetter(this, "volumeService", + "@mozilla.org/telephony/volume-service;1", + "nsIVolumeService"); + +/** + * The content process implementations of navigator.mozDownloadManager and its + * DOMDownload download objects. Uses DownloadsIPC.jsm to communicate with + * DownloadsAPI.jsm in the parent process. + */ + +function debug(aStr) { +#ifdef MOZ_DEBUG + dump("-*- DownloadsAPI.js : " + aStr + "\n"); +#endif +} + +function DOMDownloadManagerImpl() { + debug("DOMDownloadManagerImpl constructor"); +} + +DOMDownloadManagerImpl.prototype = { + __proto__: DOMRequestIpcHelper.prototype, + + // nsIDOMGlobalPropertyInitializer implementation + init: function(aWindow) { + debug("DownloadsManager init"); + this.initDOMRequestHelper(aWindow, + ["Downloads:Added", + "Downloads:Removed"]); + + // Get the manifest URL if this is an installed app + let appsService = Cc["@mozilla.org/AppsService;1"] + .getService(Ci.nsIAppsService); + let principal = aWindow.document.nodePrincipal; + // This returns the empty string if we're not an installed app. Coerce to + // null. + this._manifestURL = appsService.getManifestURLByLocalId(principal.appId) || + null; + }, + + uninit: function() { + debug("uninit"); + downloadsCache.evict(this._window); + }, + + set ondownloadstart(aHandler) { + this.__DOM_IMPL__.setEventHandler("ondownloadstart", aHandler); + }, + + get ondownloadstart() { + return this.__DOM_IMPL__.getEventHandler("ondownloadstart"); + }, + + getDownloads: function() { + debug("getDownloads()"); + + return this.createPromise(function (aResolve, aReject) { + DownloadsIPC.getDownloads().then( + function(aDownloads) { + // Turn the list of download objects into DOM objects and + // send them. + let array = new this._window.Array(); + for (let id in aDownloads) { + let dom = createDOMDownloadObject(this._window, aDownloads[id]); + array.push(this._prepareForContent(dom)); + } + aResolve(array); + }.bind(this), + function() { + aReject("GetDownloadsError"); + } + ); + }.bind(this)); + }, + + clearAllDone: function() { + debug("clearAllDone()"); + // This is a void function; we just kick it off. No promises, etc. + DownloadsIPC.clearAllDone(); + }, + + remove: function(aDownload) { + debug("remove " + aDownload.url + " " + aDownload.id); + return this.createPromise(function (aResolve, aReject) { + if (!downloadsCache.has(this._window, aDownload.id)) { + debug("no download " + aDownload.id); + aReject("InvalidDownload"); + return; + } + + DownloadsIPC.remove(aDownload.id).then( + function(aResult) { + let dom = createDOMDownloadObject(this._window, aResult); + // Change the state right away to not race against the update message. + dom.wrappedJSObject.state = "finalized"; + aResolve(this._prepareForContent(dom)); + }.bind(this), + function() { + aReject("RemoveError"); + } + ); + }.bind(this)); + }, + + adoptDownload: function(aAdoptDownloadDict) { + // Our AdoptDownloadDict only includes simple types, which WebIDL enforces. + // We have no object/any types so we do not need to worry about invoking + // JSON.stringify (and it inheriting our security privileges). + debug("adoptDownload"); + return this.createPromise(function (aResolve, aReject) { + if (!aAdoptDownloadDict) { + debug("Download dictionary is required!"); + aReject("InvalidDownload"); + return; + } + if (!aAdoptDownloadDict.storageName || !aAdoptDownloadDict.storagePath || + !aAdoptDownloadDict.contentType) { + debug("Missing one of: storageName, storagePath, contentType"); + aReject("InvalidDownload"); + return; + } + + // Convert storageName/storagePath to a local filesystem path. + let volume; + // getVolumeByName throws if you give it something it doesn't like + // because XPConnect converts the NS_ERROR_NOT_AVAILABLE to an + // exception. So catch it. + try { + volume = volumeService.getVolumeByName(aAdoptDownloadDict.storageName); + } catch (ex) {} + if (!volume) { + debug("Invalid storage name: " + aAdoptDownloadDict.storageName); + aReject("InvalidDownload"); + return; + } + let computedPath = volume.mountPoint + '/' + + aAdoptDownloadDict.storagePath; + // We validate that there is actually a file at the given path in the + // parent process in DownloadsAPI.js because that's where the file + // access would actually occur either way. + + // Create a DownloadsAPI.jsm 'jsonDownload' style representation. + let jsonDownload = { + url: aAdoptDownloadDict.url, + path: computedPath, + contentType: aAdoptDownloadDict.contentType, + startTime: aAdoptDownloadDict.startTime.valueOf() || Date.now(), + sourceAppManifestURL: this._manifestURL + }; + + DownloadsIPC.adoptDownload(jsonDownload).then( + function(aResult) { + let domDownload = createDOMDownloadObject(this._window, aResult); + aResolve(this._prepareForContent(domDownload)); + }.bind(this), + function(aResult) { + // This will be one of: AdoptError (generic catch-all), + // AdoptNoSuchFile, AdoptFileIsDirectory + aReject(aResult.error); + } + ); + }.bind(this)); + }, + + + /** + * Turns a chrome download object into a content accessible one. + * When we have __DOM_IMPL__ available we just use that, otherwise + * we run _create() with the wrapped js object. + */ + _prepareForContent: function(aChromeObject) { + if (aChromeObject.__DOM_IMPL__) { + return aChromeObject.__DOM_IMPL__; + } + let res = this._window.DOMDownload._create(this._window, + aChromeObject.wrappedJSObject); + return res; + }, + + receiveMessage: function(aMessage) { + let data = aMessage.data; + switch(aMessage.name) { + case "Downloads:Added": + debug("Adding " + uneval(data)); + let event = new this._window.DownloadEvent("downloadstart", { + download: + this._prepareForContent(createDOMDownloadObject(this._window, data)) + }); + this.__DOM_IMPL__.dispatchEvent(event); + break; + } + }, + + classID: Components.ID("{c6587afa-0696-469f-9eff-9dac0dd727fe}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, + Ci.nsISupportsWeakReference, + Ci.nsIObserver, + Ci.nsIDOMGlobalPropertyInitializer]), + +}; + +/** + * Keep track of download objects per window. + */ +var downloadsCache = { + init: function() { + this.cache = new WeakMap(); + }, + + has: function(aWindow, aId) { + let downloads = this.cache.get(aWindow); + return !!(downloads && downloads[aId]); + }, + + get: function(aWindow, aDownload) { + let downloads = this.cache.get(aWindow); + if (!(downloads && downloads[aDownload.id])) { + debug("Adding download " + aDownload.id + " to cache."); + if (!downloads) { + this.cache.set(aWindow, {}); + downloads = this.cache.get(aWindow); + } + // Create the object and add it to the cache. + let impl = Cc["@mozilla.org/downloads/download;1"] + .createInstance(Ci.nsISupports); + impl.wrappedJSObject._init(aWindow, aDownload); + downloads[aDownload.id] = impl; + } + return downloads[aDownload.id]; + }, + + evict: function(aWindow) { + this.cache.delete(aWindow); + } +}; + +downloadsCache.init(); + +/** + * The DOM facade of a download object. + */ + +function createDOMDownloadObject(aWindow, aDownload) { + return downloadsCache.get(aWindow, aDownload); +} + +function DOMDownloadImpl() { + debug("DOMDownloadImpl constructor "); + + this.wrappedJSObject = this; + this.totalBytes = 0; + this.currentBytes = 0; + this.url = null; + this.path = null; + this.storageName = null; + this.storagePath = null; + this.contentType = null; + + /* fields that require getters/setters */ + this._error = null; + this._startTime = new Date(); + this._state = "stopped"; + + /* private fields */ + this.id = null; +} + +DOMDownloadImpl.prototype = { + + createPromise: function(aPromiseInit) { + return new this._window.Promise(aPromiseInit); + }, + + pause: function() { + debug("DOMDownloadImpl pause"); + let id = this.id; + // We need to wrap the Promise.jsm promise in a "real" DOM promise... + return this.createPromise(function(aResolve, aReject) { + DownloadsIPC.pause(id).then(aResolve, aReject); + }); + }, + + resume: function() { + debug("DOMDownloadImpl resume"); + let id = this.id; + // We need to wrap the Promise.jsm promise in a "real" DOM promise... + return this.createPromise(function(aResolve, aReject) { + DownloadsIPC.resume(id).then(aResolve, aReject); + }); + }, + + set onstatechange(aHandler) { + this.__DOM_IMPL__.setEventHandler("onstatechange", aHandler); + }, + + get onstatechange() { + return this.__DOM_IMPL__.getEventHandler("onstatechange"); + }, + + get error() { + return this._error; + }, + + set error(aError) { + this._error = aError; + }, + + get startTime() { + return this._startTime; + }, + + set startTime(aStartTime) { + if (aStartTime instanceof Date) { + this._startTime = aStartTime; + } + else { + this._startTime = new Date(aStartTime); + } + }, + + get state() { + return this._state; + }, + + // We require a setter here to simplify the internals of the Download Manager + // since we actually pass dummy JSON objects to the child process and update + // them. This is the case for all other setters for read-only attributes + // implemented in this object. + set state(aState) { + // We need to ensure that XPCOM consumers of this API respect the enum + // values as well. + if (["downloading", + "stopped", + "succeeded", + "finalized"].indexOf(aState) != -1) { + this._state = aState; + } + }, + + /** + * Initialize a DOMDownload instance for the given window using the + * 'jsonDownload' serialized format of the download encoded by + * DownloadsAPI.jsm. + */ + _init: function(aWindow, aDownload) { + this._window = aWindow; + this.id = aDownload.id; + this._update(aDownload); + Services.obs.addObserver(this, "downloads-state-change-" + this.id, + /* ownsWeak */ true); + debug("observer set for " + this.id); + }, + + /** + * Updates the state of the object and fires the statechange event. + */ + _update: function(aDownload) { + debug("update " + uneval(aDownload)); + if (this.id != aDownload.id) { + return; + } + + let props = ["totalBytes", "currentBytes", "url", "path", "storageName", + "storagePath", "state", "contentType", "startTime", + "sourceAppManifestURL"]; + let changed = false; + let changedProps = {}; + + props.forEach((prop) => { + if (prop in aDownload && (aDownload[prop] != this[prop])) { + this[prop] = aDownload[prop]; + changedProps[prop] = changed = true; + } + }); + + // When the path changes, we should update the storage name and + // storage path used for our downloaded file in case our download + // was re-targetted to a different storage and/or filename. + if (changedProps["path"]) { + let storages = this._window.navigator.getDeviceStorages("sdcard"); + let preferredStorageName; + // Use the first one or the default storage. Just like jsdownloads picks + // the default / preferred download directory. + storages.forEach((aStorage) => { + if (aStorage.default || !preferredStorageName) { + preferredStorageName = aStorage.storageName; + } + }); + // Now get the path for this storage area. + let volume; + if (preferredStorageName) { + let volume = volumeService.getVolumeByName(preferredStorageName); + if (volume) { + // Finally, create the relative path of the file that can be used + // later on to retrieve the file via DeviceStorage. Our path + // needs to omit the starting '/'. + this.storageName = preferredStorageName; + this.storagePath = + this.path.substring(this.path.indexOf(volume.mountPoint) + + volume.mountPoint.length + 1); + } + } + } + + if (aDownload.error) { + // + // When we get a generic error failure back from the js downloads api + // we will verify the status of device storage to see if we can't provide + // a better error result value. + // + // XXX If these checks expand further, consider moving them into their + // own function. + // + let result = aDownload.error.result; + let storage = this._window.navigator.getDeviceStorage("sdcard"); + + // If we don't have access to device storage we'll opt out of these + // extra checks as they are all dependent on the state of the storage. + if (result == Cr.NS_ERROR_FAILURE && storage) { + // We will delay sending the notification until we've inferred which + // error is really happening. + changed = false; + debug("Attempting to infer error via device storage sanity checks."); + // Get device storage and request availability status. + let available = storage.available(); + available.onsuccess = (function() { + debug("Storage Status = '" + available.result + "'"); + let inferredError = result; + switch (available.result) { + case "unavailable": + inferredError = Cr.NS_ERROR_FILE_NOT_FOUND; + break; + case "shared": + inferredError = Cr.NS_ERROR_FILE_ACCESS_DENIED; + break; + } + this._updateWithError(aDownload, inferredError); + }).bind(this); + available.onerror = (function() { + this._updateWithError(aDownload, result); + }).bind(this); + } + + this.error = + new this._window.DOMError("DownloadError", result); + } else { + this.error = null; + } + + // The visible state has not changed, so no need to fire an event. + if (!changed) { + return; + } + + this._sendStateChange(); + }, + + _updateWithError: function(aDownload, aError) { + this.error = + new this._window.DOMError("DownloadError", aError); + this._sendStateChange(); + }, + + _sendStateChange: function() { + // __DOM_IMPL__ may not be available at first update. + if (this.__DOM_IMPL__) { + let event = new this._window.DownloadEvent("statechange", { + download: this.__DOM_IMPL__ + }); + debug("Dispatching statechange event. state=" + this.state); + this.__DOM_IMPL__.dispatchEvent(event); + } + }, + + observe: function(aSubject, aTopic, aData) { + debug("DOMDownloadImpl observe " + aTopic); + if (aTopic !== "downloads-state-change-" + this.id) { + return; + } + + try { + let download = JSON.parse(aData); + // We get the start time as milliseconds, not as a Date object. + if (download.startTime) { + download.startTime = new Date(download.startTime); + } + this._update(download); + } catch(e) {} + }, + + classID: Components.ID("{96b81b99-aa96-439d-8c59-92eeed34705f}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, + Ci.nsIObserver, + Ci.nsISupportsWeakReference]) +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DOMDownloadManagerImpl, + DOMDownloadImpl]); |