Bug 825318 - Implement adoptDownload for mozDownloadManager, r=aus, r=sicking

Implement mozDownloadManager.adoptDownload as a certified-only API.

This also fixes and re-enables many of the existing dom/downloads tests
failures by virtue of cleanup and not running them on non-gonk toolkits
where exceptions will be thrown and things will fail.  This should
resolve bug 979446 about re-enabling the tests.
This commit is contained in:
Andrew Sutherland 2015-02-24 11:06:59 -05:00
Родитель e7a8e00fc5
Коммит f4ea4e85aa
19 изменённых файлов: 1123 добавлений и 58 удалений

Просмотреть файл

@ -21,6 +21,12 @@ 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");
@ -40,6 +46,15 @@ DOMDownloadManagerImpl.prototype = {
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() {
@ -79,23 +94,8 @@ DOMDownloadManagerImpl.prototype = {
clearAllDone: function() {
debug("clearAllDone()");
return this.createPromise(function (aResolve, aReject) {
DownloadsIPC.clearAllDone().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("ClearAllDoneError");
}
);
}.bind(this));
// This is a void function; we just kick it off. No promises, etc.
DownloadsIPC.clearAllDone();
},
remove: function(aDownload) {
@ -121,6 +121,67 @@ DOMDownloadManagerImpl.prototype = {
}.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
@ -295,6 +356,11 @@ DOMDownloadImpl.prototype = {
}
},
/**
* 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;
@ -314,12 +380,13 @@ DOMDownloadImpl.prototype = {
}
let props = ["totalBytes", "currentBytes", "url", "path", "storageName",
"storagePath", "state", "contentType", "startTime"];
"storagePath", "state", "contentType", "startTime",
"sourceAppManifestURL"];
let changed = false;
let changedProps = {};
props.forEach((prop) => {
if (aDownload[prop] && (aDownload[prop] != this[prop])) {
if (prop in aDownload && (aDownload[prop] != this[prop])) {
this[prop] = aDownload[prop];
changedProps[prop] = changed = true;
}

Просмотреть файл

@ -13,11 +13,19 @@ this.EXPORTED_SYMBOLS = [];
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Downloads.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
"@mozilla.org/parentprocessmessagemanager;1",
"nsIMessageBroadcaster");
/**
* Parent process logic that services download API requests from the
* DownloadAPI.js instances in content processeses. The actual work of managing
* downloads is done by Toolkit's Downloads.jsm. This module is loaded by B2G's
* shell.js
*/
function debug(aStr) {
#ifdef MOZ_DEBUG
dump("-*- DownloadsAPI.jsm : " + aStr + "\n");
@ -49,7 +57,8 @@ let DownloadsAPI = {
"Downloads:ClearAllDone",
"Downloads:Remove",
"Downloads:Pause",
"Downloads:Resume"].forEach((msgName) => {
"Downloads:Resume",
"Downloads:Adopt"].forEach((msgName) => {
ppmm.addMessageListener(msgName, this);
});
@ -92,7 +101,9 @@ let DownloadsAPI = {
url: aDownload.source.url,
path: aDownload.target.path,
contentType: aDownload.contentType,
startTime: aDownload.startTime.getTime()
startTime: aDownload.startTime.getTime(),
sourceAppManifestURL: aDownload._unknownProperties &&
aDownload._unknownProperties.sourceAppManifestURL
};
if (aDownload.error) {
@ -165,6 +176,9 @@ let DownloadsAPI = {
case "Downloads:Resume":
this.resume(aMessage.data, aMessage.target);
break;
case "Downloads:Adopt":
this.adoptDownload(aMessage.data, aMessage.target);
break;
default:
debug("Invalid message: " + aMessage.name);
}
@ -186,17 +200,9 @@ let DownloadsAPI = {
clearAllDone: function(aData, aMm) {
debug("clearAllDone called!");
let self = this;
Task.spawn(function () {
let list = yield Downloads.getList(Downloads.ALL);
yield list.removeFinished();
list = yield Downloads.getList(Downloads.ALL);
let downloads = yield list.getAll();
let res = [];
downloads.forEach((aDownload) => {
res.push(self.jsonDownload(aDownload));
});
aMm.sendAsyncMessage("Downloads:ClearAllDone:Return", res);
list.removeFinished();
}).then(null, Components.utils.reportError);
},
@ -262,6 +268,97 @@ let DownloadsAPI = {
aData, "ResumeError");
}
);
},
/**
* Receive a download to adopt in the same representation we produce from
* our "jsonDownload" normalizer and add it to the list of downloads.
*/
adoptDownload: function(aData, aMm) {
let adoptJsonRep = aData.jsonDownload;
debug("adoptDownload " + uneval(adoptJsonRep));
Task.spawn(function* () {
// Verify that the file exists on disk. This will result in a rejection
// if the file does not exist. We will also use this information for the
// file size to avoid weird inconsistencies. We ignore the filesystem
// timestamp in favor of whatever the caller is telling us.
let fileInfo = yield OS.File.stat(adoptJsonRep.path);
// We also require that the file is not a directory.
if (fileInfo.isDir) {
throw new Error("AdoptFileIsDirectory");
}
// We need to create a Download instance to add to the list. Create a
// serialized representation and then from there the instance.
let serializedRep = {
// explicit initializations in toSerializable
source: {
url: adoptJsonRep.url
// This is where isPrivate would go if adoption supported private
// browsing.
},
target: {
path: adoptJsonRep.path,
},
startTime: adoptJsonRep.startTime,
// kPlainSerializableDownloadProperties propagations
succeeded: true, // (all adopted downloads are required to be completed)
totalBytes: fileInfo.size,
contentType: adoptJsonRep.contentType,
// unknown properties added/used by the DownloadsAPI
currentBytes: fileInfo.size,
sourceAppManifestURL: adoptJsonRep.sourceAppManifestURL
};
let download = yield Downloads.createDownload(serializedRep);
// The ALL list is a DownloadCombinedList instance that combines the
// PUBLIC (persisted to disk) and PRIVATE (ephemeral) download lists..
// When we call add on it, it dispatches to the appropriate list based on
// the 'isPrivate' field of the source. (Which we don't initialize and
// defaults to false.)
let allDownloadList = yield Downloads.getList(Downloads.ALL);
// This add will automatically notify all views of the added download,
// including DownloadsAPI instances and the DownloadAutoSaveView that's
// subscribed to the PUBLIC list and will save the download.
yield allDownloadList.add(download);
debug("download adopted");
// The notification above occurred synchronously, and so we will have
// already dispatched an added notification for our download to the child
// process in question. As such, we only need to relay the download id
// since the download will already have been cached.
return download;
}.bind(this)).then(
(download) => {
sendPromiseMessage(aMm, "Downloads:Adopt:Return",
{
id: this.downloadId(download),
promiseId: aData.promiseId
});
},
(ex) => {
let reportAs = "AdoptError";
// Provide better error codes for expected errors.
if (ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
reportAs = "AdoptNoSuchFile";
} else if (ex.message === "AdoptFileIsDirectory") {
reportAs = ex.message;
} else {
// Anything else is unexpected and should be reported to help track
// down what's going wrong.
debug("unexpected download error: " + ex);
Cu.reportError(ex);
}
sendPromiseMessage(aMm, "Downloads:Adopt:Return",
{
promiseId: aData.promiseId
},
reportAs);
});
}
};

Просмотреть файл

@ -36,10 +36,10 @@ const ipcMessages = ["Downloads:Added",
"Downloads:Removed",
"Downloads:Changed",
"Downloads:GetList:Return",
"Downloads:ClearAllDone:Return",
"Downloads:Remove:Return",
"Downloads:Pause:Return",
"Downloads:Resume:Return"];
"Downloads:Resume:Return",
"Downloads:Adopt:Return"];
this.DownloadsIPC = {
downloads: {},
@ -54,7 +54,6 @@ this.DownloadsIPC = {
// We need to get the list of current downloads.
this.ready = false;
this.getListPromises = [];
this.clearAllPromises = [];
this.downloadPromises = {};
cpmm.sendAsyncMessage("Downloads:GetList", {});
this._promiseId = 0;
@ -93,12 +92,6 @@ this.DownloadsIPC = {
}
this.ready = true;
break;
case "Downloads:ClearAllDone:Return":
this._updateDownloadsArray(download);
this.clearAllPromises.forEach(aPromise =>
aPromise.resolve(this.downloads));
this.clearAllPromises.length = 0;
break;
case "Downloads:Added":
this.downloads[download.id] = download;
this.notifyChanges(download.id);
@ -139,6 +132,7 @@ this.DownloadsIPC = {
case "Downloads:Remove:Return":
case "Downloads:Pause:Return":
case "Downloads:Resume:Return":
case "Downloads:Adopt:Return":
if (this.downloadPromises[download.promiseId]) {
if (!download.error) {
this.downloadPromises[download.promiseId].resolve(download);
@ -167,14 +161,11 @@ this.DownloadsIPC = {
},
/**
* Returns a promise that is resolved with the list of current downloads.
*/
* Void function to trigger removal of completed downloads.
*/
clearAllDone: function() {
debug("clearAllDone");
let deferred = Promise.defer();
this.clearAllPromises.push(deferred);
cpmm.sendAsyncMessage("Downloads:ClearAllDone", {});
return deferred.promise;
},
promiseId: function() {
@ -211,6 +202,16 @@ this.DownloadsIPC = {
return deferred.promise;
},
adoptDownload: function(aJsonDownload) {
debug("adoptDownload");
let deferred = Promise.defer();
let pId = this.promiseId();
this.downloadPromises[pId] = deferred;
cpmm.sendAsyncMessage("Downloads:Adopt",
{ jsonDownload: aJsonDownload, promiseId: pId });
return deferred.promise;
},
observe: function(aSubject, aTopic, aData) {
if (aTopic == "xpcom-shutdown") {
ipcMessages.forEach((aMessage) => {

Просмотреть файл

@ -0,0 +1,67 @@
/**
* A helper to clear out the existing downloads known to the mozDownloadManager
* / downloads.js.
*
* It exists because previously mozDownloadManager.clearAllDone() thought that
* when it returned that all the completed downloads would be cleared out. It
* was wrong and this led to various intermittent test failurse. In discussion
* on https://bugzil.la/979446#c13 and onwards, it was decided that
* clearAllDone() was in the wrong and that the jsdownloads API it depends on
* was not going to change to make it be in the right.
*
* The existing uses of clearAllDone() in tests seemed to be about:
* - Exploding if there was somehow still a download in progress
* - Clearing out the download list at the start of a test so that calls to
* getDownloads() wouldn't have to worry about existing downloads, etc.
*
* From discussion, the right way to handle clearing is to wait for the expected
* removal events to occur for the existing downloads. So that's what we do.
* We still generate a test failure if there are any in-progress downloads.
*
* @param {Boolean} [getDownloads=false]
* If true, invoke getDownloads after clearing the download list and return
* its value.
*/
function clearAllDoneHelper(getDownloads) {
var clearedPromise = new Promise(function(resolve, reject) {
function gotDownloads(downloads) {
// If there are no downloads, we're already done.
if (downloads.length === 0) {
resolve();
return;
}
// Track the set of expected downloads that will be finalized.
var expectedIds = new Set();
function changeHandler(evt) {
var download = evt.download;
if (download.state === "finalized") {
expectedIds.delete(download.id);
if (expectedIds.size === 0) {
resolve();
}
}
}
downloads.forEach(function(download) {
if (download.state === "downloading") {
ok(false, "A download is still active: " + download.path);
reject("Active download");
}
download.onstatechange = changeHandler;
expectedIds.add(download.id);
});
navigator.mozDownloadManager.clearAllDone();
}
function gotBadNews(err) {
ok(false, "Problem clearing all downloads: " + err);
reject(err);
}
navigator.mozDownloadManager.getDownloads().then(gotDownloads, gotBadNews);
});
if (!getDownloads) {
return clearedPromise;
}
return clearedPromise.then(function() {
return navigator.mozDownloadManager.getDownloads();
});
}

Просмотреть файл

@ -0,0 +1,19 @@
function is(a, b, msg) {
alert((a === b ? 'OK' : 'KO') + ' ' + a + ' should equal ' + b + ': ' + msg);
}
function ok(a, msg) {
alert((a ? 'OK' : 'KO')+ ' ' + msg);
}
function info(msg) {
alert('INFO ' + msg);
}
function cbError() {
alert('KO error');
}
function finish() {
alert('DONE');
}

Просмотреть файл

@ -0,0 +1,55 @@
var gBasePath = "tests/dom/downloads/tests/";
var gTemplate = "file_app.template.webapp";
function handleRequest(request, response) {
var query = getQuery(request);
var testToken = query.testToken || '';
var appType = query.appType || 'web';
var template = gBasePath + gTemplate;
response.setHeader("Content-Type", "application/x-web-app-manifest+json", false);
var body = readTemplate(template)
.replace(/TESTTOKEN/g, testToken)
.replace(/APPTYPE/g, appType);
response.write();
}
// Copy-pasted incantations. There ought to be a better way to synchronously read
// a file into a string, but I guess we're trying to discourage that.
function readTemplate(path) {
var file = Components.classes["@mozilla.org/file/directory_service;1"].
getService(Components.interfaces.nsIProperties).
get("CurWorkD", Components.interfaces.nsILocalFile);
var fis = Components.classes['@mozilla.org/network/file-input-stream;1'].
createInstance(Components.interfaces.nsIFileInputStream);
var cis = Components.classes["@mozilla.org/intl/converter-input-stream;1"].
createInstance(Components.interfaces.nsIConverterInputStream);
var split = path.split("/");
for(var i = 0; i < split.length; ++i) {
file.append(split[i]);
}
fis.init(file, -1, -1, false);
cis.init(fis, "UTF-8", 0, 0);
var data = "";
let (str = {}) {
let read = 0;
do {
read = cis.readString(0xffffffff, str); // read as much as we can and put it in str.value
data += str.value;
} while (read != 0);
}
cis.close();
return data;
}
function getQuery(request) {
var query = {};
request.queryString.split('&').forEach(function (val) {
var [name, value] = val.split('=');
query[name] = unescape(value);
});
return query;
}

Просмотреть файл

@ -0,0 +1,7 @@
{
"name": "Really Rapid Release (hosted)",
"description": "Updated even faster than <a href='http://mozilla.org'>Firefox</a>, just to annoy slashdotters.",
"type": "APPTYPE",
"launch_path": "/tests/dom/downloads/tests/TESTTOKEN",
"icons": { "128": "default_icon" }
}

Просмотреть файл

@ -1,12 +1,24 @@
[DEFAULT]
skip-if = buildapp == 'mulet' || buildapp == 'b2g' # bug 979446, frequent failures
# The actual requirement for mozDownloadManager is MOZ_GONK because of
# the nsIVolumeService dependency. Until https://bugzil.la/1130264 is
# addressed, there is no way for mulet to run these tests.
run-if = toolkit == 'gonk'
support-files =
serve_file.sjs
clear_all_done_helper.js
file_app.template.webapp
file_app.sjs
common_app.js
shim_app_as_test.js
shim_app_as_test_chrome.js
testapp_downloads_adopt_download.html
testapp_downloads_adopt_download.js
testapp_downloads_adopt_download.manifest
[test_downloads_navigator_object.html]
[test_downloads_basic.html]
[test_downloads_large.html]
[test_downloads_adopt_download.html]
[test_downloads_bad_file.html]
[test_downloads_pause_remove.html]
[test_downloads_pause_resume.html]
skip-if = toolkit=='gonk' # b2g(bug 947167) b2g-debug(bug 947167)

Просмотреть файл

@ -0,0 +1,210 @@
/**
* Support logic to run a test file as an installed app. This file is derived
* from dom/requestsync/tests/test_basic_app.html but uses
* DOMApplicationRegistry in a chrome script (shim_app_as_test_chrome.js) to
* directly install the apps instead of mozApps.install because mozApps.install
* can't install privileged/certified apps. (This is the same mechanism used by
* the Firefox OS Gaia email app's backend test runner.)
*
* You really only want to do this if your test cares about the app's origin
* or you REALLY want to double-check AvailableIn and other WebIDL-provided
* security mechanisms.
*
* If you trust WebIDL, your life may be made significantly easier by just
* setting the pref "dom.ignore_webidl_scope_checks" to true, which makes
* BindingUtils.cpp's IsInPrivilegedApp and IsInCertifiedApp return true no
* matter what *on the main thread*. You are potentially out of luck on
* workers since at the time of writing this since the values stored on
* WorkerPrivateParent are based on the app status and ignore the pref.
*
* TO USE THIS:
*
* Make sure you have the usual header boilerplate:
* <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
* <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
*
* You also want to add this file!
* <script type="application/javascript" src="shim_app_as_test.js"></script>
*
* In your script body, issue a call like so:
* runAppTest({
* appFile: 'testapp_downloads_adopt_download.html',
* appManifest: 'testapp_downloads_adopt_download.manifest',
* appType: 'certified',
* extraPrefs: {
* set: [["dom.mozDownloads.enabled", true]]
* }
* });
*
* You shouldn't be adding other stuff to that file. Instead, you want
* everything in your testapp_*.html file. And you probably just want to copy
* and paste from an existing one of those...
*/
var gManifestURL;
var gApp;
var gOptions;
// Load the chrome script.
var gChromeHelper = SpecialPowers.loadChromeScript(
SimpleTest.getTestFileURL('shim_app_as_test_chrome.js'));
function installApp() {
info("installing app");
var useOrigin = document.location.origin;
gChromeHelper.sendAsyncMessage(
'install',
{
origin: useOrigin,
manifestURL: SimpleTest.getTestFileURL(gOptions.appManifest),
});
}
function installedApp(appInfo) {
gApp = appInfo;
ok(!!appInfo, 'installed app');
runTests();
}
gChromeHelper.addMessageListener('installed', installedApp);
function uninstallApp() {
info('uninstalling app');
gChromeHelper.sendAsyncMessage('uninstall', gApp);
}
function uninstalledApp(success) {
ok(success, 'uninstalled app');
runTests();
}
gChromeHelper.addMessageListener('uninstalled', uninstalledApp);
function testApp() {
var cleanupFrame;
var handleTestMessage = function(message) {
if (/^OK/.exec(message)) {
ok(true, "Message from app: " + message);
} else if (/^KO/.exec(message)) {
ok(false, "Message from app: " + message);
} else if (/^INFO/.exec(message)) {
info("Message from app: " + message.substring(5));
} else if (/^DONE$/.exec(message)) {
ok(true, "Messaging from app complete");
cleanupFrame();
runTests();
}
};
// Bug 1097479 means that embed-webapps does not work if you are already
// OOP, as we are for b2g. So we need to have the chrome script run our
// app in a sibling iframe to the one we're living in. When that bug is
// fixed or we are run in a non-b2g context, we can set this value to false
// or otherwise conditionalize based on behaviour.
var needSiblingIframeHack = true;
if (needSiblingIframeHack) {
gChromeHelper.sendAsyncMessage('run', gApp);
gChromeHelper.addMessageListener('appMessage', handleTestMessage);
gChromeHelper.addMessageListener('appError', function(data) {
ok(false, "Error in app frame: " + data.message);
});
cleanupFrame = function() {
gChromeHelper.sendAsyncMessage('close', {});
};
} else {
var ifr = document.createElement('iframe');
ifr.setAttribute('mozbrowser', 'true');
ifr.setAttribute('mozapp', gApp.manifestURL);
cleanupFrame = function() {
ifr.removeEventListener('mozbrowsershowmodalprompt', listener);
domParent.removeChild(ifr);
};
// Set us up to listen for messages from the app.
var listener = function(e) {
var message = e.detail.message; // e.detail.message;
handleTestMessage(message);
};
// This event is triggered when the app calls "alert".
ifr.addEventListener('mozbrowsershowmodalprompt', listener, false);
ifr.addEventListener('mozbrowsererror', function(evt) {
ok(false, "Error in app frame: " + evt.detail);
});
ifr.setAttribute('src', gApp.manifest.launch_path);
var domParent = document.getElementById('content');
if (!domParent) {
document.createElement('div');
document.body.insertBefore(domParent, document.body.firstChild);
}
domParent.appendChild(ifr);
}
}
var tests = [
// Permissions
function() {
info("pushing permissions");
SpecialPowers.pushPermissions(
[{ "type": "browser", "allow": 1, "context": document },
{ "type": "embed-apps", "allow": 1, "context": document },
{ "type": "webapps-manage", "allow": 1, "context": document }
],
runTests);
},
// Preferences
function() {
info("pushing preferences: " + gOptions.extraPrefs.set);
SpecialPowers.pushPrefEnv({
"set": gOptions.extraPrefs.set
}, runTests);
},
function() {
info("enabling use of mozbrowser");
//SpecialPowers.setAllAppsLaunchable(true);
SpecialPowers.setBoolPref("dom.mozBrowserFramesEnabled", true);
runTests();
},
// No confirmation needed when an app is installed
function() {
SpecialPowers.autoConfirmAppInstall(function() {
SpecialPowers.autoConfirmAppUninstall(runTests);
});
},
// Installing the app
installApp,
// Run tests in app
testApp,
// Uninstall the app
uninstallApp,
];
function runTests() {
if (!tests.length) {
ok(true, 'DONE!');
SimpleTest.finish();
return;
}
var test = tests.shift();
test();
}
SimpleTest.waitForExplicitFinish();
function runAppTest(options) {
gOptions = options;
var href = document.location.href;
gManifestURL = href.substring(0, href.lastIndexOf('/') + 1) +
options.appManifest;
runTests();
}

Просмотреть файл

@ -0,0 +1,178 @@
/**
* This is the chrome helper for shim_app_as_test.js. Its load is triggered by
* shim_app_as_test.js by a call to SpecialPowers.loadChromeScript and runs
* in the parent process in a sandbox created with the system principal. (Which
* seems like it can never get collected because it's reachable via the
* apparently singleton SpecialPowersObserverAPI instance and there's no logic
* to support reaping. Wuh-oh.)
*
* It exists to help install fake privileged/certified applications. It needs
* to exist because:
* - We need to poke at DOMApplicationRegistry directly.
* - By using SpecialPowers.loadChromeScript we are able to ensure this file
* is run in the parent process. This is important because
* DOMApplicationRegistry only lives in the parent process!
* - By running entirely in a chrome privileged compartment, we avoid crazy
* wrapper problems that we would otherwise face with our shenanigans of
* directly meddling with DOMApplicationRegistry. (And hopefully save
* anyone changing DOMApplicationRegistry from frustration/hating us if
* things were just barely working.)
* - Bug 1097479 means that embed-webapps doesn't work when the content process
* that is telling us to do things is itself OOP. So it falls upon us to
* handle the running of the app by creating a sibling mozbrowser/mozapp
* iframe to the one running the mochitests.
*
* Note that in this file we try to do *only* those things that can't otherwise
* be cleanly done using SpecialPowers.
*
* Want to better understand our execution context? Check out
* SpecialPowersObserverAPI.js and search on SPLoadChromeScript.
*/
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const CC = Components.Constructor;
Cu.import('resource://gre/modules/Webapps.jsm'); // for DOMApplicationRegistry
Cu.import('resource://gre/modules/AppsUtils.jsm'); // for AppUtils
Cu.import('resource://gre/modules/Services.jsm'); // for AppUtils
// Yes, you would think there was something like this already exposed easily
// in a JSM somewhere. No.
function fetchManifest(manifestURL) {
return new Promise(function(resolve, reject) {
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Ci.nsIXMLHttpRequest);
xhr.open("GET", manifestURL, true);
xhr.responseType = "json";
xhr.addEventListener("load", function() {
if (xhr.status == 200) {
resolve(xhr.response);
} else {
reject();
}
});
xhr.addEventListener("error", function() {
reject();
});
xhr.send(null);
});
}
/**
* Install an app using confirmInstall using pre-chewed data. This avoids the
* check in the normal installApp flow that gets all judgemental about the
* installation of privileged and certified apps.
*/
function installApp(req) {
fetchManifest(req.manifestURL).then(function(manifestObj) {
var data = {
// cloneAppObj normalizes the representation for us
app: AppsUtils.cloneAppObject({
installOrigin: req.origin,
origin: req.origin,
manifestURL: req.manifestURL,
appStatus: AppsUtils.getAppManifestStatus(manifestObj),
receipts: [],
categories: []
}),
from: req.origin, // unused?
oid: 0, // unused?
requestID: 0, // unused-ish
appId: 0, // unused
isBrowser: false,
isPackage: false, // used
// magic to auto-ack... don't think we care about this...
forceSuccessAck: false
// stuff that probably doesn't matter: 'mm', 'apkInstall',
};
// cloneAppObject does not propagate the manifest
data.app.manifest = manifestObj;
return DOMApplicationRegistry.confirmInstall(data).then(
function() {
var appId =
DOMApplicationRegistry.getAppLocalIdByManifestURL(req.manifestURL);
// act like this is a privileged app having all of its permissions
// authorized at first run.
DOMApplicationRegistry.updatePermissionsForApp(
appId,
/* preinstalled */ true,
/* system update? */ true);
sendAsyncMessage(
'installed',
{
appId: appId,
manifestURL: req.manifestURL,
manifest: manifestObj
});
},
function(err) {
sendAsyncMessage('installed', false);
});
});
}
function uninstallApp(appInfo) {
DOMApplicationRegistry.uninstall(appInfo.manifestURL).then(
function() {
sendAsyncMessage('uninstalled', true);
},
function() {
sendAsyncMessage('uninstalled', false);
});
}
var activeIframe = null;
/**
* Run our app in a sibling mozbrowser/mozapp iframe to the mochitest iframe.
* This is needed because we can't nest mozbrowser/mozapp iframes inside our
* already-OOP iframe until bug 1097479 is resolved.
*/
function runApp(appInfo) {
let shellDomWindow = Services.wm.getMostRecentWindow('navigator:browser');
let sysAppFrame = shellDomWindow.document.body.querySelector('#systemapp');
let sysAppDoc = sysAppFrame.contentDocument;
let siblingFrame = sysAppDoc.body.querySelector('#test-container');
let ifr = activeIframe = sysAppDoc.createElement('iframe');
ifr.setAttribute('mozbrowser', 'true');
ifr.setAttribute('remote', 'true');
ifr.setAttribute('mozapp', appInfo.manifestURL);
ifr.addEventListener('mozbrowsershowmodalprompt', function(evt) {
var message = evt.detail.message;
// only send the message as long as we haven't been told to clean up.
if (activeIframe) {
sendAsyncMessage('appMessage', message);
}
}, false);
ifr.addEventListener('mozbrowsererror', function(evt) {
if (activeIframe) {
sendAsyncMessage('appError', { message: '' + evt.detail });
}
});
ifr.setAttribute('src', appInfo.manifest.launch_path);
siblingFrame.parentElement.appendChild(ifr);
}
function closeApp() {
if (activeIframe) {
activeIframe.parentElement.removeChild(activeIframe);
activeIframe = null;
}
}
addMessageListener('install', installApp);
addMessageListener('uninstall', uninstallApp);
addMessageListener('run', runApp);
addMessageListener('close', closeApp);

Просмотреть файл

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=825318
-->
<head>
<title>Test for Bug 825318 mozDownloadManager.adoptDownload</title>
<script type="application/javascript;version=1.7" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript;version=1.7" src="shim_app_as_test.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=825318">Mozilla Bug 825318</a>
<p id="display"></p>
<div id="content">
</div>
<pre id="test">
<script class="testbody" type="text/javascript;version=1.7">
runAppTest({
appFile: 'testapp_downloads_adopt_download.html',
appManifest: 'testapp_downloads_adopt_download.manifest',
appType: 'certified',
extraPrefs: {
set: [["dom.mozDownloads.enabled", true]]
}
});
</script>
</pre>
</body>
</html>

Просмотреть файл

@ -72,6 +72,10 @@ function downloadChange(evt) {
is(download.currentBytes, 1024, "Download current size is 1024 bytes");
SimpleTest.finish();
} else if (download.state === "downloading") {
// Note that this case may or may not trigger, depending on whether the
// download is initially reported with 0 bytes (we should happen) or with
// 1024 bytes (we should not happen). If we do happen, an additional 8
// TEST-PASS events should be logged.
ok(download.currentBytes > lastKnownCurrentBytes,
"Download current size is larger than last download change event");
lastKnownCurrentBytes = download.currentBytes;
@ -84,7 +88,8 @@ function downloadStart(evt) {
var download = evt.download;
checkConsistentDownloadAttributes(download);
is(download.currentBytes, 0, "Download current size is zero");
// We used to check that the currentBytes was 0. This was incorrect. It
// is very common to first hear about the download already at 1024 bytes.
is(download.state, "downloading", "Download state is downloading");
download.onstatechange = downloadChange;

Просмотреть файл

@ -7,6 +7,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=938023
<title>Test for Bug 938023 Downloads API</title>
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="clear_all_done_helper.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
@ -46,7 +47,7 @@ function error() {
function getDownloads(downloads) {
ok(downloads.length == 1, "One downloads after getDownloads");
navigator.mozDownloadManager.clearAllDone().then(clearAllDone, error);
clearAllDoneHelper(true).then(clearAllDone, error);
}
function clearAllDone(downloads) {
@ -76,7 +77,7 @@ var steps = [
SpecialPowers.pushPermissions([
{type: "downloads", allow: true, context: document}
], function() {
navigator.mozDownloadManager.clearAllDone().then(next, error);
clearAllDoneHelper(true).then(next, error);
});
},

Просмотреть файл

@ -7,6 +7,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=938023
<title>Test for Bug 938023 Downloads API</title>
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="clear_all_done_helper.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
@ -83,7 +84,7 @@ var steps = [
SpecialPowers.pushPermissions([
{type: "downloads", allow: true, context: document}
], function() {
navigator.mozDownloadManager.clearAllDone().then(next, error);
clearAllDoneHelper(true).then(next, error);
});
},

Просмотреть файл

@ -7,6 +7,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=938023
<title>Test for Bug 938023 Downloads API</title>
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="clear_all_done_helper.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
@ -54,8 +55,7 @@ function checkDownloadList(downloads) {
function checkResumeSucceeded(download) {
ok(download.state == "succeeded", "Download resumed successfully.");
navigator.mozDownloadManager.clearAllDone()
.then(checkDownloadList, error);
clearAllDoneHelper(true).then(checkDownloadList, error);
}
function downloadChange(evt) {
@ -85,7 +85,7 @@ var steps = [
SpecialPowers.pushPermissions([
{type: "downloads", allow: true, context: document}
], function() {
navigator.mozDownloadManager.clearAllDone().then(next, error);
clearAllDoneHelper(true).then(next, error);
});
},

Просмотреть файл

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<script type="application/javascript" src="common_app.js"></script>
<meta charset="utf-8">
</head>
<body>
<div id="blah">initial text</div>
<pre id="test">
<!-- because of certified CSP, this code must NOT be inline -->
<script class="testbody" type="text/javascript;version=1.7" src="testapp_downloads_adopt_download.js"></script>
</pre>
</body>
</html>

Просмотреть файл

@ -0,0 +1,218 @@
/**
* Test the adoptDownload API. Specifically, we expect that when we call
* adoptDownload with a valid payload that:
* - The method will be resolved with a valid, fully populated DOMDownload
* instance, including an id.
* - An ondownloadstart notification will be generated and the DOMDownload
* instance it receives will be logically equivalent.
*
* We also explicitly verify that invalid adoptDownload payloads result in a
* rejection and that no download is added.
*
* This test explicitly does not test that the download is correctly persisted
* to the database. This is done because Downloads.jsm does not provide a means
* of safely restarting itself, so Firefox would need to be restarted. Because
* the adoptDownload code is using the Downloads API in a straightforward
* manner, it's not considered likely this would regress, and certainly not
* considered worth the automated testing overhead of a restart.
*/
function checkInvalidResult(dict, expectedErr, explanation) {
navigator.mozDownloadManager.ondownloadstart = function() {
ok(false, "No download should have been added!");
};
navigator.mozDownloadManager.adoptDownload(dict).then(
function() {
ok(false, "Invalid adoptDownload did not reject!");
runTests();
},
function(rejectedWith) {
is(rejectedWith, expectedErr, explanation + " rejection value");
runTests();
});
}
// Pick a date that Date.now() could not possibly return by picking a date in
// the past. (We want to make sure the date we provide works.)
var arbitraryDate = new Date(Date.now() - 60000);
var blobContents = new Uint8Array(256);
var memBlob = new Blob([blobContents], { type: 'application/octet-stream' });
var blobStorageName;
var blobStoragePath = 'blobby.blob';
function checkAdoptedDownload(download, validPayload) {
is(download.totalBytes, memBlob.size, 'size');
is(download.url, validPayload.url, 'url');
// The filesystem path is not practical to check since we can't hard-code it
// and the only way to check is to effectively duplicate the logic in
// DownloadsAPI.js. The good news, however, is that the value is
// round-tripped from storageName/storagePath to path and back again, and we
// also verify the file exists on disk, so we can be reasonably confident this
// is correct. We output it to aid in debugging if things should break,
// of course.
info('path (not checked): ' + download.path);
is(download.storageName, validPayload.storageName, 'storageName');
is(download.storagePath, validPayload.storagePath, 'storagePath');
is(download.state, 'succeeded', 'state');
is(download.contentType, validPayload.contentType, 'contentType');
is(download.startTime.valueOf(), arbitraryDate.valueOf(), 'startTime');
is(download.sourceAppManifestURL,
'http://mochi.test:8888/' +
'tests/dom/downloads/tests/testapp_downloads_adopt_download.manifest',
'app manifest');
};
var tests = [
function saveBlobToDeviceStorage() {
// Only sdcard can handle arbitrary MIME types and is guaranteed to be a
// thing.
var storage = navigator.getDeviceStorage('sdcard');
// We used the non-array helper, so the name we get may be different than
// what we asked for.
blobStorageName = storage.storageName;
ok(!!storage, 'have storage');
var req = storage.addNamed(memBlob, blobStoragePath);
req.onerror = function() {
ok(false, 'problem saving blob to storage: ' + req.error.name);
};
req.onsuccess = function(evt) {
ok(true, 'saved blob: ' + evt.target.result);
runTests();
};
},
function addValid() {
var validPayload = {
// All currently expected consumers are unable to provide a valid URL, and
// as a result need to provide an empty string.
url: "",
storageName: blobStorageName,
storagePath: blobStoragePath,
contentType: memBlob.type,
startTime: arbitraryDate
};
// Wrap the notification in a check so we can force our logic to be
// consistently ordered in the test even if it's not in reality.
var notifiedPromise = new Promise(function(resolve, reject) {
navigator.mozDownloadManager.ondownloadstart = function(evt) {
resolve(evt.download);
};
});
// Start the download
navigator.mozDownloadManager.adoptDownload(validPayload).then(
function(apiDownload) {
checkAdoptedDownload(apiDownload, validPayload);
ok(!!apiDownload.id, "Need a download id!");
notifiedPromise.then(function(notifiedDownload) {
checkAdoptedDownload(notifiedDownload, validPayload);
is(apiDownload.id, notifiedDownload.id,
"Notification should be for the download we adopted");
runTests();
});
},
function() {
ok(false, "adoptDownload should not have rejected");
runTests();
});
},
function dictionaryNotProvided() {
checkInvalidResult(undefined, "InvalidDownload");
},
// Missing fields immediately result in rejection with InvalidDownload
function missingStorageName() {
checkInvalidResult({
url: "",
// no storageName
storagePath: "relpath/filename.txt",
contentType: "text/plain",
startTime: arbitraryDate
}, "InvalidDownload", "missing storage name");
},
function nullStorageName() {
checkInvalidResult({
url: "",
storageName: null,
storagePath: "relpath/filename.txt",
contentType: "text/plain",
startTime: arbitraryDate
}, "InvalidDownload", "null storage name");
},
function missingStoragePath() {
checkInvalidResult({
url: "",
storageName: blobStorageName,
// no storagePath
contentType: "text/plain",
startTime: arbitraryDate
}, "InvalidDownload", "missing storage path");
},
function nullStoragePath() {
checkInvalidResult({
url: "",
storageName: blobStorageName,
storagePath: null,
contentType: "text/plain",
startTime: arbitraryDate
}, "InvalidDownload", "null storage path");
},
function missingContentType() {
checkInvalidResult({
url: "",
storageName: "sdcard",
storagePath: "relpath/filename.txt",
// no contentType
startTime: arbitraryDate
}, "InvalidDownload", "missing content type");
},
function nullContentType() {
checkInvalidResult({
url: "",
storageName: "sdcard",
storagePath: "relpath/filename.txt",
contentType: null,
startTime: arbitraryDate
}, "InvalidDownload", "null content type");
},
// Incorrect storage names are likewise immediately invalidated
function invalidStorageName() {
checkInvalidResult({
url: "",
storageName: "ALMOST CERTAINLY DOES NOT EXIST",
storagePath: "relpath/filename.txt",
contentType: "text/plain",
startTime: arbitraryDate
}, "InvalidDownload", "invalid storage name");
},
// The existence of the file is validated in the parent process
function legitStorageInvalidPath() {
checkInvalidResult({
url: "",
storageName: blobStorageName,
storagePath: "ALMOST CERTAINLY DOES NOT EXIST",
contentType: "text/plain",
startTime: arbitraryDate
}, "AdoptNoSuchFile", "invalid path");
},
function allDone() {
// Just in case, make sure no other mochitest could mess with us after we've
// finished.
navigator.mozDownloadManager.ondownloadstart = null;
runTests();
}
];
function runTests() {
if (!tests.length) {
finish();
return;
}
var test = tests.shift();
if (test.name) {
info('starting test: ' + test.name);
}
test();
}
runTests();

Просмотреть файл

@ -0,0 +1,10 @@
{
"name": "Downloads certified test fake app",
"description": "Test",
"launch_path": "http://mochi.test:8888/tests/dom/downloads/tests/testapp_downloads_adopt_download.html",
"type": "certified",
"permissions": {
"device-storage:sdcard":{ "access": "readcreate" },
"downloads": {}
}
}

Просмотреть файл

@ -36,10 +36,32 @@ interface DOMDownloadManager : EventTarget {
[UnsafeInPrerendering]
Promise<DOMDownload> remove(DOMDownload download);
// Removes all the completed downloads from the set. Returns an
// array of the completed downloads that were removed.
// Removes all completed downloads. This kicks off an asynchronous process
// that will eventually complete, but will not have completed by the time this
// method returns. If you care about the side-effects of this method, know
// that each existing download will have its onstatechange method invoked and
// will have a new state of "finalized". (After the download is finalized, no
// further events will be generated on it.)
[UnsafeInPrerendering]
Promise<sequence<DOMDownload>> clearAllDone();
void clearAllDone();
// Add completed downloads from applications that must perform the download
// process themselves. For example, email. The method is resolved with a
// fully populated DOMDownload instance on success, or rejected in the
// event all required options were not provided.
//
// The adopted download will also be reported via the ondownloadstart event
// handler.
//
// Applications must currently be certified to use this, but it could be
// widened at a later time.
//
// Note that "download" is not actually optional, but WebIDL requires that it
// be marked as such because it is not followed by a required argument. The
// promise will be rejected if the dictionary is omitted or the specified
// file does not exist on disk.
[AvailableIn=CertifiedApps]
Promise<DOMDownload> adoptDownload(optional AdoptDownloadDict download);
// Fires when a new download starts.
attribute EventHandler ondownloadstart;
@ -59,7 +81,9 @@ interface DOMDownload : EventTarget {
readonly attribute DOMString url;
// The full path in local storage where the file will end up once the download
// is complete.
// is complete. This is equivalent to the concatenation of the 'storagePath'
// to the 'mountPoint' of the nsIVolume associated with the 'storageName'
// (with delimiter).
readonly attribute DOMString path;
// The DeviceStorage volume name on which the file is being downloaded.
@ -69,7 +93,10 @@ interface DOMDownload : EventTarget {
// downloaded.
readonly attribute DOMString storagePath;
// The state of the download.
// The state of the download. One of: downloading, stopped, succeeded, or
// finalized. A finalized download is a download that has been removed /
// cleared and is no longer tracked by the download manager and will not
// receive any further onstatechange updates.
readonly attribute DownloadState state;
// The mime type for this resource.
@ -82,6 +109,10 @@ interface DOMDownload : EventTarget {
// download (eg. in different windows) will have the same id.
readonly attribute DOMString id;
// The manifestURL of the application that added this download. Only used for
// downloads added via the adoptDownload API call.
readonly attribute DOMString? sourceAppManifestURL;
// A DOM error object, that will be not null when a download is stopped
// because something failed.
readonly attribute DOMError? error;
@ -100,3 +131,41 @@ interface DOMDownload : EventTarget {
// - when the state and/or error attributes change.
attribute EventHandler onstatechange;
};
// Used to initialize the DOMDownload object for adopted downloads.
// fields directly maps to the DOMDownload fields.
dictionary AdoptDownloadDict {
// The URL of this resource if there is one available. An empty string if
// the download is not accessible via URL. An empty string is chosen over
// null so that existinc code does not need to null-check but the value is
// still falsey. (Note: If you do have a usable URL, you should probably not
// be using the adoptDownload API and instead be initiating downloads the
// normal way.)
DOMString url;
// The storageName of the DeviceStorage instance the file was saved to.
// Required but marked as optional so the bindings don't auto-coerce the value
// null to "null".
DOMString? storageName;
// The path of the file within the DeviceStorage instance named by
// 'storageName'. This is used to automatically compute the 'path' of the
// download. Note that when DeviceStorage gives you a path to a file, the
// first path segment is the name of the specific device storage and you do
// *not* want to include this. For example, if DeviceStorage tells you the
// file has a path of '/sdcard1/actual/path/file.ext', then the storageName
// should be 'sdcard1' and the storagePath should be 'actual/path/file.ext'.
//
// The existence of the file will be validated will be validated with stat()
// and the size the file-system tells us will be what we use.
//
// Required but marked as optional so the bindings don't auto-coerce the value
// null to "null".
DOMString? storagePath;
// The mime type for this resource. Required, but marked as optional because
// WebIDL otherwise auto-coerces the value null to "null".
DOMString? contentType;
// The time the download was started. If omitted, the current time is used.
Date? startTime;
};