Bug 1516709 support incognito in downloads api r=aswan
authorShane Caraveo <scaraveo@mozilla.com>
Wed, 13 Feb 2019 15:20:04 +0000
changeset 458908 b3e21f09ee45
parent 458907 266c1eee61a8
child 458909 dbf72abf5597
push id35551
push usershindli@mozilla.com
push dateWed, 13 Feb 2019 21:34:09 +0000
treeherdermozilla-central@08f794a4928e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaswan
bugs1516709
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1516709 support incognito in downloads api r=aswan Differential Revision: https://phabricator.services.mozilla.com/D17426
toolkit/components/extensions/parent/ext-downloads.js
toolkit/components/extensions/test/xpcshell/test_ext_downloads_private.js
--- a/toolkit/components/extensions/parent/ext-downloads.js
+++ b/toolkit/components/extensions/parent/ext-downloads.js
@@ -205,19 +205,19 @@ const DownloadMap = new class extends Ev
   getDownloadList() {
     return this.lazyInit();
   }
 
   getAll() {
     return this.lazyInit().then(() => this.byId.values());
   }
 
-  fromId(id) {
+  fromId(id, privateAllowed = true) {
     const download = this.byId.get(id);
-    if (!download) {
+    if (!download || (!privateAllowed && download.incognito)) {
       throw new Error(`Invalid download id ${id}`);
     }
     return download;
   }
 
   newFromDownload(download, extension) {
     if (this.byDownload.has(download)) {
       return this.byDownload.get(download);
@@ -327,17 +327,17 @@ const downloadQuery = query => {
         return false;
       }
     } else if (item.totalBytes <= totalBytesGreater || item.totalBytes >= totalBytesLess) {
       return false;
     }
 
     // todo: include danger
     const SIMPLE_ITEMS = ["id", "mime", "startTime", "endTime", "state",
-                          "paused", "error",
+                          "paused", "error", "incognito",
                           "bytesReceived", "totalBytes", "fileSize", "exists"];
     for (let field of SIMPLE_ITEMS) {
       if (query[field] != null && item[field] != query[field]) {
         return false;
       }
     }
 
     return true;
@@ -392,16 +392,36 @@ const queryHelper = query => {
       if (matchFn(download)) {
         results.push(download);
       }
     }
     return results;
   });
 };
 
