/******************************************************************************* uBlock Origin - a browser extension to block requests. 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 IDBDatabase, indexedDB */ 'use strict'; /******************************************************************************/ import µBlock from './background.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. µBlock.cacheStorage = (function() { const STORAGE_NAME = 'uBlock0CacheStorage'; // Default to webext storage. const localStorage = webext.storage.local; const api = { name: 'browser.storage.local', get: localStorage.get, set: localStorage.set, remove: localStorage.remove, clear: localStorage.clear, getBytesInUse: localStorage.getBytesInUse, 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(); return 'indexedDB'; } clearIDB(); return 'browser.storage.local'; }); } if ( actualBackend === 'browser.storage.local' ) { clearIDB(); } return Promise.resolve('browser.storage.local'); }, error: undefined }; // Reassign API entries to that of indexedDB-based ones const selectIDB = async function() { let db; let dbPromise; let dbTimer; const noopfn = function () { }; const disconnect = function() { if ( dbTimer !== undefined ) { clearTimeout(dbTimer); dbTimer = undefined; } if ( db instanceof IDBDatabase ) { db.close(); db = undefined; } }; const keepAlive = function() { if ( dbTimer !== undefined ) { clearTimeout(dbTimer); } dbTimer = vAPI.setTimeout( ( ) => { dbTimer = undefined; disconnect(); }, Math.max( µBlock.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 propery 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) { 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; }; setTimeout(( ) => { if ( resolve === undefined ) { return; } db = null; dbPromise = undefined; resolve(null); resolve = undefined; }, 5000); }); 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 µBlock.lz4Codec.encode(data, toBlob).then(value => { store.push({ key, value }); }); }; const decompress = function(store, key, data) { return µBlock.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 = µBlock.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; } api.name = 'indexedDB'; api.get = function get(keys) { return 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)); }); }; api.set = function set(keys) { return new Promise(resolve => { putToDb(keys, details => resolve(details)); }); }; api.remove = function remove(keys) { return new Promise(resolve => { deleteFromDb(keys, ( ) => resolve()); }); }; api.clear = function clear() { return new Promise(resolve => { clearDb(( ) => resolve()); }); }; api.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() { const bin = await webext.storage.local.get('assetCacheRegistry'); if ( bin instanceof Object === false || bin.assetCacheRegistry instanceof Object === false ) { return; } const toRemove = [ 'assetCacheRegistry', 'assetSourceRegistry', 'resourcesSelfie', 'selfie' ]; 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) { } }; return api; }()); /******************************************************************************/