diff --git a/customize.dist/loading.js b/customize.dist/loading.js index 32841f253..4f8b79125 100644 --- a/customize.dist/loading.js +++ b/customize.dist/loading.js @@ -1,7 +1,18 @@ // dark #326599 // light #4591c4 -define([], function () { +define(['/customize/messages.js'], function (Messages) { var loadingStyle = (function(){/* +@font-face { + font-family: 'Open Sans'; + src: url('/bower_components/open-sans-fontface/fonts/Regular/OpenSans-Regular.eot'); + src: url('/bower_components/open-sans-fontface/fonts/Regular/OpenSans-Regular.eot?#iefix') format('embedded-opentype'), + url('/bower_components/open-sans-fontface/fonts/Regular/OpenSans-Regular.woff') format('woff'), + url('/bower_components/open-sans-fontface/fonts/Regular/OpenSans-Regular.ttf') format('truetype'), + url('/bower_components/open-sans-fontface/fonts/Regular/OpenSans-Regular.svg#OpenSansRegular') format('svg'); + font-weight: normal; + font-style: normal; +} + #cp-loading { visibility: visible; position: fixed; @@ -19,6 +30,7 @@ define([], function () { flex-flow: column; justify-content: center; align-items: center; + font: 20px 'Open Sans', 'Helvetica Neue', sans-serif !important; } #cp-loading.cp-loading-hidden { opacity: 0; @@ -163,35 +175,10 @@ p.cp-password-info{ #cp-loading .cp-loading-spinner-container > div { height: 100px; } -#cp-loading-tip { - position: fixed; - z-index: 10000000; - top: 80%; - left: 0; - right: 0; - text-align: center; - transition: opacity 750ms; - transition-delay: 3000ms; -} -@media screen and (max-height: 600px) { - #cp-loading-tip { - display: none; - } -} -#cp-loading-tip span { - background: #222; - color: #fafafa; - text-align: center; - font-size: 1.3em; - opacity: 0.7; - font-family: 'Open Sans', 'Helvetica Neue', sans-serif; - padding: 15px; - max-width: 60%; - display: inline-block; -} .cp-loading-progress { width: 100%; margin: 20px; + text-align: center; } .cp-loading-progress p { margin: 5px; @@ -199,6 +186,31 @@ p.cp-password-info{ white-space: nowrap; text-overflow: ellipsis; } +.cp-loading-progress-list { + text-align: left; + display: inline-block; +} +.cp-loading-progress-list ul { + list-style: none; + padding-left: 0; +} +.cp-loading-progress-list li { + padding: 0px 5px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.cp-loading-progress-list li i { + width: 22px; +} +.cp-loading-progress-list li span{ + margin-left: 10px; +} +.cp-loading-progress-list li span.percent { + position: absolute; +} + .cp-loading-progress-bar { height: 24px; background: white; @@ -257,10 +269,72 @@ button.primary:hover{ '
', '', '
', + '
', + '
', + '
', + '
', '

