From 9a317018e28297853012d87289bebb1f913ee7f9 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 25 Jan 2018 17:54:21 +0100 Subject: [PATCH 01/22] Remove deleted pads from the drive --- www/common/common-hash.js | 2 +- www/common/common-util.js | 2 +- www/common/cryptpad-common.js | 7 ++ www/common/outer/async-store.js | 76 ++++++++++++++------- www/common/outer/chainpad-netflux-worker.js | 20 +----- www/common/outer/store-rpc.js | 3 + www/common/sframe-protocol.js | 2 + www/common/userObject.js | 12 ++++ www/drive/inner.js | 9 ++- www/drive/main.js | 6 ++ 10 files changed, 96 insertions(+), 43 deletions(-) diff --git a/www/common/common-hash.js b/www/common/common-hash.js index d7d95e78a..d62204ac4 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -6,7 +6,7 @@ define([ ], function (Util, Messages, Crypto) { var Nacl = window.nacl; - var Hash = {}; + var Hash = window.CryptPad_Hash = {}; var uint8ArrayToHex = Util.uint8ArrayToHex; var hexToBase64 = Util.hexToBase64; diff --git a/www/common/common-util.js b/www/common/common-util.js index 030536751..98eda7373 100644 --- a/www/common/common-util.js +++ b/www/common/common-util.js @@ -1,6 +1,6 @@ (function (window) { define([], function () { - var Util = {}; + var Util = window.CryptPad_Util = {}; // If once is true, after the event has been fired, any further handlers which are // registered will fire immediately, and this type of event cannot be fired twice. diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 5ec198a44..214939072 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -196,6 +196,13 @@ define([ postMessage("CLEAR_OWNED_CHANNEL", channel, cb); }; + common.getDeletedPads = function (cb) { + postMessage("GET_DELETED_PADS", null, function (obj) { + if (obj && obj.error) { return void cb(obj.error); } + cb(null, obj); + }); + }; + common.uploadComplete = function (cb) { postMessage("UPLOAD_COMPLETE", null, function (obj) { if (obj && obj.error) { return void cb(obj.error); } diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index e00500472..5c135a3fb 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -94,9 +94,27 @@ define([ return list; }; - var getCanonicalChannelList = function () { - return Util.deduplicateString(getUserChannelList()).sort(); + var getExpirableChannelList = function () { + var list = []; + store.userObject.getFiles([store.userObject.FILES_DATA]).forEach(function (id) { + var data = store.userObject.getFileData(id); + var edPublic = store.proxy.edPublic; + + // Push channels owned by someone else or channel that should have expired + // because of the expiration time + if ((data.owners && data.owners.indexOf(edPublic) === -1) || + data.expire < (+new Date())) { + list.push(Hash.hrefToHexChannelId(data.href)); + } + }); + return list; + }; + + var getCanonicalChannelList = function (expirable) { + var list = expirable ? getExpirableChannelList() : getUserChannelList(); + return Util.deduplicateString(list).sort(); }; + ////////////////////////////////////////////////////////////////// /////////////////////// RPC ////////////////////////////////////// ////////////////////////////////////////////////////////////////// @@ -172,6 +190,37 @@ define([ }); }; + var arePinsSynced = function (cb) { + if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } + + var list = getCanonicalChannelList(false); + var local = Hash.hashChannelList(list); + store.rpc.getServerHash(function (e, hash) { + if (e) { return void cb(e); } + cb(null, hash === local); + }); + }; + + var resetPins = function (cb) { + if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } + + var list = getCanonicalChannelList(false); + store.rpc.reset(list, function (e, hash) { + if (e) { return void cb(e); } + cb(null, hash); + }); + }; + + Store.getDeletedPads = function (data, cb) { + if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } + + //var list = getCanonicalChannelList(true); + + // TODO: rpc to get the deleted pads here and send this list in the callback + + cb([]); + }; + Store.uploadComplete = function (data, cb) { if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } store.rpc.uploadComplete(function (err, res) { @@ -196,27 +245,6 @@ define([ }); }; - var arePinsSynced = function (cb) { - if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } - - var list = getCanonicalChannelList(); - var local = Hash.hashChannelList(list); - store.rpc.getServerHash(function (e, hash) { - if (e) { return void cb(e); } - cb(null, hash === local); - }); - }; - - var resetPins = function (cb) { - if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } - - var list = getCanonicalChannelList(); - store.rpc.reset(list, function (e, hash) { - if (e) { return void cb(e); } - cb(null, hash); - }); - }; - Store.uploadChunk = function (data, cb) { store.rpc.send.unauthenticated('UPLOAD', data.chunk, function (e, msg) { cb({ @@ -579,6 +607,8 @@ define([ contains = true; pad.atime = +new Date(); pad.title = title; + pad.owners = owners; + pad.expire = expire; // If the href is different, it means we have a stronger one if (href !== pad.href) { isStronger = true; } diff --git a/www/common/outer/chainpad-netflux-worker.js b/www/common/outer/chainpad-netflux-worker.js index f136e2a1e..5c07cd93f 100644 --- a/www/common/outer/chainpad-netflux-worker.js +++ b/www/common/outer/chainpad-netflux-worker.js @@ -57,27 +57,11 @@ define([], function () { // shim between chainpad and netflux var msgIn = function (peerId, msg) { return msg.replace(/^cp\|/, ''); - - /*try { - var decryptedMsg = Crypto.decrypt(msg, validateKey); - return decryptedMsg; - } catch (err) { - console.error(err); - return msg; - }*/ }; var msgOut = function (msg) { if (readOnly) { return; } return msg; - /*try { - var cmsg = Crypto.encrypt(msg); - if (msg.indexOf('[4') === 0) { cmsg = 'cp|' + cmsg; } - return cmsg; - } catch (err) { - console.log(msg); - throw err; - }*/ }; var onMsg = function(peer, msg, wc, network, direct) { @@ -93,7 +77,9 @@ define([], function () { if (parsed.channel === wc.id && !validateKey) { validateKey = parsed.validateKey; } - padData = parsed; + if (parsed.channel === wc.id) { + padData = parsed; + } // We have to return even if it is not the current channel: // we don't want to continue with other channels messages here return; diff --git a/www/common/outer/store-rpc.js b/www/common/outer/store-rpc.js index 39895b68c..c1b69dab7 100644 --- a/www/common/outer/store-rpc.js +++ b/www/common/outer/store-rpc.js @@ -49,6 +49,9 @@ define([ case 'UNPIN_PADS': { Store.unpinPads(data, cb); break; } + case 'GET_DELETED_PADS': { + Store.getDeletedPads(data, cb); break; + } case 'GET_PINNED_USAGE': { Store.getPinnedUsage(data, cb); break; } diff --git a/www/common/sframe-protocol.js b/www/common/sframe-protocol.js index 96014c992..5035e2af2 100644 --- a/www/common/sframe-protocol.js +++ b/www/common/sframe-protocol.js @@ -201,6 +201,8 @@ define({ // Inner drive needs to send command and receive updates from the async store 'Q_DRIVE_USEROBJECT': true, 'Q_DRIVE_GETOBJECT': true, + // Get the pads deleted from the server by other users to remove them from the drive + 'Q_DRIVE_GETDELETED': true, // Store's userObject need to send log messages to inner to display them in the UI 'EV_DRIVE_LOG': true, // Refresh the drive when the drive has changed ('change' or 'remove' events) diff --git a/www/common/userObject.js b/www/common/userObject.js index ecead9f2b..223376963 100644 --- a/www/common/userObject.js +++ b/www/common/userObject.js @@ -380,6 +380,18 @@ define([ var trashpaths = _findFileInTrash([TRASH], file); return rootpaths.concat(templatepaths, trashpaths); }; + + // Get drive ids of files from their channel ids + exp.findChannels = function (channels) { + var allFilesList = files[FILES_DATA]; + var channels64 = channels.slice().map(Util.hexToBase64); + return getFiles([FILES_DATA]).filter(function (k) { + var data = allFilesList[k]; + var parsed = Hash.parsePadUrl(data.href); + return parsed.hashData && channels64.indexOf(parsed.hashData.channel) !== -1; + }); + }; + exp.search = function (value) { if (typeof(value) !== "string") { return []; } value = value.trim(); diff --git a/www/drive/inner.js b/www/drive/inner.js index b2a1a774b..afdf8165c 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -2975,6 +2975,14 @@ define([ refresh(); UI.removeLoadingScreen(); + + sframeChan.query('Q_DRIVE_GETDELETED', null, function (err, data) { + var ids = filesOp.findChannels(data); + ids.forEach(function (id) { + var paths = filesOp.findFile(id); + filesOp.delete(paths, refresh); + }); + }); }; var setHistory = function (bool, update) { @@ -3091,7 +3099,6 @@ define([ throw new Error("Corrupted drive"); } andThen(common, proxy); - UI.removeLoadingScreen(); var onDisconnect = APP.onDisconnect = function (noAlert) { setEditable(false); diff --git a/www/drive/main.js b/www/drive/main.js index 552a6a0c6..61afafb2f 100644 --- a/www/drive/main.js +++ b/www/drive/main.js @@ -56,6 +56,12 @@ define([ cb(obj); }); }); + sframeChan.on('Q_DRIVE_GETDELETED', function (data, cb) { + Cryptpad.getDeletedPads(function (err, obj) { + if (err) { return void console.error(err); } + cb(obj); + }); + }); Cryptpad.onNetworkDisconnect.reg(function () { sframeChan.event('EV_NETWORK_DISCONNECT'); }); From 86144eda22ed94d55936863598577b998aad2c0a Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 26 Jan 2018 12:26:58 +0100 Subject: [PATCH 02/22] Display a message about pads deleted from the server --- customize.dist/translations/messages.fr.js | 1 + customize.dist/translations/messages.js | 1 + www/common/outer/async-store.js | 4 ++-- www/drive/inner.js | 6 ++++++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index 83e652588..a9aa54448 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -415,6 +415,7 @@ define(function () { "Cette action supprimera votre CryptDrive et son historique de votre navigateur, mais les pads existeront toujours (de manière chiffrée) sur notre serveur."; out.fm_padIsOwned = "Vous êtes le propriétaire de ce pad"; out.fm_padIsOwnedOther = "Ce pad est la propriété d'un autre utilisateur"; + out.fm_deletedPads = "Ces pads n'existent plus sur le serveur, ils ont été supprimés de votre CryptDrive: {0}"; // File - Context menu out.fc_newfolder = "Nouveau dossier"; out.fc_rename = "Renommer"; diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index f096341b6..551e36516 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -417,6 +417,7 @@ define(function () { "This will remove your CryptDrive and its history from your browser, but your pads will still exist (encrypted) on our server."; out.fm_padIsOwned = "You are the owner of this pad"; out.fm_padIsOwnedOther = "This pad is owned by another user"; + out.fm_deletedPads = "These pads no longer exist on the server, they've been removed from your CryptDrive: {0}"; // File - Context menu out.fc_newfolder = "New folder"; out.fc_rename = "Rename"; diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 5c135a3fb..d8fb6ef3b 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -102,8 +102,8 @@ define([ // Push channels owned by someone else or channel that should have expired // because of the expiration time - if ((data.owners && data.owners.indexOf(edPublic) === -1) || - data.expire < (+new Date())) { + if ((data.owners && data.owners.length && data.owners.indexOf(edPublic) === -1) || + (data.expire && data.expire < (+new Date()))) { list.push(Hash.hrefToHexChannelId(data.href)); } }); diff --git a/www/drive/inner.js b/www/drive/inner.js index afdf8165c..ba1c8c4d8 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -1435,6 +1435,7 @@ define([ case FILES_DATA: pName = FILES_DATA_NAME; break; case SEARCH: pName = SEARCH_NAME; break; case RECENT: pName = RECENT_NAME; break; + case OWNED: pName = OWNED_NAME; break; default: pName = name; } return pName; @@ -2978,10 +2979,15 @@ define([ sframeChan.query('Q_DRIVE_GETDELETED', null, function (err, data) { var ids = filesOp.findChannels(data); + var titles = []; ids.forEach(function (id) { + var title = filesOp.getTitle(id); + titles.push(title); var paths = filesOp.findFile(id); filesOp.delete(paths, refresh); }); + if (!titles.length) { return; } + UI.log(Messages._getKey('fm_deletedPads', [titles.join(', ')])); }); }; From 462c229b172685b711b6724042c1b754655161dc Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 26 Jan 2018 12:52:25 +0100 Subject: [PATCH 03/22] Add links to features table --- customize.dist/pages.js | 9 +++++++++ customize.dist/src/less2/pages/page-features.less | 15 +++++++++++++++ customize.dist/translations/messages.fr.js | 2 ++ customize.dist/translations/messages.js | 2 ++ 4 files changed, 28 insertions(+) diff --git a/customize.dist/pages.js b/customize.dist/pages.js index 40ea81467..4de6e6594 100644 --- a/customize.dist/pages.js +++ b/customize.dist/pages.js @@ -100,6 +100,7 @@ define([ h('div.collapse.navbar-collapse.justify-content-end#menuCollapse', [ h('a.nav-item.nav-link', { href: '/what-is-cryptpad.html'}, Msg.topbar_whatIsCryptpad), h('a.nav-item.nav-link', { href: 'https://blog.cryptpad.fr/'}, Msg.blog), + h('a.nav-item.nav-link', { href: '/features.html'}, Msg.features), h('a.nav-item.nav-link', { href: '/contact.html'}, Msg.contact), h('a.nav-item.nav-link', { href: '/about.html'}, Msg.about), ].concat(rightLinks)) @@ -332,6 +333,11 @@ define([ h('td') ]), ]) + ]), + h('div#cp-features-register', [ + h('a', { + href: '/register/' + }, h('button.cp-features-register-button', 'Register for free')) ]) ]), infopageFooter() @@ -601,6 +607,9 @@ define([ setHTML(h('p.register-explanation'), Msg.register_explanation) ]), h('div#userForm.form-group.hidden.col-md-6', [ + h('a', { + href: '/features.html' + }, Msg.register_whyRegister), h('input.form-control#username', { type: 'text', autocomplete: 'off', diff --git a/customize.dist/src/less2/pages/page-features.less b/customize.dist/src/less2/pages/page-features.less index cc44bc89d..37f2be245 100644 --- a/customize.dist/src/less2/pages/page-features.less +++ b/customize.dist/src/less2/pages/page-features.less @@ -50,3 +50,18 @@ table#cp-features-table { } } +#cp-features-register { + text-align: center; + padding: 20px; +} +.cp-features-register-button { + font-size: 20px; + color: #fff; + background: @cryptpad_color_blue; + border: 2px solid @cryptpad_color_blue; + border-radius: 0; + &:hover { + transform: scale(1.05); + } +} + diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index a9aa54448..1de3f4856 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -475,6 +475,7 @@ define(function () { out.register_warning = "Zero Knowledge signifie que nous ne pouvons pas récupérer vos données si vous perdez vos identifiants."; out.register_alreadyRegistered = "Cet utilisateur existe déjà, souhaitez-vous vous connecter ?"; + out.register_whyRegister = "Pourquoi s'inscrire ?"; out.register_header = "Bienvenue dans CryptPad"; out.register_explanation = [ "

Faisons d'abord le point sur certaines choses

", @@ -708,6 +709,7 @@ define(function () { // features.html + out.features = "Fonctionnalités"; out.features_title = "Tableau des fonctionnalités"; out.features_feature = "Fonctionnalité"; out.features_anon = "Utilisateur anonyme"; diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 551e36516..8b4f4502f 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -473,6 +473,7 @@ define(function () { out.register_mustAcceptTerms = "You must accept the terms of service."; out.register_mustRememberPass = "We cannot reset your password if you forget it. It's very important that you remember it! Please check the checkbox to confirm."; + out.register_whyRegister = "Why signing up?"; out.register_header = "Welcome to CryptPad"; out.register_explanation = [ "

Lets go over a couple things first:

", @@ -718,6 +719,7 @@ define(function () { // features.html + out.features = "Features"; out.features_title = "Features table"; out.features_feature = "Feature"; out.features_anon = "Anonymous user"; From b4b51ed86af6ee900b1f50830f54734be9af5f6b Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 26 Jan 2018 15:24:07 +0100 Subject: [PATCH 04/22] add a subsystem for scheduling tasks --- .gitignore | 1 + server.js | 56 ++++++++++++++++------------- storage/tasks.js | 94 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 25 deletions(-) create mode 100644 storage/tasks.js diff --git a/.gitignore b/.gitignore index fc1136152..741aedaf7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ datastore +tasks www/bower_components/* node_modules /config.js diff --git a/server.js b/server.js index a51c94343..5a2cbfbb8 100644 --- a/server.js +++ b/server.js @@ -9,6 +9,7 @@ var WebSocketServer = require('ws').Server; var NetfluxSrv = require('./node_modules/chainpad-server/NetfluxWebsocketSrv'); var Package = require('./package.json'); var Path = require("path"); +var nThen = require("nthen"); var config; try { @@ -198,32 +199,37 @@ if (config.httpSafePort) { var wsConfig = { server: httpServer }; -var createSocketServer = function (err, rpc) { - if(!config.useExternalWebsocket) { - if (websocketPort !== config.httpPort) { - console.log("setting up a new websocket server"); - wsConfig = { port: websocketPort}; - } - var wsSrv = new WebSocketServer(wsConfig); - Storage.create(config, function (store) { - NetfluxSrv.run(store, wsSrv, config, rpc); - }); - } -}; +var rpc; -var loadRPC = function (cb) { - config.rpc = typeof(config.rpc) === 'undefined'? './rpc.js' : config.rpc; +var nt = nThen(function (w) { + if (!config.enableTaskScheduling) { return; } + var Tasks = require("./storage/tasks"); - if (typeof(config.rpc) === 'string') { - // load pin store... - var Rpc = require(config.rpc); - Rpc.create(config, function (e, rpc) { - if (e) { throw e; } - cb(void 0, rpc); - }); - } else { - cb(); + console.log("loading task scheduler"); + Tasks.create(config, w(function (e, tasks) { + config.tasks = tasks; + })); +}).nThen(function (w) { + config.rpc = typeof(config.rpc) === 'undefined'? './rpc.js' : config.rpc; + if (typeof(config.rpc) !== 'string') { return; } + // load pin store... + var Rpc = require(config.rpc); + Rpc.create(config, w(function (e, _rpc) { + if (e) { + w.abort(); + throw e; + } + rpc = _rpc; + })); +}).nThen(function () { + if(config.useExternalWebsocket) { return; } + if (websocketPort !== config.httpPort) { + console.log("setting up a new websocket server"); + wsConfig = { port: websocketPort}; } -}; + var wsSrv = new WebSocketServer(wsConfig); + Storage.create(config, function (store) { + NetfluxSrv.run(store, wsSrv, config, rpc); + }); +}); -loadRPC(createSocketServer); diff --git a/storage/tasks.js b/storage/tasks.js new file mode 100644 index 000000000..b25a3a637 --- /dev/null +++ b/storage/tasks.js @@ -0,0 +1,94 @@ +var Fs = require("fs"); +var Path = require("path"); +var nacl = require("tweetnacl"); +var nThen = require("nthen"); + +var Tasks = module.exports; + +var encode = function (time, command, args) { + if (typeof(time) !== 'number') { return null; } + if (typeof(command) !== 'string') { return null; } + if (!Array.isArray(args)) { return [time, command]; } + return [time, command].concat(args); +}; + +var randomId = function () { + var bytes = Array.prototype.slice.call(nacl.randomBytes(16)); + return bytes.map(function (b) { + var n = Number(b & 0xff).toString(16); + return n.length === 1? '0' + n: n; + }).join(''); +}; + +var mkPath = function (env, id) { + return Path.join(env.root, id.slice(0, 2), id) + '.ndjson'; +}; + +var getFreeId = function (env, cb, tries) { + if (tries > 5) { return void cb('ETOOMANYTRIES'); } + + // generate a unique id + var id = randomId(); + + // derive a path from that id + var path = mkPath(env, id); + + Fs.stat(path, function (err) { + if (err && err.code === "ENOENT") { + cb(void 0, id); + } else { + getFreeId(env, cb); + } + }); +}; + +var write = function (env, task, cb) { + var str = JSON.stringify(task) + '\n'; + var id = nacl.util.encodeBase64(nacl.hash(nacl.util.decodeUTF8(str))).replace(/\//g, '-'); + + var path = mkPath(env, id); + nThen(function (w) { + // check if the file already exists... + Fs.stat(path, w(function (err) { + if (err && err.code === 'ENOENT') { return; } + w.abort(); cb(); + })); + }).nThen(function (w) { + // create the parent directory if it does not exist + var dir = id.slice(0, 2); + var dirpath = Path.join(env.root, dir); + + Fs.mkdir(dirpath, 0x1ff, w(function (err) { + if (err && err.code !== 'EEXIST') { + return void cb(err); + } + })); + }).nThen(function (w) { + // write the file to the path + Fs.writeFile(mkPath(env, id), str, function (e) { + if (e) { return void cb(e); } + cb(); + }); + }); +}; + +Tasks.create = function (config, cb) { + var env = { + root: config.taskPath || './tasks', + }; + + // make sure the path exists... + Fs.mkdir(env.root, 0x1ff, function (err) { + if (err && err.code !== 'EEXIST') { + throw err; + } + cb(void 0, { + write: function (time, command, args, cb) { + var task = encode(time, command, args); + write(env, task, cb); + }, + }); + }); +}; + + From 36550b781f6fc0261b304f281f5820c554d321db Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 26 Jan 2018 15:44:34 +0100 Subject: [PATCH 05/22] Don't check for stronger hash if we have an edit hash --- www/common/common-hash.js | 2 ++ www/common/cryptpad-common.js | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/www/common/common-hash.js b/www/common/common-hash.js index d62204ac4..2ebe18331 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -300,6 +300,8 @@ Version 1 var rHref = href || getRelativeHref(window.location.href); var parsed = parsePadUrl(rHref); if (!parsed.hash) { return false; } + // We can't have a stronger hash if we're already in edit mode + if (parsed.hashData && parsed.hashData.mode === 'edit') { return; } var stronger; Object.keys(recents).some(function (id) { var pad = recents[id]; diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 214939072..ad5141c03 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -557,6 +557,11 @@ define([ return void cb(null, hashes); } + if (hashes.editHash) { + // no need to find stronger if we already have edit hash + return void cb(null, hashes); + } + postMessage("GET_STRONGER_HASH", { href: window.location.href }, function (hash) { From 74deb60f0f250b80ec26c7c90f7677ab39346aca Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 26 Jan 2018 16:34:36 +0100 Subject: [PATCH 06/22] Fix issues with the new context menu in drive --- www/drive/inner.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/www/drive/inner.js b/www/drive/inner.js index ba1c8c4d8..b2e8359ba 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -802,6 +802,7 @@ define([ hide.push('properties'); hide.push('rename'); hide.push('openparent'); + hide.push('hashtag'); } if (containsFolder && paths.length > 1) { // Cannot open multiple folders @@ -911,6 +912,7 @@ define([ //var actions = []; var toShow = filterContextMenu(menuType, paths); var $actions = $contextMenu.find('a'); + $contextMenu.data('paths', paths); $actions = $actions.filter(function (i, el) { return toShow.some(function (className) { return $(el).is(className); }); }); @@ -922,9 +924,6 @@ define([ } else { $a.text($(el).text()); } - $(el).data('paths', paths); - //$(el).data('path', path); - //:$(el).data('element', $element); $container.append($a); $a.click(function() { $(el).click(); }); }); @@ -2733,6 +2732,7 @@ define([ var parsed = Hash.parsePadUrl(data.href); var channel = Util.base64ToHex(parsed.hashData.channel); n = n.nThen(function (waitFor) { + // XXX use the delete channel rpc sframeChan.query('Q_CONTACTS_CLEAR_OWNED_CHANNEL', channel, waitFor(function (e) { if (e) { return void console.error(e); } From 5089f1206d5c829d7249cf71a1c2b1d06bf92110 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 29 Jan 2018 10:38:00 +0100 Subject: [PATCH 07/22] Pad creation screen translations and link to settings --- customize.dist/src/less2/include/creation.less | 9 +++++++++ customize.dist/translations/messages.fr.js | 18 ++++++++++++++++++ customize.dist/translations/messages.js | 1 + www/common/common-hash.js | 2 +- www/common/common-ui-elements.js | 7 +++++++ www/settings/inner.js | 4 ++-- www/settings/main.js | 11 ++++++++++- 7 files changed, 48 insertions(+), 4 deletions(-) diff --git a/customize.dist/src/less2/include/creation.less b/customize.dist/src/less2/include/creation.less index c7b795e4f..aa94beb28 100644 --- a/customize.dist/src/less2/include/creation.less +++ b/customize.dist/src/less2/include/creation.less @@ -139,5 +139,14 @@ } } } + .cp-creation-settings { + justify-content: left; + a { + color: #0275d8; + &:hover { + color: lighten(#0275d8, 10%); + } + } + } } } diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index 1de3f4856..c0ebd20ff 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -842,6 +842,24 @@ define(function () { out.feedback_optout = "Si vous le souhaitez, vous pouvez désactiver ces requêtes en vous rendant dans votre page de préférences, où vous trouverez une case à cocher pour désactiver le retour d'expérience."; // Creation page + out.creation_404 = "Le pad auquel vous souhaitez accéder n'existe plus. Vous pouvez créer un nouveau pad en utilisant le formulaire suivant."; + out.creation_ownedTitle = "Type ode pad"; + out.creation_ownedTrue = "Pad possédé"; + out.creation_ownedFalse = "Pad ouvert"; + out.creation_owned1 = "Un pad possédé est un pad que vous pouvez supprimer du serveur à n'importe quel moment depuis votre CryptDrive. Une fois supprimé, personne d'autre ne peut y accéder, même si le pad est stocké dans un autre CryptDrive."; + out.creation_owned2 = "Un pad ouvert n'a pas de propriétaire et ne peut donc pas être supprimé du serveur par un utilisateur. Il pourra tout de même être supprimé automatiquement si sa date d'expiration est dépassée."; + out.creation_expireTitle = "Durée de vie"; + out.creation_expireTrue = "Ajouter durée de vie"; + out.creation_expireFalse = "Illimitée"; + out.creation_expireHours = "Heures"; + out.creation_expireDays = "Jours"; + out.creation_expireMonths = "Mois"; + out.creation_expire1 = "Par défault, un pad stocké dans le CryptDrive d'un utilisateur enregistré ne sera jamais supprimé du serveur, même s'il est inactif (à moins qu'il possède un propriétaire souhaitement le supprimer)."; + out.creation_expire2 = "Si vous le souhaitez, vous pouvez ajouter une durée de vie au pad afin d'être sûr qu'il soit supprimé du serveur, de manière permanente, à la date voulue."; + out.creation_createTitle = "Créer un pad"; + out.creation_createFromTemplate = "Depuis un modèle"; + out.creation_createFromScratch = "Nouveau pad vide"; + out.creation_settings = "Préférences des nouveaux pads"; // Properties about creation data out.creation_owners = "Propriétaires"; out.creation_ownedByOther = "Possédé par un autre utilisateur"; diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 8b4f4502f..cf2ed10f3 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -876,6 +876,7 @@ define(function () { out.creation_createTitle = "Create a pad"; out.creation_createFromTemplate = "From template"; out.creation_createFromScratch = "From scratch"; + out.creation_settings = "New Pad settings"; // Properties about creation data out.creation_owners = "Owners"; out.creation_ownedByOther = "Owned by another user"; diff --git a/www/common/common-hash.js b/www/common/common-hash.js index 2ebe18331..870b19dfe 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -176,7 +176,7 @@ Version 1 secret.keys = Crypto.createEditCryptor(); secret.key = Crypto.createEditCryptor().editKeyStr; }; - if (!secretHash && !/#/.test(window.location.href)) { + if (!secretHash && !window.location.hash) { //!/#/.test(window.location.href)) { generate(); return secret; } else { diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index a11ec5859..d31fe0a70 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1816,6 +1816,13 @@ define([ $button.click(function () { create(); }); + + // Settings button + var origin = common.getMetadataMgr().getPrivateData().origin; + $(h('div.cp-creation-settings', h('a', { + href: origin + '/settings/#creation', + target: '_blank' + }, Messages.creation_settings))).appendTo($creation); }; return UIElements; diff --git a/www/settings/inner.js b/www/settings/inner.js index 8df9c9564..5105ad2b7 100644 --- a/www/settings/inner.js +++ b/www/settings/inner.js @@ -75,7 +75,7 @@ define([ } }; - if (!AppConfig.dislayCreationScreen) { + if (!AppConfig.displayCreationScreen) { delete categories.creation; } if (AppConfig.disableFeedback) { @@ -789,7 +789,7 @@ define([ var $categories = $('
', {'class': 'cp-sidebarlayout-categories'}) .appendTo(APP.$leftside); APP.$usage = $('
', {'class': 'usage'}).appendTo(APP.$leftside); - var active = 'account'; + var active = privateData.category || 'account'; Object.keys(categories).forEach(function (key) { var $category = $('
', {'class': 'cp-sidebarlayout-category'}).appendTo($categories); if (key === 'account') { $category.append($('', {'class': 'fa fa-user-o'})); } diff --git a/www/settings/main.js b/www/settings/main.js index 42f501075..6b4dd8b8b 100644 --- a/www/settings/main.js +++ b/www/settings/main.js @@ -66,9 +66,18 @@ define([ Cryptpad.mergeAnonDrive(cb); }); }; + var category; + if (window.location.hash) { + category = window.location.hash.slice(1); + window.location.hash = ''; + } + var addData = function (obj) { + if (category) { obj.category = category; } + }; SFCommonO.start({ noRealtime: true, - addRpc: addRpc + addRpc: addRpc, + addData: addData }); }); }); From f134f11b7318abed25ce11d20406f97c309f075a Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 29 Jan 2018 12:40:09 +0100 Subject: [PATCH 08/22] new getDeletedPads rpc --- rpc.js | 40 ++++++++++++++++++++++++++++++++- www/common/outer/async-store.js | 30 +++++++++++++++---------- www/common/pinpad.js | 17 +++++++++++++- 3 files changed, 73 insertions(+), 14 deletions(-) diff --git a/rpc.js b/rpc.js index ebc6ae96d..dad806a5b 100644 --- a/rpc.js +++ b/rpc.js @@ -11,6 +11,8 @@ var Path = require("path"); var Https = require("https"); const Package = require('./package.json'); const Pinned = require('./pinned'); +const Saferphore = require("saferphore"); +const nThen = require("nthen"); var RPC = module.exports; @@ -355,6 +357,33 @@ var getMultipleFileSize = function (Env, channels, cb) { }); }; +/* accepts a list, and returns a sublist of channel or file ids which seem + to have been deleted from the server (file size 0) + + we might consider that we should only say a file is gone if fs.stat returns + ENOENT, but for now it's simplest to just rely on getFileSize... +*/ +var getDeletedPads = function (Env, channels, cb) { + if (!Array.isArray(channels)) { return cb('INVALID_LIST'); } + var L = channels.length; + + var sem = Saferphore.create(10); + var absentees = []; + nThen(function (w) { + for (var i = 0; i < L; i++) { + let channel = channels[i]; + sem.take(function (give) { + getFileSize(Env, channel, w(give(function (e, size) { + if (e) { return; } + if (size === 0) { absentees.push(channel); } + }))); + }); + } + }).nThen(function () { + cb(void 0, absentees); + }); +}; + var getTotalSize = function (Env, publicKey, cb) { var bytes = 0; return void getChannelList(Env, publicKey, function (channels) { @@ -1005,7 +1034,8 @@ var isUnauthenticatedCall = function (call) { 'GET_MULTIPLE_FILE_SIZE', 'IS_CHANNEL_PINNED', 'IS_NEW_CHANNEL', - 'GET_HISTORY_OFFSET' + 'GET_HISTORY_OFFSET', + 'GET_DELETED_PADS', ].indexOf(call) !== -1; }; @@ -1128,6 +1158,14 @@ RPC.create = function (config /*:Config_t*/, cb /*:(?Error, ?Function)=>void*/) } respond(e, [null, dict, null]); }); + case 'GET_DELETED_PADS': + return void getDeletedPads(Env, msg[1], function (e, list) { + if (e) { + WARN(e, msg[1]); + return respond(e); + } + respond(e, [null, list, null]); + }); case 'IS_CHANNEL_PINNED': return void isChannelPinned(Env, msg[1], function (isPinned) { respond(null, [null, isPinned, null]); diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index d8fb6ef3b..bf8879d6f 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -211,16 +211,6 @@ define([ }); }; - Store.getDeletedPads = function (data, cb) { - if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } - - //var list = getCanonicalChannelList(true); - - // TODO: rpc to get the deleted pads here and send this list in the callback - - cb([]); - }; - Store.uploadComplete = function (data, cb) { if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } store.rpc.uploadComplete(function (err, res) { @@ -337,6 +327,24 @@ define([ }); }; + Store.getDeletedPads = function (data, cb) { + if (!store.anon_rpc) { return void cb({error: 'ANON_RPC_NOT_READY'}); } + var list = getCanonicalChannelList(true); + if (!Array.isArray(list)) { + return void cb({error: 'INVALID_FILE_LIST'}); + } + + store.anon_rpc.send('GET_DELETED_PADS', list, function (e, res) { + console.log(e, res); + if (e) { return void cb({error: e}); } + if (res && res.length && Array.isArray(res[0])) { + cb(res[0]); + } else { + cb({error: 'UNEXPECTED_RESPONSE'}); + } + }); + }; + Store.initAnonRpc = function (data, cb) { require([ '/common/rpc.js', @@ -349,8 +357,6 @@ define([ }); }; - - ////////////////////////////////////////////////////////////////// /////////////////////// Store //////////////////////////////////// ////////////////////////////////////////////////////////////////// diff --git a/www/common/pinpad.js b/www/common/pinpad.js index 47eb97892..0bbaddd37 100644 --- a/www/common/pinpad.js +++ b/www/common/pinpad.js @@ -145,7 +145,22 @@ define([ if (response && response.length) { cb(void 0, response[0]); } else { - cb(); + cb('INVALID_RESPONSE'); + } + }); + }; + + exp.removeOwnedChannel = function (channel, cb) { + if (typeof(channel) !== 'string' || channel.length !== 32) { + // can't use this on files because files can't be owned... + return void cb('INVALID_ARGUMENTS'); + } + rpc.send('REMOVE_OWNED_CHANNEL', channel, function (e, response) { + if (e) { return void cb(e); } + if (response && response.length) { + cb(void 0, response[0]); // I haven't tested this... + } else { + cb('INVALID_RESPONSE'); } }); }; From a1d9f44bbd196179f49b67d46da204a9be407da7 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 29 Jan 2018 12:45:38 +0100 Subject: [PATCH 09/22] Delete owned pads from server when removed from the owner's drive --- customize.dist/translations/messages.js | 3 +- www/common/outer/async-store.js | 2 + www/common/outer/userObject.js | 11 +++- www/common/sframe-common.js | 11 ++-- www/drive/inner.js | 73 +++++++++++++------------ 5 files changed, 54 insertions(+), 46 deletions(-) diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index cf2ed10f3..f68714e45 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -381,7 +381,8 @@ define(function () { out.fm_removePermanentlyDialog = "Are you sure you want to remove that element from your CryptDrive permanently?"; out.fm_removeSeveralDialog = "Are you sure you want to move these {0} elements to the trash?"; out.fm_removeDialog = "Are you sure you want to move {0} to the trash?"; - out.fm_deleteOwnedPads = "Are you sure you want to remove permanently this pad from the server?"; + out.fm_deleteOwnedPad = "Are you sure you want to remove permanently this pad from the server?"; + out.fm_deleteOwnedPads = "Are you sure you want to remove permanently these pads from the server?"; out.fm_restoreDialog = "Are you sure you want to restore {0} to its previous location?"; out.fm_unknownFolderError = "The selected or last visited directory no longer exist. Opening the parent folder..."; out.fm_contextMenuError = "Unable to open the context menu for that element. If the problem persist, try to reload the page."; diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index d8fb6ef3b..d8447fb2e 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -901,6 +901,8 @@ define([ var userObject = store.userObject = UserObject.init(proxy.drive, { pinPads: Store.pinPads, unpinPads: Store.unpinPads, + removeOwnedChannel: function () {}, // XXX + edPublic: store.proxy.edPublic, loggedIn: store.loggedIn, log: function (msg) { postMessage("DRIVE_LOG", msg); diff --git a/www/common/outer/userObject.js b/www/common/outer/userObject.js index 32e3e3b92..e50a5f219 100644 --- a/www/common/outer/userObject.js +++ b/www/common/outer/userObject.js @@ -17,8 +17,12 @@ define([ console.error("unpinPads was not provided"); }; var pinPads = config.pinPads; + var removeOwnedChannel = config.removeOwnedChannel || function () { + console.error("removeOwnedChannel was not provided"); + }; var loggedIn = config.loggedIn; var workgroup = config.workgroup; + var edPublic = config.edPublic; var ROOT = exp.ROOT; var FILES_DATA = exp.FILES_DATA; @@ -90,9 +94,12 @@ define([ exp.getFiles([FILES_DATA]).forEach(function (id) { if (filesList.indexOf(id) === -1) { var fd = exp.getFileData(id); - if (fd && fd.href) { - toClean.push(Hash.hrefToHexChannelId(fd.href)); + var channelId = fd && fd.href && Hash.hrefToHexChannelId(fd.href); + // If trying to remove an owned pad, remove it from server also + if (fd.owners && fd.owners.indexOf(edPublic) !== -1 && channelId) { + removeOwnedChannel(channelId); } + if (channelId) { toClean.push(channelId); } spliceFileData(id); } }); diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index d2aaf90ea..029118b88 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -392,15 +392,12 @@ define([ UI.log(data.logText); }); - ctx.metadataMgr.onChange(function () { - try { - var feedback = ctx.metadataMgr.getPrivateData().feedbackAllowed; - Feedback.init(feedback); - } catch (e) { Feedback.init(false); } - }); - ctx.metadataMgr.onReady(waitFor()); }).nThen(function () { + try { + var feedback = ctx.metadataMgr.getPrivateData().feedbackAllowed; + Feedback.init(feedback); + } catch (e) { Feedback.init(false); } ctx.sframeChan.ready(); cb(funcs); }); diff --git a/www/drive/inner.js b/www/drive/inner.js index b2e8359ba..65547c60d 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -2682,9 +2682,11 @@ define([ $contextMenu.find('.cp-app-drive-context-delete').text(Messages.fc_remove) .attr('data-icon', 'fa-eraser'); } - var deletePaths = function (paths) { - var pathsList = []; - paths.forEach(function (p) { pathsList.push(p.path); }); + var deletePaths = function (paths, pathsList) { + pathsList = pathsList || []; + if (paths) { + paths.forEach(function (p) { pathsList.push(p.path); }); + } var msg = Messages._getKey("fm_removeSeveralPermanentlyDialog", [paths.length]); if (paths.length === 1) { msg = Messages.fm_removePermanentlyDialog; @@ -2695,6 +2697,33 @@ define([ filesOp.delete(pathsList, refresh); }); }; + var deleteOwnedPaths = function (paths, pathsList) { + pathsList = pathsList || []; + if (paths) { + paths.forEach(function (p) { pathsList.push(p.path); }); + } + var msgD = paths ? Messages.fm_deleteOwnedPads : Messages.fm_deleteContainsOwned; + UI.confirm(msgD, function(res) { + $(window).focus(); + if (!res) { return; } + // Try to delete each selected pad from server, and delete from drive if no error + var n = nThen(function () {}); + pathsList.forEach(function (p) { + var el = filesOp.find(p); + var data = filesOp.getFileData(el); + var parsed = Hash.parsePadUrl(data.href); + var channel = Util.base64ToHex(parsed.hashData.channel); + n = n.nThen(function (waitFor) { + // XXX use the delete channel rpc + sframeChan.query('Q_CONTACTS_CLEAR_OWNED_CHANNEL', channel, + waitFor(function (e) { + if (e) { return void console.error(e); } + filesOp.delete([p.path], refresh); + })); + }); + }); + }); + }; $contextMenu.on("click", "a", function(e) { e.stopPropagation(); var paths = $contextMenu.data('paths'); @@ -2720,28 +2749,7 @@ define([ moveElements(pathsList, [TRASH], false, refresh); } else if ($(this).hasClass('cp-app-drive-context-deleteowned')) { - var msgD = Messages.fm_deleteOwnedPads; - UI.confirm(msgD, function(res) { - $(window).focus(); - if (!res) { return; } - // Try to delete each selected pad from server, and delete from drive if no error - var n = nThen(function () {}); - paths.forEach(function (p) { - var el = filesOp.find(p.path); - var data = filesOp.getFileData(el); - var parsed = Hash.parsePadUrl(data.href); - var channel = Util.base64ToHex(parsed.hashData.channel); - n = n.nThen(function (waitFor) { - // XXX use the delete channel rpc - sframeChan.query('Q_CONTACTS_CLEAR_OWNED_CHANNEL', channel, - waitFor(function (e) { - if (e) { return void console.error(e); } - filesOp.delete([p.path], refresh); - })); - }); - }); - }); - return; + deleteOwnedPaths(paths); } else if ($(this).hasClass('cp-app-drive-context-open')) { paths.forEach(function (p) { @@ -2879,18 +2887,11 @@ define([ if (!$(elmt).data('path')) { return; } paths.push($(elmt).data('path')); }); - // If we are in the trash or anon pad or if we are holding the "shift" key, delete permanently, + if (!paths.length) { return; } + // If we are in the trash or anon pad or if we are holding the "shift" key, + // delete permanently if (!APP.loggedIn || isTrash || e.shiftKey) { - var msg = Messages._getKey("fm_removeSeveralPermanentlyDialog", [paths.length]); - if (paths.length === 1) { - msg = Messages.fm_removePermanentlyDialog; - } - - UI.confirm(msg, function(res) { - $(window).focus(); - if (!res) { return; } - filesOp.delete(paths, refresh); - }); + deletePaths(null, paths); return; } // else move to trash From 8e2c9ebfa325d529327eded45da09152623f4fd9 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 29 Jan 2018 14:26:24 +0100 Subject: [PATCH 10/22] lint compliance or something --- rpc.js | 18 +++++++++++------- storage/tasks.js | 2 +- www/common/outer/async-store.js | 1 - 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/rpc.js b/rpc.js index dad806a5b..83d166e2e 100644 --- a/rpc.js +++ b/rpc.js @@ -369,15 +369,19 @@ var getDeletedPads = function (Env, channels, cb) { var sem = Saferphore.create(10); var absentees = []; + + var job = function (channel, wait) { + return function (give) { + getFileSize(Env, channel, wait(give(function (e, size) { + if (e) { return; } + if (size === 0) { absentees.push(channel); } + }))); + }; + }; + nThen(function (w) { for (var i = 0; i < L; i++) { - let channel = channels[i]; - sem.take(function (give) { - getFileSize(Env, channel, w(give(function (e, size) { - if (e) { return; } - if (size === 0) { absentees.push(channel); } - }))); - }); + sem.take(job(channels[i], w)); } }).nThen(function () { cb(void 0, absentees); diff --git a/storage/tasks.js b/storage/tasks.js index b25a3a637..85df70a8d 100644 --- a/storage/tasks.js +++ b/storage/tasks.js @@ -63,7 +63,7 @@ var write = function (env, task, cb) { return void cb(err); } })); - }).nThen(function (w) { + }).nThen(function () { // write the file to the path Fs.writeFile(mkPath(env, id), str, function (e) { if (e) { return void cb(e); } diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index bf8879d6f..7198c819f 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -335,7 +335,6 @@ define([ } store.anon_rpc.send('GET_DELETED_PADS', list, function (e, res) { - console.log(e, res); if (e) { return void cb({error: e}); } if (res && res.length && Array.isArray(res[0])) { cb(res[0]); From 1c39e8b1bb1283803849efceefc7fe5bb7e470a8 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 29 Jan 2018 15:17:20 +0100 Subject: [PATCH 11/22] Fix worker app --- www/worker/inner.js | 1 + www/worker/worker.js | 16 +++++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/www/worker/inner.js b/www/worker/inner.js index 7b2186d2c..3f7df3f16 100644 --- a/www/worker/inner.js +++ b/www/worker/inner.js @@ -32,6 +32,7 @@ define([ sFrameChan.onReady(waitFor()); }).nThen(function (/*waitFor*/) { var $container = $('#cp-app-worker-container'); + $('', {href:'http://localhost:3000/worker/', target:'_blank'}).text('other').appendTo($container); var $bar = $('.cp-toolbar-container'); var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle']; diff --git a/www/worker/worker.js b/www/worker/worker.js index 095ef8980..f1e0c8a0f 100644 --- a/www/worker/worker.js +++ b/www/worker/worker.js @@ -29,16 +29,17 @@ require.config({ }); var i = 0; +var id = Math.floor(Math.random()*100000); onconnect = function(e) { console.log(e); console.log(i); var port = e.ports[0]; console.log('here'); - require([ - '/common/outer/async-store.js' - ], function (Store) { - console.log(Store); + //require([ + // '/common/outer/async-store.js' + //], function (Store) { + //console.log(Store); console.log(self.Proxy); var n = i; port.postMessage({state: 'READY'}); @@ -46,8 +47,9 @@ onconnect = function(e) { console.log('worker received'); console.log(e.data); port.postMessage('hello CryptPad'+n); + port.postMessage('This is '+id); }; - var data = { + /*var data = { query: function (cmd, data, cb) { console.log(cmd, data); }, @@ -64,7 +66,7 @@ onconnect = function(e) { }); port.postMessage('Store is connected!'); port.postMessage('Your username is ' + ret.store.proxy['cryptpad.username']); - }); + });*/ i++; - }); + //}); }; From bdafc101c9edb1ea141fbd077f153573fcf1276f Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 29 Jan 2018 15:17:53 +0100 Subject: [PATCH 12/22] Ability to remove owned channels from the drive --- customize.dist/translations/messages.fr.js | 3 ++- www/common/cryptpad-common.js | 3 +++ www/common/outer/async-store.js | 10 ++++++++-- www/common/outer/store-rpc.js | 3 +++ www/common/outer/userObject.js | 4 +++- www/common/sframe-common-outer.js | 3 +++ www/common/sframe-protocol.js | 3 +++ www/drive/inner.js | 12 ++++++------ 8 files changed, 31 insertions(+), 10 deletions(-) diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index c0ebd20ff..81003b3c6 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -377,7 +377,8 @@ define(function () { out.fm_emptyTrashDialog = "Êtes-vous sûr de vouloir vider la corbeille ?"; out.fm_removeSeveralPermanentlyDialog = "Êtes-vous sûr de vouloir supprimer ces {0} éléments de votre CryptDrive de manière permanente ?"; out.fm_removePermanentlyDialog = "Êtes-vous sûr de vouloir supprimer cet élément de votre CryptDrive de manière permanente ?"; - out.fm_deleteOwnedPads = "Êtes-vous sûr de vouloir supprimer définitivement ce pad du serveur ?"; + out.fm_deleteOwnedPad = "Êtes-vous sûr de vouloir supprimer définitivement ce pad du serveur ?"; + out.fm_deleteOwnedPads = "Êtes-vous sûr de vouloir supprimer définitivement ces pads du serveur ?"; out.fm_restoreDialog = "Êtes-vous sûr de vouloir restaurer {0} à son emplacement précédent ?"; out.fm_removeSeveralDialog = "Êtes-vous sûr de vouloir déplacer ces {0} éléments vers la corbeille ?"; out.fm_removeDialog = "Êtes-vous sûr de vouloir déplacer {0} vers la corbeille ?"; diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index ad5141c03..fcfab50f0 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -195,6 +195,9 @@ define([ common.clearOwnedChannel = function (channel, cb) { postMessage("CLEAR_OWNED_CHANNEL", channel, cb); }; + common.removeOwnedChannel = function (channel, cb) { + postMessage("REMOVE_OWNED_CHANNEL", channel, cb); + }; common.getDeletedPads = function (cb) { postMessage("GET_DELETED_PADS", null, function (obj) { diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index c0ac5b915..3b61acaa5 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -190,6 +190,13 @@ define([ }); }; + Store.removeOwnedChannel = function (data, cb) { + if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } + store.rpc.removeOwnedChannel(data, function (err) { + cb({error:err}); + }); + }; + var arePinsSynced = function (cb) { if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } @@ -335,7 +342,6 @@ define([ } store.anon_rpc.send('GET_DELETED_PADS', list, function (e, res) { - console.log(e, res); if (e) { return void cb({error: e}); } if (res && res.length && Array.isArray(res[0])) { cb(res[0]); @@ -907,7 +913,7 @@ define([ var userObject = store.userObject = UserObject.init(proxy.drive, { pinPads: Store.pinPads, unpinPads: Store.unpinPads, - removeOwnedChannel: function () {}, // XXX + removeOwnedChannel: Store.removeOwnedChannel, edPublic: store.proxy.edPublic, loggedIn: store.loggedIn, log: function (msg) { diff --git a/www/common/outer/store-rpc.js b/www/common/outer/store-rpc.js index c1b69dab7..9fe45556e 100644 --- a/www/common/outer/store-rpc.js +++ b/www/common/outer/store-rpc.js @@ -31,6 +31,9 @@ define([ case 'CLEAR_OWNED_CHANNEL': { Store.clearOwnedChannel(data, cb); break; } + case 'REMOVE_OWNED_CHANNEL': { + Store.removeOwnedChannel(data, cb); break; + } case 'UPLOAD_CHUNK': { Store.uploadChunk(data, cb); break; } diff --git a/www/common/outer/userObject.js b/www/common/outer/userObject.js index e50a5f219..3293dcb36 100644 --- a/www/common/outer/userObject.js +++ b/www/common/outer/userObject.js @@ -97,7 +97,9 @@ define([ var channelId = fd && fd.href && Hash.hrefToHexChannelId(fd.href); // If trying to remove an owned pad, remove it from server also if (fd.owners && fd.owners.indexOf(edPublic) !== -1 && channelId) { - removeOwnedChannel(channelId); + removeOwnedChannel(channelId, function (obj) { + if (obj && obj.error) { console.error(obj.error); } + }); } if (channelId) { toClean.push(channelId); } spliceFileData(id); diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 55346db6a..0bbd40eea 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -501,6 +501,9 @@ define([ sframeChan.on('Q_CONTACTS_CLEAR_OWNED_CHANNEL', function (channel, cb) { Cryptpad.clearOwnedChannel(channel, cb); }); + sframeChan.on('Q_REMOVE_OWNED_CHANNEL', function (channel, cb) { + Cryptpad.removeOwnedChannel(channel, cb); + }); if (cfg.addRpc) { cfg.addRpc(sframeChan, Cryptpad, Utils); diff --git a/www/common/sframe-protocol.js b/www/common/sframe-protocol.js index 5035e2af2..46aa93348 100644 --- a/www/common/sframe-protocol.js +++ b/www/common/sframe-protocol.js @@ -209,6 +209,9 @@ define({ 'EV_DRIVE_CHANGE': true, 'EV_DRIVE_REMOVE': true, + // Remove an owned pad from the server + 'Q_REMOVE_OWNED_CHANNEL': true, + // Notifications about connection and disconnection from the network 'EV_NETWORK_DISCONNECT': true, 'EV_NETWORK_RECONNECT': true, diff --git a/www/drive/inner.js b/www/drive/inner.js index 65547c60d..5bb678a9c 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -2687,8 +2687,8 @@ define([ if (paths) { paths.forEach(function (p) { pathsList.push(p.path); }); } - var msg = Messages._getKey("fm_removeSeveralPermanentlyDialog", [paths.length]); - if (paths.length === 1) { + var msg = Messages._getKey("fm_removeSeveralPermanentlyDialog", [pathsList.length]); + if (pathsList.length === 1) { msg = Messages.fm_removePermanentlyDialog; } UI.confirm(msg, function(res) { @@ -2702,7 +2702,8 @@ define([ if (paths) { paths.forEach(function (p) { pathsList.push(p.path); }); } - var msgD = paths ? Messages.fm_deleteOwnedPads : Messages.fm_deleteContainsOwned; + var msgD = pathsList.length === 1 ? Messages.fm_deleteOwnedPad : + Messages.fm_deleteOwnedPads; UI.confirm(msgD, function(res) { $(window).focus(); if (!res) { return; } @@ -2714,11 +2715,10 @@ define([ var parsed = Hash.parsePadUrl(data.href); var channel = Util.base64ToHex(parsed.hashData.channel); n = n.nThen(function (waitFor) { - // XXX use the delete channel rpc - sframeChan.query('Q_CONTACTS_CLEAR_OWNED_CHANNEL', channel, + sframeChan.query('Q_REMOVE_OWNED_CHANNEL', channel, waitFor(function (e) { if (e) { return void console.error(e); } - filesOp.delete([p.path], refresh); + filesOp.delete([p], refresh); })); }); }); From 8a1c04ff5864589f40b99a4ce25d6fb1bc8471bf Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 29 Jan 2018 15:33:34 +0100 Subject: [PATCH 13/22] Missing french translation keys --- customize.dist/translations/messages.fr.js | 3 +++ customize.dist/translations/messages.js | 1 + 2 files changed, 4 insertions(+) diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index 81003b3c6..fa48452d4 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -570,7 +570,9 @@ define(function () { out.upload_uploadPending = "Vous avez déjà un fichier en cours d'importation. Souhaitez-vous l'annuler et importer ce nouveau fichier ?"; out.upload_success = "Votre fichier ({0}) a été importé avec succès et ajouté à votre CryptDrive."; out.upload_notEnoughSpace = "Il n'y a pas assez d'espace libre dans votre CryptDrive pour ce fichier."; + out.upload_notEnoughSpaceBrief = "Pas assez d'espace"; out.upload_tooLarge = "Ce fichier dépasse la taille maximale autorisée."; + out.upload_tooLargeBrief = 'Fichier trop volumineux'; out.upload_choose = "Choisir un fichier"; out.upload_pending = "En attente"; out.upload_cancelled = "Annulé"; @@ -580,6 +582,7 @@ define(function () { out.upload_mustLogin = "Vous devez vous connecter pour importer un fichier"; out.download_button = "Déchiffrer et télécharger"; out.download_mt_button = "Télécharger"; + out.download_resourceNotAvailable = "Le fichier demandé n'est pas disponible..."; out.todo_title = "CryptTodo"; out.todo_newTodoNamePlaceholder = "Décrivez votre tâche..."; diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index f68714e45..e8783ec74 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -742,6 +742,7 @@ define(function () { out.features_f_multiple = "Use on multiple devices"; out.features_f_multiple_notes = "Easy way to access your pads from any device"; out.features_f_logoutEverywhere = "Log out from other devices"; + out.features_f_logoutEverywhere_notes = ""; // Used in the French translation to explain out.features_f_templates = "Use templates"; out.features_f_templates_notes = "Create templates and create new pads from your templates"; out.features_f_profile = "Create a profile"; From 8294686f80dc0fd6a2c1b1b77c3abbf783506ec2 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 29 Jan 2018 15:37:23 +0100 Subject: [PATCH 14/22] Fix assert translations for deprecated keys --- customize.dist/messages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/customize.dist/messages.js b/customize.dist/messages.js index 612994d54..65b306f42 100755 --- a/customize.dist/messages.js +++ b/customize.dist/messages.js @@ -79,7 +79,7 @@ define(req, function(Util, Default, Language) { }); Object.keys(translation).forEach(function (k) { if (/^_/.test(k) || k === 'driveReadme') { return; } - if (!Default[k]) { + if (typeof Default[k] === "undefined") { missing.push([code, k, 0]); } }); From 5db0274fc35b918b538b4d7b968102297ff6baff Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 29 Jan 2018 15:48:53 +0100 Subject: [PATCH 15/22] add default config for new server options --- config.example.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/config.example.js b/config.example.js index 8fb4acb00..0af71afbb 100644 --- a/config.example.js +++ b/config.example.js @@ -194,6 +194,18 @@ module.exports = { */ }, + /* some features may require that the server be able to schedule tasks + far into the future, such as: + > "three months from now, this channel should expire" + To disable these features, set 'enableTaskScheduling' to false + */ + enableTaskScheduling: true, + + /* if you would like the list of scheduled tasks to be stored in + a custom location, change the path below: + */ + taskPath: './tasks', + /* * By default, CryptPad also contacts our accounts server once a day to check for changes in * the people who have accounts. This check-in will also send the version of your CryptPad From de838a3184ac8bfc5807c93c6add797c63260003 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 29 Jan 2018 16:12:35 +0100 Subject: [PATCH 16/22] fix usage bar styles --- customize.dist/src/less2/include/limit-bar.less | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/customize.dist/src/less2/include/limit-bar.less b/customize.dist/src/less2/include/limit-bar.less index 12f26ef4b..4c11183c4 100644 --- a/customize.dist/src/less2/include/limit-bar.less +++ b/customize.dist/src/less2/include/limit-bar.less @@ -8,9 +8,10 @@ width: 100%; margin-top: 20px; .cp-limit-bar { - padding: 5px; + display: inline-flex; + justify-content: center; + align-items: center; - display: inline-block; max-width: 100%; margin: 3px; box-sizing: border-box; @@ -18,7 +19,6 @@ background: white; position: relative; text-align: center; - vertical-align: middle; width: ~"calc(100% - 6px)"; height: 35px; line-height: 25px; From e80e8339c2ad4eec689e37742928982fe151141e Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 29 Jan 2018 16:16:42 +0100 Subject: [PATCH 17/22] Typo in translation --- customize.dist/translations/messages.fr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index fa48452d4..b13f2bc7f 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -847,7 +847,7 @@ define(function () { // Creation page out.creation_404 = "Le pad auquel vous souhaitez accéder n'existe plus. Vous pouvez créer un nouveau pad en utilisant le formulaire suivant."; - out.creation_ownedTitle = "Type ode pad"; + out.creation_ownedTitle = "Type de pad"; out.creation_ownedTrue = "Pad possédé"; out.creation_ownedFalse = "Pad ouvert"; out.creation_owned1 = "Un pad possédé est un pad que vous pouvez supprimer du serveur à n'importe quel moment depuis votre CryptDrive. Une fois supprimé, personne d'autre ne peut y accéder, même si le pad est stocké dans un autre CryptDrive."; From ad2cde4705ecbcc60743106fc03939c68d9dd348 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 29 Jan 2018 19:04:00 +0100 Subject: [PATCH 18/22] Add warning about anonymous drive in localStorage --- customize.dist/translations/messages.fr.js | 3 ++- customize.dist/translations/messages.js | 3 ++- www/drive/inner.js | 5 +++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index b13f2bc7f..2c4bb6ec4 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -393,7 +393,8 @@ define(function () { out.updated_0_fm_info_trash = "Vider la corbeille permet de libérer de l'espace dans votre CryptDrive"; out.fm_info_trash = out.updated_0_fm_info_trash; out.fm_info_allFiles = 'Contient tous les fichiers de "Documents", "Fichiers non triés" et "Corbeille". Vous ne pouvez pas supprimer ou déplacer des fichiers depuis cet endroit.'; // Same here - out.fm_info_anonymous = 'Vous n\'êtes pas connecté, ces pads risquent donc d\'être supprimés (découvrez pourquoi). ' + + out.fm_info_anonymous = 'Vous n\'êtes pas connecté, ces pads seront donc supprimés après 3 mois d\'inactivité (découvrez pourquoi). ' + + 'Ils sont stockés dans votre navigateur donc nettoyer votre historique peut les faire disparaître.
' + 'Inscrivez-vous ou connectez-vous pour les maintenir en vie.'; out.fm_info_owned = "Vous êtes propriétaire des pads affichés dans cette catégorie. Cela signifie que vous pouvez choisir de les supprimer définitivement du serveur à n'importe quel moment. Ils seront alors inaccessibles pour tous les autres utilisateurs."; out.fm_alert_backupUrl = "Lien de secours pour ce CryptDrive.
" + diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index e8783ec74..32c61825f 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -396,7 +396,8 @@ define(function () { out.fm_info_trash = out.updated_0_fm_info_trash; out.fm_info_allFiles = 'Contains all the files from "Documents", "Unsorted" and "Trash". You can\'t move or remove files from here.'; // Same here out.fm_info_anonymous = 'You are not logged in so your pads will expire after 3 months (find out more). ' + - 'Sign up or Log in to keep them alive.'; + 'They are stored in your browser so clearing history may make them disappear.
' + + 'Sign up or Log in to keep them alive.
'; out.fm_info_owned = "You are the owner of the pads displayed here. This means you can remove them permanently from the server whenever you want. If you do so, other users won't be able to access them anymore."; out.fm_alert_backupUrl = "Backup link for this drive.
" + "It is highly recommended that you keep it secret.
" + diff --git a/www/drive/inner.js b/www/drive/inner.js index 5bb678a9c..11d96df42 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -1509,6 +1509,11 @@ define([ if (!APP.loggedIn) { msg = Messages.fm_info_anonymous; $box.html(msg); + $box.find('a[target!="_blank"]').click(function (e) { + e.preventDefault(); + var href = $(this).attr('href'); + common.gotoURL(href); + }); return $box; } if (!msg || APP.store['hide-info-' + path[0]] === '1') { From 0b1795e5a1fb52bdba6b0a23e0b18b163606521e Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 31 Jan 2018 12:08:55 +0100 Subject: [PATCH 19/22] Fix an issue preventing the share modal to be used more than once --- www/common/common-interface.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/www/common/common-interface.js b/www/common/common-interface.js index f35b361ba..c4e95ae63 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -317,11 +317,11 @@ define([ message = dialog.message(msg); } - var close = Util.once(function (el) { + var close = function (el) { var $el = $(el).fadeOut(150, function () { - $el.remove(); + $el.detach(); }); - }); + }; var navs = []; opt.buttons.forEach(function (b) { @@ -329,7 +329,7 @@ define([ var button = h('button', { tabindex: '1', 'class': b.className || '' }, b.name); $(button).click(function () { b.onClick(); - close($(this).parents('.alertify').first()); + close($(button).parents('.alertify').first()); }); if (b.keys && b.keys.length) { $(button).attr('data-keys', JSON.stringify(b.keys)); } navs.push(button); @@ -352,6 +352,7 @@ define([ if (!$(el).is(':visible')) { return; } $(el).click(); }, frame); + $(el).removeAttr('data-keys'); }); document.body.appendChild(frame); $(frame).focus(); From 91e42f1518549b6e526db46007a408ed4c2fd3db Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 31 Jan 2018 12:11:10 +0100 Subject: [PATCH 20/22] Fix the bug introduced in the share modal fix --- www/common/common-interface.js | 1 - 1 file changed, 1 deletion(-) diff --git a/www/common/common-interface.js b/www/common/common-interface.js index c4e95ae63..f20bb4224 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -352,7 +352,6 @@ define([ if (!$(el).is(':visible')) { return; } $(el).click(); }, frame); - $(el).removeAttr('data-keys'); }); document.body.appendChild(frame); $(frame).focus(); From fb192a2c457594f20a9583120826475468eb61ae Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 31 Jan 2018 14:42:34 +0100 Subject: [PATCH 21/22] Add a configurable background color in the file app in view mode --- www/common/application_config_internal.js | 5 +++++ www/file/inner.js | 2 ++ www/pad/inner.js | 7 +++++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/www/common/application_config_internal.js b/www/common/application_config_internal.js index 24c46b790..91520c99a 100644 --- a/www/common/application_config_internal.js +++ b/www/common/application_config_internal.js @@ -42,6 +42,11 @@ define(function() { '#800080', // purple ]; + // Background color in the apps with centered content: + // - file app in view mode + // - rich text app when editor's width reduced in settings + config.appBackgroundColor = '#666'; + // Set enableTemplates to false to remove the button allowing users to save a pad as a template // and remove the template category in CryptDrive config.enableTemplates = true; diff --git a/www/file/inner.js b/www/file/inner.js index 2dbe765ce..b4b327998 100644 --- a/www/file/inner.js +++ b/www/file/inner.js @@ -224,6 +224,8 @@ define([ if (decrypting) { return; } decrypting = true; displayFile(ev, sizeMb, function (err) { + $appContainer.css('background-color', + common.getAppConfig().appBackgroundColor); if (err) { UI.alert(err); } }); }; diff --git a/www/pad/inner.js b/www/pad/inner.js index b54dd7dd3..6945f6d45 100644 --- a/www/pad/inner.js +++ b/www/pad/inner.js @@ -31,6 +31,7 @@ define([ '/common/common-hash.js', '/common/common-util.js', '/bower_components/chainpad/chainpad.dist.js', + '/customize/application_config.js', '/bower_components/diff-dom/diffDOM.js', @@ -50,7 +51,8 @@ define([ ApiConfig, Hash, Util, - ChainPad) + ChainPad, + AppConfig) { var DiffDom = window.diffDOM; @@ -592,7 +594,8 @@ define([ } // Used in ckeditor-config.js Ckeditor.CRYPTPAD_URLARGS = ApiConfig.requireConf.urlArgs; - var newCss = '.cke_body_width { background: #666; height: 100%; }' + + var backColor = AppConfig.appBackgroundColor; + var newCss = '.cke_body_width { background: '+ backColor +'; height: 100%; }' + '.cke_body_width body {' + 'max-width: 50em; padding: 10px 30px; margin: 0 auto; min-height: 100%;'+ 'box-sizing: border-box;'+ From 8213d0d9265ec26468f2e316963bf07f70789c62 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 5 Feb 2018 15:24:30 +0100 Subject: [PATCH 22/22] guard against null pointer exception --- www/todo/inner.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/www/todo/inner.js b/www/todo/inner.js index 8d5de7778..6e6bcf297 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'; @@ -108,6 +112,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');