diff --git a/bower.json b/bower.json index f9501dfeb..7bf775e64 100644 --- a/bower.json +++ b/bower.json @@ -45,7 +45,8 @@ "bootstrap-tokenfield": "^0.12.1", "localforage": "^1.5.2", "html2canvas": "^0.4.1", - "croppie": "^2.5.0" + "croppie": "^2.5.0", + "sortablejs": "#^1.6.0" }, "resolutions": { "bootstrap": "v4.0.0-alpha.6" 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/delta-words.js b/customize.dist/delta-words.js new file mode 100644 index 000000000..4eec0d0b3 --- /dev/null +++ b/customize.dist/delta-words.js @@ -0,0 +1,61 @@ +define([ + '/bower_components/chainpad/chainpad.dist.js', +], function (ChainPad) { + var Diff = ChainPad.Diff; + + var isSpace = function (S, i) { + return /^\s$/.test(S.charAt(i)); + }; + + var leadingBoundary = function (S, offset) { + if (/\s/.test(S.charAt(offset))) { return offset; } + while (offset > 0) { + offset--; + if (isSpace(S, offset)) { offset++; break; } + } + return offset; + }; + + var trailingBoundary = function (S, offset) { + if (isSpace(S, offset)) { return offset; } + while (offset < S.length && !/\s/.test(S.charAt(offset))) { + offset++; + } + return offset; + }; + + var opsToWords = function (previous, current) { + var output = []; + Diff.diff(previous, current).forEach(function (op) { + // ignore deleted sections... + var offset = op.offset; + var toInsert = op.toInsert; + + // given an operation, check whether it is a word fragment, + // if it is, expand it to its word boundaries + var first = current.slice(leadingBoundary(current, offset), offset); + var last = current.slice(offset + toInsert.length, trailingBoundary(current, offset + toInsert.length)); + + var result = first + toInsert + last; + // concat-in-place + Array.prototype.push.apply(output, result.split(/\s+/)); + }); + return output.filter(Boolean); + }; + + var runningDiff = function (getter, f, time) { + var last = getter(); + // first time through, send all the words :D + f(opsToWords("", last)); + return setInterval(function () { + var current = getter(); + + // find inserted words... + var words = opsToWords(last, current); + last = current; + f(words); + }, time); + }; + + return runningDiff; +}); diff --git a/customize.dist/login.js b/customize.dist/login.js index b7a9d4e84..5511637e8 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, }; @@ -73,7 +80,7 @@ define([ var rt = opt.rt = Listmap.create(config); rt.proxy .on('ready', function () { - cb(void 0, rt); + setTimeout(function () { cb(void 0, rt); }); }) .on('disconnect', function (info) { cb('E_DISCONNECT', info); @@ -84,7 +91,7 @@ define([ return Object.keys(proxy).length === 0; }; - Exports.loginOrRegister = function (uname, passwd, isRegister, cb) { + Exports.loginOrRegister = function (uname, passwd, isRegister, shouldImport, cb) { if (typeof(cb) !== 'function') { return; } // Usernames are all lowercase. No going back on this one @@ -137,10 +144,140 @@ define([ return void cb('ALREADY_REGISTERED', res); } - setTimeout(function () { cb(void 0, res); }); + if (isRegister) { + var proxy = rt.proxy; + proxy.edPublic = res.edPublic; + proxy.edPrivate = res.edPrivate; + proxy.curvePublic = res.curvePublic; + proxy.curvePrivate = res.curvePrivate; + proxy.login_name = uname; + proxy[Constants.displayNameKey] = uname; + sessionStorage.createReadme = 1; + Feedback.send('REGISTRATION', true); + } else { + Feedback.send('LOGIN', true); + } + + if (shouldImport) { + sessionStorage.migrateAnonDrive = 1; + } + + // We have to call whenRealtimeSyncs asynchronously here because in the current + // version of listmap, onLocal calls `chainpad.contentUpdate(newValue)` + // asynchronously. + // The following setTimeout is here to make sure whenRealtimeSyncs is called after + // `contentUpdate` so that we have an update userDoc in chainpad. + setTimeout(function () { + Realtime.whenRealtimeSyncs(rt.realtime, function () { + LocalStore.login(res.userHash, res.userName, function () { + setTimeout(function () { cb(void 0, res); }); + }); + }); + }); }); }); }; + Exports.redirect = function () { + 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/'; + }; + + Exports.loginOrRegisterUI = function (uname, passwd, isRegister, shouldImport, testing, test) { + var hashing = true; + + var proceed = function (result) { + hashing = false; + if (test && typeof test === "function" && test()) { return; } + Realtime.whenRealtimeSyncs(result.realtime, function () { + Exports.redirect(); + }); + }; + + // 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, shouldImport, 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); } + + proceed(result); + }); + }, 0); + }, 200); + }; return Exports; }); diff --git a/customize.dist/messages.js b/customize.dist/messages.js index 65b306f42..bffb95654 100755 --- a/customize.dist/messages.js +++ b/customize.dist/messages.js @@ -43,7 +43,7 @@ define(req, function(Util, Default, Language) { messages._checkTranslationState = function (cb) { if (typeof(cb) !== "function") { return; } - var missing = []; + var allMissing = []; var reqs = []; Object.keys(map).forEach(function (code) { if (code === defaultLanguage) { return; } @@ -54,37 +54,60 @@ define(req, function(Util, Default, Language) { Object.keys(map).forEach(function (code, i) { if (code === defaultLanguage) { return; } var translation = langs[i]; - var updated = {}; - Object.keys(Default).forEach(function (k) { - if (/^updated_[0-9]+_/.test(k) && !translation[k]) { - var key = k.split('_').slice(2).join('_'); - // Make sure we don't already have an update for that key. It should not happen - // but if it does, keep the latest version - if (updated[key]) { - var ek = updated[key]; - if (parseInt(ek.split('_')[1]) > parseInt(k.split('_')[1])) { return; } + var missing = []; + var checkInObject = function (ref, translated, path) { + var updated = {}; + Object.keys(ref).forEach(function (k) { + if (/^updated_[0-9]+_/.test(k) && !translated[k]) { + var key = k.split('_').slice(2).join('_'); + // Make sure we don't already have an update for that key. It should not happen + // but if it does, keep the latest version + if (updated[key]) { + var ek = updated[key]; + if (parseInt(ek.split('_')[1]) > parseInt(k.split('_')[1])) { return; } + } + updated[key] = k; } - updated[key] = k; - } - }); - Object.keys(Default).forEach(function (k) { - if (/^_/.test(k) || k === 'driveReadme') { return; } - if (!translation[k] || updated[k]) { - if (updated[k]) { - missing.push([code, k, 2, 'out.' + updated[k]]); - return; + }); + Object.keys(ref).forEach(function (k) { + if (/^_/.test(k) || k === 'driveReadme') { return; } + var nPath = path.slice(); + nPath.push(k); + if (!translated[k] || updated[k]) { + if (updated[k]) { + var uPath = path.slice(); + uPath.unshift('out'); + missing.push([code, nPath, 2, uPath.join('.') + '.' + updated[k]]); + return; + } + return void missing.push([code, nPath, 1]); } - missing.push([code, k, 1]); - } - }); - Object.keys(translation).forEach(function (k) { - if (/^_/.test(k) || k === 'driveReadme') { return; } - if (typeof Default[k] === "undefined") { - missing.push([code, k, 0]); - } + if (typeof ref[k] !== typeof translated[k]) { + return void missing.push([code, nPath, 3]); + } + if (typeof ref[k] === "object" && !Array.isArray(ref[k])) { + checkInObject(ref[k], translated[k], nPath); + } + }); + Object.keys(translated).forEach(function (k) { + if (/^_/.test(k) || k === 'driveReadme') { return; } + var nPath = path.slice(); + nPath.push(k); + if (typeof ref[k] === "undefined") { + missing.push([code, nPath, 0]); + } + }); + }; + checkInObject(Default, translation, []); + // Push the removals at the end + missing.sort(function (a, b) { + if (a[2] === 0 && b[2] !== 0) { return 1; } + if (a[2] !== 0 && b[2] === 0) { return -1; } + return 0; }); + Array.prototype.push.apply(allMissing, missing); // Destructive concat }); - cb(missing); + cb(allMissing); }); }; diff --git a/customize.dist/pages.js b/customize.dist/pages.js index 4de6e6594..a9fbeabde 100644 --- a/customize.dist/pages.js +++ b/customize.dist/pages.js @@ -72,7 +72,7 @@ define([ ]) ]) ]), - h('div.cp-version-footer', "CryptPad v1.25.0 (Zombie)") + h('div.cp-version-footer', "CryptPad v1.26.0 (undefined)") ]); }; diff --git a/customize.dist/src/less2/include/alertify.less b/customize.dist/src/less2/include/alertify.less index 194b780d1..b845a5d6f 100644 --- a/customize.dist/src/less2/include/alertify.less +++ b/customize.dist/src/less2/include/alertify.less @@ -139,7 +139,7 @@ > * { width: 100%; - min-width: 300px; + min-width: 260px; max-width: 500px; margin: 0 auto; text-align: left; @@ -184,7 +184,7 @@ } } .alertify-tabs-contents { - flex: 1; + flex: 1 1 auto; min-height: 0; & > div { max-height: 100%; diff --git a/customize.dist/src/less2/include/creation.less b/customize.dist/src/less2/include/creation.less index aa94beb28..58d5a53ba 100644 --- a/customize.dist/src/less2/include/creation.less +++ b/customize.dist/src/less2/include/creation.less @@ -12,15 +12,16 @@ background: @colortheme_loading-bg; color: @colortheme_loading-color; display: flex; - align-items: center; + flex-flow: column; /* we need column so that the child can shrink vertically */ + justify-content: center; width: 100%; height: 100%; overflow: auto; - @media screen and (max-height: 600px), screen and (max-width: 500px) { - align-items: baseline; - } } #cp-creation { + flex: 0 1 auto; /* allows shrink */ + min-height: 0; + overflow: auto; text-align: center; font: @colortheme_app-font; width: 100%; @@ -33,24 +34,35 @@ 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 { + width: 100%; + 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)"; } - @media screen and (max-height: 600px), screen and (max-width: 500px) { + @media screen and (max-height: 800px), screen and (max-width: 500px) { h2 .cp-creation-help { display: inline; } - p { + .cp-creation-help-container { display: none; } } @@ -148,5 +160,11 @@ } } } + .cp-creation-deleted { + background: #111; + padding: 10px; + text-align: justify; + font-weight: bold; + } } } diff --git a/customize.dist/src/less2/include/limit-bar.less b/customize.dist/src/less2/include/limit-bar.less index 4c11183c4..b2ea5f230 100644 --- a/customize.dist/src/less2/include/limit-bar.less +++ b/customize.dist/src/less2/include/limit-bar.less @@ -29,6 +29,7 @@ background: blue; position: absolute; left: 0; + top: 0; z-index: 1; // .usage &.cp-limit-usage-normal { background: @colortheme_green; diff --git a/customize.dist/src/less2/include/toolbar.less b/customize.dist/src/less2/include/toolbar.less index 5bdc47f43..435476090 100644 --- a/customize.dist/src/less2/include/toolbar.less +++ b/customize.dist/src/less2/include/toolbar.less @@ -131,6 +131,7 @@ white-space: nowrap; display: flex; flex-flow: column; + height: 100%; .cp-toolbar-userlist-name { flex: 1; overflow: hidden; @@ -759,7 +760,7 @@ display: inline-flex; align-items: center; max-width: 100%; - flex: 1; + flex: 1 1 auto; //margin-bottom: -1px; .cp-toolbar-users { pre { 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.fr.js b/customize.dist/translations/messages.fr.js index 2c4bb6ec4..2d21cf9a6 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -31,12 +31,16 @@ define(function () { out.wrongApp = "Impossible d'afficher le contenu de ce document temps-réel dans votre navigateur. Vous pouvez essayer de recharger la page."; out.padNotPinned = 'Ce pad va expirer dans 3 mois, {0}connectez-vous{1} ou {2}enregistrez-vous{3} pour le préserver.'; out.anonymousStoreDisabled = "L'administrateur de cette instance de CryptPad a désactivé le drive pour les utilisateurs non enregistrés. Vous devez vous connecter pour pouvoir utiliser CryptDrive."; + out.expiredError = "Ce pad a atteint sa date d'expiration est n'est donc plus disponible."; + out.expiredErrorCopy = ' Vous pouvez toujours copier son contenu ailleurs en appuyant sur Échap.
Dés que vous aurez quitté la page, il sera impossible de le récupérer.'; + out.deletedError = 'Ce pad a été supprimé par son propriétaire et n\'est donc plus disponible.'; out.loading = "Chargement..."; out.error = "Erreur"; out.saved = "Enregistré"; out.synced = "Tout est enregistré"; out.deleted = "Pad supprimé de votre CryptDrive"; + out.deletedFromServer = "Pad supprimé du serveur"; out.realtime_unrecoverableError = "Le moteur temps-réel a rencontré une erreur critique. Cliquez sur OK pour recharger la page."; @@ -847,20 +851,20 @@ 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_404 = "Ce pad n'existe plus. Vous pouvez créer un nouveau pad en utilisant le formulaire suivant."; 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."; - 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_owned1 = "Un pad possédé peut être supprimé du serveur à tout moment quand son propriétaire le souhaite. Une fois supprimé, il disparaît du CryptDrive des autres utilisateurs."; + out.creation_owned2 = "Un pad ouvert n'a pas de propriétaire et ne peut donc pas être supprimé du serveur à moins d'avoir dépassé sa date d'expiration."; 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_expireFalse = "Illimité"; + out.creation_expireHours = "Heure(s)"; + out.creation_expireDays = "Jour(s)"; 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_expire1 = "Un pad illimité ne sera pas supprimé du serveur à moins que son propriétaire ne le décide."; + out.creation_expire2 = "Un pad expirant a une durée de vie définie, après laquelle il sera supprimé automatiquement du serveur et du CryptDrive des utilisateurs."; out.creation_createTitle = "Créer un pad"; out.creation_createFromTemplate = "Depuis un modèle"; out.creation_createFromScratch = "Nouveau pad vide"; diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 1ec8d708e..956aa2147 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -22,8 +22,7 @@ define(function () { out.button_newslide = 'New Presentation'; out.button_newwhiteboard = 'New Whiteboard'; - // NOTE: We want to update the 'common_connectionLost' key. - // Please do not add a new 'updated_common_connectionLostAndInfo' but change directly the value of 'common_connectionLost' + // NOTE: Remove updated_0_ if we need an updated_1_ out.updated_0_common_connectionLost = "Server Connection Lost
You're now in read-only mode until the connection is back."; out.common_connectionLost = out.updated_0_common_connectionLost; @@ -33,12 +32,16 @@ define(function () { out.wrongApp = "Unable to display the content of that realtime session in your browser. Please try to reload that page."; out.padNotPinned = 'This pad will expire in 3 months, {0}login{1} or {2}register{3} to preserve it.'; out.anonymousStoreDisabled = "The webmaster of this CryptPad instance has disabled the store for anonymous users. You have to log in to be able to use CryptDrive."; + out.expiredError = 'This pad has reached its expiration time and is no longer available.'; + out.expiredErrorCopy = ' You can still copy the content to another location by pressing Esc.
Once you leave this page, it will disappear forever!'; + out.deletedError = 'This pad has been deleted by its owner and is no longer available.'; out.loading = "Loading..."; out.error = "Error"; out.saved = "Saved"; out.synced = "Everything is saved"; out.deleted = "Pad deleted from your CryptDrive"; + out.deletedFromServer = "Pad deleted from the server"; out.realtime_unrecoverableError = "The realtime engine has encountered an unrecoverable error. Click OK to reload."; @@ -862,20 +865,20 @@ define(function () { out.feedback_optout = "If you would like to opt out, visit your user settings page, where you'll find a checkbox to enable or disable user feedback"; // Creation page - out.creation_404 = "This pad not longer exists. Use the following form to create a new pad"; + out.creation_404 = "This pad not longer exists. Use the following form to create a new pad."; 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"; @@ -887,7 +890,7 @@ define(function () { out.creation_expiration = "Expiration time"; out.creation_propertiesTitle = "Availability"; out.creation_appMenuName = "Advanced mode (Ctrl + E)"; - out.creation_newPadModalDescription = "Click on a pad type to create it. You can check the box if you want to display the pad creation screen (for owned pad, expiration pad, etc.)."; + out.creation_newPadModalDescription = "Click on a pad type to create it. You can check the box if you want to display the pad creation screen (for owned pads, expiring pads, etc.)."; out.creation_newPadModalAdvanced = "Display the pad creation screen"; // New share modal diff --git a/expire-channels.js b/expire-channels.js new file mode 100644 index 000000000..a37a4677b --- /dev/null +++ b/expire-channels.js @@ -0,0 +1,111 @@ +var Fs = require("fs"); +var Path = require("path"); + +var nThen = require("nthen"); + +var config; +try { + config = require('./config'); +} catch (e) { + console.log("You can customize the configuration by copying config.example.js to config.js"); + config = require('./config.example'); +} + +var FileStorage = require(config.storage || './storage/file'); +var root = Path.resolve(config.taskPath || './tasks'); + +var dirs; +var nt; +var store; + +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 (waitFor) { + switch (command) { + case 'EXPIRE': + console.log("expiring: %s", args[0]); + store.removeChannel(args[0], waitFor()); + break; + default: + console.log("unknown command", command); + } + }).nThen(function () { + // remove the task 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 (waitFor) { + FileStorage.create(config, waitFor(function (_store) { + store = _store; + })); +}).nThen(function () { + dirs.forEach(function (dir, dIdx) { + 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); + }); + }); + }); + if (dIdx === (dirs.length - 1)) { + queue(function () { + store.shutdown(); + }); + } + })); + }); + }); +}); + + diff --git a/package.json b/package.json index b9cac6c82..16e4f69fb 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "name": "cryptpad", "description": "realtime collaborative visual editor with zero knowlege server", - "version": "1.25.0", + "version": "1.26.0", "dependencies": { "chainpad-server": "^2.0.0", "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..465413fd1 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; @@ -1176,7 +1181,7 @@ RPC.create = function (config /*:Config_t*/, cb /*:(?Error, ?Function)=>void*/) }); case 'IS_NEW_CHANNEL': return void isNewChannel(Env, msg[1], function (e, isNew) { - respond(null, [null, isNew, null]); + respond(e, [null, isNew, null]); }); default: console.error("unsupported!"); @@ -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); @@ -1341,9 +1346,9 @@ RPC.create = function (config /*:Config_t*/, cb /*:(?Error, ?Function)=>void*/) }); case 'REMOVE_OWNED_CHANNEL': - return void removeOwnedChannel(Env, msg[1], publicKey, function (e, response) { + return void removeOwnedChannel(Env, msg[1], publicKey, function (e) { if (e) { return void Respond(e); } - Respond(void 0, response); + Respond(void 0, "OK"); }); // restricted to privileged users... case 'UPLOAD': @@ -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..a7830cbaf 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; @@ -102,6 +117,7 @@ Fs.exists(__dirname + "/customize", function (e) { var mainPages = config.mainPages || ['index', 'privacy', 'terms', 'about', 'contact']; var mainPagePattern = new RegExp('^\/(' + mainPages.join('|') + ').html$'); +app.get(mainPagePattern, Express.static(__dirname + '/customize')); app.get(mainPagePattern, Express.static(__dirname + '/customize.dist')); app.use("/blob", Express.static(Path.join(__dirname, (config.blobPath || './blob')), { @@ -204,7 +220,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 +229,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 +248,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/storage/file.js b/storage/file.js index 33df00692..08a9f19cd 100644 --- a/storage/file.js +++ b/storage/file.js @@ -418,6 +418,7 @@ module.exports.create = function ( openFileLimit: conf.openFileLimit || 2048, }; // 0x1ff -> 777 + var it; Fs.mkdir(env.root, 0x1ff, function (err) { if (err && err.code !== 'EEXIST') { // TODO: somehow return a nice error @@ -465,9 +466,12 @@ module.exports.create = function ( if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); } clearChannel(env, channelName, cb); }, + shutdown: function () { + clearInterval(it); + } }); }); - setInterval(function () { + it = setInterval(function () { flushUnusedChannels(env, function () { }); }, 5000); }; diff --git a/www/assert/translations/main.js b/www/assert/translations/main.js index 9a5397c73..db005ce0a 100644 --- a/www/assert/translations/main.js +++ b/www/assert/translations/main.js @@ -1,8 +1,9 @@ define([ 'jquery', - '/common/cryptpad-common.js', + '/common/common-util.js', + '/customize/messages.js', '/customize/translations/messages.js', -], function ($, Cryptpad, English) { +], function ($, Util, Messages, English) { var $body = $('body'); @@ -11,38 +12,40 @@ define([ }; var todo = function (missing) { - var str = ""; - var need = 1; + var currentLang = ""; + var currentState = 1; if (missing.length) { $body.append(pre(missing.map(function (msg) { var res = ""; - var code = msg[0]; - var key = msg[1]; - var needed = msg[2]; + var lang = msg[0]; + var key = msg[1]; // Array + var state = msg[2]; // 0 === toDelete, 1 === missing, 2 === updated, 3 === invalid (wrong type) var value = msg[3] || '""'; - if (str !== code) { - if (str !== "") + if (currentLang !== lang) { + if (currentLang !== "") { res += '\n'; } - str = code; - res += '/*\n *\n * ' + code + '\n *\n */\n\n'; + currentLang = lang; + res += '/*\n *\n * ' + lang + '\n *\n */\n\n'; } - if (need !== needed) { - need = needed; - if (need === 0) + if (currentState !== state) { + currentState = state; + if (currentState === 0) { - res += '\n// TODO: These keys are not needed anymore and should be removed ('+ code + ')\n\n'; + res += '\n// TODO: These keys are not needed anymore and should be removed ('+ lang + ')\n\n'; } } - res += (need ? '' : '// ') + 'out.' + key + ' = ' + value + ';'; - if (need === 1) { - res += ' // ' + JSON.stringify(English[key]); - } else if (need === 2) { - res += ' // TODO: Key updated --> make sure the updated key "'+ value +'" exists and is translated before that one.'; + res += (currentState ? '' : '// ') + 'out.' + key.join('.') + ' = ' + value + ';'; + if (currentState === 1) { + res += ' // ' + JSON.stringify(Util.find(English, key)); + } else if (currentState === 2) { + res += ' // TODO: Key updated --> make sure the updated key "'+ value +'" exists and is translated before this one.'; + } else if (currentState === 3) { + res += ' // NOTE: this key has an invalid type! Original value: ' + JSON.stringify(Util.find(English, key)); } return res; }).join('\n'))); @@ -50,5 +53,5 @@ define([ $body.text('// All keys are present in all translations'); } }; - Cryptpad.Messages._checkTranslationState(todo); + Messages._checkTranslationState(todo); }); diff --git a/www/common/common-interface.js b/www/common/common-interface.js index f20bb4224..d0c0e1961 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); @@ -600,11 +601,20 @@ define([ }, 3750); // jquery.fadeout can get stuck }; - UI.errorLoadingScreen = function (error, transparent) { - if (!$('#' + LOADING).is(':visible')) { UI.addLoadingScreen({hideTips: true}); } + UI.errorLoadingScreen = function (error, transparent, exitable) { + if (!$('#' + LOADING).is(':visible') || $('#' + LOADING).hasClass('cp-loading-hidden')) { + UI.addLoadingScreen({hideTips: true}); + } $('.cp-loading-spinner-container').hide(); + $('#cp-loading-tip').remove(); if (transparent) { $('#' + LOADING).css('opacity', 0.8); } $('#' + LOADING).find('p').html(error || Messages.error); + if (exitable) { + $(window).focus(); + $(window).keydown(function (e) { + if (e.which === 27) { $('#' + LOADING).hide(); } + }); + } }; var $defaultIcon = $('', {"class": "fa fa-file-text-o"}); diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index d31fe0a70..8e4fce0e4 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1655,8 +1655,6 @@ define([ var metadataMgr = common.getMetadataMgr(); var type = metadataMgr.getMetadataLazy().type; - // XXX check text for pad creation screen + translate it in French - var $body = $('body'); var $creationContainer = $('
', { id: 'cp-creation-container' }).appendTo($body); var $creation = $('
', { id: 'cp-creation' }).appendTo($creationContainer); @@ -1687,7 +1685,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 +1716,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', @@ -1825,5 +1829,24 @@ define([ }, Messages.creation_settings))).appendTo($creation); }; + UIElements.onServerError = function (common, err, toolbar, cb) { + if (["EDELETED", "EEXPIRED"].indexOf(err.type) === -1) { return; } + var msg = err.type; + if (err.type === 'EEXPIRED') { + msg = Messages.expiredError; + if (err.loaded) { + msg += Messages.expiredErrorCopy; + } + } else if (err.type === 'EDELETED') { + msg = Messages.deletedError; + if (err.loaded) { + msg += Messages.expiredErrorCopy; + } + } + if (toolbar && typeof toolbar.deleted === "function") { toolbar.deleted(); } + UI.errorLoadingScreen(msg, true, true); + (cb || function () {})(); + }; + return UIElements; }); diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index fcfab50f0..5bb00e6f3 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -538,6 +538,7 @@ define([ pad.onJoinEvent = Util.mkEvent(); pad.onLeaveEvent = Util.mkEvent(); pad.onDisconnectEvent = Util.mkEvent(); + pad.onErrorEvent = Util.mkEvent(); common.getFullHistory = function (data, cb) { postMessage("GET_FULL_HISTORY", data, cb); @@ -679,6 +680,9 @@ define([ case 'PAD_DISCONNECT': { common.padRpc.onDisconnectEvent.fire(data); break; } + case 'PAD_ERROR': { + common.padRpc.onErrorEvent.fire(data); break; + } // Drive case 'DRIVE_LOG': { common.drive.onLog.fire(data); break; diff --git a/www/common/metadata-manager.js b/www/common/metadata-manager.js index 0ec1c07ea..dfc855863 100644 --- a/www/common/metadata-manager.js +++ b/www/common/metadata-manager.js @@ -115,6 +115,12 @@ define(['json.sortify'], function (Sortify) { if (!meta.user) { return; } change(true); }); + sframeChan.on('EV_RT_ERROR', function (err) { + if (err.type !== 'EEXPIRED' && err.type !== 'EDELETED') { return; } + members = []; + if (!meta.user) { return; } + change(true); + }); return Object.freeze({ updateMetadata: function (m) { diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 3b61acaa5..ae7d752b6 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -815,6 +815,9 @@ define([ onDisconnect: function () { postMessage("PAD_DISCONNECT"); }, // post EV_PAD_DISCONNECT + onError: function (err) { + postMessage("PAD_ERROR", err); + }, // post EV_PAD_ERROR channel: data.channel, validateKey: data.validateKey, owners: data.owners, @@ -894,7 +897,7 @@ define([ case 'addFolder': store.userObject.addFolder(data.path, data.name, cb); break; case 'delete': - store.userObject.delete(data.paths, cb, data.nocheck); break; + store.userObject.delete(data.paths, cb, data.nocheck, data.isOwnPadRemoved); break; case 'emptyTrash': store.userObject.emptyTrash(cb); break; case 'rename': diff --git a/www/common/outer/chainpad-netflux-worker.js b/www/common/outer/chainpad-netflux-worker.js index 5c07cd93f..adb6242de 100644 --- a/www/common/outer/chainpad-netflux-worker.js +++ b/www/common/outer/chainpad-netflux-worker.js @@ -33,6 +33,7 @@ define([], function () { var onLeave = conf.onLeave; var onReady = conf.onReady; var onDisconnect = conf.onDisconnect; + var onError = conf.onError; var owners = conf.owners; var password = conf.password; var expire = conf.expire; @@ -44,6 +45,17 @@ define([], function () { var messageFromOuter = function () {}; + var error = function (err, wc) { + if (onError) { + onError({ + type: err, + loaded: !initializing + }); + if (wc && (err === "EEXPIRED" || err === "EDELETED")) { wc.leave(); } + } + else { console.error(err); } + }; + var onRdy = function (padData) { // Trigger onReady only if not ready yet. This is important because the history keeper sends a direct // message through "network" when it is synced, and it triggers onReady for each channel joined. @@ -96,11 +108,17 @@ define([], function () { if (peer === hk) { // if the peer is the 'history keeper', extract their message var parsed1 = JSON.parse(msg); + // First check if it is an error message (EXPIRED/DELETED) + if (parsed1.channel === wc.id && parsed1.error) { + return void error(parsed1.error, wc); + } + msg = parsed1[4]; // Check that this is a message for our channel if (parsed1[3] !== wc.id) { return; } } + lastKnownHash = msg.slice(0,64); var message = msgIn(peer, msg); @@ -177,7 +195,12 @@ define([], function () { }; var msg = ['GET_HISTORY', wc.id, cfg]; // Add the validateKey if we are the channel creator and we have a validateKey - if (hk) { network.sendto(hk, JSON.stringify(msg)); } + if (hk) { + network.sendto(hk, JSON.stringify(msg)).then(function () { + }, function (err) { + console.error(err); + }); + } } else { onRdy(); } @@ -204,8 +227,8 @@ define([], function () { // join the netflux network, promise to handle opening of the channel network.join(channel || null).then(function(wc) { onOpen(wc, network, firstConnection); - }, function(error) { - console.error(error); + }, function(err) { + console.error(err); }); }; diff --git a/www/common/outer/userObject.js b/www/common/outer/userObject.js index 3293dcb36..12d6d225e 100644 --- a/www/common/outer/userObject.js +++ b/www/common/outer/userObject.js @@ -3,8 +3,9 @@ define([ '/common/common-util.js', '/common/common-hash.js', '/common/common-realtime.js', + '/common/common-feedback.js', '/customize/messages.js' -], function (AppConfig, Util, Hash, Realtime, Messages) { +], function (AppConfig, Util, Hash, Realtime, Feedback, Messages) { var module = {}; var clone = function (o) { @@ -85,8 +86,11 @@ define([ delete files[FILES_DATA][id]; }; - exp.checkDeletedFiles = function () { - // Nothing in OLD_FILES_DATA for workgroups + // Find files in FILES_DATA that are not anymore in the drive, and remove them from + // FILES_DATA. If there are owned pads, remove them from server too, unless the flag tells + // us they're already removed + exp.checkDeletedFiles = function (isOwnPadRemoved) { + // Nothing in FILES_DATA for workgroups if (workgroup || (!loggedIn && !config.testMode)) { return; } var filesList = exp.getFiles([ROOT, 'hrefArray', TRASH]); @@ -96,9 +100,15 @@ define([ var fd = exp.getFileData(id); 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) { + if (!isOwnPadRemoved && + fd.owners && fd.owners.indexOf(edPublic) !== -1 && channelId) { removeOwnedChannel(channelId, function (obj) { - if (obj && obj.error) { console.error(obj.error); } + if (obj && obj.error) { + console.error(obj.error); + // RPC may not be responding + // Send a report that can be handled manually + Feedback.send('ERROR_DELETING_OWNED_PAD=' + channelId, true); + } }); } if (channelId) { toClean.push(channelId); } @@ -123,7 +133,7 @@ define([ files[TRASH][obj.name].splice(idx, 1); }); }; - exp.deleteMultiplePermanently = function (paths, nocheck) { + exp.deleteMultiplePermanently = function (paths, nocheck, isOwnPadRemoved) { var hrefPaths = paths.filter(function(x) { return exp.isPathIn(x, ['hrefArray']); }); var rootPaths = paths.filter(function(x) { return exp.isPathIn(x, [ROOT]); }); var trashPaths = paths.filter(function(x) { return exp.isPathIn(x, [TRASH]); }); @@ -179,7 +189,7 @@ define([ // In some cases, we want to remove pads from a location without removing them from // OLD_FILES_DATA (replaceHref) - if (!nocheck) { exp.checkDeletedFiles(); } + if (!nocheck) { exp.checkDeletedFiles(isOwnPadRemoved); } }; // Move diff --git a/www/common/pinpad.js b/www/common/pinpad.js index 0bbaddd37..f38d7fc57 100644 --- a/www/common/pinpad.js +++ b/www/common/pinpad.js @@ -157,8 +157,8 @@ define([ } 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... + if (response && response.length && response[0] === "OK") { + cb(); } else { cb('INVALID_RESPONSE'); } diff --git a/www/common/rpc.js b/www/common/rpc.js index e86615111..7c5012592 100644 --- a/www/common/rpc.js +++ b/www/common/rpc.js @@ -102,7 +102,7 @@ types of messages: } // HACK to hide messages from the anon rpc - if (parsed.length !== 4) { + if (parsed.length !== 4 && parsed[1] !== 'ERROR') { console.log(parsed); console.error("received message [%s] for txid[%s] with no callback", msg, txid); } @@ -217,6 +217,15 @@ types of messages: }); }); + if (network.onHistoryKeeperChange) { + network.onHistoryKeeperChange(function () { + send('COOKIE', "", function (e) { + if (e) { return void cb(e); } + ctx.connected = true; + }); + }); + } + send('COOKIE', "", function (e) { if (e) { return void cb(e); } // callback to provide 'send' method to whatever needs it diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js index 29845697e..7540f4a0b 100644 --- a/www/common/sframe-app-framework.js +++ b/www/common/sframe-app-framework.js @@ -7,6 +7,7 @@ define([ '/common/sframe-common.js', '/customize/messages.js', '/common/common-util.js', + '/common/common-hash.js', '/common/common-interface.js', '/common/common-thumbnail.js', '/common/common-feedback.js', @@ -27,6 +28,7 @@ define([ SFCommon, Messages, Util, + Hash, UI, Thumb, Feedback, @@ -41,6 +43,7 @@ define([ var STATE = Object.freeze({ DISCONNECTED: 'DISCONNECTED', FORGOTTEN: 'FORGOTTEN', + DELETED: 'DELETED', INFINITE_SPINNER: 'INFINITE_SPINNER', INITIALIZING: 'INITIALIZING', HISTORY_MODE: 'HISTORY_MODE', @@ -84,6 +87,7 @@ define([ }); }); + var textContentGetter; var titleRecommender = function () { return false; }; var contentGetter = function () { return UNINITIALIZED; }; var normalize0 = function (x) { return x; }; @@ -116,8 +120,9 @@ define([ var stateChange = function (newState) { var wasEditable = (state === STATE.READY); + if (state === STATE.DELETED) { return; } if (state === STATE.INFINITE_SPINNER && newState !== STATE.READY) { return; } - if (newState === STATE.INFINITE_SPINNER) { + if (newState === STATE.INFINITE_SPINNER || newState === STATE.DELETED) { state = newState; } else if (state === STATE.DISCONNECTED && newState !== STATE.INITIALIZING) { throw new Error("Cannot transition from DISCONNECTED to " + newState); @@ -146,6 +151,10 @@ define([ evStart.reg(function () { toolbar.forgotten(); }); break; } + case STATE.DELETED: { + evStart.reg(function () { toolbar.deleted(); }); + break; + } default: } if (wasEditable !== (state === STATE.READY)) { @@ -254,6 +263,7 @@ define([ var onReady = function () { var newContentStr = cpNfInner.chainpad.getUserDoc(); + if (state === STATE.DELETED) { return; } var newPad = false; if (newContentStr === '') { newPad = true; } @@ -287,11 +297,17 @@ define([ UI.removeLoadingScreen(emitResize); var privateDat = cpNfInner.metadataMgr.getPrivateData(); + var hash = privateDat.availableHashes.editHash || + privateDat.availableHashes.viewHash; + var href = privateDat.pathname + '#' + hash; + if (AppConfig.textAnalyzer && textContentGetter) { + var channelId = Hash.hrefToHexChannelId(href); + AppConfig.textAnalyzer(textContentGetter, channelId); + } + if (options.thumbnail && privateDat.thumbnails) { - var hash = privateDat.availableHashes.editHash || - privateDat.availableHashes.viewHash; if (hash) { - options.thumbnail.href = privateDat.pathname + '#' + hash; + options.thumbnail.href = href; options.thumbnail.getContent = function () { if (!cpNfInner.chainpad) { return; } return cpNfInner.chainpad.getUserDoc(); @@ -307,6 +323,7 @@ define([ } }; var onConnectionChange = function (info) { + if (state === STATE.DELETED) { return; } stateChange(info.state ? STATE.INITIALIZING : STATE.DISCONNECTED); if (info.state) { UI.findOKButton().click(); @@ -315,6 +332,12 @@ define([ } }; + var onError = function (err) { + common.onServerError(err, toolbar, function () { + stateChange(STATE.DELETED); + }); + }; + var setFileExporter = function (extension, fe, async) { var $export = common.createButton('export', true, {}, function () { var ext = (typeof(extension) === 'function') ? extension() : extension; @@ -407,7 +430,9 @@ define([ var priv = common.getMetadataMgr().getPrivateData(); if (priv.isNewFile) { var c = (priv.settings.general && priv.settings.general.creation) || {}; - if (c.skip && !priv.forceCreationScreen) { return void common.createPad(c, waitFor()); } + if (c.skip && !priv.forceCreationScreen) { + return void common.createPad(c, waitFor()); + } common.getPadCreationScreen(c, waitFor()); } }).nThen(function (waitFor) { @@ -432,7 +457,8 @@ define([ onLocal: onLocal, onInit: function () { stateChange(STATE.INITIALIZING); }, onReady: function () { evStart.reg(onReady); }, - onConnectionChange: onConnectionChange + onConnectionChange: onConnectionChange, + onError: onError }); var privReady = Util.once(waitFor()); @@ -448,6 +474,7 @@ define([ var infiniteSpinnerModal = false; window.setInterval(function () { if (state === STATE.DISCONNECTED) { return; } + if (state === STATE.DELETED) { return; } var l; try { l = cpNfInner.chainpad.getLag(); @@ -567,6 +594,10 @@ define([ // in the pad when requested by the framework. setContentGetter: function (cg) { contentGetter = cg; }, + // Set a text content supplier, this is a function which will give a text + // representation of the pad content if a text analyzer is configured + setTextContentGetter: function (tcg) { textContentGetter = tcg; }, + // Inform the framework that the content of the pad has been changed locally. localChange: onLocal, diff --git a/www/common/sframe-chainpad-netflux-inner.js b/www/common/sframe-chainpad-netflux-inner.js index 9467d8dae..5b8f883c5 100644 --- a/www/common/sframe-chainpad-netflux-inner.js +++ b/www/common/sframe-chainpad-netflux-inner.js @@ -34,6 +34,7 @@ define([ var onLocal = config.onLocal || function () { }; var setMyID = config.setMyID || function () { }; var onReady = config.onReady || function () { }; + var onError = config.onError || function () { }; var userName = config.userName; var initialState = config.initialState; if (config.transformFunction) { throw new Error("transformFunction is nolonger allowed"); } @@ -83,6 +84,11 @@ define([ chainpad.abort(); onConnectionChange({ state: false }); }); + sframeChan.on('EV_RT_ERROR', function (err) { + isReady = false; + chainpad.abort(); + onError(err); + }); sframeChan.on('EV_RT_CONNECT', function (content) { //content.members.forEach(userList.onJoin); isReady = false; diff --git a/www/common/sframe-chainpad-netflux-outer.js b/www/common/sframe-chainpad-netflux-outer.js index fedd90b46..2d592b65d 100644 --- a/www/common/sframe-chainpad-netflux-outer.js +++ b/www/common/sframe-chainpad-netflux-outer.js @@ -102,6 +102,10 @@ define([], function () { sframeChan.event('EV_RT_DISCONNECT'); }); + padRpc.onErrorEvent.reg(function (err) { + sframeChan.event('EV_RT_ERROR', err); + }); + // join the netflux network, promise to handle opening of the channel padRpc.joinPad({ channel: channel || null, diff --git a/www/common/sframe-common-history.js b/www/common/sframe-common-history.js index dd80cf884..27503d07a 100644 --- a/www/common/sframe-common-history.js +++ b/www/common/sframe-common-history.js @@ -44,12 +44,12 @@ define([ History.readOnly = common.getMetadataMgr().getPrivateData().readOnly; - var to = window.setTimeout(function () { + /*var to = window.setTimeout(function () { cb('[GET_FULL_HISTORY_TIMEOUT]'); - }, 30000); + }, 30000);*/ common.getFullHistory(realtime, function () { - window.clearTimeout(to); + //window.clearTimeout(to); cb(null, realtime); }); }; diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 0bbd40eea..57261372d 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -132,10 +132,12 @@ define([ // Check if the pad exists on server if (!window.location.hash) { isNewFile = true; return; } - Cryptpad.isNewChannel(window.location.href, waitFor(function (e, isNew) { - if (e) { return console.error(e); } - isNewFile = Boolean(isNew); - })); + if (realtime) { + Cryptpad.isNewChannel(window.location.href, waitFor(function (e, isNew) { + if (e) { return console.error(e); } + isNewFile = Boolean(isNew); + })); + } }).nThen(function () { var readOnly = secret.keys && !secret.keys.editKeyStr; var isNewHash = true; diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index 029118b88..3f4070eaf 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -92,6 +92,7 @@ define([ funcs.createMarkdownToolbar = callWithCommon(UIElements.createMarkdownToolbar); funcs.getPadCreationScreen = callWithCommon(UIElements.getPadCreationScreen); funcs.createNewPadModal = callWithCommon(UIElements.createNewPadModal); + funcs.onServerError = callWithCommon(UIElements.onServerError); // Thumb funcs.displayThumbnail = callWithCommon(Thumb.displayThumbnail); diff --git a/www/common/sframe-protocol.js b/www/common/sframe-protocol.js index 46aa93348..b7fa01b28 100644 --- a/www/common/sframe-protocol.js +++ b/www/common/sframe-protocol.js @@ -31,6 +31,8 @@ define({ 'EV_RT_CONNECT': true, // Called after the history is finished synchronizing, no arguments. 'EV_RT_READY': true, + // Called when the server returns an error in a pad (EEXPIRED, EDELETED). + 'EV_RT_ERROR': true, // Called from both outside and inside, argument is a (string) chainpad message. 'Q_RT_MESSAGE': true, diff --git a/www/common/toolbar3.js b/www/common/toolbar3.js index a82e1ff6e..e014503c6 100644 --- a/www/common/toolbar3.js +++ b/www/common/toolbar3.js @@ -709,6 +709,7 @@ define([ typing = 1; $spin.text(Messages.typing); $spin.interval = window.setInterval(function () { + if (toolbar.isErrorState) { return; } var dots = Array(typing+1).join('.'); $spin.text(Messages.typing + dots); typing++; @@ -718,6 +719,7 @@ define([ var onSynced = function () { if ($spin.timeout) { clearTimeout($spin.timeout); } $spin.timeout = setTimeout(function () { + if (toolbar.isErrorState) { return; } window.clearInterval($spin.interval); typing = -1; $spin.text(Messages.saved); @@ -1094,6 +1096,16 @@ define([ } }; + // When the pad is deleted from the server + toolbar.deleted = function (/*userId*/) { + toolbar.isErrorState = true; + toolbar.connected = false; + updateUserList(toolbar, config); + if (toolbar.spinner) { + toolbar.spinner.text(Messages.deletedFromServer); + } + }; + // On log out, remove permanently the realtime elements of the toolbar Common.onLogout(function () { failed(); diff --git a/www/common/userObject.js b/www/common/userObject.js index 223376963..4f7f03b23 100644 --- a/www/common/userObject.js +++ b/www/common/userObject.js @@ -556,17 +556,18 @@ define([ // DELETE // Permanently delete multiple files at once using a list of paths // NOTE: We have to be careful when removing elements from arrays (trash root, unsorted or template) - exp.delete = function (paths, cb, nocheck) { + exp.delete = function (paths, cb, nocheck, isOwnPadRemoved) { if (sframeChan) { return void sframeChan.query("Q_DRIVE_USEROBJECT", { cmd: "delete", data: { paths: paths, - nocheck: nocheck + nocheck: nocheck, + isOwnPadRemoved: isOwnPadRemoved } }, cb); } - exp.deleteMultiplePermanently(paths, nocheck); + exp.deleteMultiplePermanently(paths, nocheck, isOwnPadRemoved); if (typeof cb === "function") { cb(); } }; exp.emptyTrash = function (cb) { diff --git a/www/drive/inner.js b/www/drive/inner.js index 11d96df42..2f5ff9fa0 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -2673,11 +2673,14 @@ define([ var data = JSON.parse(JSON.stringify(filesOp.getFileData(el))); if (!data || !data.href) { return void cb('INVALID_FILE'); } data.href = base + data.href; + + var roUrl; if (ro) { data.roHref = data.href; delete data.href; } else { - data.roHref = base + getReadOnlyUrl(el); + roUrl = getReadOnlyUrl(el); + if (roUrl) { data.roHref = base + roUrl; } } UIElements.getProperties(common, data, cb); @@ -2712,6 +2715,8 @@ define([ UI.confirm(msgD, function(res) { $(window).focus(); if (!res) { return; } + filesOp.delete(pathsList, refresh); + /* // Try to delete each selected pad from server, and delete from drive if no error var n = nThen(function () {}); pathsList.forEach(function (p) { @@ -2723,10 +2728,12 @@ define([ sframeChan.query('Q_REMOVE_OWNED_CHANNEL', channel, waitFor(function (e) { if (e) { return void console.error(e); } - filesOp.delete([p], refresh); + filesOp.delete([p], function () {}, false, true); })); }); }); + n.nThen(function () { refresh(); }); + */ }); }; $contextMenu.on("click", "a", function(e) { 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/logout/index.html b/www/logout/index.html new file mode 100644 index 000000000..365c4bbe2 --- /dev/null +++ b/www/logout/index.html @@ -0,0 +1,16 @@ + + + + + CryptPad: Zero Knowledge, Collaborative Real Time Editing + + + + + + + + diff --git a/www/logout/main.js b/www/logout/main.js new file mode 100644 index 000000000..acd8e8b02 --- /dev/null +++ b/www/logout/main.js @@ -0,0 +1,5 @@ +define(['/bower_components/localforage/dist/localforage.min.js'], function (localForage) { + localForage.clear(); + sessionStorage.clear(); + localStorage.clear(); +}); diff --git a/www/pad/inner.js b/www/pad/inner.js index 6945f6d45..22ec21e40 100644 --- a/www/pad/inner.js +++ b/www/pad/inner.js @@ -402,6 +402,17 @@ define([ } }); + framework.setTextContentGetter(function () { + var innerCopy = inner.cloneNode(true); + displayMediaTags(framework, innerCopy, mediaTagMap); + innerCopy.normalize(); + $(innerCopy).find('*').each(function (i, el) { + $(el).append(' '); + }); + var str = $(innerCopy).text(); + str = str.replace(/\s\s+/g, ' '); + return str; + }); framework.setContentGetter(function () { displayMediaTags(framework, inner, mediaTagMap); inner.normalize(); diff --git a/www/poll/app-poll.less b/www/poll/app-poll.less index 88839f96e..0a5d5ca59 100644 --- a/www/poll/app-poll.less +++ b/www/poll/app-poll.less @@ -6,6 +6,7 @@ @import (once) '../../customize/src/less2/include/tokenfield.less'; @import (once) '../../customize/src/less2/include/tools.less'; @import (once) '../../customize/src/less2/include/avatar.less'; +@import (once) '../../customize/src/less2/include/creation.less'; .toolbar_main( @bg-color: @colortheme_poll-bg, @@ -15,6 +16,7 @@ .fileupload_main(); .alertify_main(); .tokenfield_main(); +.creation_main(); @poll-fore: #555; diff --git a/www/poll/inner.js b/www/poll/inner.js index dd585ec31..8f31116a1 100644 --- a/www/poll/inner.js +++ b/www/poll/inner.js @@ -1119,17 +1119,31 @@ define([ } UI.removeLoadingScreen(); - if (isNew) { + var privateDat = metadataMgr.getPrivateData(); + var skipTemp = Util.find(privateDat, + ['settings', 'general', 'creation', 'noTemplate']); + var skipCreation = Util.find(privateDat, ['settings', 'general', 'creation', 'skip']); + if (isNew && (!AppConfig.displayCreationScreen || (!skipTemp && skipCreation))) { common.openTemplatePicker(); } }; - var onDisconnect = function () { + // Manage disconnections because of network or error + var onDisconnect = function (info) { + if (APP.unrecoverable) { return; } + if (info && info.type) { + // Server error + return void common.onServerError(info, APP.toolbar, function () { + APP.unrecoverable = true; + setEditable(false); + }); + } setEditable(false); UI.alert(Messages.common_connectionLost, undefined, true); }; var onReconnect = function () { + if (APP.unrecoverable) { return; } setEditable(true); UI.findOKButton().click(); }; @@ -1175,6 +1189,7 @@ define([ Title.setToolbar(APP.toolbar); var $rightside = APP.toolbar.$rightside; + var $drawer = APP.toolbar.$drawer; metadataMgr.onChange(function () { var md = copyObject(metadataMgr.getMetadata()); @@ -1189,6 +1204,9 @@ define([ var $forgetPad = common.createButton('forget', true, {}, forgetCb); $rightside.append($forgetPad); + var $properties = common.createButton('properties', true); + $drawer.append($properties); + /* save as template */ if (!metadataMgr.getPrivateData().isTemplate) { var templateObj = { @@ -1201,7 +1219,7 @@ define([ /* add an export button */ var $export = common.createButton('export', true, {}, exportFile); - $rightside.append($export); + $drawer.append($export); var $help = common.createButton('', true).click(function () { showHelp(); }) .appendTo($rightside); @@ -1255,6 +1273,16 @@ define([ SFCommon.create(waitFor(function (c) { APP.common = common = c; })); }).nThen(function (waitFor) { common.getSframeChannel().onReady(waitFor()); + }).nThen(function (waitFor) { + if (!AppConfig.displayCreationScreen) { return; } + var priv = common.getMetadataMgr().getPrivateData(); + if (priv.isNewFile) { + var c = (priv.settings.general && priv.settings.general.creation) || {}; + if (c.skip && !priv.forceCreationScreen) { + return void common.createPad(c, waitFor()); + } + common.getPadCreationScreen(c, waitFor()); + } }).nThen(function (/* waitFor */) { Test.registerInner(common.getSframeChannel()); var metadataMgr = common.getMetadataMgr(); diff --git a/www/poll/main.js b/www/poll/main.js index 737038ead..85bbb6f62 100644 --- a/www/poll/main.js +++ b/www/poll/main.js @@ -36,6 +36,8 @@ define([ }; window.addEventListener('message', onMsg); }).nThen(function (/*waitFor*/) { - SFCommonO.start(); + SFCommonO.start({ + useCreationScreen: true + }); }); }); 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/app-todo.less b/www/todo/app-todo.less index d4fe830b3..5008498b1 100644 --- a/www/todo/app-todo.less +++ b/www/todo/app-todo.less @@ -88,6 +88,13 @@ color: #777; } + .cp-app-todo-task-input { + margin: @spacing; + flex: 1; + min-width: 0; + font-weight: bold; + display: none; + } .cp-app-todo-task-text { margin: @spacing; flex: 1; diff --git a/www/todo/inner.js b/www/todo/inner.js index 8d5de7778..f34592568 100644 --- a/www/todo/inner.js +++ b/www/todo/inner.js @@ -9,6 +9,7 @@ define([ '/common/common-hash.js', '/todo/todo.js', '/customize/messages.js', + '/bower_components/sortablejs/Sortable.min.js', 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', 'less!/bower_components/components-font-awesome/css/font-awesome.min.css', @@ -23,7 +24,8 @@ define([ UI, Hash, Todo, - Messages + Messages, + Sortable ) { var APP = window.APP = {}; @@ -47,6 +49,17 @@ define([ var onReady = function () { var todo = Todo.init(APP.lm.proxy); + Sortable.create($list[0], { + store: { + get: function () { + return todo.getOrder(); + }, + set: function (sortable) { + todo.reorder(sortable.toArray()); + } + } + }); + var deleteTask = function(id) { todo.remove(id); @@ -70,6 +83,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 +109,7 @@ define([ }; var addTaskUI = function (el, animate) { + if (!el) { return; } var $taskDiv = $('
', { 'class': 'cp-app-todo-task' }); @@ -101,6 +119,7 @@ define([ $taskDiv.appendTo($list); } $taskDiv.data('id', el); + $taskDiv.attr('data-id', el); makeCheckbox(el, function (/*state*/) { APP.display(); @@ -108,14 +127,34 @@ 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'); } - $('', { 'class': 'cp-app-todo-task-text' }) - .text(entry.task) - .appendTo($taskDiv); + var $span = $('', { 'class': 'cp-app-todo-task-text' }); + + var $input = $('', { + type: 'text', + 'class': 'cp-app-todo-task-input' + }).val(entry.task).keydown(function (e) { + if (e.which === 13) { + todo.val(el, 'task', $input.val().trim()); + $input.hide(); + $span.text($input.val().trim()); + $span.show(); + } + }).appendTo($taskDiv); + + $span.text(entry.task) + .appendTo($taskDiv) + .click(function () { + $input.show(); + $span.hide(); + }); /*$('', { 'class': 'cp-app-todo-task-date' }) .text(new Date(entry.ctime).toLocaleString()) .appendTo($taskDiv);*/ diff --git a/www/todo/todo.js b/www/todo/todo.js index 07b9962cc..8727180c8 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 */ @@ -59,6 +77,17 @@ define([ if (proxy.data[id]) { delete proxy.data[id]; } }; + /* change the order in the proxy (with a check to make sure that nothing is missing */ + var reorder = function (proxy, order) { + var existingOrder = proxy.order.slice().sort(); + var newOrder = order.slice().sort(); + if (JSON.stringify(existingOrder) === JSON.stringify(newOrder)) { + proxy.order = order.slice(); + } else { + console.error("Can't reorder the tasks. Some tasks are missing or added"); + } + }; + Todo.init = function (proxy) { var api = {}; initialize(proxy); @@ -72,6 +101,12 @@ define([ api.remove = function (id) { return remove(proxy, id); }; + api.getOrder = function () { + return proxy.order.slice(); + }; + api.reorder = function (order) { + return reorder(proxy, order); + }; return api; }; diff --git a/www/whiteboard/app-whiteboard.less b/www/whiteboard/app-whiteboard.less index 28e6a6e0c..29c739b6e 100644 --- a/www/whiteboard/app-whiteboard.less +++ b/www/whiteboard/app-whiteboard.less @@ -5,6 +5,7 @@ @import (once) '../../customize/src/less2/include/alertify.less'; @import (once) '../../customize/src/less2/include/tools.less'; @import (once) '../../customize/src/less2/include/tokenfield.less'; +@import (once) '../../customize/src/less2/include/creation.less'; .toolbar_main( @bg-color: @colortheme_whiteboard-bg, @@ -14,6 +15,7 @@ .fileupload_main(); .alertify_main(); .tokenfield_main(); +.creation_main(); // body &.cp-app-whiteboard { diff --git a/www/whiteboard/inner.js b/www/whiteboard/inner.js index 4e5729fa8..8d2621aaf 100644 --- a/www/whiteboard/inner.js +++ b/www/whiteboard/inner.js @@ -415,6 +415,7 @@ define([ Title.setToolbar(toolbar); var $rightside = toolbar.$rightside; + var $drawer = toolbar.$drawer; /* save as template */ if (!metadataMgr.getPrivateData().isTemplate) { @@ -428,7 +429,7 @@ define([ /* add an export button */ var $export = common.createButton('export', true, {}, saveImage); - $rightside.append($export); + $drawer.append($export); if (common.isLoggedIn()) { common.createButton('savetodrive', true, {}, function () {}) @@ -449,6 +450,9 @@ define([ }); $rightside.append($forget); + var $properties = common.createButton('properties', true); + toolbar.$drawer.append($properties); + if (!readOnly) { makeColorButton($rightside); @@ -562,7 +566,12 @@ define([ if (readOnly) { return; } - if (isNew) { + + var privateDat = metadataMgr.getPrivateData(); + var skipTemp = Util.find(privateDat, + ['settings', 'general', 'creation', 'noTemplate']); + var skipCreation = Util.find(privateDat, ['settings', 'general', 'creation', 'skip']); + if (isNew && (!AppConfig.displayCreationScreen || (!skipTemp && skipCreation))) { common.openTemplatePicker(); } }); @@ -589,6 +598,7 @@ define([ }; config.onAbort = function () { + if (APP.unrecoverable) { return; } // inform of network disconnect setEditable(false); toolbar.failed(); @@ -596,6 +606,7 @@ define([ }; config.onConnectionChange = function (info) { + if (APP.unrecoverable) { return; } setEditable(info.state); if (info.state) { initializing = true; @@ -605,10 +616,18 @@ define([ } }; + config.onError = function (err) { + common.onServerError(err, toolbar, function () { + APP.unrecoverable = true; + setEditable(false); + }); + }; + cpNfInner = common.startRealtime(config); metadataMgr = cpNfInner.metadataMgr; cpNfInner.onInfiniteSpinner(function () { + if (APP.unrecoverable) { return; } setEditable(false); UI.confirm(Messages.realtime_unrecoverableError, function (yes) { if (!yes) { return; } @@ -640,6 +659,18 @@ define([ $('body').append($div.html()); })); SFCommon.create(waitFor(function (c) { APP.common = common = c; })); + }).nThen(function (waitFor) { + common.getSframeChannel().onReady(waitFor()); + }).nThen(function (waitFor) { + if (!AppConfig.displayCreationScreen) { return; } + var priv = common.getMetadataMgr().getPrivateData(); + if (priv.isNewFile) { + var c = (priv.settings.general && priv.settings.general.creation) || {}; + if (c.skip && !priv.forceCreationScreen) { + return void common.createPad(c, waitFor()); + } + common.getPadCreationScreen(c, waitFor()); + } }).nThen(function (/*waitFor*/) { andThen(common); }); diff --git a/www/whiteboard/main.js b/www/whiteboard/main.js index ce1f14d9c..1c63ad811 100644 --- a/www/whiteboard/main.js +++ b/www/whiteboard/main.js @@ -36,6 +36,8 @@ define([ }; window.addEventListener('message', onMsg); }).nThen(function (/*waitFor*/) { - SFCommonO.start(); + SFCommonO.start({ + useCreationScreen: true + }); }); });