1
0
mirror of https://github.com/gorhill/uBlock.git synced 2024-11-17 07:52:42 +01:00
uBlock/src/js/cachestorage.js

390 lines
14 KiB
JavaScript
Raw Normal View History

/*******************************************************************************
uBlock Origin - a browser extension to block requests.
2018-08-06 18:34:41 +02:00
Copyright (C) 2016-present The uBlock Origin authors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
*/
2018-08-06 18:34:41 +02:00
/* global IDBDatabase, indexedDB */
2017-08-30 00:32:00 +02:00
'use strict';
/******************************************************************************/
// The code below has been originally manually imported from:
// Commit: https://github.com/nikrolls/uBlock-Edge/commit/d1538ea9bea89d507219d3219592382eee306134
// Commit date: 29 October 2016
// Commit author: https://github.com/nikrolls
// Commit message: "Implement cacheStorage using IndexedDB"
2017-08-30 00:32:00 +02:00
// The original imported code has been subsequently modified as it was not
// compatible with Firefox.
// (a Promise thing, see https://github.com/dfahlander/Dexie.js/issues/317)
// Furthermore, code to migrate from browser.storage.local to vAPI.cacheStorage
// has been added, for seamless migration of cache-related entries into
// indexedDB.
µBlock.cacheStorage = (function() {
// Firefox-specific: we use indexedDB because chrome.storage.local() has
// poor performance in Firefox. See:
// https://bugzilla.mozilla.org/show_bug.cgi?id=1371255
if ( vAPI.webextFlavor.soup.has('firefox') === false ) {
return vAPI.cacheStorage;
}
2017-08-30 00:32:00 +02:00
const STORAGE_NAME = 'uBlock0CacheStorage';
2018-08-06 18:34:41 +02:00
let db;
let pendingInitialization;
let dbByteLength;
2018-08-06 18:34:41 +02:00
let get = function get(input, callback) {
2017-08-30 00:32:00 +02:00
if ( typeof callback !== 'function' ) { return; }
if ( input === null ) {
return getAllFromDb(callback);
}
var toRead, output = {};
if ( typeof input === 'string' ) {
toRead = [ input ];
} else if ( Array.isArray(input) ) {
toRead = input;
} else /* if ( typeof input === 'object' ) */ {
toRead = Object.keys(input);
output = input;
}
return getFromDb(toRead, output, callback);
2018-08-06 18:34:41 +02:00
};
2018-08-06 18:34:41 +02:00
let set = function set(input, callback) {
2017-08-30 00:32:00 +02:00
putToDb(input, callback);
2018-08-06 18:34:41 +02:00
};
2018-08-06 18:34:41 +02:00
let remove = function remove(key, callback) {
2017-08-30 00:32:00 +02:00
deleteFromDb(key, callback);
2018-08-06 18:34:41 +02:00
};
2018-08-06 18:34:41 +02:00
let clear = function clear(callback) {
2017-08-30 00:32:00 +02:00
clearDb(callback);
2018-08-06 18:34:41 +02:00
};
2018-08-06 18:34:41 +02:00
let getBytesInUse = function getBytesInUse(keys, callback) {
getDbSize(callback);
2018-08-06 18:34:41 +02:00
};
let api = {
get,
set,
remove,
clear,
getBytesInUse,
error: undefined
};
2018-08-06 18:34:41 +02:00
let genericErrorHandler = function(ev) {
let error = ev.target && ev.target.error;
if ( error && error.name === 'QuotaExceededError' ) {
api.error = error.name;
}
2018-08-06 18:34:41 +02:00
console.error('[%s]', STORAGE_NAME, error && error.name);
};
function noopfn() {
}
2018-08-06 18:34:41 +02:00
let getDb = function getDb() {
if ( db instanceof IDBDatabase ) {
return Promise.resolve(db);
}
2018-08-06 18:34:41 +02:00
if ( db === null ) {
return Promise.resolve(null);
2017-08-30 00:32:00 +02:00
}
2018-08-06 18:34:41 +02:00
if ( pendingInitialization !== undefined ) {
return pendingInitialization;
2017-08-30 00:32:00 +02:00
}
2017-10-21 14:43:45 +02:00
// https://github.com/gorhill/uBlock/issues/3156
// I have observed that no event was fired in Tor Browser 7.0.7 +
// medium security level after the request to open the database was
// created. When this occurs, I have also observed that the `error`
// property was already set, so this means uBO can detect here whether
// the database can be opened successfully. A try-catch block is
// necessary when reading the `error` property because we are not
// allowed to read this propery outside of event handlers in newer
// implementation of IDBRequest (my understanding).
2018-08-06 18:34:41 +02:00
pendingInitialization = new Promise(resolve => {
let req;
try {
req = indexedDB.open(STORAGE_NAME, 1);
if ( req.error ) {
console.log(req.error);
req = undefined;
}
} catch(ex) {
2017-10-21 14:43:45 +02:00
}
2018-08-06 18:34:41 +02:00
if ( req === undefined ) {
pendingInitialization = undefined;
db = null;
resolve(null);
return;
}
req.onupgradeneeded = function(ev) {
req = undefined;
let db = ev.target.result;
db.onerror = db.onabort = genericErrorHandler;
let table = db.createObjectStore(STORAGE_NAME, { keyPath: 'key' });
table.createIndex('value', 'value', { unique: false });
};
req.onsuccess = function(ev) {
pendingInitialization = undefined;
req = undefined;
db = ev.target.result;
db.onerror = db.onabort = genericErrorHandler;
resolve(db);
};
req.onerror = req.onblocked = function() {
pendingInitialization = undefined;
req = undefined;
db = null;
console.log(this.error);
resolve(null);
};
});
return pendingInitialization;
};
let getFromDb = function(keys, keyvalStore, callback) {
2017-08-30 00:32:00 +02:00
if ( typeof callback !== 'function' ) { return; }
if ( keys.length === 0 ) { return callback(keyvalStore); }
let promises = [];
2018-08-06 18:34:41 +02:00
let gotOne = function() {
if ( typeof this.result !== 'object' ) { return; }
keyvalStore[this.result.key] = this.result.value;
if ( this.result.value instanceof Blob === false ) { return; }
promises.push(
µBlock.lz4Codec.decode(
this.result.key,
this.result.value
).then(result => {
keyvalStore[result.key] = result.data;
})
);
2017-08-30 00:32:00 +02:00
};
2018-08-06 18:34:41 +02:00
getDb().then(( ) => {
2017-08-30 00:32:00 +02:00
if ( !db ) { return callback(); }
2018-08-06 18:34:41 +02:00
let transaction = db.transaction(STORAGE_NAME);
transaction.oncomplete =
transaction.onerror =
transaction.onabort = ( ) => {
Promise.all(promises).then(( ) => {
callback(keyvalStore);
});
};
2018-08-06 18:34:41 +02:00
let table = transaction.objectStore(STORAGE_NAME);
for ( let key of keys ) {
let req = table.get(key);
2017-08-30 00:32:00 +02:00
req.onsuccess = gotOne;
req.onerror = noopfn;
req = undefined;
2017-08-30 00:32:00 +02:00
}
});
2018-08-06 18:34:41 +02:00
};
let visitAllFromDb = function(visitFn) {
2018-08-06 18:34:41 +02:00
getDb().then(( ) => {
if ( !db ) { return visitFn(); }
2018-08-06 18:34:41 +02:00
let transaction = db.transaction(STORAGE_NAME);
transaction.oncomplete =
transaction.onerror =
transaction.onabort = ( ) => visitFn();
let table = transaction.objectStore(STORAGE_NAME);
let req = table.openCursor();
2017-08-30 00:32:00 +02:00
req.onsuccess = function(ev) {
let cursor = ev.target && ev.target.result;
2017-08-30 00:32:00 +02:00
if ( !cursor ) { return; }
let entry = cursor.value;
visitFn(entry);
2017-08-30 00:32:00 +02:00
cursor.continue();
};
});
2018-08-06 18:34:41 +02:00
};
let getAllFromDb = function(callback) {
if ( typeof callback !== 'function' ) { return; }
let promises = [];
let keyvalStore = {};
visitAllFromDb(entry => {
if ( entry === undefined ) {
Promise.all(promises).then(( ) => {
callback(keyvalStore);
});
return;
}
keyvalStore[entry.key] = entry.value;
if ( entry.value instanceof Blob === false ) { return; }
promises.push(
µBlock.lz4Codec.decode(
entry.key,
entry.value
).then(result => {
keyvalStore[result.key] = result.value;
})
);
});
};
let getDbSize = function(callback) {
if ( typeof callback !== 'function' ) { return; }
if ( typeof dbByteLength === 'number' ) {
return Promise.resolve().then(( ) => {
callback(dbByteLength);
});
}
let textEncoder = new TextEncoder();
let totalByteLength = 0;
visitAllFromDb(entry => {
if ( entry === undefined ) {
dbByteLength = totalByteLength;
return callback(totalByteLength);
}
let value = entry.value;
if ( typeof value === 'string' ) {
totalByteLength += textEncoder.encode(value).byteLength;
} else if ( value instanceof Blob ) {
totalByteLength += value.size;
} else {
totalByteLength += textEncoder.encode(JSON.stringify(value)).byteLength;
}
if ( typeof entry.key === 'string' ) {
totalByteLength += textEncoder.encode(entry.key).byteLength;
}
});
};
// https://github.com/uBlockOrigin/uBlock-issues/issues/141
// Mind that IDBDatabase.transaction() and IDBObjectStore.put()
// can throw:
// https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/transaction
// https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/put
let putToDb = function(keyvalStore, callback) {
2017-08-30 00:32:00 +02:00
if ( typeof callback !== 'function' ) {
callback = noopfn;
}
let keys = Object.keys(keyvalStore);
2017-08-30 00:32:00 +02:00
if ( keys.length === 0 ) { return callback(); }
let promises = [ getDb() ];
let entries = [];
let dontCompress = µBlock.hiddenSettings.cacheStorageCompression !== true;
let handleEncodingResult = result => {
entries.push({ key: result.key, value: result.data });
};
for ( let key of keys ) {
let data = keyvalStore[key];
if ( typeof data !== 'string' || dontCompress ) {
entries.push({ key, value: data });
continue;
}
promises.push(
µBlock.lz4Codec.encode(key, data).then(handleEncodingResult)
);
}
Promise.all(promises).then(( ) => {
2017-08-30 00:32:00 +02:00
if ( !db ) { return callback(); }
2018-08-06 18:34:41 +02:00
let finish = ( ) => {
dbByteLength = undefined;
2018-08-06 18:34:41 +02:00
if ( callback === undefined ) { return; }
let cb = callback;
callback = undefined;
cb();
};
try {
let transaction = db.transaction(STORAGE_NAME, 'readwrite');
transaction.oncomplete =
transaction.onerror =
transaction.onabort = finish;
let table = transaction.objectStore(STORAGE_NAME);
for ( let entry of entries ) {
table.put(entry);
}
} catch (ex) {
finish();
2017-08-30 00:32:00 +02:00
}
});
2018-08-06 18:34:41 +02:00
};
2018-08-06 18:34:41 +02:00
let deleteFromDb = function(input, callback) {
2017-08-30 00:32:00 +02:00
if ( typeof callback !== 'function' ) {
callback = noopfn;
}
2018-08-06 18:34:41 +02:00
let keys = Array.isArray(input) ? input.slice() : [ input ];
2017-08-30 00:32:00 +02:00
if ( keys.length === 0 ) { return callback(); }
2018-08-06 18:34:41 +02:00
getDb().then(db => {
2017-08-30 00:32:00 +02:00
if ( !db ) { return callback(); }
2018-08-06 18:34:41 +02:00
let finish = ( ) => {
dbByteLength = undefined;
2018-08-06 18:34:41 +02:00
if ( callback === undefined ) { return; }
let cb = callback;
callback = undefined;
cb();
};
try {
let transaction = db.transaction(STORAGE_NAME, 'readwrite');
transaction.oncomplete =
transaction.onerror =
transaction.onabort = finish;
let table = transaction.objectStore(STORAGE_NAME);
for ( let key of keys ) {
table.delete(key);
}
} catch (ex) {
finish();
2017-08-30 00:32:00 +02:00
}
});
2018-08-06 18:34:41 +02:00
};
2018-08-06 18:34:41 +02:00
let clearDb = function(callback) {
2017-08-30 00:32:00 +02:00
if ( typeof callback !== 'function' ) {
callback = noopfn;
}
2018-08-06 18:34:41 +02:00
getDb().then(db => {
2017-08-30 00:32:00 +02:00
if ( !db ) { return callback(); }
2018-08-06 18:34:41 +02:00
let finish = ( ) => {
dbByteLength = undefined;
2018-08-06 18:34:41 +02:00
if ( callback === undefined ) { return; }
let cb = callback;
callback = undefined;
cb();
};
try {
let req = db.transaction(STORAGE_NAME, 'readwrite')
.objectStore(STORAGE_NAME)
.clear();
req.onsuccess = req.onerror = finish;
} catch (ex) {
finish();
}
2017-08-30 00:32:00 +02:00
});
2018-08-06 18:34:41 +02:00
};
// prime the db so that it's ready asap for next access.
getDb(noopfn);
return api;
}());
/******************************************************************************/