Source code

Revision control

Copy as Markdown

Other Tools

/* 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";
const {
BaseStorageActor,
class CacheStorageActor extends BaseStorageActor {
constructor(storageActor) {
super(storageActor, "Cache");
}
async populateStoresForHost(host) {
const storeMap = new Map();
const caches = await this.getCachesForHost(host);
try {
for (const name of await caches.keys()) {
storeMap.set(name, await caches.open(name));
}
} catch (ex) {
console.warn(`Failed to enumerate CacheStorage for host ${host}: ${ex}`);
}
this.hostVsStores.set(host, storeMap);
}
async getCachesForHost(host) {
const win = this.storageActor.getWindowFromHost(host);
if (!win) {
return null;
}
const principal = win.document.effectiveStoragePrincipal;
// The first argument tells if you want to get |content| cache or |chrome|
// cache.
// The |content| cache is the cache explicitely named by the web content
// (service worker or web page).
// The |chrome| cache is the cache implicitely cached by the platform,
// hosting the source file of the service worker.
const { CacheStorage } = win;
if (!CacheStorage) {
return null;
}
const cache = new CacheStorage("content", principal);
return cache;
}
form() {
const hosts = {};
for (const host of this.hosts) {
hosts[host] = this.getNamesForHost(host);
}
return {
actor: this.actorID,
hosts,
traits: this._getTraits(),
};
}
getNamesForHost(host) {
// UI code expect each name to be a JSON string of an array :/
return [...this.hostVsStores.get(host).keys()].map(a => {
return JSON.stringify([a]);
});
}
async getValuesForHost(host, name) {
if (!name) {
// if we get here, we most likely clicked on the refresh button
// which called getStoreObjects, itself calling this method,
// all that, without having selected any particular cache name.
//
// Try to detect if a new cache has been added and notify the client
// asynchronously, via a RDP event.
const previousCaches = [...this.hostVsStores.get(host).keys()];
await this.populateStoresForHosts();
const updatedCaches = [...this.hostVsStores.get(host).keys()];
const newCaches = updatedCaches.filter(
cacheName => !previousCaches.includes(cacheName)
);
newCaches.forEach(cacheName =>
this.onItemUpdated("added", host, [cacheName])
);
const removedCaches = previousCaches.filter(
cacheName => !updatedCaches.includes(cacheName)
);
removedCaches.forEach(cacheName =>
this.onItemUpdated("deleted", host, [cacheName])
);
return [];
}
// UI is weird and expect a JSON stringified array... and pass it back :/
name = JSON.parse(name)[0];
const cache = this.hostVsStores.get(host).get(name);
const requests = await cache.keys();
const results = [];
for (const request of requests) {
let response = await cache.match(request);
// Unwrap the response to get access to all its properties if the
// response happen to be 'opaque', when it is a Cross Origin Request.
response = response.cloneUnfiltered();
results.push(await this.processEntry(request, response));
}
return results;
}
async processEntry(request, response) {
return {
url: String(request.url),
status: String(response.statusText),
};
}
async getFields() {
return [
{ name: "url", editable: false },
{ name: "status", editable: false },
];
}
/**
* Given a url, correctly determine its protocol + hostname part.
*/
getSchemaAndHost(url) {
const uri = Services.io.newURI(url);
return uri.scheme + "://" + uri.hostPort;
}
toStoreObject(item) {
return item;
}
async removeItem(host, name) {
const cacheMap = this.hostVsStores.get(host);
if (!cacheMap) {
return;
}
const parsedName = JSON.parse(name);
if (parsedName.length == 1) {
// Delete the whole Cache object
const [cacheName] = parsedName;
cacheMap.delete(cacheName);
const cacheStorage = await this.getCachesForHost(host);
await cacheStorage.delete(cacheName);
this.onItemUpdated("deleted", host, [cacheName]);
} else if (parsedName.length == 2) {
// Delete one cached request
const [cacheName, url] = parsedName;
const cache = cacheMap.get(cacheName);
if (cache) {
await cache.delete(url);
this.onItemUpdated("deleted", host, [cacheName, url]);
}
}
}
async removeAll(host, name) {
const cacheMap = this.hostVsStores.get(host);
if (!cacheMap) {
return;
}
const parsedName = JSON.parse(name);
// Only a Cache object is a valid object to clear
if (parsedName.length == 1) {
const [cacheName] = parsedName;
const cache = cacheMap.get(cacheName);
if (cache) {
const keys = await cache.keys();
await Promise.all(keys.map(key => cache.delete(key)));
this.onItemUpdated("cleared", host, [cacheName]);
}
}
}
/**
* CacheStorage API doesn't support any notifications, we must fake them
*/
onItemUpdated(action, host, path) {
this.storageActor.update(action, "Cache", {
[host]: [JSON.stringify(path)],
});
}
}
exports.CacheStorageActor = CacheStorageActor;