summaryrefslogtreecommitdiffstats
path: root/src/js/cachestorage.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 05:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 05:47:55 +0000
commit31d6ff6f931696850c348007241195ab3b2eddc7 (patch)
tree615cb1c57ce9f6611bad93326b9105098f379609 /src/js/cachestorage.js
parentInitial commit. (diff)
downloadublock-origin-31d6ff6f931696850c348007241195ab3b2eddc7.tar.xz
ublock-origin-31d6ff6f931696850c348007241195ab3b2eddc7.zip
Adding upstream version 1.55.0+dfsg.upstream/1.55.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/js/cachestorage.js')
-rw-r--r--src/js/cachestorage.js533
1 files changed, 533 insertions, 0 deletions
diff --git a/src/js/cachestorage.js b/src/js/cachestorage.js
new file mode 100644
index 0000000..ef056af
--- /dev/null
+++ b/src/js/cachestorage.js
@@ -0,0 +1,533 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ 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
+*/
+
+/* global browser, IDBDatabase, indexedDB */
+
+'use strict';
+
+/******************************************************************************/
+
+import lz4Codec from './lz4.js';
+import µb from './background.js';
+import webext from './webext.js';
+
+/******************************************************************************/
+
+// 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"
+
+// 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.storage
+// has been added, for seamless migration of cache-related entries into
+// indexedDB.
+
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1371255
+// Firefox-specific: we use indexedDB because browser.storage.local() has
+// poor performance in Firefox.
+// https://github.com/uBlockOrigin/uBlock-issues/issues/328
+// Use IndexedDB for Chromium as well, to take advantage of LZ4
+// compression.
+// https://github.com/uBlockOrigin/uBlock-issues/issues/399
+// Revert Chromium support of IndexedDB, use advanced setting to force
+// IndexedDB.
+// https://github.com/uBlockOrigin/uBlock-issues/issues/409
+// Allow forcing the use of webext storage on Firefox.
+
+const STORAGE_NAME = 'uBlock0CacheStorage';
+
+// Default to webext storage.
+const storageLocal = webext.storage.local;
+
+let storageReadyResolve;
+const storageReadyPromise = new Promise(resolve => {
+ storageReadyResolve = resolve;
+});
+
+const cacheStorage = {
+ name: 'browser.storage.local',
+ get(...args) {
+ return storageReadyPromise.then(( ) =>
+ storageLocal.get(...args).catch(reason => {
+ console.log(reason);
+ })
+ );
+ },
+ set(...args) {
+ return storageReadyPromise.then(( ) =>
+ storageLocal.set(...args).catch(reason => {
+ console.log(reason);
+ })
+ );
+ },
+ remove(...args) {
+ return storageReadyPromise.then(( ) =>
+ storageLocal.remove(...args).catch(reason => {
+ console.log(reason);
+ })
+ );
+ },
+ clear(...args) {
+ return storageReadyPromise.then(( ) =>
+ storageLocal.clear(...args).catch(reason => {
+ console.log(reason);
+ })
+ );
+ },
+ select: function(selectedBackend) {
+ let actualBackend = selectedBackend;
+ if ( actualBackend === undefined || actualBackend === 'unset' ) {
+ actualBackend = vAPI.webextFlavor.soup.has('firefox')
+ ? 'indexedDB'
+ : 'browser.storage.local';
+ }
+ if ( actualBackend === 'indexedDB' ) {
+ return selectIDB().then(success => {
+ if ( success || selectedBackend === 'indexedDB' ) {
+ clearWebext();
+ storageReadyResolve();
+ return 'indexedDB';
+ }
+ clearIDB();
+ storageReadyResolve();
+ return 'browser.storage.local';
+ });
+ }
+ if ( actualBackend === 'browser.storage.local' ) {
+ clearIDB();
+ }
+ storageReadyResolve();
+ return Promise.resolve('browser.storage.local');
+
+ },
+ error: undefined
+};
+
+// Not all platforms support getBytesInUse
+if ( storageLocal.getBytesInUse instanceof Function ) {
+ cacheStorage.getBytesInUse = function(...args) {
+ return storageLocal.getBytesInUse(...args).catch(reason => {
+ console.log(reason);
+ });
+ };
+}
+
+// Reassign API entries to that of indexedDB-based ones
+const selectIDB = async function() {
+ let db;
+ let dbPromise;
+
+ const noopfn = function () {
+ };
+
+ const disconnect = function() {
+ dbTimer.off();
+ if ( db instanceof IDBDatabase ) {
+ db.close();
+ db = undefined;
+ }
+ };
+
+ const dbTimer = vAPI.defer.create(( ) => {
+ disconnect();
+ });
+
+ const keepAlive = function() {
+ dbTimer.offon(Math.max(
+ µb.hiddenSettings.autoUpdateAssetFetchPeriod * 2 * 1000,
+ 180000
+ ));
+ };
+
+ // 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 property outside of event handlers in newer
+ // implementation of IDBRequest (my understanding).
+
+ const getDb = function() {
+ keepAlive();
+ if ( db !== undefined ) {
+ return Promise.resolve(db);
+ }
+ if ( dbPromise !== undefined ) {
+ return dbPromise;
+ }
+ dbPromise = new Promise(resolve => {
+ let req;
+ try {
+ req = indexedDB.open(STORAGE_NAME, 1);
+ if ( req.error ) {
+ console.log(req.error);
+ req = undefined;
+ }
+ } catch(ex) {
+ }
+ if ( req === undefined ) {
+ db = null;
+ dbPromise = undefined;
+ return resolve(null);
+ }
+ req.onupgradeneeded = function(ev) {
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/2725
+ // If context Firefox + incognito mode, fall back to
+ // browser.storage.local for cache storage purpose.
+ if (
+ vAPI.webextFlavor.soup.has('firefox') &&
+ browser.extension.inIncognitoContext === true
+ ) {
+ return req.onerror();
+ }
+ if ( ev.oldVersion === 1 ) { return; }
+ try {
+ const db = ev.target.result;
+ db.createObjectStore(STORAGE_NAME, { keyPath: 'key' });
+ } catch(ex) {
+ req.onerror();
+ }
+ };
+ req.onsuccess = function(ev) {
+ if ( resolve === undefined ) { return; }
+ req = undefined;
+ db = ev.target.result;
+ dbPromise = undefined;
+ resolve(db);
+ resolve = undefined;
+ };
+ req.onerror = req.onblocked = function() {
+ if ( resolve === undefined ) { return; }
+ req = undefined;
+ console.log(this.error);
+ db = null;
+ dbPromise = undefined;
+ resolve(null);
+ resolve = undefined;
+ };
+ vAPI.defer.once(5000).then(( ) => {
+ if ( resolve === undefined ) { return; }
+ db = null;
+ dbPromise = undefined;
+ resolve(null);
+ resolve = undefined;
+ });
+ });
+ return dbPromise;
+ };
+
+ const fromBlob = function(data) {
+ if ( data instanceof Blob === false ) {
+ return Promise.resolve(data);
+ }
+ return new Promise(resolve => {
+ const blobReader = new FileReader();
+ blobReader.onloadend = ev => {
+ resolve(new Uint8Array(ev.target.result));
+ };
+ blobReader.readAsArrayBuffer(data);
+ });
+ };
+
+ const toBlob = function(data) {
+ const value = data instanceof Uint8Array
+ ? new Blob([ data ])
+ : data;
+ return Promise.resolve(value);
+ };
+
+ const compress = function(store, key, data) {
+ return lz4Codec.encode(data, toBlob).then(value => {
+ store.push({ key, value });
+ });
+ };
+
+ const decompress = function(store, key, data) {
+ return lz4Codec.decode(data, fromBlob).then(data => {
+ store[key] = data;
+ });
+ };
+
+ const getFromDb = async function(keys, keyvalStore, callback) {
+ if ( typeof callback !== 'function' ) { return; }
+ if ( keys.length === 0 ) { return callback(keyvalStore); }
+ const promises = [];
+ const gotOne = function() {
+ if ( typeof this.result !== 'object' ) { return; }
+ const { key, value } = this.result;
+ keyvalStore[key] = value;
+ if ( value instanceof Blob === false ) { return; }
+ promises.push(decompress(keyvalStore, key, value));
+ };
+ try {
+ const db = await getDb();
+ if ( !db ) { return callback(); }
+ const transaction = db.transaction(STORAGE_NAME, 'readonly');
+ transaction.oncomplete =
+ transaction.onerror =
+ transaction.onabort = ( ) => {
+ Promise.all(promises).then(( ) => {
+ callback(keyvalStore);
+ });
+ };
+ const table = transaction.objectStore(STORAGE_NAME);
+ for ( const key of keys ) {
+ const req = table.get(key);
+ req.onsuccess = gotOne;
+ req.onerror = noopfn;
+ }
+ }
+ catch(reason) {
+ console.info(`cacheStorage.getFromDb() failed: ${reason}`);
+ callback();
+ }
+ };
+
+ const visitAllFromDb = async function(visitFn) {
+ const db = await getDb();
+ if ( !db ) { return visitFn(); }
+ const transaction = db.transaction(STORAGE_NAME, 'readonly');
+ transaction.oncomplete =
+ transaction.onerror =
+ transaction.onabort = ( ) => visitFn();
+ const table = transaction.objectStore(STORAGE_NAME);
+ const req = table.openCursor();
+ req.onsuccess = function(ev) {
+ let cursor = ev.target && ev.target.result;
+ if ( !cursor ) { return; }
+ let entry = cursor.value;
+ visitFn(entry);
+ cursor.continue();
+ };
+ };
+
+ const getAllFromDb = function(callback) {
+ if ( typeof callback !== 'function' ) { return; }
+ const promises = [];
+ const keyvalStore = {};
+ visitAllFromDb(entry => {
+ if ( entry === undefined ) {
+ Promise.all(promises).then(( ) => {
+ callback(keyvalStore);
+ });
+ return;
+ }
+ const { key, value } = entry;
+ keyvalStore[key] = value;
+ if ( entry.value instanceof Blob === false ) { return; }
+ promises.push(decompress(keyvalStore, key, value));
+ }).catch(reason => {
+ console.info(`cacheStorage.getAllFromDb() failed: ${reason}`);
+ callback();
+ });
+ };
+
+ // 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
+
+ const putToDb = async function(keyvalStore, callback) {
+ if ( typeof callback !== 'function' ) {
+ callback = noopfn;
+ }
+ const keys = Object.keys(keyvalStore);
+ if ( keys.length === 0 ) { return callback(); }
+ const promises = [ getDb() ];
+ const entries = [];
+ const dontCompress =
+ µb.hiddenSettings.cacheStorageCompression !== true;
+ for ( const key of keys ) {
+ const value = keyvalStore[key];
+ const isString = typeof value === 'string';
+ if ( isString === false || dontCompress ) {
+ entries.push({ key, value });
+ continue;
+ }
+ promises.push(compress(entries, key, value));
+ }
+ const finish = ( ) => {
+ if ( callback === undefined ) { return; }
+ let cb = callback;
+ callback = undefined;
+ cb();
+ };
+ try {
+ const results = await Promise.all(promises);
+ const db = results[0];
+ if ( !db ) { return callback(); }
+ const transaction = db.transaction(
+ STORAGE_NAME,
+ 'readwrite'
+ );
+ transaction.oncomplete =
+ transaction.onerror =
+ transaction.onabort = finish;
+ const table = transaction.objectStore(STORAGE_NAME);
+ for ( const entry of entries ) {
+ table.put(entry);
+ }
+ } catch (ex) {
+ finish();
+ }
+ };
+
+ const deleteFromDb = async function(input, callback) {
+ if ( typeof callback !== 'function' ) {
+ callback = noopfn;
+ }
+ const keys = Array.isArray(input) ? input.slice() : [ input ];
+ if ( keys.length === 0 ) { return callback(); }
+ const finish = ( ) => {
+ if ( callback === undefined ) { return; }
+ let cb = callback;
+ callback = undefined;
+ cb();
+ };
+ try {
+ const db = await getDb();
+ if ( !db ) { return callback(); }
+ const transaction = db.transaction(STORAGE_NAME, 'readwrite');
+ transaction.oncomplete =
+ transaction.onerror =
+ transaction.onabort = finish;
+ const table = transaction.objectStore(STORAGE_NAME);
+ for ( const key of keys ) {
+ table.delete(key);
+ }
+ } catch (ex) {
+ finish();
+ }
+ };
+
+ const clearDb = async function(callback) {
+ if ( typeof callback !== 'function' ) {
+ callback = noopfn;
+ }
+ try {
+ const db = await getDb();
+ if ( !db ) { return callback(); }
+ const transaction = db.transaction(STORAGE_NAME, 'readwrite');
+ transaction.oncomplete =
+ transaction.onerror =
+ transaction.onabort = ( ) => {
+ callback();
+ };
+ transaction.objectStore(STORAGE_NAME).clear();
+ }
+ catch(reason) {
+ console.info(`cacheStorage.clearDb() failed: ${reason}`);
+ callback();
+ }
+ };
+
+ await getDb();
+ if ( !db ) { return false; }
+
+ cacheStorage.name = 'indexedDB';
+ cacheStorage.get = function get(keys) {
+ return storageReadyPromise.then(( ) =>
+ new Promise(resolve => {
+ if ( keys === null ) {
+ return getAllFromDb(bin => resolve(bin));
+ }
+ let toRead, output = {};
+ if ( typeof keys === 'string' ) {
+ toRead = [ keys ];
+ } else if ( Array.isArray(keys) ) {
+ toRead = keys;
+ } else /* if ( typeof keys === 'object' ) */ {
+ toRead = Object.keys(keys);
+ output = keys;
+ }
+ getFromDb(toRead, output, bin => resolve(bin));
+ })
+ );
+ };
+ cacheStorage.set = function set(keys) {
+ return storageReadyPromise.then(( ) =>
+ new Promise(resolve => {
+ putToDb(keys, details => resolve(details));
+ })
+ );
+ };
+ cacheStorage.remove = function remove(keys) {
+ return storageReadyPromise.then(( ) =>
+ new Promise(resolve => {
+ deleteFromDb(keys, ( ) => resolve());
+ })
+ );
+ };
+ cacheStorage.clear = function clear() {
+ return storageReadyPromise.then(( ) =>
+ new Promise(resolve => {
+ clearDb(( ) => resolve());
+ })
+ );
+ };
+ cacheStorage.getBytesInUse = function getBytesInUse() {
+ return Promise.resolve(0);
+ };
+ return true;
+};
+
+// https://github.com/uBlockOrigin/uBlock-issues/issues/328
+// Delete cache-related entries from webext storage.
+const clearWebext = async function() {
+ let bin;
+ try {
+ bin = await webext.storage.local.get('assetCacheRegistry');
+ } catch(ex) {
+ console.error(ex);
+ }
+ if ( bin instanceof Object === false ) { return; }
+ if ( bin.assetCacheRegistry instanceof Object === false ) { return; }
+ const toRemove = [
+ 'assetCacheRegistry',
+ 'assetSourceRegistry',
+ ];
+ for ( const key in bin.assetCacheRegistry ) {
+ if ( bin.assetCacheRegistry.hasOwnProperty(key) ) {
+ toRemove.push('cache/' + key);
+ }
+ }
+ webext.storage.local.remove(toRemove);
+};
+
+const clearIDB = function() {
+ try {
+ indexedDB.deleteDatabase(STORAGE_NAME);
+ } catch(ex) {
+ }
+};
+
+/******************************************************************************/
+
+export default cacheStorage;
+
+/******************************************************************************/