diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 4c64182ee..22875299d 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -985,6 +985,24 @@ define([ }, {timeout: -1}); }; + common.disableCache = function (disabled, cb) { + postMessage("CACHE_DISABLE", disabled, cb); + }; + window.addEventListener('storage', function (e) { + if (e.key !== 'CRYPTPAD_STORE|disableCache') { return; } + var o = e.oldValue; + var n = e.newValue; + if (n) { + Cache.disable(); + common.disableCache(true, function () {}); + } else { + Cache.enable(); + common.disableCache(false, function () {}); + } + }); + if (localStorage['CRYPTPAD_STORE|disableCache']) { + Cache.disable(); + } // Admin common.adminRpc = function (data, cb) { @@ -2226,6 +2244,7 @@ define([ localToken: tryParsing(localStorage.getItem(Constants.tokenKey)), // TODO move this to LocalStore ? language: common.getLanguage(), cache: rdyCfg.cache, + disableCache: localStorage['CRYPTPAD_STORE|disableCache'], driveEvents: true //rdyCfg.driveEvents // Boolean }; common.userHash = userHash; diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 1745d0dd7..23412bf60 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -37,6 +37,9 @@ define([ var onReadyEvt = Util.mkEvent(true); var onCacheReadyEvt = Util.mkEvent(true); + // XXX Number of days before deleting the cache for a channel or blob + var CACHE_MAX_AGE = 90; // DAYS + // Default settings for new users var NEW_USER_SETTINGS = { drive: { @@ -334,7 +337,6 @@ define([ if (!s.rpc) { return void cb({error: 'RPC_NOT_READY'}); } s.rpc.removeOwnedChannel(channel, function (err) { - if (!err) { Cache.clearChannel(channel); } cb({error:err}); }); }; @@ -2861,6 +2863,15 @@ define([ }, PING_INTERVAL); }; + Store.disableCache = function (clientId, disabled, cb) { + if (disabled) { + Cache.disable(); + } else { + Cache.enable(); + } + cb(); + }; + /** * Data: * - userHash or anonHash @@ -2901,6 +2912,11 @@ define([ }); }); } + + if (data.disableCache) { + Cache.disable(); + } + initialized = true; postMessage = function (clientId, cmd, d, cb) { data.query(clientId, cmd, d, cb); @@ -2920,6 +2936,27 @@ define([ callback(ret); }); + + // Clear inactive channels from cache + onReadyEvt.reg(function () { + var inactiveTime = (+new Date()) - CACHE_MAX_AGE * (24 * 3600 * 1000); + Cache.getKeys(function (err, keys) { + if (err) { return void console.error(err); } + var next = function (cb) { + if (!keys.length) { return; } + var key = keys.pop(); + var value = Cache.getTime(key, function (err, atime) { + if (err) { return void next(); } + if (!atime || atime < inactiveTime) { + Cache.clearChannel(key, next()); + return; + } + next(); + }); + }; + next(); + }); + }); }; Store.disconnect = function () { diff --git a/www/common/outer/cache-store.js b/www/common/outer/cache-store.js index a7b1bd3d7..45f334bb4 100644 --- a/www/common/outer/cache-store.js +++ b/www/common/outer/cache-store.js @@ -7,10 +7,14 @@ define([ // Check if indexedDB is allowed var allowed = false; + var disabled = false; + var supported = false; + try { var request = window.indexedDB.open('test_db', 1); request.onsuccess = function () { - allowed = true; + supported = true; + allowed = supported && !disabled; onReady.fire(); }; request.onerror = function () { @@ -20,6 +24,15 @@ define([ onReady.fire(); } + S.enable = function () { + disabled = false; + allowed = supported && !disabled; + }; + S.disable = function () { + disabled = true; + allowed = supported && !disabled; + }; + var cache = localForage.createInstance({ driver: localForage.INDEXEDDB, name: "cp_cache" @@ -141,6 +154,30 @@ define([ }); }; + S.getKeys = function (cb) { + cb = Util.once(Util.mkAsync(cb || function () {})); + onReady.reg(function () { + if (!allowed) { return void cb('NOCACHE'); } + cache.keys().then(function (keys) { + cb(null, keys); + }).catch(function (err) { + cb(err); + }); + }); + }; + S.getTime = function (id, cb) { + cb = Util.once(Util.mkAsync(cb || function () {})); + onReady.reg(function () { + if (!allowed) { return void cb('NOCACHE'); } + cache.getItem(id, function (err, obj) { + if (err || !obj || !obj.c) { + return void cb(Util.serializeError(err || 'EINVAL')); + } + cb(null, obj.t); + }); + }); + }; + self.CryptPad_clearIndexedDB = S.clear; return S; diff --git a/www/common/outer/store-rpc.js b/www/common/outer/store-rpc.js index 767448284..5bae1506f 100644 --- a/www/common/outer/store-rpc.js +++ b/www/common/outer/store-rpc.js @@ -14,6 +14,7 @@ define([ CREATE_README: Store.createReadme, MIGRATE_ANON_DRIVE: Store.migrateAnonDrive, PING: function (cId, data, cb) { cb(); }, + CACHE_DISABLE: Store.disableCache, // RPC UPDATE_PIN_LIMIT: Store.updatePinLimit, GET_PIN_LIMIT: Store.getPinLimit, diff --git a/www/common/outer/worker-channel.js b/www/common/outer/worker-channel.js index a359d8836..06b38a96b 100644 --- a/www/common/outer/worker-channel.js +++ b/www/common/outer/worker-channel.js @@ -76,9 +76,15 @@ define([ }; // Fire an event. channel.event('EV_SOMETHING', { args: "whatever" }); - var event = chan.event = function (e, content) { + var event = chan.event = function (e, content, opts) { + opts = opts || {}; evReady.reg(function () { - postMsg(JSON.stringify({ content: content, q: e })); + var toSend = { + content: content, + q: e, + raw: opts.raw + }; + postMsg(opts.raw ? toSend : JSON.stringify(toSend)); }); }; diff --git a/www/common/pinpad.js b/www/common/pinpad.js index 2841656bc..1e2031088 100644 --- a/www/common/pinpad.js +++ b/www/common/pinpad.js @@ -1,6 +1,6 @@ (function () { var factory = function (Util, Rpc) { - var create = function (network, proxy, _cb) { + var create = function (network, proxy, _cb, Cache) { if (typeof(_cb) !== 'function') { throw new Error("Expected callback"); } var cb = Util.once(Util.mkAsync(_cb)); @@ -155,6 +155,9 @@ var factory = function (Util, Rpc) { if (e) { return void cb(e); } if (response && response.length && response[0] === "OK") { cb(); + if (Cache && Cache.clearChannel) { + Cache.clearChannel(channel); + } } else { cb('INVALID_RESPONSE'); } diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 1d5597ab2..70aef650a 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -1476,6 +1476,21 @@ define([ }); }); + sframeChan.on('Q_CACHE_DISABLE', function (data, cb) { + if (data.disabled) { + Utils.Cache.clear(function () { + Utils.Cache.disable(); + }); + Cryptpad.disableCache(true, cb); + return; + } + Utils.Cache.enable(); + Cryptpad.disableCache(false, cb); + }); + sframeChan.on('Q_CLEAR_CACHE', function (data, cb) { + Utils.Cache.clear(cb); + }); + sframeChan.on('Q_PIN_GET_USAGE', function (teamId, cb) { Cryptpad.isOverPinLimit(teamId, function (err, overLimit, data) { cb({ diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index 6fecd26f0..9304d9469 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -758,7 +758,7 @@ define([ window.cryptpadStore._put(k, v, cb); var x = {}; x[k] = v; - ctx.sframeChan.event('EV_LOCALSTORE_PUT', x); + ctx.sframeChan.event('EV_LOCALSTORE_PUT', x, {raw:true}); }; }); diff --git a/www/settings/inner.js b/www/settings/inner.js index b4290becb..7d175cab6 100644 --- a/www/settings/inner.js +++ b/www/settings/inner.js @@ -60,6 +60,7 @@ define([ 'cp-settings-autostore', 'cp-settings-safe-links', 'cp-settings-userfeedback', + 'cp-settings-cache', ], 'drive': [ 'cp-settings-resettips', @@ -359,6 +360,59 @@ define([ return $div; }; + // XXX + Messages.settings_cacheTitle = "Cache"; + Messages.settings_cacheHint = "CryptPad stores parts of your documents in your browser's memory in order to save network usage and improve loading times. The documents stored in cache can then be loaded faster the next time you visit them. You can disable the cache if your device doesn't have a lot of free storage space. For security reasons, the cache is always cleared when you log out, but you can clear it manually if you want to reclaim storage space on your machine."; + Messages.settings_cacheCheckbox = "Enable cache on this device"; + Messages.settings_cacheButton = "Clear existing cache"; + makeBlock('cache', function (cb, $div) { + var store = window.cryptpadStore; + + var $cbox = $(UI.createCheckbox('cp-settings-cache', + Messages.settings_cacheCheckbox, + false, { label: { class: 'noTitle' } })); + var spinner = UI.makeSpinner($cbox); + + // Checkbox: "Enable safe links" + var $checkbox = $cbox.find('input').on('change', function() { + spinner.spin(); + var val = !$checkbox.is(':checked') ? '1' : undefined; + store.put('disableCache', val, function () { + sframeChan.query('Q_CACHE_DISABLE', { + disabled: Boolean(val) + }, function () { + spinner.done(); + }); + }); + }); + + store.get('disableCache', function (val) { + if (!val) { + $checkbox.attr('checked', 'checked'); + } + }); + + var button = h('button.btn.btn-danger', [ + h('i.fa.fa-trash-o'), + h('span', Messages.settings_cacheButton) + ]); + var buttonContainer = h('div.cp-settings-clear-cache', button); + var spinner2 = UI.makeSpinner($(buttonContainer)); + UI.confirmButton(button, { + classes: 'btn-danger' + }, function () { + spinner.spin(); + sframeChan.query('Q_CLEAR_CACHE', null, function() { + spinner.done(); + }); + }); + + cb([ + $cbox[0], + buttonContainer + ]); + }, true); + create['delete'] = function() { if (!common.isLoggedIn()) { return; } var $div = $('