diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index 3f90e4d84..87caa929f 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -31,12 +31,15 @@ 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.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."; diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 16c18d678..367e704c9 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -32,12 +32,15 @@ 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.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."; diff --git a/expire-channels.js b/expire-channels.js index d64f3c349..fda2c20bb 100644 --- a/expire-channels.js +++ b/expire-channels.js @@ -2,12 +2,21 @@ var Fs = require("fs"); var Path = require("path"); var nThen = require("nthen"); -var config = require("./config"); +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); @@ -41,17 +50,17 @@ var handleTask = function (str, path, cb) { return cb(); } - nThen(function () { + nThen(function (waitFor) { switch (command) { case 'EXPIRE': console.log("expiring: %s", args[0]); - // TODO actually remove the file... + store.removeChannel(args[0], waitFor()); break; default: console.log("unknown command", command); } }).nThen(function () { - // remove the file... + // remove the task file... Fs.unlink(path, function (err) { if (err) { console.error(err); } cb(); @@ -64,6 +73,10 @@ nt = nThen(function (w) { if (e) { throw e; } dirs = list; })); +}).nThen(function (waitFor) { + FileStorage.create(config, waitFor(function (_store) { + store = _store; + })); }).nThen(function () { dirs.forEach(function (dir) { queue(function (w) { diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 248bd5e70..d0c0e1961 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -601,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/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..1ca46e7f1 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, 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/sframe-app-framework.js b/www/common/sframe-app-framework.js index 9c9c1b786..2e51776d6 100644 --- a/www/common/sframe-app-framework.js +++ b/www/common/sframe-app-framework.js @@ -43,6 +43,7 @@ define([ var STATE = Object.freeze({ DISCONNECTED: 'DISCONNECTED', FORGOTTEN: 'FORGOTTEN', + DELETED: 'DELETED', INFINITE_SPINNER: 'INFINITE_SPINNER', INITIALIZING: 'INITIALIZING', HISTORY_MODE: 'HISTORY_MODE', @@ -119,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); @@ -149,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)) { @@ -257,6 +263,7 @@ define([ var onReady = function () { var newContentStr = cpNfInner.chainpad.getUserDoc(); + if (state === STATE.DELETED) { return; } var newPad = false; if (newContentStr === '') { newPad = true; } @@ -316,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(); @@ -324,6 +332,18 @@ define([ } }; + var onError = function (err) { + stateChange(STATE.DELETED); + var msg = err.type; + if (err.type === 'EEXPIRED') { + msg = Messages.expiredError; + if (err.loaded) { + msg += Messages.expiredErrorCopy; + } + } + UI.errorLoadingScreen(msg, true, true); + }; + var setFileExporter = function (extension, fe, async) { var $export = common.createButton('export', true, {}, function () { var ext = (typeof(extension) === 'function') ? extension() : extension; @@ -441,7 +461,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()); @@ -457,6 +478,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(); 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-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..cb1bb764c 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,15 @@ define([ } }; + // When the pad is deleted from the server + toolbar.deleted = function (/*userId*/) { + toolbar.isErrorState = true; + toolbar.connected = false; + if (toolbar.spinner) { + toolbar.spinner.text(Messages.deletedFromServer); + } + }; + // On log out, remove permanently the realtime elements of the toolbar Common.onLogout(function () { failed();