Bug 1669566 - Add cookieStoreId functionality to browser.download.download, browser.download.search, and browser.download.erase; add unit tests. r=robwu,geckoview-reviewers,agi

Differential Revision: https://phabricator.services.mozilla.com/D119967
This commit is contained in:
Karim Rahal 2021-07-23 15:13:37 +00:00
parent ba8f96b91d
commit 4d5eecd33f
6 changed files with 523 additions and 0 deletions

View File

@ -193,6 +193,11 @@ this.downloads = class extends ExtensionAPI {
});
}
if (options.cookieStoreId != null) {
// https://bugzilla.mozilla.org/show_bug.cgi?id=1721460
throw new ExtensionError("Not implemented");
}
if (options.headers) {
for (const { name } of options.headers) {
if (

View File

@ -2361,6 +2361,15 @@ DownloadCopySaver.prototype = {
download.source.cookieJarSettings;
}
if (download.source.userContextId) {
// Getters and setters only exist on originAttributes,
// so it has to be cloned, changed, and re-set
channel.loadInfo.originAttributes = {
...channel.loadInfo.originAttributes,
userContextId: download.source.userContextId,
};
}
// This makes the channel be corretly throttled during page loads
// and also prevents its caching.
if (channel instanceof Ci.nsIHttpChannelInternal) {

View File

@ -34,6 +34,7 @@ const DOWNLOAD_ITEM_FIELDS = [
"referrer",
"filename",
"incognito",
"cookieStoreId",
"danger",
"mime",
"startTime",
@ -187,6 +188,15 @@ class DownloadItem {
get incognito() {
return this.download.source.isPrivate;
}
get cookieStoreId() {
if (this.download.source.isPrivate) {
return PRIVATE_STORE;
}
if (this.download.source.userContextId) {
return getCookieStoreIdForContainer(this.download.source.userContextId);
}
return DEFAULT_STORE;
}
get danger() {
return "safe";
} // TODO
@ -526,6 +536,7 @@ const downloadQuery = query => {
"paused",
"error",
"incognito",
"cookieStoreId",
"bytesReceived",
"totalBytes",
"fileSize",
@ -688,6 +699,15 @@ this.downloads = class extends ExtensionAPI {
}
}
let userContextId = null;
if (options.cookieStoreId != null) {
userContextId = getUserContextIdForCookieStoreId(
extension,
options.cookieStoreId,
options.incognito
);
}
// Handle method, headers and body options.
function adjustChannel(channel) {
if (channel instanceof Ci.nsIHttpChannel) {
@ -931,6 +951,10 @@ this.downloads = class extends ExtensionAPI {
cookieJarSettings,
};
if (userContextId) {
source.userContextId = userContextId;
}
// blob:-URLs can only be loaded by the principal with which they
// are associated. This principal may have origin attributes.
// `context.principal` does sometimes not have these attributes

View File

@ -106,6 +106,11 @@
"description": "False if this download is recorded in the history, true if it is not recorded.",
"type": "boolean"
},
"cookieStoreId": {
"type": "string",
"optional": true,
"description": "The cookie store ID of the contextual identity."
},
"danger": {
"$ref": "DangerType",
"description": "Indication of whether this download is thought to be safe or known to be suspicious."
@ -300,6 +305,11 @@
"optional": true,
"type": "string"
},
"cookieStoreId": {
"type": "string",
"optional": true,
"description": "The cookie store ID of the contextual identity."
},
"danger": {
"$ref": "DangerType",
"description": "Indication of whether this download is thought to be safe or known to be suspicious.",
@ -383,6 +393,11 @@
"default": false,
"type": "boolean"
},
"cookieStoreId": {
"type": "string",
"optional": true,
"description": "The cookie store ID of the contextual identity; requires \"cookies\" permission."
},
"conflictAction": {
"$ref": "FilenameConflictAction",
"optional": true

View File

@ -0,0 +1,468 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
function cookiesToMime(cookies) {
return `dummy/${encodeURIComponent(cookies)}`.toLowerCase();
}
function mimeToCookies(mime) {
return decodeURIComponent(mime.replace("dummy/", ""));
}
const server = createHttpServer({ hosts: ["example.net"] });
server.registerPathHandler("/download", (request, response) => {
response.setStatusLine(request.httpVersion, 200, "OK");
let cookies = request.hasHeader("Cookie") ? request.getHeader("Cookie") : "";
// Assign the result through the MIME-type, to make it easier to read the
// result via the downloads API.
response.setHeader("Content-Type", cookiesToMime(cookies));
// Response of length 7.
response.write("1234567");
});
const DOWNLOAD_URL = "http://example.net/download";
async function setUpCookies() {
Services.cookies.removeAll();
let extension = ExtensionTestUtils.loadExtension({
incognitoOverride: "spanning",
manifest: {
permissions: ["cookies", "http://example.net/download"],
},
async background() {
let url = "http://example.net/download";
// Add default cookie
await browser.cookies.set({
url,
name: "cookie_normal",
value: "1",
});
// Add private cookie
await browser.cookies.set({
url,
storeId: "firefox-private",
name: "cookie_private",
value: "1",
});
// Add container cookie
await browser.cookies.set({
url,
storeId: "firefox-container-1",
name: "cookie_container",
value: "1",
});
browser.test.sendMessage("cookies set");
},
});
await extension.startup();
await extension.awaitMessage("cookies set");
await extension.unload();
}
function createDownloadTestExtension(extraPermissions = [], incognito = false) {
let extensionOptions = {
manifest: {
permissions: ["downloads", ...extraPermissions],
},
background() {
browser.test.onMessage.addListener(async (method, data) => {
async function getDownload(data) {
let donePromise = new Promise(resolve => {
browser.downloads.onChanged.addListener(async delta => {
if (delta.state?.current === "complete") {
resolve(delta.id);
}
});
});
let downloadId = await browser.downloads.download(data);
browser.test.assertEq(await donePromise, downloadId, "got download");
let [download] = await browser.downloads.search({ id: downloadId });
browser.test.log(`Download results: ${JSON.stringify(download)}`);
// Delete the file since we aren't interested in it.
// TODO bug 1654819: On Windows the file may be recreated.
await browser.downloads.removeFile(download.id);
// Sanity check to verify that we got the result from /download.
browser.test.assertEq(7, download.fileSize, "download succeeded");
return download;
}
function checkDownloadError(data) {
return browser.test.assertRejects(
browser.downloads.download(data.downloadData),
data.exceptionRe
);
}
function search(data) {
return browser.downloads.search(data);
}
function erase(data) {
return browser.downloads.erase(data);
}
switch (method) {
case "getDownload":
return browser.test.sendMessage(method, await getDownload(data));
case "checkDownloadError":
return browser.test.sendMessage(
method,
await checkDownloadError(data)
);
case "search":
return browser.test.sendMessage(method, await search(data));
case "erase":
return browser.test.sendMessage(method, await erase(data));
}
});
},
};
if (incognito) {
extensionOptions.incognitoOverride = "spanning";
}
return ExtensionTestUtils.loadExtension(extensionOptions);
}
function getResult(extension, method, data) {
extension.sendMessage(method, data);
return extension.awaitMessage(method);
}
async function getCookies(extension, data) {
let download = await getResult(extension, "getDownload", data);
// The "/download" endpoint mirrors received cookies via Content-Type.
let cookies = mimeToCookies(download.mime);
return cookies;
}
async function runTests(extension, containerDownloadAllowed, privateAllowed) {
let forcedIncognitoException = null;
if (!privateAllowed) {
forcedIncognitoException = /private browsing access not allowed/;
} else if (!containerDownloadAllowed) {
forcedIncognitoException = /No permission for cookieStoreId/;
}
// Test default container download
if (containerDownloadAllowed) {
equal(
await getCookies(extension, {
url: DOWNLOAD_URL,
cookieStoreId: "firefox-default",
}),
"cookie_normal=1",
"Default container cookies for downloads.download"
);
} else {
await getResult(extension, "checkDownloadError", {
exceptionRe: /No permission for cookieStoreId/,
downloadData: {
url: DOWNLOAD_URL,
cookieStoreId: "firefox-default",
},
});
}
// Test private container download
if (privateAllowed && containerDownloadAllowed) {
equal(
await getCookies(extension, {
url: DOWNLOAD_URL,
cookieStoreId: "firefox-private",
incognito: true,
}),
"cookie_private=1",
"Private container cookies for downloads.download"
);
} else {
await getResult(extension, "checkDownloadError", {
exceptionRe: forcedIncognitoException,
downloadData: {
url: DOWNLOAD_URL,
cookieStoreId: "firefox-private",
incognito: true,
},
});
}
// Test firefox-container-1 download
if (containerDownloadAllowed) {
equal(
await getCookies(extension, {
url: DOWNLOAD_URL,
cookieStoreId: "firefox-container-1",
}),
"cookie_container=1",
"firefox-container-1 cookies for downloads.download"
);
} else {
await getResult(extension, "checkDownloadError", {
exceptionRe: /No permission for cookieStoreId/,
downloadData: {
url: DOWNLOAD_URL,
cookieStoreId: "firefox-container-1",
},
});
}
// Test mismatched incognito and cookieStoreId download
await getResult(extension, "checkDownloadError", {
exceptionRe: forcedIncognitoException
? forcedIncognitoException
: /Illegal to set non-private cookieStoreId in a private window/,
downloadData: {
url: DOWNLOAD_URL,
incognito: true,
cookieStoreId: "firefox-container-1",
},
});
await getResult(extension, "checkDownloadError", {
exceptionRe: containerDownloadAllowed
? /Illegal to set private cookieStoreId in a non-private window/
: /No permission for cookieStoreId/,
downloadData: {
url: DOWNLOAD_URL,
incognito: false,
cookieStoreId: "firefox-private",
},
});
// Test invalid cookieStoreId download
await getResult(extension, "checkDownloadError", {
exceptionRe: containerDownloadAllowed
? /Illegal cookieStoreId/
: /No permission for cookieStoreId/,
downloadData: {
url: DOWNLOAD_URL,
cookieStoreId: "invalid-invalid-invalid",
},
});
let searchRes, searchResDownload;
// Test default container search
searchRes = await getResult(extension, "search", {
cookieStoreId: "firefox-default",
});
equal(
searchRes.length,
1,
"Default container results length for downloads.search"
);
[searchResDownload] = searchRes;
equal(
mimeToCookies(searchResDownload.mime),
"cookie_normal=1",
"Default container cookies for downloads.search"
);
// Test default container search with mismatched container
searchRes = await getResult(extension, "search", {
mime: cookiesToMime("cookie_normal=1"),
cookieStoreId: "firefox-container-1",
});
equal(
searchRes.length,
0,
"Default container results length for downloads.search when container mismatched"
);
// Test private container search
searchRes = await getResult(extension, "search", {
cookieStoreId: "firefox-private",
});
if (privateAllowed) {
equal(
searchRes.length,
1,
"Private container results length for downloads.search"
);
[searchResDownload] = searchRes;
equal(
mimeToCookies(searchResDownload.mime),
"cookie_private=1",
"Private container cookies for downloads.search"
);
// Test private container search with mismatched container
searchRes = await getResult(extension, "search", {
mime: cookiesToMime("cookie_private=1"),
cookieStoreId: "firefox-container-1",
});
equal(
searchRes.length,
0,
"Private container results length for downloads.search when container mismatched"
);
} else {
equal(
searchRes.length,
0,
"Private container results length for downloads.search when private disallowed"
);
}
// Test firefox-container-1 search
searchRes = await getResult(extension, "search", {
cookieStoreId: "firefox-container-1",
});
equal(
searchRes.length,
1,
"firefox-container-1 results length for downloads.search"
);
[searchResDownload] = searchRes;
equal(
mimeToCookies(searchResDownload.mime),
"cookie_container=1",
"firefox-container-1 cookies for downloads.search"
);
// Test firefox-container-1 search with mismatched container
searchRes = await getResult(extension, "search", {
mime: cookiesToMime("cookie_container=1"),
cookieStoreId: "firefox-default",
});
equal(
searchRes.length,
0,
"firefox-container-1 container results length for downloads.search when container mismatched"
);
// Test default container erase with mismatched container
await getResult(extension, "erase", {
mime: cookiesToMime("cookie_normal=1"),
cookieStoreId: "firefox-container-1",
});
searchRes = await getResult(extension, "search", {
mime: cookiesToMime("cookie_normal=1"),
});
equal(
searchRes.length,
1,
"Default container results length for downloads.search after erase with mismatched container"
);
// Test private container erase with mismatched container
await getResult(extension, "erase", {
mime: cookiesToMime("cookie_private=1"),
cookieStoreId: "firefox-container-1",
});
searchRes = await getResult(extension, "search", {
mime: cookiesToMime("cookie_private=1"),
});
equal(
searchRes.length,
privateAllowed ? 1 : 0,
"Private container results length for downloads.search after erase with mismatched container"
);
// Test firefox-container-1 erase with mismatched container
await getResult(extension, "erase", {
mime: cookiesToMime("cookie_container=1"),
cookieStoreId: "firefox-default",
});
searchRes = await getResult(extension, "search", {
mime: cookiesToMime("cookie_container=1"),
});
equal(
searchRes.length,
1,
"firefox-container-1 results length for downloads.search after erase with mismatched container"
);
// Test default container erase
await getResult(extension, "erase", {
cookieStoreId: "firefox-default",
});
searchRes = await getResult(extension, "search", {
mime: cookiesToMime("cookie_normal=1"),
});
equal(
searchRes.length,
0,
"Default container results length for downloads.search after erase"
);
// Test private container erase
await getResult(extension, "erase", {
cookieStoreId: "firefox-private",
});
searchRes = await getResult(extension, "search", {
mime: cookiesToMime("cookie_private=1"),
});
// The following will also pass when incognito disabled
equal(
searchRes.length,
0,
"Private container results length for downloads.search after erase"
);
// Test firefox-container-1 erase
await getResult(extension, "erase", {
cookieStoreId: "firefox-container-1",
});
searchRes = await getResult(extension, "search", {
mime: cookiesToMime("cookie_container=1"),
});
equal(
searchRes.length,
0,
"firefox-container-1 results length for downloads.search after erase"
);
}
async function populateDownloads(extension) {
await getResult(extension, "erase", {});
await getResult(extension, "getDownload", {
url: DOWNLOAD_URL,
});
await getResult(extension, "getDownload", {
url: DOWNLOAD_URL,
incognito: true,
});
await getResult(extension, "getDownload", {
url: DOWNLOAD_URL,
cookieStoreId: "firefox-container-1",
});
}
add_task(async function setup() {
const nsIFile = Ci.nsIFile;
const downloadDir = FileUtils.getDir("TmpD", ["downloads"]);
downloadDir.createUnique(nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
Services.prefs.setIntPref("browser.download.folderList", 2);
Services.prefs.setComplexValue("browser.download.dir", nsIFile, downloadDir);
await setUpCookies();
registerCleanupFunction(() => {
Services.cookies.removeAll();
Services.prefs.clearUserPref("browser.download.folderList");
Services.prefs.clearUserPref("browser.download.dir");
downloadDir.remove(false);
});
});
add_task(async function download_cookieStoreId() {
// Test extension with cookies permission and incognito enabled
let extension = createDownloadTestExtension(["cookies"], true);
await extension.startup();
await runTests(extension, true, true);
// Test extension with incognito enabled and no cookies permission
await populateDownloads(extension);
let noCookiesExtension = createDownloadTestExtension([], true);
await noCookiesExtension.startup();
await runTests(noCookiesExtension, false, true);
await noCookiesExtension.unload();
// Test extension with incognito disabled and no cookies permission
await populateDownloads(extension);
let noCookiesAndPrivateExtension = createDownloadTestExtension([], false);
await noCookiesAndPrivateExtension.startup();
await runTests(noCookiesAndPrivateExtension, false, false);
await noCookiesAndPrivateExtension.unload();
// Verify that incognito disabled test did not delete private download
let searchRes = await getResult(extension, "search", {
mime: cookiesToMime("cookie_private=1"),
});
ok(searchRes.length, "Incognito disabled does not delete private download");
await extension.unload();
});

View File

@ -79,6 +79,8 @@ skip-if = socketprocess_networking || os == "android" # Android: Bug 1680132
[test_ext_downloads.js]
[test_ext_downloads_cookies.js]
skip-if = os == "android" # downloads API needs to be implemented in GeckoView - bug 1538348
[test_ext_downloads_cookieStoreId.js]
skip-if = os == "android"
[test_ext_downloads_download.js]
skip-if = appname == "thunderbird" || os == "android" || tsan # tsan: bug 1612707
[test_ext_downloads_misc.js]