', '' ].join(''); + var built = false; + + var types = ['less', 'drive', 'migrate', 'sf', 'team', 'pad', 'end']; + var current; + var makeList = function (data) { + var c = types.indexOf(data.type); + current = c; + var getLi = function (i) { + var check = (i < c || (i === c && data.progress >= 100)) ? 'fa-check-square-o' + : 'fa-square-o'; + var percentStr = ''; + if (i === c) { + var p = Math.min(Math.floor(data.progress), 100); + percentStr = '('+p+'%)'; + } + return '
  • '+Messages['loading_state_'+i]+'' + percentStr; + }; + var list = ''; + return list; + }; + var makeBar = function (data) { + var c = types.indexOf(data.type); + var l = types.length; + var progress = Math.min(data.progress, 100); + var p = (progress / l) + (100 * c / l); + var bar = '
    '+ + '
    '+ + '
    '; + return bar; + }; + + var updateLoadingProgress = function (data) { + if (!built) { return; } + var c = types.indexOf(data.type); + if (c < current) { return console.error(data); } + try { + document.querySelector('.cp-loading-spinner-container').style.display = 'none'; + document.querySelector('.cp-loading-progress-list').innerHTML = makeList(data); + document.querySelector('.cp-loading-progress-container').innerHTML = makeBar(data); + } catch (e) { console.error(e); } + }; + window.CryptPad_updateLoadingProgress = updateLoadingProgress; + window.CryptPad_loadingError = function (err) { + if (!built) { return; } + try { + var node = document.querySelector('.cp-loading-progress'); + if (node.parentNode) { node.parentNode.removeChild(node); } + document.querySelector('.cp-loading-spinner-container').setAttribute('style', 'display:none;'); + document.querySelector('#cp-loading-message').setAttribute('style', 'display:block;'); + document.querySelector('#cp-loading-message').innerText = err; + } catch (e) { console.error(e); } + }; return function () { + built = true; var intr; var append = function () { if (!document.body) { return; } diff --git a/customize.dist/messages.js b/customize.dist/messages.js index 11d814540..40dbbfb95 100755 --- a/customize.dist/messages.js +++ b/customize.dist/messages.js @@ -52,13 +52,12 @@ require.config({ }); var req = [ - '/common/common-util.js', '/customize/application_config.js', '/customize/translations/messages.js' ]; if (language && map[language]) { req.push('/customize/translations/messages.' + language + '.js'); } -define(req, function(Util, AppConfig, Default, Language) { +define(req, function(AppConfig, Default, Language) { map.en = 'English'; var defaultLanguage = 'en'; @@ -78,15 +77,15 @@ define(req, function(Util, AppConfig, Default, Language) { var extend = function (a, b) { for (var k in b) { - if (Util.isObject(b[k])) { - a[k] = Util.isObject(a[k]) ? a[k] : {}; - extend(a[k], b[k]); - continue; - } if (Array.isArray(b[k])) { a[k] = b[k].slice(); continue; } + if (b[k] && typeof(b[k]) === "object") { + a[k] = (a[k] && typeof(a[k]) === "object" && !Array.isArray(a[k])) ? a[k] : {}; + extend(a[k], b[k]); + continue; + } a[k] = b[k] || a[k]; } }; diff --git a/customize.dist/src/less2/include/tokenfield.less b/customize.dist/src/less2/include/tokenfield.less index 42a06624c..e3d2c3e5d 100644 --- a/customize.dist/src/less2/include/tokenfield.less +++ b/customize.dist/src/less2/include/tokenfield.less @@ -20,6 +20,22 @@ margin: 0 10px; padding: 0; width: ~"calc(100% - 20px)"; + span.tokenfield-empty { + font-size: 14px; + font-style: italic; + color: lighten(@cryptpad_text_col, 10%); + } + .cp-tokenfield-container { + width: 100%; + } + .cp-tokenfield-form { + display: flex; + width: 100%; + input { + flex: 1; + min-width: 0 !important; + } + } .token { box-sizing: border-box; display: inline-flex; diff --git a/docs/example.nginx.conf b/docs/example.nginx.conf index 117eb2cc4..8319c657b 100644 --- a/docs/example.nginx.conf +++ b/docs/example.nginx.conf @@ -57,9 +57,12 @@ server { add_header Access-Control-Allow-Origin "*"; # add_header X-Frame-Options "SAMEORIGIN"; + set $coop ''; + if ($uri ~ ^\/sheet\/.*$) { set $coop 'same-origin'; } + # Enable SharedArrayBuffer in Firefox (for .xlsx export) add_header Cross-Origin-Resource-Policy cross-origin; - add_header Cross-Origin-Opener-Policy same-origin; + add_header Cross-Origin-Opener-Policy $coop; add_header Cross-Origin-Embedder-Policy require-corp; # Insert the path to your CryptPad repository root here diff --git a/lib/commands/metadata.js b/lib/commands/metadata.js index 1d758564b..3d20f36e6 100644 --- a/lib/commands/metadata.js +++ b/lib/commands/metadata.js @@ -2,7 +2,6 @@ const Data = module.exports; const Meta = require("../metadata"); -const WriteQueue = require("../write-queue"); const Core = require("./core"); const Util = require("../common-util"); const HK = require("../hk-util"); @@ -53,7 +52,6 @@ Data.getMetadata = function (Env, channel, cb, Server, netfluxId) { value: value } */ -var queueMetadata = WriteQueue(); Data.setMetadata = function (Env, safeKey, data, cb, Server) { var unsafeKey = Util.unescapeKeyCharacters(safeKey); @@ -63,7 +61,7 @@ Data.setMetadata = function (Env, safeKey, data, cb, Server) { if (!command || typeof (command) !== 'string') { return void cb('INVALID_COMMAND'); } if (Meta.commands.indexOf(command) === -1) { return void cb('UNSUPPORTED_COMMAND'); } - queueMetadata(channel, function (next) { + Env.queueMetadata(channel, function (next) { Data.getMetadataRaw(Env, channel, function (err, metadata) { if (err) { cb(err); diff --git a/lib/commands/pin-rpc.js b/lib/commands/pin-rpc.js index 8b111438d..93d922620 100644 --- a/lib/commands/pin-rpc.js +++ b/lib/commands/pin-rpc.js @@ -162,7 +162,7 @@ Pinning.pinChannel = function (Env, safeKey, channels, cb) { // only pin channels which are not already pinned var toStore = channels.filter(function (channel) { - return pinned.indexOf(channel) === -1; + return channel && pinned.indexOf(channel) === -1; }); if (toStore.length === 0) { @@ -204,7 +204,7 @@ Pinning.unpinChannel = function (Env, safeKey, channels, cb) { // only unpin channels which are pinned var toStore = channels.filter(function (channel) { - return pinned.indexOf(channel) !== -1; + return channel && pinned.indexOf(channel) !== -1; }); if (toStore.length === 0) { diff --git a/lib/defaults.js b/lib/defaults.js index 329e16f4c..4110e63d4 100644 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -48,9 +48,6 @@ Default.httpHeaders = function () { "X-XSS-Protection": "1; mode=block", "X-Content-Type-Options": "nosniff", "Access-Control-Allow-Origin": "*", - "Cross-Origin-Resource-Policy": 'cross-origin', - "Cross-Origin-Opener-Policy": 'same-origin', - "Cross-Origin-Embedder-Policy": 'require-corp', }; }; diff --git a/lib/env.js b/lib/env.js index 97ebd20ce..b1fc6680b 100644 --- a/lib/env.js +++ b/lib/env.js @@ -45,6 +45,7 @@ module.exports.create = function (config) { queueStorage: WriteQueue(), queueDeletes: WriteQueue(), queueValidation: WriteQueue(), + queueMetadata: WriteQueue(), batchIndexReads: BatchRead("HK_GET_INDEX"), batchMetadata: BatchRead('GET_METADATA'), diff --git a/lib/historyKeeper.js b/lib/historyKeeper.js index d95c0e99f..cfdb14717 100644 --- a/lib/historyKeeper.js +++ b/lib/historyKeeper.js @@ -80,6 +80,10 @@ module.exports.create = function (Env, cb) { return void cb(); } + // If the channel is restricted, send the history keeper ID so that they + // can try to authenticate + allowed.unshift(Env.id); + // otherwise they're not allowed. // respond with a special error that includes the list of keys // which would be allowed... diff --git a/server.js b/server.js index 0e0c2d79e..60247f47a 100644 --- a/server.js +++ b/server.js @@ -60,6 +60,10 @@ var app = Express(); } }()); +var applyHeaderMap = function (res, map) { + for (let header in map) { res.setHeader(header, map[header]); } +}; + var setHeaders = (function () { // load the default http headers unless the admin has provided their own via the config file var headers; @@ -96,14 +100,21 @@ var setHeaders = (function () { } if (Object.keys(headers).length) { return function (req, res) { + // apply a bunch of cross-origin headers for XLSX export in FF and printing elsewhere + applyHeaderMap(res, { + "Cross-Origin-Resource-Policy": 'cross-origin', + "Cross-Origin-Opener-Policy": /^\/sheet\//.test(req.url)? 'same-origin': '', + "Cross-Origin-Embedder-Policy": 'require-corp', + }); + + // targeted CSP, generic policies, maybe custom headers const h = [ - ///^\/pad\/inner\.html.*/, /^\/common\/onlyoffice\/.*\/index\.html.*/, /^\/(sheet|ooslide|oodoc)\/inner\.html.*/, ].some((regex) => { return regex.test(req.url); }) ? padHeaders : headers; - for (let header in h) { res.setHeader(header, h[header]); } + applyHeaderMap(res, h); }; } return function () {}; @@ -139,6 +150,7 @@ app.use(function (req, res, next) { setHeaders(req, res); if (/[\?\&]ver=[^\/]+$/.test(req.url)) { res.setHeader("Cache-Control", "max-age=31536000"); } + else { res.setHeader("Cache-Control", "no-cache"); } next(); }); diff --git a/www/common/LessLoader.js b/www/common/LessLoader.js index b463a126c..5b52b3d2c 100644 --- a/www/common/LessLoader.js +++ b/www/common/LessLoader.js @@ -150,6 +150,7 @@ define([ }).nThen(function () { cb(); }); }; + var idx = 0; module.exports.load = function (url /*:string*/, cb /*:()=>void*/, stack /*:?Array*/) { var btime = stack ? null : +new Date(); stack = stack || []; @@ -163,11 +164,22 @@ define([ cb(); }; stack.push(url); + if (window.CryptPad_updateLoadingProgress) { + window.CryptPad_updateLoadingProgress({ + type: 'less', + progress: 4*idx++ + }); + } cacheGet(url, function (css) { if (css) { return void loadSubmodulesAndInject(css, url, done, stack); } console.debug('CACHE MISS ' + url); ((/\.less([\?\#].*)?$/.test(url)) ? loadLess : loadCSS)(url, function (err, css) { - if (!css) { return void console.error(err); } + if (!css) { + if (window.CryptPad_loadingError) { + window.CryptPad_loadingError('LESS: ' + (err && err.message)); + } + return void console.error(err); + } var output = fixAllURLs(css, url); cachePut(url, output); loadSubmodulesAndInject(output, url, done, stack); diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 2e6c3a609..d423b48ee 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -280,8 +280,82 @@ define([ }; var $root = $t.parent(); + + Messages.add = "Add"; // XXX + Messages.edit = "Edit"; // XXX + var $input = $root.find('.token-input'); + var $button = $(h('button.btn.btn-primary', [ + h('i.fa.fa-plus'), + h('span', Messages.add) + ])); + + + $button.click(function () { + $t.tokenfield('createToken', $input.val()); + }); + + var $container = $(h('span.cp-tokenfield-container')); + var $form = $(h('span.cp-tokenfield-form')); + $container.insertAfter($input); + + // Fix the UI to keep the "add" or "edit" button at the correct location + var isEdit = false; + var called = false; + var resetUI = function () { + called = true; + setTimeout(function () { + $container.find('.tokenfield-empty').remove(); + var $tokens = $root.find('.token').prependTo($container); + if (!$tokens.length) { + $container.prepend(h('span.tokenfield-empty', Messages.kanban_noTags)); + } + $form.append($input); + $form.append($button); + if (isEdit) { $button.find('span').text(Messages.edit); } + else { $button.find('span').text(Messages.add); } + $container.append($form); + $input.focus(); + isEdit = false; + called = false; + }); + }; + resetUI(); + + $t.on('tokenfield:removedtoken', function () { + resetUI(); + }); + $t.on('tokenfield:editedtoken', function () { + resetUI(); + }); + $t.on('tokenfield:createdtoken', function () { + $input.val(''); + resetUI(); + }); + $t.on('tokenfield:edittoken', function () { + isEdit = true; + }); + + // Fix UI issue where the input could go outside of the container + var MutationObserver = window.MutationObserver; + var observer = new MutationObserver(function(mutations) { + if (called) { return; } + mutations.forEach(function(mutation) { + for (var i = 0; i < mutation.addedNodes.length; i++) { + if (mutation.addedNodes[i].classList && + mutation.addedNodes[i].classList.contains('token-input')) { + resetUI(); + break; + } + } + }); + }); + observer.observe($root[0], { + childList: true, + subtree: false + }); + $t.on('tokenfield:removetoken', function () { - $root.find('.token-input').focus(); + $input.focus(); }); t.preventDuplicates = function (cb) { @@ -858,35 +932,33 @@ define([ var LOADING = 'cp-loading'; - var loading = { - error: false, - driveState: 0, - padState: 0 - }; UI.addLoadingScreen = function (config) { config = config || {}; var loadingText = config.loadingText; var todo = function () { var $loading = $('#' + LOADING); + // Show the loading screen $loading.css('display', ''); $loading.removeClass('cp-loading-hidden'); - $('.cp-loading-spinner-container').show(); - if (!config.noProgress && !$loading.find('.cp-loading-progress').length) { + if (config.newProgress) { + // XXX re-add progress bar for step 6 after password prompt for PPP + // XXX also burn after reading var progress = h('div.cp-loading-progress', [ - h('p.cp-loading-progress-drive'), - h('p.cp-loading-progress-pad') + h('p.cp-loading-progress-list'), + h('p.cp-loading-progress-container') ]); - $(progress).hide(); - $loading.find('.cp-loading-container').append(progress); - } else if (config.noProgress) { - $loading.find('.cp-loading-progress').remove(); + $loading.find('.cp-loading-spinner-container').after(progress); } + if (!$loading.find('.cp-loading-progress').length) { + // Add spinner + $('.cp-loading-spinner-container').show(); + } + // Add loading text if (loadingText) { $('#' + LOADING).find('#cp-loading-message').show().text(loadingText); } else { $('#' + LOADING).find('#cp-loading-message').hide().text(''); } - loading.error = false; }; if ($('#' + LOADING).length) { todo(); @@ -895,57 +967,9 @@ define([ todo(); } }; - UI.updateLoadingProgress = function (data, isDrive) { - var $loading = $('#' + LOADING); - if (!$loading.length || loading.error) { return; } - $loading.find('.cp-loading-progress').show(); - var $progress; - if (isDrive) { - // Drive state - if (loading.driveState === -1) { return; } // Already loaded - $progress = $loading.find('.cp-loading-progress-drive'); - if (!$progress.length) { return; } // Can't find the box to display data - - // If state is -1, remove the box, drive is loaded - if (data.state === -1) { - loading.driveState = -1; - $progress.remove(); - } else { - if (data.state < loading.driveState) { return; } // We should not display old data - // Update the current state - loading.driveState = data.state; - data.progress = data.progress || 100; - data.msg = Messages['loading_drive_'+ Math.floor(data.state)] || ''; - $progress.html(data.msg); - if (data.progress) { - $progress.append(h('div.cp-loading-progress-bar', [ - h('div.cp-loading-progress-bar-value', {style: 'width:'+data.progress+'%;'}) - ])); - } - } - } else { - // Pad state - if (loading.padState === -1) { return; } // Already loaded - $progress = $loading.find('.cp-loading-progress-pad'); - if (!$progress.length) { return; } // Can't find the box to display data - - // If state is -1, remove the box, pad is loaded - if (data.state === -1) { - loading.padState = -1; - $progress.remove(); - } else { - if (data.state < loading.padState) { return; } // We should not display old data - // Update the current state - loading.padState = data.state; - data.progress = data.progress || 100; - data.msg = Messages['loading_pad_'+data.state] || ''; - $progress.html(data.msg); - if (data.progress) { - $progress.append(h('div.cp-loading-progress-bar', [ - h('div.cp-loading-progress-bar-value', {style: 'width:'+data.progress+'%;'}) - ])); - } - } + UI.updateLoadingProgress = function (data) { + if (window.CryptPad_updateLoadingProgress) { + window.CryptPad_updateLoadingProgress(data); } }; UI.removeLoadingScreen = function (cb) { @@ -954,31 +978,23 @@ define([ cb = cb || function () {}; if (Test.__ASYNC_BLOCKER__) { Test.__ASYNC_BLOCKER__.pass(); } - $('#' + LOADING).addClass("cp-loading-hidden"); + var $loading = $('#' + LOADING); + $loading.addClass("cp-loading-hidden"); // Hide the loading screen + $loading.find('.cp-loading-progress').remove(); // Remove the progress list setTimeout(cb, 750); - loading.error = false; - var $tip = $('#cp-loading-tip').css('top', '') - // loading.less sets transition-delay: $wait-time - // and transition: opacity $fadeout-time - .css({ - 'opacity': 0, - 'pointer-events': 'none', - }); - window.setTimeout(function () { - $tip.remove(); - }, 3750); - // jquery.fadeout can get stuck }; UI.errorLoadingScreen = function (error, transparent, exitable) { var $loading = $('#' + LOADING); if (!$loading.is(':visible') || $loading.hasClass('cp-loading-hidden')) { - UI.addLoadingScreen({hideTips: true}); + UI.addLoadingScreen(); } - loading.error = true; + // Remove the progress list $loading.find('.cp-loading-progress').remove(); + // Hide the spinner $('.cp-loading-spinner-container').hide(); - $('#cp-loading-tip').remove(); if (transparent) { $loading.css('opacity', 0.9); } + + // Add the error message var $error = $loading.find('#cp-loading-message').show(); if (error instanceof Element) { $error.html('').append(error); @@ -990,7 +1006,6 @@ define([ $(window).keydown(function (e) { if (e.which === 27) { $loading.hide(); - loading.error = false; if (typeof(exitable) === "function") { exitable(); } } }); diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 8b9415220..a77aa1568 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -2531,10 +2531,16 @@ define([ var submit = function () { var value = $password.find('.cp-password-input').val(); - UI.addLoadingScreen(); + UI.addLoadingScreen({newProgress: true}); + if (window.CryptPad_updateLoadingProgress) { + window.CryptPad_updateLoadingProgress({ + type: 'pad', + progress: 0 + }); + } common.getSframeChannel().query('Q_PAD_PASSWORD_VALUE', value, function (err, data) { if (!data) { - UIElements.displayPasswordPrompt(common, cfg, true); + return void UIElements.displayPasswordPrompt(common, cfg, true); } }); }; diff --git a/www/common/cryptget.js b/www/common/cryptget.js index ab707a49b..e394788d7 100644 --- a/www/common/cryptget.js +++ b/www/common/cryptget.js @@ -1,12 +1,15 @@ define([ '/bower_components/chainpad-crypto/crypto.js', '/bower_components/chainpad-netflux/chainpad-netflux.js', + '/bower_components/netflux-websocket/netflux-client.js', '/common/common-util.js', '/common/common-hash.js', '/common/common-realtime.js', '/common/outer/network-config.js', + '/common/pinpad.js', + '/bower_components/nthen/index.js', '/bower_components/chainpad/chainpad.dist.js', -], function (Crypto, CPNetflux, Util, Hash, Realtime, NetConfig) { +], function (Crypto, CPNetflux, Netflux, Util, Hash, Realtime, NetConfig, Pinpad, nThen) { var finish = function (S, err, doc) { if (S.done) { return; } S.cb(err, doc); @@ -28,6 +31,50 @@ define([ } }; + var makeNetwork = function (cb) { + var wsUrl = NetConfig.getWebsocketURL(); + Netflux.connect(wsUrl).then(function (network) { + cb(null, network); + }, function (err) { + cb(err); + }); + }; + + var start = function (Session, config) { + // Create a network and authenticate with all our keys if necessary, + // then start chainpad-netflux + nThen(function (waitFor) { + if (Session.hasNetwork) { return; } + makeNetwork(waitFor(function (err, network) { + if (err) { return; } + config.network = network; + })); + }).nThen(function () { + Session.realtime = CPNetflux.start(config); + }); + }; + + var onRejected = function (config, Session, data, cb) { + // Check if we can authenticate + if (!Array.isArray(data) || !data.length || data[0].length !== 16) { + return void cb(true); + } + if (!Array.isArray(Session.accessKeys)) { return void cb(true); } + + // Authenticate + config.network.historyKeeper = data[0]; + nThen(function (waitFor) { + Session.accessKeys.forEach(function (obj) { + Pinpad.create(config.network, obj, waitFor(function (e) { + console.log('done', obj); + if (e) { console.error(e); } + })); + }); + }).nThen(function () { + cb(); + }); + }; + var makeConfig = function (hash, opt) { var secret; if (typeof(hash) === 'string') { @@ -67,7 +114,15 @@ define([ progress = progress || function () {}; var config = makeConfig(hash, opt); - var Session = { cb: cb, hasNetwork: Boolean(opt.network) }; + var Session = { + cb: cb, + accessKeys: opt.accessKeys, + hasNetwork: Boolean(opt.network) + }; + + config.onRejected = function (data, cb) { + onRejected(config, Session, data, cb); + }; config.onReady = function (info) { var rt = Session.session = info.realtime; @@ -95,7 +150,7 @@ define([ overwrite(config, opt); - Session.realtime = CPNetflux.start(config); + start(Session, config); }; var put = function (hash, doc, cb, opt) { @@ -105,7 +160,15 @@ define([ opt = opt || {}; var config = makeConfig(hash, opt); - var Session = { cb: cb, hasNetwork: Boolean(opt.network) }; + var Session = { + cb: cb, + accessKeys: opt.accessKeys, + hasNetwork: Boolean(opt.network) + }; + + config.onRejected = function (data, cb) { + onRejected(config, Session, data, cb); + }; config.onReady = function (info) { var realtime = Session.session = info.realtime; @@ -126,7 +189,7 @@ define([ }; overwrite(config, opt); - Session.session = CPNetflux.start(config); + start(Session, config); }; return { diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 696627cbe..a9b2923a9 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -68,6 +68,38 @@ define([ }, cb); }; + common.getAccessKeys = function (cb) { + var keys = []; + Nthen(function (waitFor) { + // Push account keys + postMessage("GET", { + key: ['edPrivate'], + }, waitFor(function (obj) { + if (obj.error) { return; } + try { + keys.push({ + edPrivate: obj, + edPublic: Hash.getSignPublicFromPrivate(obj) + }); + } catch (e) { console.error(e); } + })); + // Push teams keys + postMessage("GET", { + key: ['teams'], + }, waitFor(function (obj) { + if (obj.error) { return; } + Object.keys(obj || {}).forEach(function (id) { + var t = obj[id]; + var _keys = t.keys.drive || {}; + if (!_keys.edPrivate) { return; } + keys.push(t.keys.drive); + }); + })); + }).nThen(function () { + cb(keys); + }); + }; + common.makeNetwork = function (cb) { require([ '/bower_components/netflux-websocket/netflux-client.js', @@ -629,6 +661,10 @@ define([ optsPut.password = password; })); } + common.getAccessKeys(waitFor(function (keys) { + optsGet.accessKeys = keys; + optsPut.accessKeys = keys; + })); }).nThen(function () { Crypt.get(parsed.hash, function (err, val) { if (err) { @@ -666,19 +702,28 @@ define([ password: data.password, initialState: parsed.type === 'poll' ? '{}' : undefined }; - Crypt.get(parsed.hash, _waitFor(function (err, _val) { - if (err) { - _waitFor.abort(); - return void cb(err); - } - try { - val = JSON.parse(_val); - fixPadMetadata(val, true); - } catch (e) { - _waitFor.abort(); - return void cb(e.message); - } - }), optsGet); + var next = _waitFor(); + Nthen(function (waitFor) { + // Authenticate in case the pad os restricted + common.getAccessKeys(waitFor(function (keys) { + optsGet.accessKeys = keys; + })); + }).nThen(function () { + Crypt.get(parsed.hash, function (err, _val) { + if (err) { + _waitFor.abort(); + return void cb(err); + } + try { + val = JSON.parse(_val); + fixPadMetadata(val, true); + next(); + } catch (e) { + _waitFor.abort(); + return void cb(e.message); + } + }, optsGet); + }); return; } @@ -741,9 +786,6 @@ define([ }).nThen(function () { Crypt.put(parsed2.hash, JSON.stringify(val), function () { cb(); - Crypt.get(parsed2.hash, function (err, val) { - console.warn(val); - }); }, optsPut); }); @@ -1006,7 +1048,7 @@ define([ oldSecret = Hash.getSecrets(parsed.type, parsed.hash, optsGet.password); oldChannel = oldSecret.channel; common.getPadMetadata({channel: oldChannel}, waitFor(function (metadata) { - oldMetadata = metadata; + oldMetadata = metadata || {}; })); common.getMetadata(waitFor(function (err, data) { if (err) { @@ -1058,6 +1100,11 @@ define([ if (expire) { optsPut.metadata.expire = (expire - (+new Date())) / 1000; // Lifetime in seconds } + }).nThen(function (waitFor) { + common.getAccessKeys(waitFor(function (keys) { + optsGet.accessKeys = keys; + optsPut.accessKeys = keys; + })); }).nThen(function (waitFor) { Crypt.get(parsed.hash, waitFor(function (err, val) { if (err) { @@ -1074,6 +1121,8 @@ define([ } }), optsGet); }).nThen(function (waitFor) { + optsPut.metadata.restricted = oldMetadata.restricted; + optsPut.metadata.allowed = oldMetadata.allowed; Crypt.put(newHash, cryptgetVal, waitFor(function (err) { if (err) { waitFor.abort(); @@ -1309,11 +1358,17 @@ define([ validateKey: newSecret.keys.validateKey }, }; + var optsGet = {}; Nthen(function (waitFor) { common.getPadAttribute('', waitFor(function (err, _data) { padData = _data; + optsGet.password = padData.password; }), href); + common.getAccessKeys(waitFor(function (keys) { + optsGet.accessKeys = keys; + optsPut.accessKeys = keys; + })); }).nThen(function (waitFor) { oldSecret = Hash.getSecrets(parsed.type, parsed.hash, padData.password); @@ -1392,9 +1447,7 @@ define([ waitFor.abort(); return void cb({ error: 'CANT_PARSE' }); } - }), { - password: padData.password - }); + }), optsGet); }).nThen(function (waitFor) { // Re-encrypt rtchannel oldRtChannel = Util.find(cryptgetVal, ['content', 'channel']); diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index 649b5fe86..5d6a71b9d 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -4549,6 +4549,9 @@ define([ var rEl = manager.find(restorePath); if (manager.isFile(rEl)) { restoreName = manager.getTitle(rEl); + } else if (manager.isSharedFolder(rEl)) { + var sfData = manager.getSharedFolderData(rEl); + restoreName = sfData.title || sfData.lastTitle || Messages.fm_deletedFolder; } else { restoreName = restorePath[1]; } diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index e849ce20d..bfbc08b77 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -1080,6 +1080,11 @@ define([ if (data.teamId && s.id !== data.teamId) { return; } if (storeLocally && s.id) { return; } + // If this is an edit link but we don't have edit rights, this entry is not useful + if (h.mode === "edit" && s.id && !s.secondaryKey) { + return; + } + var res = s.manager.findChannel(channel, true); if (res.length) { sendTo.push(s.id); @@ -1373,11 +1378,15 @@ define([ } } }; - var loadUniversal = function (Module, type, waitFor) { + var loadUniversal = function (Module, type, waitFor, clientId) { if (store.modules[type]) { return; } store.modules[type] = Module.init({ Store: Store, store: store, + updateLoadingProgress: function (data) { + data.type = "team"; + postMessage(clientId, 'LOADING_DRIVE', data); + }, updateMetadata: function () { broadcast([], "UPDATE_METADATA"); }, @@ -2480,32 +2489,40 @@ define([ addSharedFolderHandler(); nThen(function (waitFor) { - postMessage(clientId, 'LOADING_DRIVE', { - state: 2 - }); userObject.migrate(waitFor()); }).nThen(function (waitFor) { initAnonRpc(null, null, waitFor()); initRpc(null, null, waitFor()); + postMessage(clientId, 'LOADING_DRIVE', { + type: 'migrate', + progress: 0 + }); }).nThen(function (waitFor) { Migrate(proxy, waitFor(), function (version, progress) { postMessage(clientId, 'LOADING_DRIVE', { - state: (2 + (version / 10)), + type: 'migrate', progress: progress }); }, store); }).nThen(function (waitFor) { postMessage(clientId, 'LOADING_DRIVE', { - state: 3 + type: 'sf', + progress: 0 }); userObject.fixFiles(); - SF.loadSharedFolders(Store, store.network, store, userObject, waitFor); + SF.loadSharedFolders(Store, store.network, store, userObject, waitFor, function (obj) { + var data = { + type: 'sf', + progress: 100*obj.progress/obj.max + }; + postMessage(clientId, 'LOADING_DRIVE', data); + }); loadCursor(); loadOnlyOffice(); loadUniversal(Messenger, 'messenger', waitFor); store.messenger = store.modules['messenger']; loadUniversal(Profile, 'profile', waitFor); - loadUniversal(Team, 'team', waitFor); + loadUniversal(Team, 'team', waitFor, clientId); loadUniversal(History, 'history', waitFor); cleanFriendRequests(); }).nThen(function () { @@ -2607,6 +2624,12 @@ define([ if (!hash) { return void cb({error: '[Store.init] Unable to find or create a drive hash. Aborting...'}); } + + var updateProgress = function (data) { + data.type = 'drive'; + postMessage(clientId, 'LOADING_DRIVE', data); + }; + // No password for drive var secret = Hash.getSecrets('drive', hash); store.driveChannel = secret.channel; @@ -2620,6 +2643,7 @@ define([ userName: 'fs', logLevel: 1, ChainPad: ChainPad, + updateProgress: updateProgress, classic: true, }; var rt = window.rt = Listmap.create(listmapConfig); @@ -2643,7 +2667,6 @@ define([ && !drive['filesData']) { drive[Constants.oldStorageKey] = []; } - postMessage(clientId, 'LOADING_DRIVE', { state: 1 }); // Drive already exist: return the existing drive, don't load data from legacy store onReady(clientId, returned, cb); }) diff --git a/www/common/outer/sharedfolder.js b/www/common/outer/sharedfolder.js index f02ea9c5a..e34cd64df 100644 --- a/www/common/outer/sharedfolder.js +++ b/www/common/outer/sharedfolder.js @@ -320,9 +320,12 @@ define([ - userObject: userObject associated to the main drive - handler: a function (sfid, rt) called for each shared folder loaded */ - SF.loadSharedFolders = function (Store, network, store, userObject, waitFor) { + SF.loadSharedFolders = function (Store, network, store, userObject, waitFor, progress) { var shared = Util.find(store.proxy, ['drive', UserObject.SHARED_FOLDERS]) || {}; + var steps = Object.keys(shared).length; + var i = 1; var w = waitFor(); + progress = progress || function () {}; nThen(function (waitFor) { Object.keys(shared).forEach(function (id) { var sf = shared[id]; @@ -330,7 +333,13 @@ define([ network: network, store: store, isNewChannel: Store.isNewChannel - }, id, sf, waitFor()); + }, id, sf, waitFor(function () { + progress({ + progress: i, + max: steps + }); + i++; + })); }); }).nThen(function () { setTimeout(w); diff --git a/www/common/outer/team.js b/www/common/outer/team.js index e5a40505e..de9206511 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -328,7 +328,13 @@ define([ ctx.teams[id] = team; registerChangeEvents(ctx, team, proxy); SF.checkMigration(team.secondaryKey, proxy, team.userObject, waitFor()); - SF.loadSharedFolders(ctx.Store, ctx.store.network, team, team.userObject, waitFor); + SF.loadSharedFolders(ctx.Store, ctx.store.network, team, + team.userObject, waitFor, function (data) { + ctx.progress += 70/(ctx.numberOfTeams * data.max); + ctx.updateProgress({ + progress: ctx.progress + }); + }); }).nThen(function () { if (!team.rpc) { return; } var list = getTeamChannelList(ctx, id); @@ -361,6 +367,9 @@ define([ }; + // Progress: + // One team = (30/(#teams))% + // One shared folder = (70/(#teams * #folders))% var openChannel = function (ctx, teamData, id, _cb) { var cb = Util.once(Util.mkAsync(_cb)); @@ -526,6 +535,10 @@ define([ Feedback.send("TEAM_RIGHTS_OWNER"); } }).nThen(function () { + ctx.progress += 30/ctx.numberOfTeams; + ctx.updateProgress({ + progress: ctx.progress + }); onReady(ctx, id, lm, roster, keys, null, cb); }); }; @@ -1686,10 +1699,13 @@ define([ emit: emit, onReadyHandlers: {}, teams: {}, - updateMetadata: cfg.updateMetadata + updateMetadata: cfg.updateMetadata, + updateProgress: cfg.updateLoadingProgress, + progress: 0 }; var teams = store.proxy.teams = store.proxy.teams || {}; + ctx.numberOfTeams = Object.keys(teams).length; // Listen for changes in our access rights (if another worker receives edit access) ctx.store.proxy.on('change', ['teams'], function (o, n, p) { diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js index beaf20b10..3097cf6d2 100644 --- a/www/common/sframe-app-framework.js +++ b/www/common/sframe-app-framework.js @@ -441,9 +441,9 @@ define([ var versionHashEl; var onInit = function () { UI.updateLoadingProgress({ - state: 2, + type: 'pad', progress: 0.1 - }, false); + }); stateChange(STATE.INITIALIZING); if ($('.cp-help-container').length) { var privateDat = cpNfInner.metadataMgr.getPrivateData(); @@ -471,8 +471,6 @@ define([ var newContentStr = cpNfInner.chainpad.getUserDoc(); if (state === STATE.DELETED) { return; } - UI.updateLoadingProgress({ state: -1 }, false); - if (toolbar) { // Check if we have a new chainpad instance toolbar.resetChainpad(cpNfInner.chainpad); @@ -708,9 +706,6 @@ define([ nThen(function (waitFor) { UI.addLoadingScreen(); SFCommon.create(waitFor(function (c) { common = c; })); - UI.updateLoadingProgress({ - state: 1 - }, false); }).nThen(function (waitFor) { common.getSframeChannel().onReady(waitFor()); }).nThen(function (waitFor) { diff --git a/www/common/sframe-boot2.js b/www/common/sframe-boot2.js index 0d0eaa90c..5a68237ad 100644 --- a/www/common/sframe-boot2.js +++ b/www/common/sframe-boot2.js @@ -42,6 +42,9 @@ define([ console.error("Require.js threw a Script Error. This probably means you're missing a dependency for CryptPad.\nIt is recommended that the admin of this server runs `bower install && bower update` to get the latest code, then modify their cache version.\nBest of luck,\nThe CryptPad Developers"); return void console.log(); } + if (window.CryptPad_loadingError) { + window.CryptPad_loadingError(e); + } throw e; }; diff --git a/www/common/sframe-chainpad-netflux-inner.js b/www/common/sframe-chainpad-netflux-inner.js index b6f4c6aaf..abb1cebdf 100644 --- a/www/common/sframe-chainpad-netflux-inner.js +++ b/www/common/sframe-chainpad-netflux-inner.js @@ -140,7 +140,7 @@ define([ chainpad.message(content); if (isHistory && updateLoadingProgress) { updateLoadingProgress({ - state: 2, + type: 'pad', progress: isHistory }, false); isHistory++; @@ -149,6 +149,13 @@ define([ }); sframeChan.on('EV_RT_READY', function () { if (isReady) { return; } + if (updateLoadingProgress) { + updateLoadingProgress({ + type: 'end', + progress: 0 + }, false); + isHistory++; + } isReady = true; isHistory = false; chainpad.start(); diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index bba393afb..086841f73 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -131,13 +131,7 @@ define([ if (sframeChan) { sframeChan.event('EV_LOADING_INFO', data); } }); - Cryptpad.ready(waitFor(function () { - if (sframeChan) { - sframeChan.event('EV_LOADING_INFO', { - state: -1 - }); - } - }), { + Cryptpad.ready(waitFor(), { driveEvents: cfg.driveEvents, currentPad: currentPad }); @@ -317,10 +311,11 @@ define([ return void noPadData('NO_RESULT'); } // Data found but weaker? warn + expire = res.expire; if (edit && !res.href) { newHref = res.roHref; + return; } - expire = res.expire; // We have good data, keep the hash in memory newHref = edit ? res.href : (res.roHref || res.href); })); @@ -1380,8 +1375,10 @@ define([ }; var i = 0; sframeChan.on('Q_CRYPTGET', function (data, cb) { + var keys; var todo = function () { data.opts.network = cgNetwork; + data.opts.accessKeys = keys; Cryptget.get(data.hash, function (err, val) { cb({ error: err, @@ -1400,17 +1397,21 @@ define([ cgNetwork = undefined; } i++; - if (!cgNetwork) { - cgNetwork = true; - return void Cryptpad.makeNetwork(function (err, nw) { - console.log(nw); - cgNetwork = nw; - todo(); - }); - } else if (cgNetwork === true) { - return void whenCGReady(todo); - } - todo(); + + Cryptpad.getAccessKeys(function (_keys) { + keys = _keys; + if (!cgNetwork) { + cgNetwork = true; + return void Cryptpad.makeNetwork(function (err, nw) { + console.log(nw); + cgNetwork = nw; + todo(); + }); + } else if (cgNetwork === true) { + return void whenCGReady(todo); + } + todo(); + }); }); sframeChan.on('EV_CRYPTGET_DISCONNECT', function () { if (!cgNetwork) { return; } diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index 8d4898911..f7a2c41c3 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -345,7 +345,13 @@ define([ } if (priv.burnAfterReading) { UIElements.displayBurnAfterReadingPage(funcs, waitFor(function () { - UI.addLoadingScreen(); + UI.addLoadingScreen({newProgress: true}); + if (window.CryptPad_updateLoadingProgress) { + window.CryptPad_updateLoadingProgress({ + type: 'pad', + progress: 0 + }); + } ctx.sframeChan.event('EV_BURN_AFTER_READING'); })); } @@ -619,6 +625,13 @@ define([ } window.CryptPad_sframe_common = true; + if (window.CryptPad_updateLoadingProgress) { + window.CryptPad_updateLoadingProgress({ + type: 'drive', + progress: 0 + }); + } + nThen(function (waitFor) { var msgEv = Util.mkEvent(); var iframe = window.parent; @@ -686,7 +699,8 @@ define([ }); ctx.sframeChan.on('EV_LOADING_INFO', function (data) { - UI.updateLoadingProgress(data, 'drive'); + //UI.updateLoadingProgress(data, 'drive'); + UI.updateLoadingProgress(data); }); ctx.sframeChan.on('EV_NEW_VERSION', function () { diff --git a/www/common/translations/messages.fi.json b/www/common/translations/messages.fi.json index 3b5835ff7..fe4ee1f64 100644 --- a/www/common/translations/messages.fi.json +++ b/www/common/translations/messages.fi.json @@ -188,12 +188,12 @@ "help_button": "Ohje", "historyText": "Historia", "historyButton": "Näytä asiakirjan historia", - "history_next": "Uudempi versio", - "history_prev": "Vanhempi versio", + "history_next": "Seuraava versio", + "history_prev": "Edellinen versio", "history_loadMore": "Lataa lisää historiatietoja", "history_closeTitle": "Sulje historia", "history_restoreTitle": "Palauta asiakirjan valittu versio", - "history_restorePrompt": "Oletko varma, että haluat korvata asiakirjan nykyisen version esitetyllä versiolla?", + "history_restorePrompt": "Oletko varma, että haluat korvata asiakirjan tämänhetkisen version näytetyllä versiolla?", "history_restoreDone": "Asiakirja palautettu", "history_version": "Versio:", "openLinkInNewTab": "Avaa linkki uuteen välilehteen", @@ -452,7 +452,7 @@ "settings_backupHint": "Varmuuskopioi tai palauta CryptDrivesi sisältö kokonaisuudessaan. Varmuuskopio ei sisällä padiesi sisältöä, ainoastaan niiden käyttöön tarvittavat avaimet.", "settings_backup": "Varmuuskopioi", "settings_restore": "Palauta", - "settings_backupHint2": "Lataa kaikkien padiesi nykyinen sisältö. Padit ladataan luettavassa tiedostomuodossa, jos sellainen on saatavilla.", + "settings_backupHint2": "Lataa kaikkien asiakirjojen nykyinen sisältö tietokoneellesi. Asiakirjat ladataan muissa sovelluksissa toimivissa tiedostomuodoissa, jos se on mahdollista. Jos sopivaa tiedostomuotoa ei ole saatavilla, asiakirjat ladataan CryptPad-yhteensopivassa tiedostomuodossa.", "settings_backup2": "Lataa oma CryptDrive tietokoneellesi", "settings_backup2Confirm": "Tämä lataa kaikki CryptDrivesi padit ja tiedostot tietokoneellesi. Jos haluat jatkaa, valitse nimi ja paina OK", "settings_exportTitle": "Vie oma CryptDrive", @@ -1176,9 +1176,9 @@ "settings_safeLinksTitle": "Turvalliset linkit", "settings_cat_security": "Luottamuksellisuus", "imprint": "Oikeudellinen huomautus", - "oo_sheetMigration_anonymousEditor": "Taulukon muokkaaminen on poistettu käytöstä anonyymeille käyttäjille, kunnes rekisteröitynyt käyttäjä päivittää sen viimeisimpään versioon.", + "oo_sheetMigration_anonymousEditor": "Rekisteröitymättömät käyttäjät eivät voi muokata taulukkoa ennen kuin rekisteröitynyt käyttäjä päivittää sen viimeisimpään versioon.", "oo_sheetMigration_complete": "Päivitetty versio saatavilla, paina OK ladataksesi uudelleen.", - "oo_sheetMigration_loading": "Päivitetään taulukkoasi viimeisimpään versioon", + "oo_sheetMigration_loading": "Päivitetään taulukkoasi viimeisimpään versioon. Ole hyvä ja odota noin 1 minuutti.", "oo_exportInProgress": "Vienti menossa", "oo_importInProgress": "Tuonti menossa", "oo_invalidFormat": "Tätä tiedostoa ei voida tuoda", @@ -1404,5 +1404,60 @@ "readme_cat1_l2": "Avaa padeja CryptDrivestasi: kaksoisnapsauta padin kuvaketta avataksesi sen.", "readme_cat1_l1": "Luo padi: Siirry CryptDriveesi, napsauta {0} ja sitten {1}, ja voit luoda padin.", "readme_cat1": "Tutustu CryptDriveesi", - "readme_p2": "Tämä padi toimii pikaisena perehdytyksenä CryptPadin ominaisuuksiin - kuinka tehdä muistiinpanoja, järjestellä niitä ja tehdä yhteistyötä niiden parissa." + "readme_p2": "Tämä padi toimii pikaisena perehdytyksenä CryptPadin ominaisuuksiin - kuinka tehdä muistiinpanoja, järjestellä niitä ja tehdä yhteistyötä niiden parissa.", + "fm_shareFolderPassword": "Suojaa kansio salasanalla (vapaaehtoinen)", + "access_destroyPad": "Tuhoa tämä asiakirja tai kansio lopullisesti", + "fm_deletedFolder": "Poistettu kansio", + "admin_limitUser": "Käyttäjän julkinen avain", + "team_exportButton": "Lataa", + "team_exportHint": "Lataa kaikki tiimin CryptDriveen tallennetut asiakirjat tietokoneellesi. Asiakirjat ladataan muissa sovelluksissa toimivissa tiedostomuodoissa, jos se on mahdollista. Jos sopivaa tiedostomuotoa ei ole saatavilla, asiakirjat ladataan CryptPad-yhteensopivassa tiedostomuodossa.", + "team_exportTitle": "Lataa tiimin CryptDrive", + "admin_cat_quota": "Käyttäjätallennustila", + "admin_invalLimit": "Virheellinen arvo kiintiökentässä", + "admin_invalKey": "Virheellinen julkinen avain", + "admin_limitSetNote": "Huomautus", + "admin_limitMB": "Kiintiö (megatavuissa)", + "admin_setlimitTitle": "Ota mukautettu kiintiö käyttöön", + "admin_setlimitHint": "Aseta mukautettu tallennustilakiintiö käyttäjälle tämän julkisen avaimen avulla. Voit päivittää tai poistaa olemassaolevan tallennustilakiintiön.", + "admin_limitNote": "Huomautus: {0}", + "admin_limitPlan": "Tilaus: {0}", + "admin_defaultlimitHint": "Tallennustilakiintiö käyttäjien ja tiimien CryptDriveille, joilla ei ole erikseen määriteltyjä sääntöjä", + "admin_defaultlimitTitle": "Tallennustilakiintiö (Mt)", + "admin_getlimitsHint": "Listaa muokatut tallennustilakiintiöt, jotka ovat käytössä CryptPad-palvelimellasi.", + "admin_getlimitsTitle": "Muokatut tallennustilakiintiöt", + "admin_limit": "Nykyinen rajoitus: {0}", + "admin_setlimitButton": "Aseta rajoitus", + "admin_registrationAllow": "Avaa", + "admin_registrationButton": "Sulje", + "admin_registrationTitle": "Sulje rekisteröityminen", + "admin_registrationHint": "Älä salli uusien käyttäjien rekisteröitymistä", + "snapshots_cantMake": "Tilannevedoksen luominen epäonnistui. Yhteytesi on katkennut.", + "snapshots_notFound": "Tämä tilannevedos ei ole enää saatavilla, koska asiakirjan historia on poistettu.", + "snapshot_error_exists": "Tästä versiosta on jo olemassa tilannevedos", + "snapshots_ooPickVersion": "Valitse versio, jotta voit luoda tilannevedoksen", + "oo_version": "Versio: ", + "oo_version_latest": "Viimeisin", + "snapshots_delete": "Poista", + "oo_deletedVersion": "Tämä versio ei ole enää saatavilla historiassa.", + "snapshots_close": "Sulje", + "snapshots_restore": "Palauta", + "snapshots_open": "Avaa", + "snapshots_placeholder": "Tilannevedoksen otsikko", + "snapshots_new": "Uusi tilannevedos", + "snapshots_button": "Tilannevedokset", + "snaphot_title": "Tilannevedos", + "infobar_versionHash": "Katselet asiakirjan vanhaa versiota ({0}).", + "history_restoreDriveDone": "CryptDrive palautettu", + "history_restoreDrivePrompt": "Oletko varma, että haluat korvata CryptDriven nykyisen version valitulla versiolla?", + "history_restoreDriveTitle": "Palauta CryptDrive valitsemaasi versioon", + "history_userNext": "Seuraava laatija", + "history_fastNext": "Seuraava muokkaussessio", + "history_userPrev": "Edellinen laatija", + "history_fastPrev": "Edellinen muokkaussessio", + "share_versionHash": "Olet jakamassa valitsemasi historiaversion asiakirjastasi vain luku-tilassa. Tämä antaa myös katseluoikeuden asiakirjan kaikkiin versioihin.", + "history_shareTitle": "Jaa linkki tähän versioon", + "history_cantRestore": "Palauttaminen epäonnistui. Yhteytesi on katkennut.", + "history_close": "Sulje", + "history_restore": "Palauta", + "share_bar": "Luo linkki" } diff --git a/www/common/translations/messages.fr.json b/www/common/translations/messages.fr.json index ce19c96ad..7d8a84efb 100644 --- a/www/common/translations/messages.fr.json +++ b/www/common/translations/messages.fr.json @@ -1459,5 +1459,11 @@ "admin_limitUser": "Clé publique de l'utilisateur", "fm_shareFolderPassword": "Protéger ce dossier avec un mot de passe (optionnel)", "access_destroyPad": "Détruire ce document ou dossier définitivement", - "fm_deletedFolder": "Dossier supprimé" + "fm_deletedFolder": "Dossier supprimé", + "loading_state_5": "Reconstruction du document", + "loading_state_4": "Chargement des équipes", + "loading_state_3": "Chargement des dossiers partagés", + "loading_state_2": "Mise à jour du contenu", + "loading_state_1": "Chargement du drive", + "loading_state_0": "Construction de l'interface" } diff --git a/www/common/translations/messages.json b/www/common/translations/messages.json index 010287444..e3b84adb7 100644 --- a/www/common/translations/messages.json +++ b/www/common/translations/messages.json @@ -1459,5 +1459,11 @@ "admin_limitUser": "User's public key", "fm_deletedFolder": "Deleted folder", "access_destroyPad": "Destroy this document or folder permanently", - "fm_shareFolderPassword": "Protect this folder with a password (optional)" + "fm_shareFolderPassword": "Protect this folder with a password (optional)", + "loading_state_0": "Build interface", + "loading_state_1": "Load drive", + "loading_state_2": "Update content", + "loading_state_3": "Load shared folders", + "loading_state_4": "Load Teams", + "loading_state_5": "Reconstruct document" } diff --git a/www/common/userObject.js b/www/common/userObject.js index 680ef3d0b..ca716f25f 100644 --- a/www/common/userObject.js +++ b/www/common/userObject.js @@ -846,7 +846,8 @@ define([ }; exp.ownedInTrash = function (isOwned) { return getFiles([TRASH]).map(function (id) { - var data = exp.getFileData(id); + var data = isSharedFolder(id) ? files[SHARED_FOLDERS][id] : exp.getFileData(id); + if (!data) { return; } return isOwned(data.owners) ? data.channel : undefined; }).filter(Boolean); };