summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/downloads/DownloadLastDir.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--toolkit/mozapps/downloads/DownloadLastDir.sys.mjs252
1 files changed, 252 insertions, 0 deletions
diff --git a/toolkit/mozapps/downloads/DownloadLastDir.sys.mjs b/toolkit/mozapps/downloads/DownloadLastDir.sys.mjs
new file mode 100644
index 0000000000..9fe90a0ecd
--- /dev/null
+++ b/toolkit/mozapps/downloads/DownloadLastDir.sys.mjs
@@ -0,0 +1,252 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * The behavior implemented by gDownloadLastDir is documented here.
+ *
+ * In normal browsing sessions, gDownloadLastDir uses the browser.download.lastDir
+ * preference to store the last used download directory. The first time the user
+ * switches into the private browsing mode, the last download directory is
+ * preserved to the pref value, but if the user switches to another directory
+ * during the private browsing mode, that directory is not stored in the pref,
+ * and will be merely kept in memory. When leaving the private browsing mode,
+ * this in-memory value will be discarded, and the last download directory
+ * will be reverted to the pref value.
+ *
+ * Both the pref and the in-memory value will be cleared when clearing the
+ * browsing history. This effectively changes the last download directory
+ * to the default download directory on each platform.
+ *
+ * If passed a URI, the last used directory is also stored with that URI in the
+ * content preferences database. This can be disabled by setting the pref
+ * browser.download.lastDir.savePerSite to false.
+ */
+
+const LAST_DIR_PREF = "browser.download.lastDir";
+const SAVE_PER_SITE_PREF = LAST_DIR_PREF + ".savePerSite";
+const nsIFile = Ci.nsIFile;
+
+import { PrivateBrowsingUtils } from "resource://gre/modules/PrivateBrowsingUtils.sys.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "cps2",
+ "@mozilla.org/content-pref/service;1",
+ "nsIContentPrefService2"
+);
+
+let nonPrivateLoadContext = Cu.createLoadContext();
+let privateLoadContext = Cu.createPrivateLoadContext();
+
+var observer = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]),
+
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "last-pb-context-exited":
+ gDownloadLastDirFile = null;
+ break;
+ case "browser:purge-session-history":
+ gDownloadLastDirFile = null;
+ if (Services.prefs.prefHasUserValue(LAST_DIR_PREF)) {
+ Services.prefs.clearUserPref(LAST_DIR_PREF);
+ }
+ // Ensure that purging session history causes both the session-only PB cache
+ // and persistent prefs to be cleared.
+ let promises = [
+ new Promise(resolve =>
+ lazy.cps2.removeByName(LAST_DIR_PREF, nonPrivateLoadContext, {
+ handleCompletion: resolve,
+ })
+ ),
+ new Promise(resolve =>
+ lazy.cps2.removeByName(LAST_DIR_PREF, privateLoadContext, {
+ handleCompletion: resolve,
+ })
+ ),
+ ];
+ // This is for testing purposes.
+ if (aSubject && typeof subject == "object") {
+ aSubject.promise = Promise.all(promises);
+ }
+ break;
+ }
+ },
+};
+
+Services.obs.addObserver(observer, "last-pb-context-exited", true);
+Services.obs.addObserver(observer, "browser:purge-session-history", true);
+
+function readLastDirPref() {
+ try {
+ return Services.prefs.getComplexValue(LAST_DIR_PREF, nsIFile);
+ } catch (e) {
+ return null;
+ }
+}
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "isContentPrefEnabled",
+ SAVE_PER_SITE_PREF,
+ true
+);
+
+var gDownloadLastDirFile = readLastDirPref();
+
+export class DownloadLastDir {
+ // aForcePrivate is only used when aWindow is null.
+ constructor(aWindow, aForcePrivate) {
+ let isPrivate = false;
+ if (aWindow === null) {
+ isPrivate =
+ aForcePrivate || PrivateBrowsingUtils.permanentPrivateBrowsing;
+ } else {
+ let loadContext = aWindow.docShell.QueryInterface(Ci.nsILoadContext);
+ isPrivate = loadContext.usePrivateBrowsing;
+ }
+
+ // We always use a fake load context because we may not have one (i.e.,
+ // in the aWindow == null case) and because the load context associated
+ // with aWindow may disappear by the time we need it. This approach is
+ // safe because we only care about the private browsing state. All the
+ // rest of the load context isn't of interest to the content pref service.
+ this.fakeContext = isPrivate ? privateLoadContext : nonPrivateLoadContext;
+ }
+
+ isPrivate() {
+ return this.fakeContext.usePrivateBrowsing;
+ }
+
+ // compat shims
+ get file() {
+ return this.#getLastFile();
+ }
+ set file(val) {
+ this.setFile(null, val);
+ }
+
+ cleanupPrivateFile() {
+ gDownloadLastDirFile = null;
+ }
+
+ #getLastFile() {
+ if (gDownloadLastDirFile && !gDownloadLastDirFile.exists()) {
+ gDownloadLastDirFile = null;
+ }
+
+ if (this.isPrivate()) {
+ if (!gDownloadLastDirFile) {
+ gDownloadLastDirFile = readLastDirPref();
+ }
+ return gDownloadLastDirFile;
+ }
+ return readLastDirPref();
+ }
+
+ async getFileAsync(aURI) {
+ let plainPrefFile = this.#getLastFile();
+ if (!aURI || !lazy.isContentPrefEnabled) {
+ return plainPrefFile;
+ }
+
+ return new Promise(resolve => {
+ lazy.cps2.getByDomainAndName(
+ this.#cpsGroupFromURL(aURI),
+ LAST_DIR_PREF,
+ this.fakeContext,
+ {
+ _result: null,
+ handleResult(aResult) {
+ this._result = aResult;
+ },
+ handleCompletion(aReason) {
+ let file = plainPrefFile;
+ if (
+ aReason == Ci.nsIContentPrefCallback2.COMPLETE_OK &&
+ this._result instanceof Ci.nsIContentPref
+ ) {
+ try {
+ file = Cc["@mozilla.org/file/local;1"].createInstance(
+ Ci.nsIFile
+ );
+ file.initWithPath(this._result.value);
+ } catch (e) {
+ file = plainPrefFile;
+ }
+ }
+ resolve(file);
+ },
+ }
+ );
+ });
+ }
+
+ setFile(aURI, aFile) {
+ if (aURI && lazy.isContentPrefEnabled) {
+ if (aFile instanceof Ci.nsIFile) {
+ lazy.cps2.set(
+ this.#cpsGroupFromURL(aURI),
+ LAST_DIR_PREF,
+ aFile.path,
+ this.fakeContext
+ );
+ } else {
+ lazy.cps2.removeByDomainAndName(
+ this.#cpsGroupFromURL(aURI),
+ LAST_DIR_PREF,
+ this.fakeContext
+ );
+ }
+ }
+ if (this.isPrivate()) {
+ if (aFile instanceof Ci.nsIFile) {
+ gDownloadLastDirFile = aFile.clone();
+ } else {
+ gDownloadLastDirFile = null;
+ }
+ } else if (aFile instanceof Ci.nsIFile) {
+ Services.prefs.setComplexValue(LAST_DIR_PREF, nsIFile, aFile);
+ } else if (Services.prefs.prefHasUserValue(LAST_DIR_PREF)) {
+ Services.prefs.clearUserPref(LAST_DIR_PREF);
+ }
+ }
+
+ /**
+ * Pre-processor to extract a domain name to be used with the content-prefs
+ * service. This specially handles data and file URIs so that the download
+ * dirs are recalled in a more consistent way:
+ * - all file:/// URIs share the same folder
+ * - data: URIs share a folder per mime-type. If a mime-type is not
+ * specified text/plain is assumed.
+ * In any other case the original URL is returned as a string and ContentPrefs
+ * will do its usual parsing.
+ *
+ * @param {string|nsIURI|URL} url The URL to parse
+ * @returns {string} the domain name to use, or the original url.
+ */
+ #cpsGroupFromURL(url) {
+ if (typeof url == "string") {
+ url = new URL(url);
+ } else if (url instanceof Ci.nsIURI) {
+ url = URL.fromURI(url);
+ }
+ if (!URL.isInstance(url)) {
+ return url;
+ }
+ if (url.protocol == "data:") {
+ return url.href.match(/^data:[^;,]*/i)[0].replace(/:$/, ":text/plain");
+ }
+ if (url.protocol == "file:") {
+ return "file:///";
+ }
+ return url.href;
+ }
+}