tor-browser

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

index.js (13341B)


      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
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 const { Actor } = require("resource://devtools/shared/protocol.js");
      8 const specs = require("resource://devtools/shared/specs/storage.js");
      9 
     10 loader.lazyRequireGetter(
     11  this,
     12  "naturalSortCaseInsensitive",
     13  "resource://devtools/shared/natural-sort.js",
     14  true
     15 );
     16 
     17 // Maximum number of cookies/local storage key-value-pairs that can be sent
     18 // over the wire to the client in one request.
     19 const MAX_STORE_OBJECT_COUNT = 50;
     20 exports.MAX_STORE_OBJECT_COUNT = MAX_STORE_OBJECT_COUNT;
     21 
     22 const DEFAULT_VALUE = "value";
     23 exports.DEFAULT_VALUE = DEFAULT_VALUE;
     24 
     25 // GUID to be used as a separator in compound keys. This must match the same
     26 // constant in devtools/client/storage/ui.js,
     27 // devtools/client/storage/test/head.js and
     28 // devtools/server/tests/browser/head.js
     29 const SEPARATOR_GUID = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}";
     30 exports.SEPARATOR_GUID = SEPARATOR_GUID;
     31 
     32 class BaseStorageActor extends Actor {
     33  /**
     34   * Base class with the common methods required by all storage actors.
     35   *
     36   * This base class is missing a couple of required methods that should be
     37   * implemented seperately for each actor. They are namely:
     38   *   - observe : Method which gets triggered on the notification of the watched
     39   *               topic.
     40   *   - getNamesForHost : Given a host, get list of all known store names.
     41   *   - getValuesForHost : Given a host (and optionally a name) get all known
     42   *                        store objects.
     43   *   - toStoreObject : Given a store object, convert it to the required format
     44   *                     so that it can be transferred over wire.
     45   *   - populateStoresForHost : Given a host, populate the map of all store
     46   *                             objects for it
     47   *   - getFields: Given a subType(optional), get an array of objects containing
     48   *                column field info. The info includes,
     49   *                "name" is name of colume key.
     50   *                "editable" is 1 means editable field; 0 means uneditable.
     51   *
     52   * @param {string} typeName
     53   *        The typeName of the actor.
     54   */
     55  constructor(storageActor, typeName) {
     56    super(storageActor.conn, specs.childSpecs[typeName]);
     57 
     58    this.storageActor = storageActor;
     59 
     60    // Map keyed by host name whose values are nested Maps.
     61    // Nested maps are keyed by store names and values are store values.
     62    // Store values are specific to each sub class.
     63    // Map(host name => stores <Map(name => values )>)
     64    // Map(string => stores <Map(string => any )>)
     65    this.hostVsStores = new Map();
     66 
     67    this.onWindowReady = this.onWindowReady.bind(this);
     68    this.onWindowDestroyed = this.onWindowDestroyed.bind(this);
     69    this.storageActor.on("window-ready", this.onWindowReady);
     70    this.storageActor.on("window-destroyed", this.onWindowDestroyed);
     71  }
     72 
     73  destroy() {
     74    if (!this.storageActor) {
     75      return;
     76    }
     77 
     78    this.storageActor.off("window-ready", this.onWindowReady);
     79    this.storageActor.off("window-destroyed", this.onWindowDestroyed);
     80 
     81    this.hostVsStores.clear();
     82 
     83    super.destroy();
     84 
     85    this.storageActor = null;
     86  }
     87 
     88  /**
     89   * Returns a list of currently known hosts for the target window. This list
     90   * contains unique hosts from the window + all inner windows. If
     91   * this._internalHosts is defined then these will also be added to the list.
     92   */
     93  get hosts() {
     94    const hosts = new Set();
     95    for (const { location } of this.storageActor.windows) {
     96      const host = this.getHostName(location);
     97 
     98      if (host) {
     99        hosts.add(host);
    100      }
    101    }
    102    if (this._internalHosts) {
    103      for (const host of this._internalHosts) {
    104        hosts.add(host);
    105      }
    106    }
    107    return hosts;
    108  }
    109 
    110  /**
    111   * Returns all the windows present on the page. Includes main window + inner
    112   * iframe windows.
    113   */
    114  get windows() {
    115    return this.storageActor.windows;
    116  }
    117 
    118  /**
    119   * Converts the window.location object into a URL (e.g. http://domain.com).
    120   */
    121  getHostName(location) {
    122    if (!location) {
    123      // Debugging a legacy Firefox extension... no hostname available and no
    124      // storage possible.
    125      return null;
    126    }
    127 
    128    if (this.storageActor.getHostName) {
    129      return this.storageActor.getHostName(location);
    130    }
    131 
    132    switch (location.protocol) {
    133      case "about:":
    134        return `${location.protocol}${location.pathname}`;
    135      case "chrome:":
    136        // chrome: URLs do not support storage of any type.
    137        return null;
    138      case "data:":
    139        // data: URLs do not support storage of any type.
    140        return null;
    141      case "file:":
    142        return `${location.protocol}//${location.pathname}`;
    143      case "javascript:":
    144        return location.href;
    145      case "moz-extension:":
    146        return location.origin;
    147      case "resource:":
    148        return `${location.origin}${location.pathname}`;
    149      default:
    150        // http: or unknown protocol.
    151        return `${location.protocol}//${location.host}`;
    152    }
    153  }
    154 
    155  /**
    156   * Populates a map of known hosts vs a map of stores vs value.
    157   */
    158  async populateStoresForHosts() {
    159    for (const host of this.hosts) {
    160      await this.populateStoresForHost(host);
    161    }
    162  }
    163 
    164  getNamesForHost(host) {
    165    return [...this.hostVsStores.get(host).keys()];
    166  }
    167 
    168  getValuesForHost(host, name) {
    169    if (name) {
    170      return [this.hostVsStores.get(host).get(name)];
    171    }
    172    return [...this.hostVsStores.get(host).values()];
    173  }
    174 
    175  getObjectsSize(host, names) {
    176    return names.length;
    177  }
    178 
    179  /**
    180   * When a new window is added to the page. This generally means that a new
    181   * iframe is created, or the current window is completely reloaded.
    182   *
    183   * @param {window} window
    184   *        The window which was added.
    185   */
    186  async onWindowReady(window) {
    187    if (!this.hostVsStores) {
    188      return;
    189    }
    190    const host = this.getHostName(window.location);
    191    if (host && !this.hostVsStores.has(host)) {
    192      await this.populateStoresForHost(host, window);
    193      if (!this.storageActor) {
    194        // The actor might be destroyed during populateStoresForHost.
    195        return;
    196      }
    197 
    198      const data = {};
    199      data[host] = this.getNamesForHost(host);
    200      this.storageActor.update("added", this.typeName, data);
    201    }
    202  }
    203 
    204  /**
    205   * When a window is removed from the page. This generally means that an
    206   * iframe was removed, or the current window reload is triggered.
    207   *
    208   * @param {window} window
    209   *        The window which was removed.
    210   * @param {object} options
    211   * @param {boolean} options.dontCheckHost
    212   *        If set to true, the function won't check if the host still is in this.hosts.
    213   *        This is helpful in the case of the StorageActorMock, as the `hosts` getter
    214   *        uses its `windows` getter, and at this point in time the window which is
    215   *        going to be destroyed still exists.
    216   */
    217  onWindowDestroyed(window, { dontCheckHost } = {}) {
    218    if (!this.hostVsStores) {
    219      return;
    220    }
    221    if (!window.location) {
    222      // Nothing can be done if location object is null
    223      return;
    224    }
    225    const host = this.getHostName(window.location);
    226    if (host && (!this.hosts.has(host) || dontCheckHost)) {
    227      this.hostVsStores.delete(host);
    228      const data = {};
    229      data[host] = [];
    230      this.storageActor.update("deleted", this.typeName, data);
    231    }
    232  }
    233 
    234  form() {
    235    const hosts = {};
    236    for (const host of this.hosts) {
    237      hosts[host] = [];
    238    }
    239 
    240    return {
    241      actor: this.actorID,
    242      hosts,
    243      traits: this._getTraits(),
    244    };
    245  }
    246 
    247  // Share getTraits for child classes overriding form()
    248  _getTraits() {
    249    return {
    250      // The supportsXXX traits are not related to backward compatibility
    251      // Different storage actor types implement different APIs, the traits
    252      // help the client to know what is supported or not.
    253      supportsAddItem: typeof this.addItem === "function",
    254      // Note: supportsRemoveItem and supportsRemoveAll are always defined
    255      // for all actors. See Bug 1655001.
    256      supportsRemoveItem: typeof this.removeItem === "function",
    257      supportsRemoveAll: typeof this.removeAll === "function",
    258      supportsRemoveAllSessionCookies:
    259        typeof this.removeAllSessionCookies === "function",
    260    };
    261  }
    262 
    263  /**
    264   * Returns a list of requested store objects. Maximum values returned are
    265   * MAX_STORE_OBJECT_COUNT. This method returns paginated values whose
    266   * starting index and total size can be controlled via the options object
    267   *
    268   * @param {string} host
    269   *        The host name for which the store values are required.
    270   * @param {array:string} names
    271   *        Array containing the names of required store objects. Empty if all
    272   *        items are required.
    273   * @param {object} options
    274   *        Additional options for the request containing following
    275   *        properties:
    276   *         - offset {number} : The begin index of the returned array amongst
    277   *                  the total values
    278   *         - size {number} : The number of values required.
    279   *         - sortOn {string} : The values should be sorted on this property.
    280   *         - index {string} : In case of indexed db, the IDBIndex to be used
    281   *                 for fetching the values.
    282   *         - sessionString {string} : Client-side value of storage-expires-session
    283   *                         l10n string. Since this function can be called from both
    284   *                         the client and the server, and given that client and
    285   *                         server might have different locales, we can't compute
    286   *                         the localized string directly from here.
    287   * @return {object} An object containing following properties:
    288   *          - offset - The actual offset of the returned array. This might
    289   *                     be different from the requested offset if that was
    290   *                     invalid
    291   *          - total - The total number of entries possible.
    292   *          - data - The requested values.
    293   */
    294  async getStoreObjects(host, names, options = {}) {
    295    const offset = options.offset || 0;
    296    let size = options.size || MAX_STORE_OBJECT_COUNT;
    297    if (size > MAX_STORE_OBJECT_COUNT) {
    298      size = MAX_STORE_OBJECT_COUNT;
    299    }
    300    const sortOn = options.sortOn || "name";
    301 
    302    const toReturn = {
    303      offset,
    304      total: 0,
    305      data: [],
    306    };
    307 
    308    let principal = null;
    309    if (this.typeName === "indexedDB") {
    310      // We only acquire principal when the type of the storage is indexedDB
    311      // because the principal only matters the indexedDB.
    312      const win = this.storageActor.getWindowFromHost(host);
    313      principal = this.getPrincipal(win);
    314    }
    315 
    316    if (names) {
    317      for (const name of names) {
    318        const values = await this.getValuesForHost(
    319          host,
    320          name,
    321          options,
    322          this.hostVsStores,
    323          principal
    324        );
    325 
    326        const { result, objectStores } = values;
    327 
    328        if (result && typeof result.objectsSize !== "undefined") {
    329          for (const { key, count } of result.objectsSize) {
    330            this.objectsSize[key] = count;
    331          }
    332        }
    333 
    334        if (result) {
    335          toReturn.data.push(...result.data);
    336        } else if (objectStores) {
    337          toReturn.data.push(...objectStores);
    338        } else {
    339          toReturn.data.push(...values);
    340        }
    341      }
    342 
    343      if (this.typeName === "Cache") {
    344        // Cache storage contains several items per name but misses a custom
    345        // `getObjectsSize` implementation, as implemented for IndexedDB.
    346        // See Bug 1745242.
    347        toReturn.total = toReturn.data.length;
    348      } else {
    349        toReturn.total = this.getObjectsSize(host, names, options);
    350      }
    351    } else {
    352      let obj = await this.getValuesForHost(
    353        host,
    354        undefined,
    355        undefined,
    356        this.hostVsStores,
    357        principal
    358      );
    359      if (obj.dbs) {
    360        obj = obj.dbs;
    361      }
    362 
    363      toReturn.total = obj.length;
    364      toReturn.data = obj;
    365    }
    366 
    367    if (offset > toReturn.total) {
    368      // In this case, toReturn.data is an empty array.
    369      toReturn.offset = toReturn.total;
    370      toReturn.data = [];
    371    } else {
    372      // We need to use natural sort before slicing.
    373      const sorted = toReturn.data.sort((a, b) => {
    374        return naturalSortCaseInsensitive(
    375          a[sortOn],
    376          b[sortOn],
    377          options.sessionString
    378        );
    379      });
    380      let sliced;
    381      if (this.typeName === "indexedDB") {
    382        // indexedDB's getValuesForHost never returns *all* values available but only
    383        // a slice, starting at the expected offset. Therefore the result is already
    384        // sliced as expected.
    385        sliced = sorted;
    386      } else {
    387        sliced = sorted.slice(offset, offset + size);
    388      }
    389      toReturn.data = sliced.map(a => this.toStoreObject(a));
    390    }
    391 
    392    return toReturn;
    393  }
    394 
    395  getPrincipal(win) {
    396    if (win) {
    397      return win.document.effectiveStoragePrincipal;
    398    }
    399    // We are running in the browser toolbox and viewing system DBs so we
    400    // need to use system principal.
    401    return Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal);
    402  }
    403 }
    404 exports.BaseStorageActor = BaseStorageActor;