mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 10:18:41 +02:00 
			
		
		
		
	Original Revision: https://phabricator.services.mozilla.com/D218719 Differential Revision: https://phabricator.services.mozilla.com/D219740
		
			
				
	
	
		
			313 lines
		
	
	
	
		
			8.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			313 lines
		
	
	
	
		
			8.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* 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/. */
 | 
						|
 | 
						|
"use strict";
 | 
						|
 | 
						|
ChromeUtils.defineESModuleGetters(this, {
 | 
						|
  DownloadPaths: "resource://gre/modules/DownloadPaths.sys.mjs",
 | 
						|
  DownloadTracker: "resource://gre/modules/GeckoViewWebExtension.sys.mjs",
 | 
						|
});
 | 
						|
 | 
						|
Cu.importGlobalProperties(["PathUtils"]);
 | 
						|
 | 
						|
var { ignoreEvent } = ExtensionCommon;
 | 
						|
 | 
						|
const REQUEST_DOWNLOAD_MESSAGE = "GeckoView:WebExtension:Download";
 | 
						|
 | 
						|
const FORBIDDEN_HEADERS = [
 | 
						|
  "ACCEPT-CHARSET",
 | 
						|
  "ACCEPT-ENCODING",
 | 
						|
  "ACCESS-CONTROL-REQUEST-HEADERS",
 | 
						|
  "ACCESS-CONTROL-REQUEST-METHOD",
 | 
						|
  "CONNECTION",
 | 
						|
  "CONTENT-LENGTH",
 | 
						|
  "COOKIE",
 | 
						|
  "COOKIE2",
 | 
						|
  "DATE",
 | 
						|
  "DNT",
 | 
						|
  "EXPECT",
 | 
						|
  "HOST",
 | 
						|
  "KEEP-ALIVE",
 | 
						|
  "ORIGIN",
 | 
						|
  "TE",
 | 
						|
  "TRAILER",
 | 
						|
  "TRANSFER-ENCODING",
 | 
						|
  "UPGRADE",
 | 
						|
  "VIA",
 | 
						|
];
 | 
						|
 | 
						|
const FORBIDDEN_PREFIXES = /^PROXY-|^SEC-/i;
 | 
						|
 | 
						|
const State = {
 | 
						|
  IN_PROGRESS: "in_progress",
 | 
						|
  INTERRUPTED: "interrupted",
 | 
						|
  COMPLETE: "complete",
 | 
						|
};
 | 
						|
 | 
						|
const STATE_MAP = new Map([
 | 
						|
  [0, State.IN_PROGRESS],
 | 
						|
  [1, State.INTERRUPTED],
 | 
						|
  [2, State.COMPLETE],
 | 
						|
]);
 | 
						|
 | 
						|
const INTERRUPT_REASON_MAP = new Map([
 | 
						|
  [0, undefined],
 | 
						|
  [1, "FILE_FAILED"],
 | 
						|
  [2, "FILE_ACCESS_DENIED"],
 | 
						|
  [3, "FILE_NO_SPACE"],
 | 
						|
  [4, "FILE_NAME_TOO_LONG"],
 | 
						|
  [5, "FILE_TOO_LARGE"],
 | 
						|
  [6, "FILE_VIRUS_INFECTED"],
 | 
						|
  [7, "FILE_TRANSIENT_ERROR"],
 | 
						|
  [8, "FILE_BLOCKED"],
 | 
						|
  [9, "FILE_SECURITY_CHECK_FAILED"],
 | 
						|
  [10, "FILE_TOO_SHORT"],
 | 
						|
  [11, "NETWORK_FAILED"],
 | 
						|
  [12, "NETWORK_TIMEOUT"],
 | 
						|
  [13, "NETWORK_DISCONNECTED"],
 | 
						|
  [14, "NETWORK_SERVER_DOWN"],
 | 
						|
  [15, "NETWORK_INVALID_REQUEST"],
 | 
						|
  [16, "SERVER_FAILED"],
 | 
						|
  [17, "SERVER_NO_RANGE"],
 | 
						|
  [18, "SERVER_BAD_CONTENT"],
 | 
						|
  [19, "SERVER_UNAUTHORIZED"],
 | 
						|
  [20, "SERVER_CERT_PROBLEM"],
 | 
						|
  [21, "SERVER_FORBIDDEN"],
 | 
						|
  [22, "USER_CANCELED"],
 | 
						|
  [23, "USER_SHUTDOWN"],
 | 
						|
  [24, "CRASH"],
 | 
						|
]);
 | 
						|
 | 
						|
