tor-browser

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

cookies.js (21740B)


      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 {
      8  BaseStorageActor,
      9  DEFAULT_VALUE,
     10  SEPARATOR_GUID,
     11 } = require("resource://devtools/server/actors/resources/storage/index.js");
     12 const {
     13  LongStringActor,
     14 } = require("resource://devtools/server/actors/string.js");
     15 
     16 // "Lax", "Strict" and "None" are special values of the SameSite property
     17 // that should not be translated.
     18 const COOKIE_SAMESITE = {
     19  LAX: "Lax",
     20  STRICT: "Strict",
     21  NONE: "None",
     22 };
     23 
     24 // MAX_COOKIE_EXPIRY should be 2^63-1, but JavaScript can't handle that
     25 // precision.
     26 const MAX_COOKIE_EXPIRY = Math.pow(2, 62);
     27 
     28 /**
     29 * General helpers
     30 */
     31 function trimHttpHttpsPort(url) {
     32  const match = url.match(/(.+):\d+$/);
     33 
     34  if (match) {
     35    url = match[1];
     36  }
     37  if (url.startsWith("http://")) {
     38    return url.substr(7);
     39  }
     40  if (url.startsWith("https://")) {
     41    return url.substr(8);
     42  }
     43  return url;
     44 }
     45 
     46 class CookiesStorageActor extends BaseStorageActor {
     47  constructor(storageActor) {
     48    super(storageActor, "cookies");
     49 
     50    Services.obs.addObserver(this, "cookie-changed");
     51    Services.obs.addObserver(this, "private-cookie-changed");
     52  }
     53 
     54  destroy() {
     55    Services.obs.removeObserver(this, "cookie-changed");
     56    Services.obs.removeObserver(this, "private-cookie-changed");
     57 
     58    super.destroy();
     59  }
     60 
     61  static UNIQUE_KEY_INDEXES = { name: 0, host: 1, path: 2, partitionKey: 3 };
     62 
     63  #getCookieUniqueKey(cookie) {
     64    return (
     65      cookie.name +
     66      SEPARATOR_GUID +
     67      cookie.host +
     68      SEPARATOR_GUID +
     69      cookie.path +
     70      SEPARATOR_GUID +
     71      cookie.originAttributes.partitionKey
     72    );
     73  }
     74 
     75  populateStoresForHost(host) {
     76    this.hostVsStores.set(host, new Map());
     77    const cookies = this.getCookiesFromHost(host);
     78    for (const cookie of cookies) {
     79      if (this.isCookieAtHost(cookie, host)) {
     80        const uniqueKey = this.#getCookieUniqueKey(cookie);
     81        this.hostVsStores.get(host).set(uniqueKey, cookie);
     82      }
     83    }
     84  }
     85 
     86  getOriginAttributesFromHost(host) {
     87    const win = this.storageActor.getWindowFromHost(host);
     88    let originAttributes;
     89    if (win) {
     90      originAttributes =
     91        win.document.effectiveStoragePrincipal.originAttributes;
     92    } else {
     93      // If we can't find the window by host, fallback to the top window
     94      // origin attributes.
     95      originAttributes =
     96        this.storageActor.document?.effectiveStoragePrincipal.originAttributes;
     97    }
     98 
     99    return originAttributes;
    100  }
    101 
    102  getCookiesFromHost(host) {
    103    // Gather originAttributes list from host
    104    const hostBrowsingContexts =
    105      this.storageActor.getBrowsingContextsFromHost(host);
    106    const originAttributesList = [];
    107    if (hostBrowsingContexts.length) {
    108      // Since we need to get all browsing contexts to get their originAttributes,
    109      // we might get "duplicated" objects, which would translate into having the same
    110      // cookies multiple times.
    111      // To avoid that, we compute a unique key from originAttributes to only have unique ones.
    112      const uniqueOriginAttributes = new Set();
    113      for (const bc of hostBrowsingContexts) {
    114        const { originAttributes } =
    115          bc.currentWindowGlobal.documentStoragePrincipal;
    116        // The object is small, seems fine to stringify it to compute a unique key
    117        const oaKey = JSON.stringify(originAttributes);
    118        if (!uniqueOriginAttributes.has(oaKey)) {
    119          originAttributesList.push(originAttributes);
    120          uniqueOriginAttributes.add(oaKey);
    121        }
    122 
    123        // A document might have an empty partitionKey in browsingContext.currentWindowGlobal.documentStoragePrincipal.originAttributes,
    124        // (e.g. a top level document), but still have partitioned cookies, in a different jar
    125        // (in CHIPS, for top level document that's first-party partitioned cookies).
    126        // In order to retrieve those, we create a new originAttribute with the
    127        // partitionKey from the window global cookie jar partitionKey
    128        if (
    129          bc.currentWindowGlobal.cookieJarSettings.partitionKey !==
    130          originAttributes.partitionKey
    131        ) {
    132          const derivedOriginAttributes = {
    133            ...originAttributes,
    134            partitionKey: bc.currentWindowGlobal.cookieJarSettings.partitionKey,
    135          };
    136          const derivedOaKey = JSON.stringify(derivedOriginAttributes);
    137          if (!uniqueOriginAttributes.has(derivedOaKey)) {
    138            originAttributesList.push(derivedOriginAttributes);
    139            uniqueOriginAttributes.add(derivedOaKey);
    140          }
    141        }
    142      }
    143    } else {
    144      // In case of WebExtension or BrowserToolbox, we may pass privileged hosts
    145      // which don't relate to any particular window. getOriginAttributesFromHost will
    146      // fallback to the top window origin attributes.
    147      originAttributesList.push(this.getOriginAttributesFromHost(host));
    148    }
    149 
    150    // Local files have no host.
    151    if (host.startsWith("file:///")) {
    152      host = "";
    153    }
    154 
    155    host = trimHttpHttpsPort(host);
    156 
    157    // Retrieve cookies all the passed originAttributes so we can get cookies from all jars
    158    let cookies;
    159    for (const originAttributes of originAttributesList) {
    160      const oaCookies = Services.cookies.getCookiesFromHost(
    161        host,
    162        originAttributes
    163      );
    164      if (!cookies) {
    165        cookies = oaCookies;
    166      } else {
    167        cookies.push(...oaCookies);
    168      }
    169    }
    170    return cookies || [];
    171  }
    172 
    173  /**
    174   * Given a cookie object, figure out all the matching hosts from the page that
    175   * the cookie belong to.
    176   */
    177  getMatchingHosts(cookies) {
    178    if (!cookies) {
    179      return [];
    180    }
    181    if (!cookies.length) {
    182      cookies = [cookies];
    183    }
    184    const hosts = new Set();
    185    for (const host of this.hosts) {
    186      for (const cookie of cookies) {
    187        if (this.isCookieAtHost(cookie, host)) {
    188          hosts.add(host);
    189        }
    190      }
    191    }
    192    return [...hosts];
    193  }
    194 
    195  /**
    196   * Given a cookie object and a host, figure out if the cookie is valid for
    197   * that host.
    198   */
    199  isCookieAtHost(cookie, host) {
    200    if (cookie.host == null) {
    201      return host == null;
    202    }
    203 
    204    host = trimHttpHttpsPort(host);
    205 
    206    if (cookie.host.startsWith(".")) {
    207      return ("." + host).endsWith(cookie.host);
    208    }
    209    if (cookie.host === "") {
    210      return host.startsWith("file://" + cookie.path);
    211    }
    212 
    213    return cookie.host == host;
    214  }
    215 
    216  toStoreObject(cookie) {
    217    if (!cookie) {
    218      return null;
    219    }
    220 
    221    const obj = {
    222      uniqueKey: this.#getCookieUniqueKey(cookie),
    223      name: cookie.name,
    224      host: cookie.host || "",
    225      path: cookie.path || "",
    226 
    227      // because expires is in mseconds
    228      expires: cookie.expires || 0,
    229 
    230      // because creationTime is in micro seconds
    231      creationTime: cookie.creationTime / 1000,
    232 
    233      // because updateTime is in micro seconds
    234      updateTime: cookie.updateTime / 1000,
    235 
    236      size: cookie.name.length + (cookie.value || "").length,
    237 
    238      // - do -
    239      lastAccessed: cookie.lastAccessed / 1000,
    240      value: new LongStringActor(this.conn, cookie.value || ""),
    241      hostOnly: !cookie.isDomain,
    242      isSecure: cookie.isSecure,
    243      isHttpOnly: cookie.isHttpOnly,
    244      sameSite: this.getSameSiteStringFromCookie(cookie),
    245    };
    246 
    247    if (cookie.isPartitioned) {
    248      const rawPartitionKey = cookie.originAttributes.partitionKey;
    249      // We need to return the site derived from the partition key.
    250      // rawPartitionKey format should be like "(<scheme>,<baseDomain>,[port],[ancestorbit])"
    251      // see https://searchfox.org/mozilla-central/rev/23efe2c8c5b3a3182d449211ff9036fb34fe0219/caps/OriginAttributes.h#132-138
    252      // We can ignore the `ancestorbit` part.
    253      const [scheme, baseDomain, port] = rawPartitionKey
    254        .replace(/(?<openingparen>^\()|(?<closingparen>\)$)/g, "")
    255        .split(",");
    256      const partitionKey = `${scheme}://${baseDomain}${
    257        port !== undefined && /^\d+$/.test(port) ? ":" + port : ""
    258      }`;
    259      obj.partitionKey = partitionKey;
    260    }
    261 
    262    return obj;
    263  }
    264 
    265  getSameSiteStringFromCookie(cookie) {
    266    switch (cookie.sameSite) {
    267      case cookie.SAMESITE_LAX:
    268        return COOKIE_SAMESITE.LAX;
    269      case cookie.SAMESITE_STRICT:
    270        return COOKIE_SAMESITE.STRICT;
    271      case cookie.SAMESITE_NONE:
    272        return COOKIE_SAMESITE.NONE;
    273    }
    274    // cookie.SAMESITE_UNSET
    275    return "";
    276  }
    277 
    278  /**
    279   * Notification observer for "cookie-change".
    280   *
    281   * @param {(nsICookie|nsICookie[])} cookie - Cookie/s changed. Depending on the action
    282   * this is either null, a single cookie or an array of cookies.
    283   * @param {nsICookieNotification_Action} action - The cookie operation, see
    284   * nsICookieNotification for details.
    285   */
    286  onCookieChanged(cookie, action) {
    287    const {
    288      COOKIE_ADDED,
    289      COOKIE_CHANGED,
    290      COOKIE_DELETED,
    291      COOKIES_BATCH_DELETED,
    292      ALL_COOKIES_CLEARED,
    293    } = Ci.nsICookieNotification;
    294 
    295    const hosts = this.getMatchingHosts(cookie);
    296    if (!hosts.length) {
    297      return;
    298    }
    299 
    300    const data = {};
    301 
    302    switch (action) {
    303      case COOKIE_ADDED:
    304      case COOKIE_CHANGED:
    305        if (hosts.length) {
    306          for (const host of hosts) {
    307            const uniqueKey = this.#getCookieUniqueKey(cookie);
    308            this.hostVsStores.get(host).set(uniqueKey, cookie);
    309            data[host] = [uniqueKey];
    310          }
    311          const actionStr = action == COOKIE_ADDED ? "added" : "changed";
    312          this.storageActor.update(actionStr, "cookies", data);
    313        }
    314        break;
    315 
    316      case COOKIE_DELETED:
    317        if (hosts.length) {
    318          for (const host of hosts) {
    319            const uniqueKey = this.#getCookieUniqueKey(cookie);
    320            this.hostVsStores.get(host).delete(uniqueKey);
    321            data[host] = [uniqueKey];
    322          }
    323          this.storageActor.update("deleted", "cookies", data);
    324        }
    325        break;
    326 
    327      case COOKIES_BATCH_DELETED:
    328        if (hosts.length) {
    329          for (const host of hosts) {
    330            const stores = [];
    331            // For COOKIES_BATCH_DELETED cookie is an array.
    332            for (const batchCookie of cookie) {
    333              const uniqueKey = this.#getCookieUniqueKey(batchCookie);
    334              this.hostVsStores.get(host).delete(uniqueKey);
    335              stores.push(uniqueKey);
    336            }
    337            data[host] = stores;
    338          }
    339          this.storageActor.update("deleted", "cookies", data);
    340        }
    341        break;
    342 
    343      case ALL_COOKIES_CLEARED:
    344        if (hosts.length) {
    345          for (const host of hosts) {
    346            data[host] = [];
    347          }
    348          this.storageActor.update("cleared", "cookies", data);
    349        }
    350        break;
    351    }
    352  }
    353 
    354  async getFields() {
    355    const fields = [
    356      { name: "uniqueKey", editable: false, private: true },
    357      { name: "name", editable: true, hidden: false },
    358      { name: "value", editable: true, hidden: false },
    359      { name: "host", editable: true, hidden: false },
    360      { name: "path", editable: true, hidden: false },
    361      { name: "expires", editable: true, hidden: false },
    362      { name: "size", editable: false, hidden: false },
    363      { name: "isHttpOnly", editable: true, hidden: false },
    364      { name: "isSecure", editable: true, hidden: false },
    365      { name: "sameSite", editable: false, hidden: false },
    366      { name: "lastAccessed", editable: false, hidden: false },
    367      { name: "creationTime", editable: false, hidden: true },
    368      { name: "updateTime", editable: false, hidden: true },
    369      { name: "hostOnly", editable: false, hidden: true },
    370    ];
    371 
    372    if (Services.prefs.getBoolPref("network.cookie.CHIPS.enabled", false)) {
    373      fields.push({ name: "partitionKey", editable: false, hidden: false });
    374    }
    375 
    376    return fields;
    377  }
    378 
    379  /**
    380   * Pass the editItem command from the content to the chrome process.
    381   *
    382   * @param {object} data
    383   *        See editCookie() for format details.
    384   * @returns {object} An object with an "errorString" property.
    385   */
    386  async editItem(data) {
    387    const potentialErrorMessage = this.editCookie(data);
    388    return { errorString: potentialErrorMessage };
    389  }
    390 
    391  /**
    392   * Add a cookie on given host
    393   *
    394   * @param {string} guid
    395   * @param {string} host
    396   * @returns {object} An object with an "errorString" property.
    397   */
    398  async addItem(guid, host) {
    399    const window = this.storageActor.getWindowFromHost(host);
    400    const principal = window.document.effectiveStoragePrincipal;
    401    const potentialErrorMessage = this.addCookie(guid, principal);
    402    return { errorString: potentialErrorMessage };
    403  }
    404 
    405  async removeItem(host, uniqueKey) {
    406    if (uniqueKey === undefined) {
    407      return;
    408    }
    409    this._removeCookies(host, { uniqueKey });
    410  }
    411 
    412  async removeAll(host, domain) {
    413    this._removeCookies(host, { domain });
    414  }
    415 
    416  async removeAllSessionCookies(host, domain) {
    417    this._removeCookies(host, { domain, session: true });
    418  }
    419 
    420  /**
    421   * Add a cookie on given principal
    422   *
    423   * @param {string} guid
    424   * @param {Principal} principal
    425   * @returns {string | null} If the cookie couldn't be added (e.g. it's invalid),
    426   *          an error string will be returned.
    427   */
    428  addCookie(guid, principal) {
    429    // Set expiry time for cookie 1 day into the future
    430    // NOTE: Services.cookies.add expects the time in mseconds.
    431    const ONE_DAY_IN_MSECONDS = 60 * 60 * 24 * 1000;
    432    const time = Date.now();
    433    const expiry = time + ONE_DAY_IN_MSECONDS;
    434 
    435    // principal throws an error when we try to access principal.host if it
    436    // does not exist (which happens at about: pages).
    437    // We check for asciiHost instead, which is always present, and has a
    438    // value of "" when the host is not available.
    439    const domain = principal.asciiHost ? principal.host : principal.baseDomain;
    440 
    441    const cv = Services.cookies.add(
    442      domain,
    443      "/",
    444      guid, // name
    445      DEFAULT_VALUE, // value
    446      false, // isSecure
    447      false, // isHttpOnly,
    448      false, // isSession,
    449      expiry, // expires,
    450      principal.originAttributes, // originAttributes
    451      Ci.nsICookie.SAMESITE_LAX, // sameSite
    452      principal.scheme === "https" // schemeMap
    453        ? Ci.nsICookie.SCHEME_HTTPS
    454        : Ci.nsICookie.SCHEME_HTTP
    455    );
    456 
    457    if (cv.result != Ci.nsICookieValidation.eOK) {
    458      return cv.errorString;
    459    }
    460 
    461    return null;
    462  }
    463 
    464  /**
    465   * Apply the results of a cookie edit.
    466   *
    467   * @param {object} data
    468   *        An object in the following format:
    469   *        {
    470   *          host: "http://www.mozilla.org",
    471   *          field: "value",
    472   *          editCookie: "name",
    473   *          oldValue: "%7BHello%7D",
    474   *          newValue: "%7BHelloo%7D",
    475   *          items: {
    476   *            name: "optimizelyBuckets",
    477   *            path: "/",
    478   *            host: ".mozilla.org",
    479   *            expires: "Mon, 02 Jun 2025 12:37:37 GMT",
    480   *            creationTime: "Tue, 18 Nov 2014 16:21:18 GMT",
    481   *            updateTime: "Tue, 18 Nov 2014 16:21:18 GMT",
    482   *            lastAccessed: "Wed, 17 Feb 2016 10:06:23 GMT",
    483   *            value: "%7BHelloo%7D",
    484   *            isDomain: "true",
    485   *            isSecure: "false",
    486   *            isHttpOnly: "false"
    487   *          }
    488   *        }
    489   * @returns {(string | null)} If cookie couldn't be updated (e.g. it's invalid), an error string
    490   *          will be returned.
    491   */
    492  // eslint-disable-next-line complexity
    493  editCookie(data) {
    494    let { field, oldValue, newValue } = data;
    495    const origName = field === "name" ? oldValue : data.items.name;
    496    const origHost = field === "host" ? oldValue : data.items.host;
    497    const origPath = field === "path" ? oldValue : data.items.path;
    498    // We can't use `data.items.partitionKey` as it's the formatted value and we need
    499    // to check against the "raw" one. Its value can't be modified, so we don't need to
    500    // look into oldValue.
    501    const partitionKey =
    502      data.items.uniqueKey.split(SEPARATOR_GUID)[
    503        CookiesStorageActor.UNIQUE_KEY_INDEXES.partitionKey
    504      ];
    505    let cookie = null;
    506 
    507    const cookies = this.getCookiesFromHost(data.host);
    508    for (const nsiCookie of cookies) {
    509      if (
    510        nsiCookie.name === origName &&
    511        nsiCookie.host === origHost &&
    512        nsiCookie.path === origPath &&
    513        nsiCookie.originAttributes.partitionKey === partitionKey
    514      ) {
    515        cookie = {
    516          host: nsiCookie.host,
    517          path: nsiCookie.path,
    518          name: nsiCookie.name,
    519          value: nsiCookie.value,
    520          isSecure: nsiCookie.isSecure,
    521          isHttpOnly: nsiCookie.isHttpOnly,
    522          isSession: nsiCookie.isSession,
    523          expires: nsiCookie.expires,
    524          originAttributes: nsiCookie.originAttributes,
    525          sameSite: nsiCookie.sameSite,
    526          schemeMap: nsiCookie.schemeMap,
    527          isPartitioned: nsiCookie.isPartitioned,
    528        };
    529        break;
    530      }
    531    }
    532 
    533    if (!cookie) {
    534      return null;
    535    }
    536 
    537    // If the date is expired set it for 10 seconds in the future.
    538    const now = new Date();
    539    if (!cookie.isSession && cookie.expires <= now) {
    540      const tenMsFromNow = now.getTime() + 10 * 1000;
    541 
    542      cookie.expires = tenMsFromNow;
    543    }
    544 
    545    let origCookieRemoved = false;
    546 
    547    switch (field) {
    548      case "isSecure":
    549      case "isHttpOnly":
    550      case "isSession":
    551        newValue = newValue === "true";
    552        break;
    553 
    554      case "expires":
    555        newValue = Date.parse(newValue);
    556 
    557        if (isNaN(newValue)) {
    558          newValue = MAX_COOKIE_EXPIRY;
    559        } else {
    560          newValue = Services.cookies.maybeCapExpiry(newValue);
    561        }
    562        break;
    563 
    564      case "host":
    565      case "name":
    566      case "path":
    567        // Remove the edited cookie.
    568        Services.cookies.remove(
    569          origHost,
    570          origName,
    571          origPath,
    572          cookie.originAttributes
    573        );
    574        origCookieRemoved = true;
    575        break;
    576    }
    577 
    578    // Apply changes.
    579    cookie[field] = newValue;
    580 
    581    // cookie.isSession is not always set correctly on session cookies so we
    582    // need to trust cookie.expires instead.
    583    cookie.isSession = !cookie.expires;
    584 
    585    // Add the edited cookie.
    586    const cv = Services.cookies.add(
    587      cookie.host,
    588      cookie.path,
    589      cookie.name,
    590      cookie.value,
    591      cookie.isSecure,
    592      cookie.isHttpOnly,
    593      cookie.isSession,
    594      cookie.isSession ? MAX_COOKIE_EXPIRY : cookie.expires,
    595      cookie.originAttributes,
    596      cookie.sameSite,
    597      cookie.schemeMap,
    598      cookie.isPartitioned
    599    );
    600 
    601    if (cv.result != Ci.nsICookieValidation.eOK) {
    602      if (origCookieRemoved) {
    603        // Re-add the cookie with the original values if it was removed.
    604        Services.cookies.add(
    605          origHost,
    606          origPath,
    607          origName,
    608          cookie.value,
    609          cookie.isSecure,
    610          cookie.isHttpOnly,
    611          cookie.isSession,
    612          cookie.isSession ? MAX_COOKIE_EXPIRY : cookie.expires,
    613          cookie.originAttributes,
    614          cookie.sameSite,
    615          cookie.schemeMap,
    616          cookie.isPartitioned
    617        );
    618      }
    619 
    620      return cv.errorString;
    621    }
    622 
    623    return null;
    624  }
    625 
    626  _removeCookies(host, opts = {}) {
    627    // We use a uniqueId to emulate compound keys for cookies. We need to
    628    // extract the cookie name to remove the correct cookie.
    629    if (opts.uniqueKey) {
    630      const uniqueKeyParts = opts.uniqueKey.split(SEPARATOR_GUID);
    631 
    632      opts.name = uniqueKeyParts[CookiesStorageActor.UNIQUE_KEY_INDEXES.name];
    633      opts.path = uniqueKeyParts[CookiesStorageActor.UNIQUE_KEY_INDEXES.path];
    634      opts.partitionKey =
    635        uniqueKeyParts[CookiesStorageActor.UNIQUE_KEY_INDEXES.partitionKey] ||
    636        "";
    637    }
    638 
    639    const cookies = this.getCookiesFromHost(host);
    640    for (const cookie of cookies) {
    641      if (
    642        this.isCookieAtHost(cookie, host) &&
    643        (!opts.name || cookie.name === opts.name) &&
    644        (!opts.domain || cookie.host === opts.domain) &&
    645        (!opts.path || cookie.path === opts.path) &&
    646        (!opts.uniqueKey ||
    647          // make sure to pick the cookie from the correct jar
    648          cookie.originAttributes.partitionKey === opts.partitionKey) &&
    649        // for session cookie removal
    650        (!opts.session || (!cookie.expires && !cookie.maxAge))
    651      ) {
    652        Services.cookies.remove(
    653          cookie.host,
    654          cookie.name,
    655          cookie.path,
    656          cookie.originAttributes
    657        );
    658      }
    659    }
    660  }
    661 
    662  removeCookie(host, name, originAttributes) {
    663    if (name !== undefined) {
    664      this._removeCookies(host, { name, originAttributes });
    665    }
    666  }
    667 
    668  removeAllCookies(host, domain, originAttributes) {
    669    this._removeCookies(host, { domain, originAttributes });
    670  }
    671 
    672  observe(subject, topic) {
    673    if (
    674      !subject ||
    675      (topic != "cookie-changed" && topic != "private-cookie-changed") ||
    676      !this.storageActor ||
    677      !this.storageActor.windows
    678    ) {
    679      return;
    680    }
    681 
    682    const notification = subject.QueryInterface(Ci.nsICookieNotification);
    683    let cookie;
    684    if (notification.action == Ci.nsICookieNotification.COOKIES_BATCH_DELETED) {
    685      // Extract the batch deleted cookies from nsIArray.
    686      const cookiesNoInterface =
    687        notification.batchDeletedCookies.QueryInterface(Ci.nsIArray);
    688      cookie = [];
    689      for (let i = 0; i < cookiesNoInterface.length; i++) {
    690        cookie.push(cookiesNoInterface.queryElementAt(i, Ci.nsICookie));
    691      }
    692    } else if (notification.cookie) {
    693      // Otherwise, get the single cookie affected by the operation.
    694      cookie = notification.cookie.QueryInterface(Ci.nsICookie);
    695    }
    696 
    697    this.onCookieChanged(cookie, notification.action);
    698  }
    699 }
    700 exports.CookiesStorageActor = CookiesStorageActor;