+function downloadEventManagerAPI(context, name, event, listener) {
+  let register = fire => {
+    const handler = (what, item) => {
+      if (context.privateBrowsingAllowed || !item.incognito) {
+        listener(fire, what, item);
+      }
+    };
+    let registerPromise = DownloadMap.getDownloadList().then(() => {
+      DownloadMap.on(event, handler);
+    });
+    return () => {
+      registerPromise.then(() => {
+        DownloadMap.off(event, handler);
+      });
+    };
+  };
+
+  return new EventManager({context, name, register}).api();
+}
+
 this.downloads = class extends ExtensionAPI {
   getAPI(context) {
     let {extension} = context;
     return {
       downloads: {
         download(options) {
           let {filename} = options;
           if (filename && AppConstants.platform === "win") {
@@ -423,16 +443,20 @@ this.downloads = class extends Extension
               return Promise.reject({message: "filename must not contain back-references (..)"});
             }
 
             if (path.components.some(component => component != DownloadPaths.sanitize(component))) {
               return Promise.reject({message: "filename must not contain illegal characters"});
             }
           }
 
+          if (options.incognito && !context.privateBrowsingAllowed) {
+            return Promise.reject({message: "private browsing access not allowed"});
+          }
+
           if (options.conflictAction == "prompt") {
             // TODO
             return Promise.reject({message: "conflictAction prompt not yet implemented"});
           }
 
           if (options.headers) {
             for (let {name} of options.headers) {
               if (FORBIDDEN_HEADERS.includes(name.toUpperCase()) || name.match(FORBIDDEN_PREFIXES)) {
@@ -575,71 +599,74 @@ this.downloads = class extends Extension
               return item.id;
             });
         },
 
         removeFile(id) {
           return DownloadMap.lazyInit().then(() => {
             let item;
             try {
-              item = DownloadMap.fromId(id);
+              item = DownloadMap.fromId(id, context.privateBrowsingAllowed);
             } catch (err) {
               return Promise.reject({message: `Invalid download id ${id}`});
             }
             if (item.state !== "complete") {
               return Promise.reject({message: `Cannot remove incomplete download id ${id}`});
             }
             return OS.File.remove(item.filename, {ignoreAbsent: false}).catch((err) => {
               return Promise.reject({message: `Could not remove download id ${item.id} because the file doesn't exist`});
             });
           });
         },
 
         search(query) {
+          if (!context.privateBrowsingAllowed) {
+            query.incognito = false;
+          }
           return queryHelper(query)
             .then(items => items.map(item => item.serialize()));
         },
 
         pause(id) {
           return DownloadMap.lazyInit().then(() => {
             let item;
             try {
-              item = DownloadMap.fromId(id);
+              item = DownloadMap.fromId(id, context.privateBrowsingAllowed);
             } catch (err) {
               return Promise.reject({message: `Invalid download id ${id}`});
             }
             if (item.state != "in_progress") {
               return Promise.reject({message: `Download ${id} cannot be paused since it is in state ${item.state}`});
             }
 
             return item.download.cancel();
           });
         },
 
         resume(id) {
           return DownloadMap.lazyInit().then(() => {
             let item;
             try {
-              item = DownloadMap.fromId(id);
+              item = DownloadMap.fromId(id, context.privateBrowsingAllowed);
             } catch (err) {
               return Promise.reject({message: `Invalid download id ${id}`});
             }
             if (!item.canResume) {
               return Promise.reject({message: `Download ${id} cannot be resumed`});
             }
 
             return item.download.start();
           });
         },
 
         cancel(id) {
           return DownloadMap.lazyInit().then(() => {
             let item;
             try {
-              item = DownloadMap.fromId(id);
+              item = DownloadMap.fromId(id, context.privateBrowsingAllowed);
             } catch (err) {
               return Promise.reject({message: `Invalid download id ${id}`});
             }
             if (item.download.succeeded) {
               return Promise.reject({message: `Download ${id} is already complete`});
             }
             return item.download.finalize(true);
           });
@@ -652,54 +679,57 @@ this.downloads = class extends Extension
               dirobj.launch();
             } else {
               throw new Error(`Download directory ${dirobj.path} is not actually a directory`);
             }
           }).catch(Cu.reportError);
         },
 
         erase(query) {
+          if (!context.privateBrowsingAllowed) {
+            query.incognito = false;
+          }
           return queryHelper(query).then(items => {
             let results = [];
             let promises = [];
             for (let item of items) {
               promises.push(DownloadMap.erase(item));
               results.push(item.id);
             }
             return Promise.all(promises).then(() => results);
           });
         },
 
         open(downloadId) {
           return DownloadMap.lazyInit().then(() => {
-            let download = DownloadMap.fromId(downloadId).download;
+            let download = DownloadMap.fromId(downloadId, context.privateBrowsingAllowed).download;
             if (download.succeeded) {
               return download.launch();
             }
             return Promise.reject({message: "Download has not completed."});
           }).catch((error) => {
             return Promise.reject({message: error.message});
           });
         },
 
         show(downloadId) {
           return DownloadMap.lazyInit().then(() => {
-            let download = DownloadMap.fromId(downloadId);
+            let download = DownloadMap.fromId(downloadId, context.privateBrowsingAllowed);
             return download.download.showContainingDirectory();
           }).then(() => {
             return true;
           }).catch(error => {
             return Promise.reject({message: error.message});
           });
         },
 
         getFileIcon(downloadId, options) {
           return DownloadMap.lazyInit().then(() => {
             let size = options && options.size ? options.size : 32;
-            let download = DownloadMap.fromId(downloadId).download;
+            let download = DownloadMap.fromId(downloadId, context.privateBrowsingAllowed).download;
             let pathPrefix = "";
             let path;
 
             if (download.succeeded) {
               let file = FileUtils.File(download.target.path);
               path = Services.io.newFileURI(file).spec;
             } else {
               path = OS.Path.basename(download.target.path);
@@ -754,81 +784,38 @@ this.downloads = class extends Extension
         // i.e.:
         // setShelfEnabled(enabled) {
         //   if (!extension.hasPermission("downloads.shelf")) {
         //     throw new context.cloneScope.Error("Permission denied because 'downloads.shelf' permission is missing.");
         //   }
         //   ...
         // }
 
-        onChanged: new EventManager({
-          context,
-          name: "downloads.onChanged",
-          register: fire => {
-            const handler = (what, item) => {
-              let changes = {};
-              const noundef = val => (val === undefined) ? null : val;
-              DOWNLOAD_ITEM_CHANGE_FIELDS.forEach(fld => {
-                if (item[fld] != item.prechange[fld]) {
-                  changes[fld] = {
-                    previous: noundef(item.prechange[fld]),
-                    current: noundef(item[fld]),
-                  };
-                }
-              });
-              if (Object.keys(changes).length > 0) {
-                changes.id = item.id;
-                fire.async(changes);
-              }
-            };
-
-            let registerPromise = DownloadMap.getDownloadList().then(() => {
-              DownloadMap.on("change", handler);
-            });
-            return () => {
-              registerPromise.then(() => {
-                DownloadMap.off("change", handler);
-              });
-            };
-          },
-        }).api(),
+        onChanged: downloadEventManagerAPI(context, "downloads.onChanged", "change", (fire, what, item) => {
+          let changes = {};
+          const noundef = val => (val === undefined) ? null : val;
+          DOWNLOAD_ITEM_CHANGE_FIELDS.forEach(fld => {
+            if (item[fld] != item.prechange[fld]) {
+              changes[fld] = {
+                previous: noundef(item.prechange[fld]),
+                current: noundef(item[fld]),
+              };
+            }
+          });
+          if (Object.keys(changes).length > 0) {
+            changes.id = item.id;
+            fire.async(changes);
+          }
+        }),
 
-        onCreated: new EventManager({
-          context,
-          name: "downloads.onCreated",
-          register: fire => {
-            const handler = (what, item) => {
-              fire.async(item.serialize());
-            };
-            let registerPromise = DownloadMap.getDownloadList().then(() => {
-              DownloadMap.on("create", handler);
-            });
-            return () => {
-              registerPromise.then(() => {
-                DownloadMap.off("create", handler);
-              });
-            };
-          },
-        }).api(),
+        onCreated: downloadEventManagerAPI(context, "downloads.onCreated", "create", (fire, what, item) => {
+          fire.async(item.serialize());
+        }),
 
-        onErased: new EventManager({
-          context,
-          name: "downloads.onErased",
-          register: fire => {
-            const handler = (what, item) => {
-              fire.async(item.id);
-            };
-            let registerPromise = DownloadMap.getDownloadList().then(() => {
-              DownloadMap.on("erase", handler);
-            });
-            return () => {
-              registerPromise.then(() => {
-                DownloadMap.off("erase", handler);
-              });
-            };
-          },
-        }).api(),
+        onErased: downloadEventManagerAPI(context, "downloads.onErased", "erase", (fire, what, item) => {
+          fire.async(item.id);
+        }),
 
         onDeterminingFilename: ignoreEvent(context, "downloads.onDeterminingFilename"),
       },
     };
   }
 };
--- a/toolkit/components/extensions/test/xpcshell/test_ext_downloads_private.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_private.js
@@ -9,20 +9,22 @@ const BASE = `http://localhost:${server.
 const TXT_FILE = "file_download.txt";
 const TXT_URL = BASE + "/" + TXT_FILE;
 
 function setup() {
   let downloadDir = FileUtils.getDir("TmpD", ["downloads"]);
   downloadDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
   info(`Using download directory ${downloadDir.path}`);
 
+  Services.prefs.setBoolPref("extensions.allowPrivateBrowsingByDefault", false);
   Services.prefs.setIntPref("browser.download.folderList", 2);
   Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, downloadDir);
 
   registerCleanupFunction(() => {
+    Services.prefs.clearUserPref("extensions.allowPrivateBrowsingByDefault");
     Services.prefs.clearUserPref("browser.download.folderList");
     Services.prefs.clearUserPref("browser.download.dir");
 
     let entries = downloadDir.directoryEntries;
     while (entries.hasMoreElements()) {
       let entry = entries.nextFile;
       ok(false, `Leftover file ${entry.path} in download directory`);
       entry.remove(false);
@@ -30,44 +32,46 @@ function setup() {
 
     downloadDir.remove(false);
   });
 }
 
 add_task(async function test_private_download() {
   setup();
 
-  let extension = ExtensionTestUtils.loadExtension({
+  let pb_extension = ExtensionTestUtils.loadExtension({
     background: async function() {
       function promiseEvent(eventTarget, accept) {
         return new Promise(resolve => {
           eventTarget.addListener(function listener(data) {
             if (accept && !accept(data)) {
               return;
             }
             eventTarget.removeListener(listener);
             resolve(data);
           });
         });
       }
       let startTestPromise = promiseEvent(browser.test.onMessage);
+      let removeTestPromise = promiseEvent(browser.test.onMessage, msg => msg == "remove");
       let onCreatedPromise = promiseEvent(browser.downloads.onCreated);
       let onDonePromise = promiseEvent(
         browser.downloads.onChanged,
         delta => delta.state && delta.state.current === "complete");
 
       browser.test.sendMessage("ready");
       let {url, filename} = await startTestPromise;
 
       browser.test.log("Starting private download");
       let downloadId = await browser.downloads.download({
         url,
         filename,
         incognito: true,
       });
+      browser.test.sendMessage("downloadId", downloadId);
 
       browser.test.log("Waiting for downloads.onCreated");
       let createdItem = await onCreatedPromise;
 
       browser.test.log("Waiting for completion notification");
       await onDonePromise;
 
       // test_ext_downloads_download.js already tests whether the file exists
@@ -77,16 +81,17 @@ add_task(async function test_private_dow
       let [downloadItem] = await browser.downloads.search({id: downloadId});
       browser.test.assertEq(url, createdItem.url, "onCreated url should match");
       browser.test.assertEq(url, downloadItem.url, "download url should match");
       browser.test.assertTrue(createdItem.incognito,
                               "created download should be private");
       browser.test.assertTrue(downloadItem.incognito,
                               "stored download should be private");
 
+      await removeTestPromise;
       browser.test.log("Removing downloaded file");
       browser.test.assertTrue(downloadItem.exists, "downloaded file exists");
       await browser.downloads.removeFile(downloadId);
 
       // Disabled because the assertion fails - https://bugzil.la/1381031
       // let [downloadItem2] = await browser.downloads.search({id: downloadId});
       // browser.test.assertFalse(downloadItem2.exists, "file should be deleted");
 
@@ -94,22 +99,111 @@ add_task(async function test_private_dow
       let erasePromise = promiseEvent(browser.downloads.onErased);
       await browser.downloads.erase({id: downloadId});
       browser.test.assertEq(downloadId, await erasePromise,
                             "onErased should be fired for the erased private download");
 
       browser.test.notifyPass("private download test done");
     },
     manifest: {
+      applications: {gecko: {id: "@spanning"}},
       permissions: ["downloads"],
     },
+    incognitoOverride: "spanning",
+  });
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      applications: {gecko: {id: "@not_allowed"}},
+      permissions: ["downloads", "downloads.open"],
+    },
+    background: async function() {
+      browser.downloads.onCreated.addListener(() => {
+        browser.test.fail("download-onCreated");
+      });
+      browser.downloads.onChanged.addListener(() => {
+        browser.test.fail("download-onChanged");
+      });
+      browser.downloads.onErased.addListener(() => {
+        browser.test.fail("download-onErased");
+      });
+      browser.test.onMessage.addListener(async (msg, data) => {
+        if (msg == "download") {
+          let {url, filename, downloadId} = data;
+          await browser.test.assertRejects(
+            browser.downloads.download({
+              url,
+              filename,
+              incognito: true,
+            }),
+            /private browsing access not allowed/,
+            "cannot download using incognito without permission.");
+
+          let downloads = await browser.downloads.search({id: downloadId});
+          browser.test.assertEq(downloads.length, 0, "cannot search for incognito downloads");
+          let erasing = await browser.downloads.erase({id: downloadId});
+          browser.test.assertEq(erasing.length, 0, "cannot erase incognito download");
+
+          await browser.test.assertRejects(
+            browser.downloads.removeFile(downloadId),
+            /Invalid download id/,
+            "cannot remove incognito download");
+          await browser.test.assertRejects(
+            browser.downloads.pause(downloadId),
+            /Invalid download id/,
+            "cannot pause incognito download");
+          await browser.test.assertRejects(
+            browser.downloads.resume(downloadId),
+            /Invalid download id/,
+            "cannot resume incognito download");
+          await browser.test.assertRejects(
+            browser.downloads.cancel(downloadId),
+            /Invalid download id/,
+            "cannot cancel incognito download");
+          await browser.test.assertRejects(
+            browser.downloads.removeFile(downloadId),
+            /Invalid download id/,
+            "cannot remove incognito download");
+          await browser.test.assertRejects(
+            browser.downloads.show(downloadId),
+            /Invalid download id/,
+            "cannot show incognito download");
+          await browser.test.assertRejects(
+            browser.downloads.getFileIcon(downloadId),
+            /Invalid download id/,
+            "cannot show incognito download");
+        }
+        if (msg == "download.open") {
+          let {downloadId} = data;
+          await browser.test.assertRejects(
+            browser.downloads.open(downloadId),
+            /Invalid download id/,
+            "cannot open incognito download");
+        }
+        browser.test.sendMessage("continue");
+      });
+    },
   });
 
   await extension.startup();
-  await extension.awaitMessage("ready");
-  extension.sendMessage({
+  await pb_extension.startup();
+  await pb_extension.awaitMessage("ready");
+  pb_extension.sendMessage({
     url: TXT_URL,
     filename: TXT_FILE,
   });
+  let downloadId = await pb_extension.awaitMessage("downloadId");
+  extension.sendMessage("download", {
+    url: TXT_URL,
+    filename: TXT_FILE,
+    downloadId,
+  });
+  await extension.awaitMessage("continue");
+  await withHandlingUserInput(extension, async () => {
+    extension.sendMessage("download.open", {downloadId});
+    await extension.awaitMessage("continue");
+  });
+  pb_extension.sendMessage("remove");
 
-  await extension.awaitFinish("private download test done");
+  await pb_extension.awaitFinish("private download test done");
+  await pb_extension.unload();
   await extension.unload();
 });