tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ext-downloads.js (8802B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 ChromeUtils.defineESModuleGetters(this, {
      8  DownloadPaths: "resource://gre/modules/DownloadPaths.sys.mjs",
      9  DownloadTracker: "resource://gre/modules/GeckoViewWebExtension.sys.mjs",
     10 });
     11 
     12 Cu.importGlobalProperties(["PathUtils"]);
     13 
     14 var { ignoreEvent } = ExtensionCommon;
     15 
     16 const REQUEST_DOWNLOAD_MESSAGE = "GeckoView:WebExtension:Download";
     17 
     18 const FORBIDDEN_HEADERS = [
     19  "ACCEPT-CHARSET",
     20  "ACCEPT-ENCODING",
     21  "ACCESS-CONTROL-REQUEST-HEADERS",
     22  "ACCESS-CONTROL-REQUEST-METHOD",
     23  "CONNECTION",
     24  "CONTENT-LENGTH",
     25  "COOKIE",
     26  "COOKIE2",
     27  "DATE",
     28  "DNT",
     29  "EXPECT",
     30  "HOST",
     31  "KEEP-ALIVE",
     32  "ORIGIN",
     33  "TE",
     34  "TRAILER",
     35  "TRANSFER-ENCODING",
     36  "UPGRADE",
     37  "VIA",
     38 ];
     39 
     40 const FORBIDDEN_PREFIXES = /^PROXY-|^SEC-/i;
     41 
     42 const State = {
     43  IN_PROGRESS: "in_progress",
     44  INTERRUPTED: "interrupted",
     45  COMPLETE: "complete",
     46 };
     47 
     48 const STATE_MAP = new Map([
     49  [0, State.IN_PROGRESS],
     50  [1, State.INTERRUPTED],
     51  [2, State.COMPLETE],
     52 ]);
     53 
     54 const INTERRUPT_REASON_MAP = new Map([
     55  [0, undefined],
     56  [1, "FILE_FAILED"],
     57  [2, "FILE_ACCESS_DENIED"],
     58  [3, "FILE_NO_SPACE"],
     59  [4, "FILE_NAME_TOO_LONG"],
     60  [5, "FILE_TOO_LARGE"],
     61  [6, "FILE_VIRUS_INFECTED"],
     62  [7, "FILE_TRANSIENT_ERROR"],
     63  [8, "FILE_BLOCKED"],
     64  [9, "FILE_SECURITY_CHECK_FAILED"],
     65  [10, "FILE_TOO_SHORT"],
     66  [11, "NETWORK_FAILED"],
     67  [12, "NETWORK_TIMEOUT"],
     68  [13, "NETWORK_DISCONNECTED"],
     69  [14, "NETWORK_SERVER_DOWN"],
     70  [15, "NETWORK_INVALID_REQUEST"],
     71  [16, "SERVER_FAILED"],
     72  [17, "SERVER_NO_RANGE"],
     73  [18, "SERVER_BAD_CONTENT"],
     74  [19, "SERVER_UNAUTHORIZED"],
     75  [20, "SERVER_CERT_PROBLEM"],
     76  [21, "SERVER_FORBIDDEN"],
     77  [22, "USER_CANCELED"],
     78  [23, "USER_SHUTDOWN"],
     79  [24, "CRASH"],
     80 ]);
     81 
     82 // TODO Bug 1247794: make id and extension info persistent
     83 class DownloadItem {
     84  /**
     85   * Initializes an object that represents a download
     86   *
     87   * @param {object} downloadInfo - an object from Java when creating a download
     88   * @param {object} options - an object passed in to download() function
     89   * @param {Extension} extension - instance of an extension object
     90   */
     91  constructor(downloadInfo, options, extension) {
     92    this.id = downloadInfo.id;
     93    this.url = options.url;
     94    this.referrer = downloadInfo.referrer || "";
     95    this.filename = downloadInfo.filename || "";
     96    this.incognito = options.incognito;
     97    this.danger = "safe"; // todo; not implemented in desktop either
     98    this.mime = downloadInfo.mime || "";
     99    this.startTime = downloadInfo.startTime;
    100    this.state = STATE_MAP.get(downloadInfo.state);
    101    this.paused = downloadInfo.paused;
    102    this.canResume = downloadInfo.canResume;
    103    this.bytesReceived = downloadInfo.bytesReceived;
    104    this.totalBytes = downloadInfo.totalBytes;
    105    this.fileSize = downloadInfo.fileSize;
    106    this.exists = downloadInfo.exists;
    107    this.byExtensionId = extension?.id;
    108    this.byExtensionName = extension?.name;
    109  }
    110 
    111  /**
    112   * This function updates the download item it was called on.
    113   *
    114   * @param {object} data that arrived from the app (Java)
    115   * @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>
    116   */
    117  update(data) {
    118    const { downloadItemId } = data;
    119    const delta = {};
    120 
    121    data.state = STATE_MAP.get(data.state);
    122    data.error = INTERRUPT_REASON_MAP.get(data.error);
    123    delete data.downloadItemId;
    124 
    125    let changed = false;
    126    for (const prop in data) {
    127      const current = data[prop] ?? null;
    128      const previous = this[prop] ?? null;
    129      if (current !== previous) {
    130        delta[prop] = { current, previous };
    131        this[prop] = current;
    132        changed = true;
    133      }
    134    }
    135 
    136    // Don't send empty onChange events
    137    if (!changed) {
    138      return null;
    139    }
    140 
    141    delta.id = downloadItemId;
    142 
    143    return delta;
    144  }
    145 }
    146 
    147 this.downloads = class extends ExtensionAPIPersistent {
    148  PERSISTENT_EVENTS = {
    149    onChanged({ fire }) {
    150      const listener = (eventName, event) => {
    151        const { delta, downloadItem } = event;
    152        const { extension } = this;
    153        if (extension.privateBrowsingAllowed || !downloadItem.incognito) {
    154          fire.async(delta);
    155        }
    156      };
    157      DownloadTracker.on("download-changed", listener);
    158 
    159      return {
    160        unregister() {
    161          DownloadTracker.off("download-changed", listener);
    162        },
    163        convert(_fire) {
    164          fire = _fire;
    165        },
    166      };
    167    },
    168  };
    169 
    170  getAPI(context) {
    171    const { extension } = context;
    172    return {
    173      downloads: {
    174        download(options) {
    175          // the validation checks should be kept in sync with the toolkit implementation
    176          let { filename } = options;
    177          if (filename != null) {
    178            if (!filename.length) {
    179              return Promise.reject({ message: "filename must not be empty" });
    180            }
    181 
    182            if (PathUtils.isAbsolute(filename)) {
    183              return Promise.reject({
    184                message: "filename must not be an absolute path",
    185              });
    186            }
    187 
    188            // % is not permitted but relatively common.
    189            filename = filename.replaceAll("%", "_");
    190 
    191            const pathComponents = PathUtils.splitRelative(filename, {
    192              allowEmpty: true,
    193              allowCurrentDir: true,
    194              allowParentDir: true,
    195            });
    196 
    197            if (pathComponents.some(component => component == "..")) {
    198              return Promise.reject({
    199                message: "filename must not contain back-references (..)",
    200              });
    201            }
    202 
    203            if (
    204              pathComponents.some((component, i) => {
    205                const sanitized = DownloadPaths.sanitize(component, {
    206                  compressWhitespaces: false,
    207                  allowDirectoryNames: i < pathComponents.length - 1,
    208                });
    209                return component != sanitized;
    210              })
    211            ) {
    212              return Promise.reject({
    213                message: "filename must not contain illegal characters",
    214              });
    215            }
    216          }
    217 
    218          if (options.incognito && !context.privateBrowsingAllowed) {
    219            return Promise.reject({
    220              message: "Private browsing access not allowed",
    221            });
    222          }
    223 
    224          if (options.cookieStoreId != null) {
    225            // https://bugzilla.mozilla.org/show_bug.cgi?id=1721460
    226            throw new ExtensionError("Not implemented");
    227          }
    228 
    229          if (options.headers) {
    230            for (const { name } of options.headers) {
    231              if (
    232                FORBIDDEN_HEADERS.includes(name.toUpperCase()) ||
    233                name.match(FORBIDDEN_PREFIXES)
    234              ) {
    235                return Promise.reject({
    236                  message: "Forbidden request header name",
    237                });
    238              }
    239            }
    240          }
    241 
    242          return EventDispatcher.instance
    243            .sendRequestForResult({
    244              type: REQUEST_DOWNLOAD_MESSAGE,
    245              options,
    246              extensionId: extension.id,
    247            })
    248            .then(value => {
    249              const downloadItem = new DownloadItem(value, options, extension);
    250              DownloadTracker.addDownloadItem(downloadItem);
    251              return downloadItem.id;
    252            });
    253        },
    254 
    255        removeFile() {
    256          throw new ExtensionError("Not implemented");
    257        },
    258 
    259        search() {
    260          throw new ExtensionError("Not implemented");
    261        },
    262 
    263        pause() {
    264          throw new ExtensionError("Not implemented");
    265        },
    266 
    267        resume() {
    268          throw new ExtensionError("Not implemented");
    269        },
    270 
    271        cancel() {
    272          throw new ExtensionError("Not implemented");
    273        },
    274 
    275        showDefaultFolder() {
    276          throw new ExtensionError("Not implemented");
    277        },
    278 
    279        erase() {
    280          throw new ExtensionError("Not implemented");
    281        },
    282 
    283        open() {
    284          throw new ExtensionError("Not implemented");
    285        },
    286 
    287        show() {
    288          throw new ExtensionError("Not implemented");
    289        },
    290 
    291        getFileIcon() {
    292          throw new ExtensionError("Not implemented");
    293        },
    294 
    295        onChanged: new EventManager({
    296          context,
    297          module: "downloads",
    298          event: "onChanged",
    299          extensionApi: this,
    300        }).api(),
    301 
    302        onCreated: ignoreEvent(context, "downloads.onCreated"),
    303 
    304        onErased: ignoreEvent(context, "downloads.onErased"),
    305 
    306        onDeterminingFilename: ignoreEvent(
    307          context,
    308          "downloads.onDeterminingFilename"
    309        ),
    310      },
    311    };
    312  }
    313 };