// TODO Bug 1247794: make id and extension info persistent
 | 
						|
class DownloadItem {
 | 
						|
  /**
 | 
						|
   * Initializes an object that represents a download
 | 
						|
   *
 | 
						|
   * @param {object} downloadInfo - an object from Java when creating a download
 | 
						|
   * @param {object} options - an object passed in to download() function
 | 
						|
   * @param {Extension} extension - instance of an extension object
 | 
						|
   */
 | 
						|
  constructor(downloadInfo, options, extension) {
 | 
						|
    this.id = downloadInfo.id;
 | 
						|
    this.url = options.url;
 | 
						|
    this.referrer = downloadInfo.referrer || "";
 | 
						|
    this.filename = downloadInfo.filename || "";
 | 
						|
    this.incognito = options.incognito;
 | 
						|
    this.danger = "safe"; // todo; not implemented in desktop either
 | 
						|
    this.mime = downloadInfo.mime || "";
 | 
						|
    this.startTime = downloadInfo.startTime;
 | 
						|
    this.state = STATE_MAP.get(downloadInfo.state);
 | 
						|
    this.paused = downloadInfo.paused;
 | 
						|
    this.canResume = downloadInfo.canResume;
 | 
						|
    this.bytesReceived = downloadInfo.bytesReceived;
 | 
						|
    this.totalBytes = downloadInfo.totalBytes;
 | 
						|
    this.fileSize = downloadInfo.fileSize;
 | 
						|
    this.exists = downloadInfo.exists;
 | 
						|
    this.byExtensionId = extension?.id;
 | 
						|
    this.byExtensionName = extension?.name;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * This function updates the download item it was called on.
 | 
						|
   *
 | 
						|
   * @param {object} data that arrived from the app (Java)
 | 
						|
   * @returns {object | null} an object of <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/downloads/onChanged#downloaddelta>downloadDelta type</a>
 | 
						|
   */
 | 
						|
  update(data) {
 | 
						|
    const { downloadItemId } = data;
 | 
						|
    const delta = {};
 | 
						|
 | 
						|
    data.state = STATE_MAP.get(data.state);
 | 
						|
    data.error = INTERRUPT_REASON_MAP.get(data.error);
 | 
						|
    delete data.downloadItemId;
 | 
						|
 | 
						|
    let changed = false;
 | 
						|
    for (const prop in data) {
 | 
						|
      const current = data[prop] ?? null;
 | 
						|
      const previous = this[prop] ?? null;
 | 
						|
      if (current !== previous) {
 | 
						|
        delta[prop] = { current, previous };
 | 
						|
        this[prop] = current;
 | 
						|
        changed = true;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // Don't send empty onChange events
 | 
						|
    if (!changed) {
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
 | 
						|
    delta.id = downloadItemId;
 | 
						|
 | 
						|
    return delta;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
this.downloads = class extends ExtensionAPIPersistent {
 | 
						|
  PERSISTENT_EVENTS = {
 | 
						|
    onChanged({ fire }) {
 | 
						|
      const listener = (eventName, event) => {
 | 
						|
        const { delta, downloadItem } = event;
 | 
						|
        const { extension } = this;
 | 
						|
        if (extension.privateBrowsingAllowed || !downloadItem.incognito) {
 | 
						|
          fire.async(delta);
 | 
						|
        }
 | 
						|
      };
 | 
						|
      DownloadTracker.on("download-changed", listener);
 | 
						|
 | 
						|
      return {
 | 
						|
        unregister() {
 | 
						|
          DownloadTracker.off("download-changed", listener);
 | 
						|
        },
 | 
						|
        convert(_fire) {
 | 
						|
          fire = _fire;
 | 
						|
        },
 | 
						|
      };
 | 
						|
    },
 | 
						|
  };
 | 
						|
 | 
						|
  getAPI(context) {
 | 
						|
    const { extension } = context;
 | 
						|
    return {
 | 
						|
      downloads: {
 | 
						|
        download(options) {
 | 
						|
          // the validation checks should be kept in sync with the toolkit implementation
 | 
						|
          let { filename } = options;
 | 
						|
          if (filename != null) {
 | 
						|
            if (!filename.length) {
 | 
						|
              return Promise.reject({ message: "filename must not be empty" });
 | 
						|
            }
 | 
						|
 | 
						|
            if (PathUtils.isAbsolute(filename)) {
 | 
						|
              return Promise.reject({
 | 
						|
                message: "filename must not be an absolute path",
 | 
						|
              });
 | 
						|
            }
 | 
						|
 | 
						|
            // % is not permitted but relatively common.
 | 
						|
            filename = filename.replaceAll("%", "_");
 | 
						|
 | 
						|
            const pathComponents = PathUtils.splitRelative(filename, {
 | 
						|
              allowEmpty: true,
 | 
						|
              allowCurrentDir: true,
 | 
						|
              allowParentDir: true,
 | 
						|
            });
 | 
						|
 | 
						|
            if (pathComponents.some(component => component == "..")) {
 | 
						|
              return Promise.reject({
 | 
						|
                message: "filename must not contain back-references (..)",
 | 
						|
              });
 | 
						|
            }
 | 
						|
 | 
						|
            if (
 | 
						|
              pathComponents.some((component, i) => {
 | 
						|
                const sanitized = DownloadPaths.sanitize(component, {
 | 
						|
                  compressWhitespaces: false,
 | 
						|
                  allowDirectoryNames: i < pathComponents.length - 1,
 | 
						|
                });
 | 
						|
                return component != sanitized;
 | 
						|
              })
 | 
						|
            ) {
 | 
						|
              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.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 (
 | 
						|
                FORBIDDEN_HEADERS.includes(name.toUpperCase()) ||
 | 
						|
                name.match(FORBIDDEN_PREFIXES)
 | 
						|
              ) {
 | 
						|
                return Promise.reject({
 | 
						|
                  message: "Forbidden request header name",
 | 
						|
                });
 | 
						|
              }
 | 
						|
            }
 | 
						|
          }
 | 
						|
 | 
						|
          return EventDispatcher.instance
 | 
						|
            .sendRequestForResult({
 | 
						|
              type: REQUEST_DOWNLOAD_MESSAGE,
 | 
						|
              options,
 | 
						|
              extensionId: extension.id,
 | 
						|
            })
 | 
						|
            .then(value => {
 | 
						|
              const downloadItem = new DownloadItem(value, options, extension);
 | 
						|
              DownloadTracker.addDownloadItem(downloadItem);
 | 
						|
              return downloadItem.id;
 | 
						|
            });
 | 
						|
        },
 | 
						|
 | 
						|
        removeFile() {
 | 
						|
          throw new ExtensionError("Not implemented");
 | 
						|
        },
 | 
						|
 | 
						|
        search() {
 | 
						|
          throw new ExtensionError("Not implemented");
 | 
						|
        },
 | 
						|
 | 
						|
        pause() {
 | 
						|
          throw new ExtensionError("Not implemented");
 | 
						|
        },
 | 
						|
 | 
						|
        resume() {
 | 
						|
          throw new ExtensionError("Not implemented");
 | 
						|
        },
 | 
						|
 | 
						|
        cancel() {
 | 
						|
          throw new ExtensionError("Not implemented");
 | 
						|
        },
 | 
						|
 | 
						|
        showDefaultFolder() {
 | 
						|
          throw new ExtensionError("Not implemented");
 | 
						|
        },
 | 
						|
 | 
						|
        erase() {
 | 
						|
          throw new ExtensionError("Not implemented");
 | 
						|
        },
 | 
						|
 | 
						|
        open() {
 | 
						|
          throw new ExtensionError("Not implemented");
 | 
						|
        },
 | 
						|
 | 
						|
        show() {
 | 
						|
          throw new ExtensionError("Not implemented");
 | 
						|
        },
 | 
						|
 | 
						|
        getFileIcon() {
 | 
						|
          throw new ExtensionError("Not implemented");
 | 
						|
        },
 | 
						|
 | 
						|
        onChanged: new EventManager({
 | 
						|
          context,
 | 
						|
          module: "downloads",
 | 
						|
          event: "onChanged",
 | 
						|
          extensionApi: this,
 | 
						|
        }).api(),
 | 
						|
 | 
						|
        onCreated: ignoreEvent(context, "downloads.onCreated"),
 | 
						|
 | 
						|
        onErased: ignoreEvent(context, "downloads.onErased"),
 | 
						|
 | 
						|
        onDeterminingFilename: ignoreEvent(
 | 
						|
          context,
 | 
						|
          "downloads.onDeterminingFilename"
 | 
						|
        ),
 | 
						|
      },
 | 
						|
    };
 | 
						|
  }
 | 
						|
};
 |