mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-31 22:25:30 +00:00
518 lines
16 KiB
JavaScript
518 lines
16 KiB
JavaScript
/* 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]);
|