diff --git a/config.example.js b/config.example.js index 0af71afbb..2c234aee4 100644 --- a/config.example.js +++ b/config.example.js @@ -325,4 +325,12 @@ module.exports = { // '/etc/apache2/ssl/my_public_cert.crt', // '/etc/apache2/ssl/my_certificate_authorities_cert_chain.ca' //], + + /* You can get a repl for debugging the server if you want it. + * to enable this, specify the debugReplName and then you can + * connect to it with `nc -U /tmp/repl/.sock` + * If you run multiple cryptpad servers, you need to use different + * repl names. + */ + //debugReplName: "cryptpad" }; diff --git a/customize.dist/login.js b/customize.dist/login.js index b7a9d4e84..eb229a6e9 100644 --- a/customize.dist/login.js +++ b/customize.dist/login.js @@ -6,10 +6,17 @@ define([ '/common/outer/network-config.js', '/customize/credential.js', '/bower_components/chainpad/chainpad.dist.js', + '/common/common-realtime.js', + '/common/common-constants.js', + '/common/common-interface.js', + '/common/common-feedback.js', + '/common/outer/local-store.js', + '/customize/messages.js', '/bower_components/tweetnacl/nacl-fast.min.js', '/bower_components/scrypt-async/scrypt-async.min.js', // better load speed -], function ($, Listmap, Crypto, Util, NetConfig, Cred, ChainPad) { +], function ($, Listmap, Crypto, Util, NetConfig, Cred, ChainPad, Realtime, Constants, UI, + Feedback, LocalStore, Messages) { var Exports = { Cred: Cred, }; @@ -142,5 +149,127 @@ define([ }); }; + Exports.loginOrRegisterUI = function (uname, passwd, isRegister, shouldImport, testing, test) { + var hashing = true; + + var proceed = function (result) { + var proxy = result.proxy; + proxy.edPublic = result.edPublic; + proxy.edPrivate = result.edPrivate; + proxy.curvePublic = result.curvePublic; + proxy.curvePrivate = result.curvePrivate; + + if (isRegister) { + Feedback.send('REGISTRATION', true); + } else { + Feedback.send('LOGIN', true); + } + + Realtime.whenRealtimeSyncs(result.realtime, function () { + try { + LocalStore.login(result.userHash, result.userName, function () { + hashing = false; + if (test && typeof test === "function" && test()) { console.log('testing'); + return; } + if (shouldImport) { + sessionStorage.migrateAnonDrive = 1; + } + if (sessionStorage.redirectTo) { + var h = sessionStorage.redirectTo; + var parser = document.createElement('a'); + parser.href = h; + if (parser.origin === window.location.origin) { + delete sessionStorage.redirectTo; + window.location.href = h; + return; + } + } + window.location.href = '/drive/'; + }); + } catch (e) { console.error(e); } + }); + }; + + // setTimeout 100ms to remove the keyboard on mobile devices before the loading screen + // pops up + window.setTimeout(function () { + UI.addLoadingScreen({ + loadingText: Messages.login_hashing, + hideTips: true, + }); + // We need a setTimeout(cb, 0) otherwise the loading screen is only displayed + // after hashing the password + window.setTimeout(function () { + Exports.loginOrRegister(uname, passwd, isRegister, function (err, result) { + var proxy; + if (result) { proxy = result.proxy; } + + if (err) { + switch (err) { + case 'NO_SUCH_USER': + UI.removeLoadingScreen(function () { + UI.alert(Messages.login_noSuchUser, function () { + hashing = false; + }); + }); + break; + case 'INVAL_USER': + UI.removeLoadingScreen(function () { + UI.alert(Messages.login_invalUser, function () { + hashing = false; + }); + }); + break; + case 'INVAL_PASS': + UI.removeLoadingScreen(function () { + UI.alert(Messages.login_invalPass, function () { + hashing = false; + }); + }); + break; + case 'PASS_TOO_SHORT': + UI.removeLoadingScreen(function () { + var warning = Messages._getKey('register_passwordTooShort', [ + Cred.MINIMUM_PASSWORD_LENGTH + ]); + UI.alert(warning, function () { + hashing = false; + }); + }); + break; + case 'ALREADY_REGISTERED': + // logMeIn should reset registering = false + UI.removeLoadingScreen(function () { + UI.confirm(Messages.register_alreadyRegistered, function (yes) { + if (!yes) { return; } + proxy.login_name = uname; + + if (!proxy[Constants.displayNameKey]) { + proxy[Constants.displayNameKey] = uname; + } + LocalStore.eraseTempSessionValues(); + proceed(result); + }); + }); + break; + default: // UNHANDLED ERROR + hashing = false; + UI.errorLoadingScreen(Messages.login_unhandledError); + } + return; + } + + if (testing) { return void proceed(result); } + + proxy.login_name = uname; + proxy[Constants.displayNameKey] = uname; + sessionStorage.createReadme = 1; + + proceed(result); + }); + }, 0); + }, 200); + }; + return Exports; }); diff --git a/customize.dist/src/less2/include/creation.less b/customize.dist/src/less2/include/creation.less index aa94beb28..17d72f28f 100644 --- a/customize.dist/src/less2/include/creation.less +++ b/customize.dist/src/less2/include/creation.less @@ -33,16 +33,26 @@ flex-wrap: wrap; justify-content: center; align-items: center; - h2, p { - width: 100%; - } h2 { + width: 100%; display: flex; + margin-bottom: 20px; justify-content: space-between; .cp-creation-help { display: none; } } + .cp-creation-help-container { + display: flex; + justify-content: space-between; + p { + padding: 0 20px; + flex-grow: 0; + flex-shrink: 0; + flex-basis: 50%; + text-align: justify; + } + } @media screen and (max-width: 500px) { width: ~"calc(100% - 30px)"; } @@ -50,7 +60,7 @@ h2 .cp-creation-help { display: inline; } - p { + .cp-creation-help-container { display: none; } } diff --git a/customize.dist/translations/messages.el.js b/customize.dist/translations/messages.el.js old mode 100755 new mode 100644 diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 32c61825f..253345c1a 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -866,16 +866,16 @@ define(function () { out.creation_ownedTitle = "Type of pad"; out.creation_ownedTrue = "Owned pad"; out.creation_ownedFalse = "Open pad"; - out.creation_owned1 = "An owned pad is a pad that you can delete from the server whenever you want. Once it is deleted, no one else can access it, even if it is stored in their CryptDrive."; + out.creation_owned1 = "An owned pad can be deleted from the server whenever the owner wants. Deleting an owned pad removes it from other users' CryptDrives."; out.creation_owned2 = "An open pad doesn't have any owner and thus, it can't be deleted from the server unless it has reached its expiration time."; out.creation_expireTitle = "Life time"; out.creation_expireTrue = "Add a life time"; out.creation_expireFalse = "Unlimited"; - out.creation_expireHours = "Hours"; - out.creation_expireDays = "Days"; - out.creation_expireMonths = "Months"; - out.creation_expire1 = "By default, a pad stored by a registered user will never be removed from the server, unless it is requested by its owner."; - out.creation_expire2 = "If you prefer, you can set a life time to make sure the pad will be permanently deleted from the server and unavailable after the specified date."; + out.creation_expireHours = "Hour(s)"; + out.creation_expireDays = "Day(s)"; + out.creation_expireMonths = "Month(s)"; + out.creation_expire1 = "An unlimited pad will not be removed from the server until its owner deletes it."; + out.creation_expire2 = "An expiring pad has a set lifetime, after which it will be automatically removed from the server and other users' CryptDrives."; out.creation_createTitle = "Create a pad"; out.creation_createFromTemplate = "From template"; out.creation_createFromScratch = "From scratch"; diff --git a/expire-channels.js b/expire-channels.js new file mode 100644 index 000000000..d64f3c349 --- /dev/null +++ b/expire-channels.js @@ -0,0 +1,93 @@ +var Fs = require("fs"); +var Path = require("path"); + +var nThen = require("nthen"); +var config = require("./config"); + +var root = Path.resolve(config.taskPath || './tasks'); + +var dirs; +var nt; + +var queue = function (f) { + nt = nt.nThen(f); +}; + +var tryParse = function (s) { + try { return JSON.parse(s); } + catch (e) { return null; } +}; + +var CURRENT = +new Date(); + +var handleTask = function (str, path, cb) { + var task = tryParse(str); + if (!Array.isArray(task)) { + console.error('invalid task: not array'); + return cb(); + } + if (task.length < 2) { + console.error('invalid task: too small'); + return cb(); + } + + var time = task[0]; + var command = task[1]; + var args = task.slice(2); + + if (time > CURRENT) { + // not time for this task yet + console.log('not yet time'); + return cb(); + } + + nThen(function () { + switch (command) { + case 'EXPIRE': + console.log("expiring: %s", args[0]); + // TODO actually remove the file... + break; + default: + console.log("unknown command", command); + } + }).nThen(function () { + // remove the file... + Fs.unlink(path, function (err) { + if (err) { console.error(err); } + cb(); + }); + }); +}; + +nt = nThen(function (w) { + Fs.readdir(root, w(function (e, list) { + if (e) { throw e; } + dirs = list; + })); +}).nThen(function () { + dirs.forEach(function (dir) { + queue(function (w) { + console.log('recursing into %s', dir); + Fs.readdir(Path.join(root, dir), w(function (e, list) { + list.forEach(function (fn) { + queue(function (w) { + var filePath = Path.join(root, dir, fn); + var cb = w(); + + console.log("processing file at %s", filePath); + Fs.readFile(filePath, 'utf8', function (e, str) { + if (e) { + console.error(e); + return void cb(); + } + + handleTask(str, filePath, cb); + }); + }); + }); + })); + }); + }); +}); + + diff --git a/package.json b/package.json index b9cac6c82..eb6149506 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "express": "~4.10.1", "nthen": "~0.1.0", "pull-stream": "^3.6.1", + "replify": "^1.2.0", "saferphore": "0.0.1", "stream-to-pull-stream": "^1.7.2", "tweetnacl": "~0.12.2", diff --git a/rpc.js b/rpc.js index 83d166e2e..94d4d4e7e 100644 --- a/rpc.js +++ b/rpc.js @@ -428,8 +428,7 @@ var getHash = function (Env, publicKey, cb) { // The limits object contains storage limits for all the publicKey that have paid // To each key is associated an object containing the 'limit' value and a 'note' explaining that limit -var limits = {}; -var updateLimits = function (config, publicKey, cb /*:(?string, ?any[])=>void*/) { +var updateLimits = function (Env, config, publicKey, cb /*:(?string, ?any[])=>void*/) { if (config.adminEmail === false) { if (config.allowSubscriptions === false) { return; } throw new Error("allowSubscriptions must be false if adminEmail is false"); @@ -494,15 +493,15 @@ var updateLimits = function (config, publicKey, cb /*:(?string, ?any[])=>void*/) response.on('end', function () { try { var json = JSON.parse(str); - limits = json; + Env.limits = json; Object.keys(customLimits).forEach(function (k) { if (!isLimit(customLimits[k])) { return; } - limits[k] = customLimits[k]; + Env.limits[k] = customLimits[k]; }); var l; if (userId) { - var limit = limits[userId]; + var limit = Env.limits[userId]; l = limit && typeof limit.limit === "number" ? [limit.limit, limit.plan, limit.note] : [defaultLimit, '', '']; } @@ -523,7 +522,7 @@ var updateLimits = function (config, publicKey, cb /*:(?string, ?any[])=>void*/) var getLimit = function (Env, publicKey, cb) { var unescapedKey = unescapeKeyCharacters(publicKey); - var limit = limits[unescapedKey]; + var limit = Env.limits[unescapedKey]; var defaultLimit = typeof(Env.defaultStorageLimit) === 'number'? Env.defaultStorageLimit: DEFAULT_LIMIT; @@ -1097,7 +1096,11 @@ type NetfluxWebsocketSrvContext_t = { )=>void }; */ -RPC.create = function (config /*:Config_t*/, cb /*:(?Error, ?Function)=>void*/) { +RPC.create = function ( + config /*:Config_t*/, + debuggable /*:(string, T)=>T*/, + cb /*:(?Error, ?Function)=>void*/ +) { // load pin-store... console.log('loading rpc module...'); @@ -1115,8 +1118,10 @@ RPC.create = function (config /*:Config_t*/, cb /*:(?Error, ?Function)=>void*/) msgStore: (undefined /*:any*/), pinStore: (undefined /*:any*/), pinnedPads: {}, - evPinnedPadsReady: mkEvent(true) + evPinnedPadsReady: mkEvent(true), + limits: {} }; + debuggable('rpc_env', Env); var Sessions = Env.Sessions; var paths = Env.paths; @@ -1306,7 +1311,7 @@ RPC.create = function (config /*:Config_t*/, cb /*:(?Error, ?Function)=>void*/) Respond(e, size); }); case 'UPDATE_LIMITS': - return void updateLimits(config, safeKey, function (e, limit) { + return void updateLimits(Env, config, safeKey, function (e, limit) { if (e) { WARN(e, limit); return void Respond(e); @@ -1418,7 +1423,7 @@ RPC.create = function (config /*:Config_t*/, cb /*:(?Error, ?Function)=>void*/) }; var updateLimitDaily = function () { - updateLimits(config, undefined, function (e) { + updateLimits(Env, config, undefined, function (e) { if (e) { WARN('limitUpdate', e); } diff --git a/server.js b/server.js index 5a2cbfbb8..e803a829e 100644 --- a/server.js +++ b/server.js @@ -21,10 +21,25 @@ try { var websocketPort = config.websocketPort || config.httpPort; var useSecureWebsockets = config.useSecureWebsockets || false; +// This is stuff which will become available to replify +const debuggableStore = new WeakMap(); +const debuggable = function (name, x) { + if (name in debuggableStore) { + try { throw new Error(); } catch (e) { + console.error('cannot add ' + name + ' more than once [' + e.stack + ']'); + } + } else { + debuggableStore[name] = x; + } + return x; +}; +debuggable('global', global); +debuggable('config', config); + // support multiple storage back ends var Storage = require(config.storage||'./storage/file'); -var app = Express(); +var app = debuggable('app', Express()); var httpsOpts; @@ -204,7 +219,6 @@ var rpc; var nt = nThen(function (w) { if (!config.enableTaskScheduling) { return; } var Tasks = require("./storage/tasks"); - console.log("loading task scheduler"); Tasks.create(config, w(function (e, tasks) { config.tasks = tasks; @@ -214,7 +228,7 @@ var nt = nThen(function (w) { if (typeof(config.rpc) !== 'string') { return; } // load pin store... var Rpc = require(config.rpc); - Rpc.create(config, w(function (e, _rpc) { + Rpc.create(config, debuggable, w(function (e, _rpc) { if (e) { w.abort(); throw e; @@ -233,3 +247,6 @@ var nt = nThen(function (w) { }); }); +if (config.debugReplName) { + require('replify')({ name: config.debugReplName, app: debuggableStore }); +} \ No newline at end of file diff --git a/www/common/common-interface.js b/www/common/common-interface.js index f20bb4224..248bd5e70 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -553,6 +553,7 @@ define([ var $loading, $container; if ($('#' + LOADING).length) { $loading = $('#' + LOADING); //.show(); + $loading.css('display', ''); $loading.removeClass('cp-loading-hidden'); if (loadingText) { $('#' + LOADING).find('p').text(loadingText); diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index d31fe0a70..432061d2e 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1687,7 +1687,10 @@ define([ Messages.creation_ownedTitle, createHelper(Messages.creation_owned1 + '\n' + Messages.creation_owned2) ]), - setHTML(h('p'), Messages.creation_owned1 + '
' + Messages.creation_owned2), + h('div.cp-creation-help-container', [ + setHTML(h('p'), Messages.creation_owned1), + setHTML(h('p'), Messages.creation_owned2) + ]), h('input#cp-creation-owned-true.cp-creation-owned-value', { type: 'radio', name: 'cp-creation-owned', @@ -1715,7 +1718,10 @@ define([ Messages.creation_expireTitle, createHelper(Messages.creation_expire1, Messages.creation_expire2) ]), - setHTML(h('p'), Messages.creation_expire1 + '
' + Messages.creation_expire2), + h('div.cp-creation-help-container', [ + setHTML(h('p'), Messages.creation_expire1), + setHTML(h('p'), Messages.creation_expire2) + ]), h('input#cp-creation-expire-false.cp-creation-expire-value', { type: 'radio', name: 'cp-creation-expire', diff --git a/www/login/main.js b/www/login/main.js index f6a9cebb6..f0c6c7d22 100644 --- a/www/login/main.js +++ b/www/login/main.js @@ -13,7 +13,6 @@ define([ $(function () { var $main = $('#mainBlock'); var $checkImport = $('#import-recent'); - var Messages = Cryptpad.Messages; // main block is hidden in case javascript is disabled $main.removeClass('hidden'); @@ -61,90 +60,15 @@ define([ hashing = true; var shouldImport = $checkImport[0].checked; - - // setTimeout 100ms to remove the keyboard on mobile devices before the loading screen pops up - window.setTimeout(function () { - UI.addLoadingScreen({ - loadingText: Messages.login_hashing, - hideTips: true, - }); - // We need a setTimeout(cb, 0) otherwise the loading screen is only displayed after hashing the password - window.setTimeout(function () { - loginReady(function () { - var uname = $uname.val(); - var passwd = $passwd.val(); - Login.loginOrRegister(uname, passwd, false, function (err, result) { - if (!err) { - var proxy = result.proxy; - - // successful validation and user already exists - // set user hash in localStorage and redirect to drive - if (!proxy.login_name) { - result.proxy.login_name = result.userName; - } - - proxy.edPrivate = result.edPrivate; - proxy.edPublic = result.edPublic; - - proxy.curvePrivate = result.curvePrivate; - proxy.curvePublic = result.curvePublic; - - Feedback.send('LOGIN', true); - Realtime.whenRealtimeSyncs(result.realtime, function() { - LocalStore.login(result.userHash, result.userName, function () { - hashing = false; - if (test) { - localStorage.clear(); - test.pass(); - return; - } - if (shouldImport) { - sessionStorage.migrateAnonDrive = 1; - } - if (sessionStorage.redirectTo) { - var h = sessionStorage.redirectTo; - var parser = document.createElement('a'); - parser.href = h; - if (parser.origin === window.location.origin) { - delete sessionStorage.redirectTo; - window.location.href = h; - return; - } - } - window.location.href = '/drive/'; - }); - }); - return; - } - switch (err) { - case 'NO_SUCH_USER': - UI.removeLoadingScreen(function () { - UI.alert(Messages.login_noSuchUser, function () { - hashing = false; - }); - }); - break; - case 'INVAL_USER': - UI.removeLoadingScreen(function () { - UI.alert(Messages.login_invalUser, function () { - hashing = false; - }); - }); - break; - case 'INVAL_PASS': - UI.removeLoadingScreen(function () { - UI.alert(Messages.login_invalPass, function () { - hashing = false; - }); - }); - break; - default: // UNHANDLED ERROR - UI.errorLoadingScreen(Messages.login_unhandledError); - } - }); - }); - }, 0); - }, 100); + var uname = $uname.val(); + var passwd = $passwd.val(); + Login.loginOrRegisterUI(uname, passwd, false, shouldImport, Test.testing, function () { + if (test) { + localStorage.clear(); + test.pass(); + return true; + } + }); }); $('#register').on('click', function () { if (sessionStorage) { diff --git a/www/register/main.js b/www/register/main.js index 172eaf8cc..0c56dcff2 100644 --- a/www/register/main.js +++ b/www/register/main.js @@ -55,39 +55,6 @@ define([ var registering = false; var test; - var logMeIn = function (result) { - LocalStore.setUserHash(result.userHash); - - var proxy = result.proxy; - proxy.edPublic = result.edPublic; - proxy.edPrivate = result.edPrivate; - proxy.curvePublic = result.curvePublic; - proxy.curvePrivate = result.curvePrivate; - - Feedback.send('REGISTRATION', true); - - Realtime.whenRealtimeSyncs(result.realtime, function () { - LocalStore.login(result.userHash, result.userName, function () { - registering = false; - if (test) { - localStorage.clear(); - test.pass(); - return; - } - if (sessionStorage.redirectTo) { - var h = sessionStorage.redirectTo; - var parser = document.createElement('a'); - parser.href = h; - if (parser.origin === window.location.origin) { - delete sessionStorage.redirectTo; - window.location.href = h; - return; - } - } - window.location.href = '/drive/'; - }); - }); - }; $register.click(function () { if (registering) { @@ -125,89 +92,14 @@ define([ function (yes) { if (!yes) { return; } + Login.loginOrRegisterUI(uname, passwd, true, shouldImport, Test.testing, function () { + if (test) { + localStorage.clear(); + test.pass(); + return true; + } + }); registering = true; - // setTimeout 100ms to remove the keyboard on mobile devices before the loading screen pops up - window.setTimeout(function () { - UI.addLoadingScreen({ - loadingText: Messages.login_hashing, - hideTips: true, - }); - // We need a setTimeout(cb, 0) otherwise the loading screen is only displayed after hashing the password - window.setTimeout(function () { - Login.loginOrRegister(uname, passwd, true, function (err, result) { - var proxy; - if (result) { proxy = result.proxy; } - - if (err) { - switch (err) { - case 'NO_SUCH_USER': - UI.removeLoadingScreen(function () { - UI.alert(Messages.login_noSuchUser, function () { - registering = false; - }); - }); - break; - case 'INVAL_USER': - UI.removeLoadingScreen(function () { - UI.alert(Messages.login_invalUser, function () { - registering = false; - }); - }); - break; - case 'INVAL_PASS': - UI.removeLoadingScreen(function () { - UI.alert(Messages.login_invalPass, function () { - registering = false; - }); - }); - break; - case 'PASS_TOO_SHORT': - UI.removeLoadingScreen(function () { - var warning = Messages._getKey('register_passwordTooShort', [ - Cred.MINIMUM_PASSWORD_LENGTH - ]); - UI.alert(warning, function () { - registering = false; - }); - }); - break; - case 'ALREADY_REGISTERED': - // logMeIn should reset registering = false - UI.removeLoadingScreen(function () { - UI.confirm(Messages.register_alreadyRegistered, function (yes) { - if (!yes) { return; } - proxy.login_name = uname; - - if (!proxy[Constants.displayNameKey]) { - proxy[Constants.displayNameKey] = uname; - } - LocalStore.eraseTempSessionValues(); - logMeIn(result); - }); - }); - break; - default: // UNHANDLED ERROR - registering = false; - UI.errorLoadingScreen(Messages.login_unhandledError); - } - return; - } - - if (Test.testing) { return void logMeIn(result); } - - LocalStore.eraseTempSessionValues(); - if (shouldImport) { - sessionStorage.migrateAnonDrive = 1; - } - - proxy.login_name = uname; - proxy[Constants.displayNameKey] = uname; - sessionStorage.createReadme = 1; - - logMeIn(result); - }); - }, 0); - }, 200); }, { ok: Messages.register_writtenPassword, cancel: Messages.register_cancel, diff --git a/www/todo/inner.js b/www/todo/inner.js index 8d5de7778..748467360 100644 --- a/www/todo/inner.js +++ b/www/todo/inner.js @@ -70,6 +70,10 @@ define([ var makeCheckbox = function (id, cb) { var entry = APP.lm.proxy.data[id]; + if (!entry || typeof(entry) !== 'object') { + return void console.log('entry undefined'); + } + var checked = entry.state === 1 ? 'cp-app-todo-task-checkbox-checked fa-check-square-o': 'cp-app-todo-task-checkbox-unchecked fa-square-o'; @@ -92,6 +96,7 @@ define([ }; var addTaskUI = function (el, animate) { + if (!el) { return; } var $taskDiv = $('
', { 'class': 'cp-app-todo-task' }); @@ -108,6 +113,9 @@ define([ .appendTo($taskDiv); var entry = APP.lm.proxy.data[el]; + if (!entry || typeof(entry) !== 'object') { + return void console.log('entry undefined'); + } if (entry.state) { $taskDiv.addClass('cp-app-todo-task-complete'); diff --git a/www/todo/todo.js b/www/todo/todo.js index 07b9962cc..8c68774dd 100644 --- a/www/todo/todo.js +++ b/www/todo/todo.js @@ -39,6 +39,24 @@ define([ if (typeof(proxy.data) !== 'object') { proxy.data = {}; } if (!Array.isArray(proxy.order)) { proxy.order = []; } if (typeof(proxy.type) !== 'string') { proxy.type = 'todo'; } + + // if a key exists in order, but there is no data for it... + // remove that key + var i = proxy.order.length - 1; + for (;i >= 0; i--) { + if (typeof(proxy.data[proxy.order[i]]) === 'undefined') { + console.log('removing todo entry with no data at [%s]', i); + proxy.order.splice(i, 1); + } + } + + // if you have data, but it's not in the order array... + // add it to the order array... + Object.keys(proxy.data).forEach(function (key) { + if (proxy.order.indexOf(key) > -1) { return; } + console.log("restoring entry with missing key"); + proxy.order.unshift(key); + }); }; /* add (id, obj) push id to order, add object to data */