Bug 1639069 - Provide helpers for getting a nsIMIMEInfo on DownloadsCommon, and confirming if a download is a given mime-type. r=jaws

Differential Revision: https://phabricator.services.mozilla.com/D79395
This commit is contained in:
Sam Foster 2020-06-26 02:34:03 +00:00
Родитель 4d9132ebfd
Коммит 1c447df70b
6 изменённых файлов: 493 добавлений и 0 удалений

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

@ -51,6 +51,7 @@ XPCOMUtils.defineLazyServiceGetters(this, {
"@mozilla.org/widget/clipboardhelper;1",
"nsIClipboardHelper",
],
gMIMEService: ["@mozilla.org/mime;1", "nsIMIMEService"],
});
XPCOMUtils.defineLazyGetter(this, "DownloadsLogger", () => {
@ -185,6 +186,12 @@ const kFileExtensions = [
"zip",
];
const kGenericContentTypes = [
"application/octet-stream",
"binary/octet-stream",
"application/unknown",
];
const TELEMETRY_EVENT_CATEGORY = "downloads";
var PrefObserver = {
@ -416,6 +423,67 @@ var DownloadsCommon = {
await download.finalize(true);
},
/**
* Get a nsIMIMEInfo object for a download
*/
getMimeInfo(download) {
if (!download.succeeded) {
return null;
}
let contentType = download.contentType;
let url = Cc["@mozilla.org/network/standard-url-mutator;1"]
.createInstance(Ci.nsIURIMutator)
.setSpec("http://example.com") // construct the URL
.setFilePath(download.target.path)
.finalize()
.QueryInterface(Ci.nsIURL);
let fileExtension = url.fileExtension;
// look at file extension if there's no contentType or it is generic
if (!contentType || kGenericContentTypes.includes(contentType)) {
try {
contentType = gMIMEService.getTypeFromExtension(fileExtension);
} catch (ex) {
DownloadsCommon.log(
"Cant get mimeType from file extension: ",
fileExtension
);
}
}
if (!(contentType || fileExtension)) {
return null;
}
let mimeInfo = null;
try {
mimeInfo = gMIMEService.getFromTypeAndExtension(
contentType || "",
fileExtension || ""
);
} catch (ex) {
DownloadsCommon.log(
"Can't get nsIMIMEInfo for contentType: ",
contentType,
"and fileExtension:",
fileExtension
);
}
return mimeInfo;
},
/**
* Confirm if the download exists on the filesystem and is a given mime-type
*/
isFileOfType(download, mimeType) {
if (!(download.succeeded && download.target?.exists)) {
DownloadsCommon.log(
`isFileOfType returning false for mimeType: ${mimeType}, succeeded: ${download.succeeded}, exists: ${download.target?.exists}`
);
return false;
}
let mimeInfo = DownloadsCommon.getMimeInfo(download);
return mimeInfo?.type === mimeType.toLowerCase();
},
/**
* Copies the source URI of the given Download object to the clipboard.
*/

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

@ -25,3 +25,5 @@ if toolkit == 'cocoa':
with Files('**'):
BUG_COMPONENT = ('Firefox', 'Downloads Panel')
XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']

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

@ -0,0 +1,87 @@
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.defineModuleGetter(
this,
"Downloads",
"resource://gre/modules/Downloads.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"DownloadsCommon",
"resource:///modules/DownloadsCommon.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"FileUtils",
"resource://gre/modules/FileUtils.jsm"
);
ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
ChromeUtils.defineModuleGetter(
this,
"FileTestUtils",
"resource://testing-common/FileTestUtils.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"NetUtil",
"resource://gre/modules/NetUtil.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"TestUtils",
"resource://testing-common/TestUtils.jsm"
);
async function createDownloadedFile(pathname, contents) {
info("createDownloadedFile: " + pathname);
let encoder = new TextEncoder();
let file = new FileUtils.File(pathname);
if (file.exists()) {
info(`File at ${pathname} already exists`);
if (!contents) {
ok(
false,
`A file already exists at ${pathname}, but createDownloadedFile was asked to create a non-existant file`
);
}
}
if (contents) {
await OS.File.writeAtomic(pathname, encoder.encode(contents));
ok(file.exists(), `Created ${pathname}`);
}
// No post-test cleanup necessary; tmp downloads directory is already removed after each test
return file;
}
let gDownloadDir;
async function setDownloadDir() {
let tmpDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
tmpDir.append("testsavedir");
if (!tmpDir.exists()) {
tmpDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
registerCleanupFunction(function() {
try {
tmpDir.remove(true);
} catch (e) {
// On Windows debug build this may fail.
}
});
}
Services.prefs.setIntPref("browser.download.folderList", 2);
Services.prefs.setCharPref("browser.download.dir", tmpDir.path);
return tmpDir.path;
}
/**
* All the tests are implemented with add_task, this starts them automatically.
*/
function run_test() {
do_get_profile();
run_next_test();
}
add_task(async function test_common_initialize() {
gDownloadDir = await setDownloadDir();
Services.prefs.setCharPref("browser.download.loglevel", "Debug");
});

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

@ -0,0 +1,175 @@
const DATA_PDF = atob(
"JVBERi0xLjANCjEgMCBvYmo8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFI+PmVuZG9iaiAyIDAgb2JqPDwvVHlwZS9QYWdlcy9LaWRzWzMgMCBSXS9Db3VudCAxPj5lbmRvYmogMyAwIG9iajw8L1R5cGUvUGFnZS9NZWRpYUJveFswIDAgMyAzXT4+ZW5kb2JqDQp4cmVmDQowIDQNCjAwMDAwMDAwMDAgNjU1MzUgZg0KMDAwMDAwMDAxMCAwMDAwMCBuDQowMDAwMDAwMDUzIDAwMDAwIG4NCjAwMDAwMDAxMDIgMDAwMDAgbg0KdHJhaWxlcjw8L1NpemUgNC9Sb290IDEgMCBSPj4NCnN0YXJ0eHJlZg0KMTQ5DQolRU9G"
);
const DOWNLOAD_TEMPLATE = {
source: {
url: "https://example.com/download",
},
target: {
path: "",
},
contentType: "text/plain",
succeeded: DownloadsCommon.DOWNLOAD_FINISHED,
canceled: false,
error: null,
hasPartialData: false,
hasBlockedData: false,
startTime: new Date(Date.now() - 1000),
};
const TESTFILES = {
"download-test.txt": "Text file contents\n",
"download-test.pdf": DATA_PDF,
"download-test.PDF": DATA_PDF,
"download-test.xxunknown": "Unknown file contents\n",
"download-test": "No extension file contents\n",
};
let gPublicList;
add_task(async function test_setup() {
Assert.ok(
OS.Constants.Path.profileDir,
"profileDir: " + OS.Constants.Path.profileDir
);
for (let [filename, contents] of Object.entries(TESTFILES)) {
TESTFILES[filename] = await createDownloadedFile(
OS.Path.join(gDownloadDir, filename),
contents
);
}
gPublicList = await Downloads.getList(Downloads.PUBLIC);
});
const TESTCASES = [
{
name: "Check returned value is null when the download did not succeed",
testFile: "download-test.txt",
contentType: "text/plain",
succeeded: false,
expected: null,
},
{
name:
"Check correct mime-info is returned when download contentType is unambiguous",
testFile: "download-test.txt",
contentType: "text/plain",
expected: {
type: "text/plain",
},
},
{
name:
"Returns correct mime-info from file extension when download contentType is missing",
testFile: "download-test.pdf",
contentType: undefined,
expected: {
type: "application/pdf",
},
},
{
name: "Returns correct mime-info from file extension case-insensitively",
testFile: "download-test.PDF",
contentType: undefined,
expected: {
type: "application/pdf",
},
},
{
name:
"Returns null when contentType is missing and file extension is unknown",
testFile: "download-test.xxunknown",
contentType: undefined,
expected: null,
},
{
name:
"Returns contentType when contentType is ambiguous and file extension is unknown",
testFile: "download-test.xxunknown",
contentType: "application/octet-stream",
expected: {
type: "application/octet-stream",
},
},
{
name:
"Returns contentType when contentType is ambiguous and there is no file extension",
testFile: "download-test",
contentType: "application/octet-stream",
expected: {
type: "application/octet-stream",
},
},
{
name: "Returns null when there's no contentType and no file extension",
testFile: "download-test",
contentType: undefined,
expected: null,
},
];
// add tests for each of the generic mime-types we recognize,
// to ensure they prefer the associated mime-type of the target file extension
for (let type of [
"application/octet-stream",
"binary/octet-stream",
"application/unknown",
]) {
TESTCASES.push({
name: `Returns correct mime-info from file extension when contentType is generic (${type})`,
testFile: "download-test.pdf",
contentType: type,
expected: {
type: "application/pdf",
},
});
}
for (let testData of TESTCASES) {
let tmp = {
async [testData.name]() {
info("testing with: " + JSON.stringify(testData));
await test_getMimeInfo_basic_function(testData);
},
};
add_task(tmp[testData.name]);
}
/**
* Sanity test the DownloadsCommon.getMimeInfo method with test parameters
*/
async function test_getMimeInfo_basic_function(testData) {
let downloadData = {
...DOWNLOAD_TEMPLATE,
source: "source" in testData ? testData.source : DOWNLOAD_TEMPLATE.source,
succeeded:
"succeeded" in testData
? testData.succeeded
: DOWNLOAD_TEMPLATE.succeeded,
target: TESTFILES[testData.testFile],
contentType: testData.contentType,
};
Assert.ok(downloadData.target instanceof Ci.nsIFile, "target is a nsIFile");
let download = await Downloads.createDownload(downloadData);
await gPublicList.add(download);
await download.refresh();
Assert.ok(
await OS.File.exists(download.target.path),
"The file should actually exist."
);
let result = await DownloadsCommon.getMimeInfo(download);
if (testData.expected) {
Assert.equal(
result.type,
testData.expected.type,
"Got expected mimeInfo.type"
);
} else {
Assert.equal(
result,
null,
`Expected null, got object with type: ${result?.type}`
);
}
}

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

@ -0,0 +1,153 @@
const DATA_PDF = atob(
"JVBERi0xLjANCjEgMCBvYmo8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFI+PmVuZG9iaiAyIDAgb2JqPDwvVHlwZS9QYWdlcy9LaWRzWzMgMCBSXS9Db3VudCAxPj5lbmRvYmogMyAwIG9iajw8L1R5cGUvUGFnZS9NZWRpYUJveFswIDAgMyAzXT4+ZW5kb2JqDQp4cmVmDQowIDQNCjAwMDAwMDAwMDAgNjU1MzUgZg0KMDAwMDAwMDAxMCAwMDAwMCBuDQowMDAwMDAwMDUzIDAwMDAwIG4NCjAwMDAwMDAxMDIgMDAwMDAgbg0KdHJhaWxlcjw8L1NpemUgNC9Sb290IDEgMCBSPj4NCnN0YXJ0eHJlZg0KMTQ5DQolRU9G"
);
const DOWNLOAD_TEMPLATE = {
source: {
url: "https://download-test.com/download",
},
target: {
path: "",
},
contentType: "text/plain",
succeeded: DownloadsCommon.DOWNLOAD_FINISHED,
canceled: false,
error: null,
hasPartialData: false,
hasBlockedData: false,
startTime: new Date(Date.now() - 1000),
};
const TESTFILES = {
"download-test.pdf": DATA_PDF,
"download-test.xxunknown": DATA_PDF,
"download-test-missing.pdf": null,
};
let gPublicList;
add_task(async function test_setup() {
Assert.ok(
OS.Constants.Path.profileDir,
"profileDir: " + OS.Constants.Path.profileDir
);
for (let [filename, contents] of Object.entries(TESTFILES)) {
TESTFILES[filename] = await createDownloadedFile(
OS.Path.join(gDownloadDir, filename),
contents
);
}
gPublicList = await Downloads.getList(Downloads.PUBLIC);
});
const TESTCASES = [
{
name: "Null download arg",
typeArg: "application/pdf",
downloadProps: null,
expected: /TypeError/,
},
{
name: "Missing type arg",
typeArg: undefined,
downloadProps: {
target: "download-test.pdf",
},
expected: /TypeError/,
},
{
name: "Empty string type arg",
typeArg: "",
downloadProps: {
target: "download-test.pdf",
},
expected: false,
},
{
name:
"download succeeded, file exists, unknown extension but contentType matches",
typeArg: "application/pdf",
downloadProps: {
target: "download-test.xxunknown",
contentType: "application/pdf",
},
expected: true,
},
{
name:
"download succeeded, file exists, contentType is generic and file extension maps to matching mime-type",
typeArg: "application/pdf",
downloadProps: {
target: "download-test.pdf",
contentType: "application/unknown",
},
expected: true,
},
{
name: "download did not succeed",
typeArg: "application/pdf",
downloadProps: {
target: "download-test.pdf",
contentType: "application/pdf",
succeeded: false,
},
expected: false,
},
{
name: "file does not exist",
typeArg: "application/pdf",
downloadProps: {
target: "download-test-missing.pdf",
contentType: "application/pdf",
},
expected: false,
},
{
name:
"contentType is missing and file extension doesnt map to a known mime-type",
typeArg: "application/pdf",
downloadProps: {
contentType: undefined,
target: "download-test.xxunknown",
},
expected: false,
},
];
for (let testData of TESTCASES) {
let tmp = {
async [testData.name]() {
info("testing with: " + JSON.stringify(testData));
await test_isFileOfType(testData);
},
};
add_task(tmp[testData.name]);
}
/**
* Sanity test the DownloadsCommon.isFileOfType method with test parameters
*/
async function test_isFileOfType({ name, typeArg, downloadProps, expected }) {
let download, result;
if (downloadProps) {
let downloadData = {
...DOWNLOAD_TEMPLATE,
...downloadProps,
};
downloadData.target = TESTFILES[downloadData.target];
Assert.ok(downloadData.target instanceof Ci.nsIFile, "target is a nsIFile");
download = await Downloads.createDownload(downloadData);
await gPublicList.add(download);
await download.refresh();
}
if (typeof expected == "boolean") {
result = await DownloadsCommon.isFileOfType(download, typeArg);
Assert.equal(result, expected, "Expected result from call to isFileOfType");
} else {
Assert.throws(
() => DownloadsCommon.isFileOfType(download, typeArg),
expected,
"isFileOfType should throw an exception if either the download object or mime-type arguments are falsey"
);
}
}

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

@ -0,0 +1,8 @@
[DEFAULT]
head = head.js
firefox-appdir = browser
skip-if = toolkit == 'android'
[test_DownloadsCommon_getMimeInfo.js]
[test_DownloadsCommon_isFileOfType.js]