diff --git a/js/backup.js b/js/backup.js index 3e0717199a..7643634333 100644 --- a/js/backup.js +++ b/js/backup.js @@ -119,7 +119,7 @@ _.each(storeNames, function(storeName) { var transaction = idb_db.transaction(storeNames, 'readwrite'); transaction.onerror = function(e) { - handleDOMException( + Whisper.Database.handleDOMException( 'exportToJsonFile transaction error (store: ' + storeName + ')', transaction.error, reject @@ -133,7 +133,7 @@ var request = store.openCursor(); var count = 0; request.onerror = function(e) { - handleDOMException( + Whisper.Database.handleDOMException( 'exportToJsonFile request error (store: ' + storeNames + ')', request.error, reject @@ -184,16 +184,6 @@ }); } - function handleDOMException(prefix, error, reject) { - console.log( - prefix + ':', - error && error.name, - error && error.message, - error && error.code - ); - reject(error || new Error(prefix)); - } - function eliminateClientConfigInBackup(data, path) { var cleaned = _.pick(data, 'conversations', 'groups'); console.log('Writing configuration-free backup file back to disk'); @@ -261,7 +251,7 @@ var transaction = idb_db.transaction(storeNames, 'readwrite'); transaction.onerror = function(e) { - handleDOMException( + Whisper.Database.handleDOMException( 'importFromJsonString transaction error', transaction.error, reject @@ -321,7 +311,7 @@ } }; request.onerror = function(e) { - handleDOMException( + Whisper.Database.handleDOMException( 'importFromJsonString request error (store: ' + storeName + ')', request.error, reject @@ -338,27 +328,6 @@ }); } - function openDatabase() { - var migrations = Whisper.Database.migrations; - var version = migrations[migrations.length - 1].version; - var DBOpenRequest = window.indexedDB.open('signal', version); - - return new Promise(function(resolve, reject) { - // these two event handlers act on the IDBDatabase object, - // when the database is opened successfully, or not - DBOpenRequest.onerror = reject; - DBOpenRequest.onsuccess = function() { - resolve(DBOpenRequest.result); - }; - - // This event handles the event whereby a new version of - // the database needs to be created Either one has not - // been created before, or a new version number has been - // submitted via the window.indexedDB.open line above - DBOpenRequest.onupgradeneeded = reject; - }); - } - function createDirectory(parent, name) { return new Promise(function(resolve, reject) { var sanitized = sanitizeFileName(name); @@ -492,7 +461,7 @@ return new Promise(function(resolve, reject) { var transaction = idb_db.transaction('messages', 'readwrite'); transaction.onerror = function(e) { - handleDOMException( + Whisper.Database.handleDOMException( 'exportConversation transaction error (conversation: ' + name + ')', transaction.error, reject @@ -514,7 +483,7 @@ stream.write('{"messages":['); request.onerror = function(e) { - handleDOMException( + Whisper.Database.handleDOMException( 'exportConversation request error (conversation: ' + name + ')', request.error, reject @@ -608,7 +577,7 @@ return new Promise(function(resolve, reject) { var transaction = idb_db.transaction('conversations', 'readwrite'); transaction.onerror = function(e) { - handleDOMException( + Whisper.Database.handleDOMException( 'exportConversations transaction error', transaction.error, reject @@ -622,7 +591,7 @@ var store = transaction.objectStore('conversations'); var request = store.openCursor(); request.onerror = function(e) { - handleDOMException( + Whisper.Database.handleDOMException( 'exportConversations request error', request.error, reject @@ -716,7 +685,7 @@ var transaction = idb_db.transaction('messages', 'readwrite'); transaction.onerror = function(e) { - handleDOMException( + Whisper.Database.handleDOMException( 'saveAllMessages transaction error', transaction.error, reject @@ -744,7 +713,7 @@ } }; request.onerror = function(e) { - handleDOMException( + Whisper.Database.handleDOMException( 'saveAllMessages request error', request.error, reject @@ -879,7 +848,7 @@ return new Promise(function(resolve, reject) { var transaction = idb_db.transaction(storeName, 'readwrite'); transaction.onerror = function(e) { - handleDOMException( + Whisper.Database.handleDOMException( 'assembleLookup(' + storeName + ') transaction error', transaction.error, reject @@ -893,7 +862,7 @@ var store = transaction.objectStore(storeName); var request = store.openCursor(); request.onerror = function(e) { - handleDOMException( + Whisper.Database.handleDOMException( 'assembleLookup(' + storeName + ') request error', request.error, reject @@ -912,59 +881,6 @@ }); } - function clearAllStores(idb_db) { - return clearStores(idb_db); - } - - function clearStores(idb_db, names) { - return new Promise(function(resolve, reject) { - var storeNames = names || idb_db.objectStoreNames; - console.log('Clearing these indexeddb stores:', storeNames); - var transaction = idb_db.transaction(storeNames, 'readwrite'); - - var finished = false; - var finish = function(via) { - console.log('clearing all stores done via', via); - if (finished) { - resolve(); - } - finished = true; - }; - - transaction.oncomplete = finish.bind(null, 'transaction complete'); - transaction.onerror = function(e) { - handleDOMException( - 'clearStores transaction error', - transaction.error, - reject - ); - }; - - var count = 0; - _.forEach(storeNames, function(storeName) { - var store = transaction.objectStore(storeName); - var request = store.clear(); - - request.onsuccess = function() { - count += 1; - console.log('Done clearing store', storeName); - - if (count >= storeNames.length) { - console.log('Done clearing all indexeddb stores'); - return finish('clears complete'); - } - }; - - request.onerror = function(e) { - handleDOMException( - 'clearStores request error', - request.error, - reject - ); - }; - }); - }); - } function getTimestamp() { return moment().format('YYYY MMM Do [at] h.mm.ss a'); @@ -972,16 +888,6 @@ // directories returned and taken by backup/import are all string paths Whisper.Backup = { - clearDatabase: function() { - return openDatabase().then(function(idb_db) { - return clearAllStores(idb_db); - }); - }, - clearStores: function(storeNames) { - return openDatabase().then(function(idb_db) { - return clearStores(idb_db, storeNames); - }); - }, getDirectoryForExport: function() { var options = { title: i18n('exportChooserTitle'), @@ -992,7 +898,7 @@ exportToDirectory: function(directory, options) { var dir; var idb; - return openDatabase().then(function(idb_db) { + return Whisper.Database.open().then(function(idb_db) { idb = idb_db; var name = 'Signal Export ' + getTimestamp(); return createDirectory(directory, name); @@ -1025,7 +931,7 @@ options = options || {}; var idb, nonMessageResult; - return openDatabase().then(function(idb_db) { + return Whisper.Database.open().then(function(idb_db) { idb = idb_db; return Promise.all([ @@ -1054,7 +960,6 @@ }); }, // for testing - handleDOMException, sanitizeFileName, trimFileName, getAttachmentFileName, diff --git a/js/database.js b/js/database.js index f10cd9b0a6..84c4332f22 100644 --- a/js/database.js +++ b/js/database.js @@ -1,4 +1,5 @@ /* global Whisper: false */ +/* global Backbone: false */ /* eslint-disable more/no-then */ // eslint-disable-next-line func-names @@ -10,6 +11,114 @@ window.Whisper.Database.id = window.Whisper.Database.id || 'signal'; window.Whisper.Database.nolog = true; + Whisper.Database.handleDOMException = (prefix, error, reject) => { + console.log( + `${prefix}:`, + error && error.name, + error && error.message, + error && error.code + ); + reject(error || new Error(prefix)); + }; + + function clearStores(db, names) { + return new Promise(((resolve, reject) => { + const storeNames = names || db.objectStoreNames; + console.log('Clearing these indexeddb stores:', storeNames); + const transaction = db.transaction(storeNames, 'readwrite'); + + let finished = false; + const finish = (via) => { + console.log('clearing all stores done via', via); + if (finished) { + resolve(); + } + finished = true; + }; + + transaction.oncomplete = finish.bind(null, 'transaction complete'); + transaction.onerror = () => { + Whisper.Database.handleDOMException( + 'clearStores transaction error', + transaction.error, + reject + ); + }; + + let count = 0; + storeNames.forEach((storeName) => { + const store = transaction.objectStore(storeName); + const request = store.clear(); + + request.onsuccess = () => { + count += 1; + console.log('Done clearing store', storeName); + + if (count >= storeNames.length) { + console.log('Done clearing all indexeddb stores'); + finish('clears complete'); + } + }; + + request.onerror = () => { + Whisper.Database.handleDOMException( + 'clearStores request error', + request.error, + reject + ); + }; + }); + })); + } + + Whisper.Database.open = () => { + const { migrations } = Whisper.Database; + const { version } = migrations[migrations.length - 1]; + const DBOpenRequest = window.indexedDB.open(Whisper.Database.id, version); + + return new Promise(((resolve, reject) => { + // these two event handlers act on the IDBDatabase object, + // when the database is opened successfully, or not + DBOpenRequest.onerror = reject; + DBOpenRequest.onsuccess = () => resolve(DBOpenRequest.result); + + // This event handles the event whereby a new version of + // the database needs to be created Either one has not + // been created before, or a new version number has been + // submitted via the window.indexedDB.open line above + DBOpenRequest.onupgradeneeded = reject; + })); + }; + + Whisper.Database.clear = async () => { + const db = await Whisper.Database.open(); + return clearStores(db); + }; + + Whisper.Database.clearStores = async (storeNames) => { + const db = await Whisper.Database.open(); + return clearStores(db, storeNames); + }; + + Whisper.Database.close = () => window.wrapDeferred(Backbone.sync('closeall')); + + Whisper.Database.drop = () => + new Promise(((resolve, reject) => { + const request = window.indexedDB.deleteDatabase(Whisper.Database.id); + + request.onblocked = () => { + reject(new Error('Error deleting database: Blocked.')); + }; + request.onupgradeneeded = () => { + reject(new Error('Error deleting database: Upgrade needed.')); + }; + request.onerror = () => { + reject(new Error('Error deleting database.')); + }; + + request.onsuccess = resolve; + })); + Whisper.Database.migrations = [ { version: '12.0', diff --git a/js/modules/logs.js b/js/modules/logs.js new file mode 100644 index 0000000000..de49e469fb --- /dev/null +++ b/js/modules/logs.js @@ -0,0 +1,19 @@ +const { ipcRenderer } = require('electron'); + +/* eslint-env node */ + +module.exports = { + deleteAll, +}; + +function deleteAll() { + return new Promise((resolve, reject) => { + ipcRenderer.once('delete-all-logs-complete', resolve); + + setTimeout(() => { + reject(new Error('Request to delete all logs timed out')); + }, 5000); + + ipcRenderer.send('delete-all-logs'); + }); +} diff --git a/js/signal_protocol_store.js b/js/signal_protocol_store.js index 83541955fe..cfa3d057d3 100644 --- a/js/signal_protocol_store.js +++ b/js/signal_protocol_store.js @@ -860,14 +860,14 @@ ConversationController.reset(); // conversations store // Then, the entire database: - return window.Whisper.Backup.clearDatabase(); + return Whisper.Database.clear(); }, removeAllConfiguration: function() { // First the in-memory cache for the items store: window.storage.reset(); // Then anything in the database that isn't a message/conversation/group: - return window.Whisper.Backup.clearStores([ + return Whisper.Database.clearStores([ 'items', 'identityKeys', 'sessions', diff --git a/js/views/import_view.js b/js/views/import_view.js index 92752a945b..f95e14f83a 100644 --- a/js/views/import_view.js +++ b/js/views/import_view.js @@ -35,7 +35,7 @@ return storage.put(IMPORT_LOCATION, location); }, reset: function() { - return Whisper.Backup.clearDatabase(); + return Whisper.Database.clear(); } }; @@ -129,7 +129,7 @@ // Wait for prior database interaction to complete this.pending = this.pending.then(function() { // For resilience to interruption, clear database both before and on failure - return Whisper.Backup.clearDatabase(); + return Whisper.Import.reset(); }).then(function() { return Promise.all([ Whisper.Import.start(), @@ -153,7 +153,7 @@ this.state = null; this.render(); - return Whisper.Backup.clearDatabase(); + return Whisper.Import.reset(); }.bind(this)); }, finishLightImport: function(directory) { diff --git a/js/views/settings_view.js b/js/views/settings_view.js index 0aac296293..723a437a15 100644 --- a/js/views/settings_view.js +++ b/js/views/settings_view.js @@ -141,7 +141,7 @@ this.step = CLEAR_DATA_STEPS.DELETING; this.render(); - window.wrapDeferred(Backbone.sync('closeall')).then(function() { + Whisper.Database.close().then(function() { console.log('All database connections closed. Starting delete.'); this.clearAllData(); }.bind(this), function(error) { @@ -150,44 +150,18 @@ }.bind(this)); }, clearAllData: function() { - var finishCount = 0; - var finish = function() { - finishCount += 1; - console.log('Deletion complete, finishCount is now', finishCount); - if (finishCount > 1) { - console.log('Deletion complete! Restarting now...'); - window.restart(); - } - }; - - var request = window.indexedDB.deleteDatabase('signal'); - - // None of the three of these should happen, since we close all database - // connections first. However, testing indicates that even if one of these - // handlers fires, the database is still deleted on restart. - request.onblocked = function(event) { - console.log('Error deleting database: Blocked.'); - finish(); - }; - request.onupgradeneeded = function(event) { - console.log('Error deleting database: Upgrade needed.'); - finish(); - }; - request.onerror = function(event) { - console.log('Error deleting database.'); - finish(); - }; - - request.onsuccess = function(event) { - console.log('Database deleted successfully.'); - finish(); - }; - - Whisper.events.once('deleteAllLogsComplete', function() { - console.log('Log deleted successfully.'); - finish(); + Promise.all([ + Signal.Logs.deleteAll(), + Whisper.Database.drop(), + ]).then(function() { + window.restart(); + }, function(error) { + console.log( + 'Something went wrong deleting all data:', + error && error.stack ? error.stack : error + ); + window.restart(); }); - window.deleteAllLogs(); }, render_attributes: function() { return { diff --git a/preload.js b/preload.js index 80c33f6114..7ce82a33bd 100644 --- a/preload.js +++ b/preload.js @@ -43,18 +43,10 @@ ipc.send('update-tray-icon', unreadCount); }; - window.deleteAllLogs = function() { - ipc.send('delete-all-logs'); - } - ipc.on('debug-log', function() { Whisper.events.trigger('showDebugLog'); }); - ipc.on('delete-all-logs-complete', function() { - Whisper.events.trigger('deleteAllLogsComplete'); - }); - ipc.on('set-up-with-import', function() { Whisper.events.trigger('setupWithImport'); }); @@ -115,6 +107,8 @@ // ES2015+ modules window.Signal = window.Signal || {}; window.Signal.OS = require('./js/modules/os'); + window.Signal.Logs = require('./js/modules/logs'); + window.Signal.Types = window.Signal.Types || {}; window.Signal.Types.Attachment = require('./js/modules/types/attachment'); window.Signal.Types.Errors = require('./js/modules/types/errors'); diff --git a/test/backup_test.js b/test/backup_test.js index 89054583ac..fadae90bdd 100644 --- a/test/backup_test.js +++ b/test/backup_test.js @@ -1,37 +1,6 @@ 'use strict'; describe('Backup', function() { - describe('handleDOMException', function() { - it('handles null, still calls reject', function() { - var called = 0; - var reject = function() { - called += 1; - }; - var error = null; - var prefix = 'something'; - - Whisper.Backup.handleDOMException(prefix, error, reject); - - assert.strictEqual(called, 1); - }); - - it('handles object code and message', function() { - var called = 0; - var reject = function() { - called += 1; - }; - var error = { - code: 4, - message: 'some cryptic error', - }; - var prefix = 'something'; - - Whisper.Backup.handleDOMException(prefix, error, reject); - - assert.strictEqual(called, 1); - }); - }); - describe('sanitizeFileName', function() { it('leaves a basic string alone', function() { var initial = 'Hello, how are you #5 (\'fine\' + great).jpg'; diff --git a/test/database_test.js b/test/database_test.js new file mode 100644 index 0000000000..7d67ad3563 --- /dev/null +++ b/test/database_test.js @@ -0,0 +1,34 @@ +'use strict'; + +describe('Database', function() { + describe('handleDOMException', function() { + it('handles null, still calls reject', function() { + var called = 0; + var reject = function() { + called += 1; + }; + var error = null; + var prefix = 'something'; + + Whisper.Database.handleDOMException(prefix, error, reject); + + assert.strictEqual(called, 1); + }); + + it('handles object code and message', function() { + var called = 0; + var reject = function() { + called += 1; + }; + var error = { + code: 4, + message: 'some cryptic error', + }; + var prefix = 'something'; + + Whisper.Database.handleDOMException(prefix, error, reject); + + assert.strictEqual(called, 1); + }); + }); +}); diff --git a/test/index.html b/test/index.html index 52a3b74bfc..2c6c101e7e 100644 --- a/test/index.html +++ b/test/index.html @@ -640,6 +640,7 @@ +