tor-browser

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

cache.js (5703B)


      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 } = require("resource://devtools/server/actors/resources/storage/index.js");
     10 
     11 class CacheStorageActor extends BaseStorageActor {
     12  constructor(storageActor) {
     13    super(storageActor, "Cache");
     14  }
     15 
     16  async populateStoresForHost(host) {
     17    const storeMap = new Map();
     18    const caches = await this.getCachesForHost(host);
     19    try {
     20      for (const name of await caches.keys()) {
     21        storeMap.set(name, await caches.open(name));
     22      }
     23    } catch (ex) {
     24      console.warn(`Failed to enumerate CacheStorage for host ${host}: ${ex}`);
     25    }
     26    this.hostVsStores.set(host, storeMap);
     27  }
     28 
     29  async getCachesForHost(host) {
     30    const win = this.storageActor.getWindowFromHost(host);
     31    if (!win) {
     32      return null;
     33    }
     34 
     35    const principal = win.document.effectiveStoragePrincipal;
     36 
     37    // The first argument tells if you want to get |content| cache or |chrome|
     38    // cache.
     39    // The |content| cache is the cache explicitely named by the web content
     40    // (service worker or web page).
     41    // The |chrome| cache is the cache implicitely cached by the platform,
     42    // hosting the source file of the service worker.
     43    const { CacheStorage } = win;
     44 
     45    if (!CacheStorage) {
     46      return null;
     47    }
     48 
     49    const cache = new CacheStorage("content", principal);
     50    return cache;
     51  }
     52 
     53  form() {
     54    const hosts = {};
     55    for (const host of this.hosts) {
     56      hosts[host] = this.getNamesForHost(host);
     57    }
     58 
     59    return {
     60      actor: this.actorID,
     61      hosts,
     62      traits: this._getTraits(),
     63    };
     64  }
     65 
     66  getNamesForHost(host) {
     67    // UI code expect each name to be a JSON string of an array :/
     68    return [...this.hostVsStores.get(host).keys()].map(a => {
     69      return JSON.stringify([a]);
     70    });
     71  }
     72 
     73  async getValuesForHost(host, name) {
     74    if (!name) {
     75      // if we get here, we most likely clicked on the refresh button
     76      // which called getStoreObjects, itself calling this method,
     77      // all that, without having selected any particular cache name.
     78      //
     79      // Try to detect if a new cache has been added and notify the client
     80      // asynchronously, via a RDP event.
     81      const previousCaches = [...this.hostVsStores.get(host).keys()];
     82      await this.populateStoresForHosts();
     83      const updatedCaches = [...this.hostVsStores.get(host).keys()];
     84      const newCaches = updatedCaches.filter(
     85        cacheName => !previousCaches.includes(cacheName)
     86      );
     87      newCaches.forEach(cacheName =>
     88        this.onItemUpdated("added", host, [cacheName])
     89      );
     90      const removedCaches = previousCaches.filter(
     91        cacheName => !updatedCaches.includes(cacheName)
     92      );
     93      removedCaches.forEach(cacheName =>
     94        this.onItemUpdated("deleted", host, [cacheName])
     95      );
     96      return [];
     97    }
     98    // UI is weird and expect a JSON stringified array... and pass it back :/
     99    name = JSON.parse(name)[0];
    100 
    101    const cache = this.hostVsStores.get(host).get(name);
    102    const requests = await cache.keys();
    103    const results = [];
    104    for (const request of requests) {
    105      let response = await cache.match(request);
    106      // Unwrap the response to get access to all its properties if the
    107      // response happen to be 'opaque', when it is a Cross Origin Request.
    108      response = response.cloneUnfiltered();
    109      results.push(await this.processEntry(request, response));
    110    }
    111    return results;
    112  }
    113 
    114  async processEntry(request, response) {
    115    return {
    116      url: String(request.url),
    117      status: String(response.statusText),
    118    };
    119  }
    120 
    121  async getFields() {
    122    return [
    123      { name: "url", editable: false },
    124      { name: "status", editable: false },
    125    ];
    126  }
    127 
    128  /**
    129   * Given a url, correctly determine its protocol + hostname part.
    130   */
    131  getSchemaAndHost(url) {
    132    const uri = Services.io.newURI(url);
    133    return uri.scheme + "://" + uri.hostPort;
    134  }
    135 
    136  toStoreObject(item) {
    137    return item;
    138  }
    139 
    140  async removeItem(host, name) {
    141    const cacheMap = this.hostVsStores.get(host);
    142    if (!cacheMap) {
    143      return;
    144    }
    145 
    146    const parsedName = JSON.parse(name);
    147 
    148    if (parsedName.length == 1) {
    149      // Delete the whole Cache object
    150      const [cacheName] = parsedName;
    151      cacheMap.delete(cacheName);
    152      const cacheStorage = await this.getCachesForHost(host);
    153      await cacheStorage.delete(cacheName);
    154      this.onItemUpdated("deleted", host, [cacheName]);
    155    } else if (parsedName.length == 2) {
    156      // Delete one cached request
    157      const [cacheName, url] = parsedName;
    158      const cache = cacheMap.get(cacheName);
    159      if (cache) {
    160        await cache.delete(url);
    161        this.onItemUpdated("deleted", host, [cacheName, url]);
    162      }
    163    }
    164  }
    165 
    166  async removeAll(host, name) {
    167    const cacheMap = this.hostVsStores.get(host);
    168    if (!cacheMap) {
    169      return;
    170    }
    171 
    172    const parsedName = JSON.parse(name);
    173 
    174    // Only a Cache object is a valid object to clear
    175    if (parsedName.length == 1) {
    176      const [cacheName] = parsedName;
    177      const cache = cacheMap.get(cacheName);
    178      if (cache) {
    179        const keys = await cache.keys();
    180        await Promise.all(keys.map(key => cache.delete(key)));
    181        this.onItemUpdated("cleared", host, [cacheName]);
    182      }
    183    }
    184  }
    185 
    186  /**
    187   * CacheStorage API doesn't support any notifications, we must fake them
    188   */
    189  onItemUpdated(action, host, path) {
    190    this.storageActor.update(action, "Cache", {
    191      [host]: [JSON.stringify(path)],
    192    });
    193  }
    194 }
    195 exports.CacheStorageActor = CacheStorageActor;