From c56815fa3be8d84b9f74c27bf6b5213db5a7e7eb Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 5 Dec 2019 19:41:45 -0500 Subject: [PATCH 01/55] WIP nginx config refactor --- docs/example.nginx.conf | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/docs/example.nginx.conf b/docs/example.nginx.conf index a54687560..462b205fd 100644 --- a/docs/example.nginx.conf +++ b/docs/example.nginx.conf @@ -6,7 +6,16 @@ server { listen 443 ssl http2; - server_name your-main-domain.com your-sandbox-domain.com; + + # XXX These two variables + set $main_domain "your-main-domain.com"; + set $sandbox_domain "your-sandbox-domain.com"; + + # The $api_domain and $files_domain are optional + set $api_domain "api.your-main-domain.com"; + set $files_domain "files.your-main-domain.com"; + + server_name $main_domain $sandbox_domain; ssl_certificate /home/cryptpad/.acme.sh/your-main-domain.com/fullchain.cer; ssl_certificate_key /home/cryptpad/.acme.sh/your-main-domain.com/your-main-domain.com.key; @@ -24,6 +33,7 @@ server { add_header X-Content-Type-Options nosniff; # add_header X-Frame-Options "SAMEORIGIN"; + # Insert the path to your CryptPad repository root here root /home/cryptpad/cryptpad; index index.html; error_page 404 /customize.dist/404.html; @@ -34,15 +44,15 @@ server { # Will not set any header if it is emptystring add_header Cache-Control $cacheControl; - set $styleSrc "'unsafe-inline' 'self' your-main-domain.com"; - set $scriptSrc "'self' your-main-domain.com"; - set $connectSrc "'self' https://your-main-domain.com wss://your-main-domain.com your-main-domain.com https://api.your-main-domain.com blob: your-main-domain.com"; - set $fontSrc "'self' data: your-main-domain.com"; - set $imgSrc "'self' data: * blob: your-main-domain.com"; - set $frameSrc "'self' your-sandbox-domain.com blob: your-sandbox-domain.com"; - set $mediaSrc "'self' data: * blob: your-main-domain.com"; - set $childSrc "https://your-main-domain.com"; - set $workerSrc "https://your-main-domain.com"; + set $styleSrc "'unsafe-inline' 'self' ${main_domain}"; + set $scriptSrc "'self' ${main_domain}"; + set $connectSrc "'self' https://${main_domain} wss://${main_domain} $main_domain https://${api_domain} blob:"; + set $fontSrc "'self' data: ${main_domain}"; + set $imgSrc "'self' data: * blob: ${main_domain}"; + set $frameSrc "'self' ${sandbox_domain} blob:"; + set $mediaSrc "'self' data: * blob: ${main_domain}"; + set $childSrc "https://${main_domain}"; + set $workerSrc "https://${main_domain}"; set $unsafe 0; if ($uri = "/pad/inner.html") { set $unsafe 1; } @@ -50,7 +60,7 @@ server { if ($uri = "/common/onlyoffice/web-apps/apps/spreadsheeteditor/main/index.html") { set $unsafe 1; } if ($host != sandbox.cryptpad.info) { set $unsafe 0; } if ($unsafe) { - set $scriptSrc "'self' 'unsafe-eval' 'unsafe-inline' new2.cryptpad.fr cryptpad.fr"; + set $scriptSrc "'self' 'unsafe-eval' 'unsafe-inline' ${main_domain}"; } add_header Content-Security-Policy "default-src 'none'; child-src $childSrc; worker-src $workerSrc; media-src $mediaSrc; style-src $styleSrc; script-src $scriptSrc; connect-src $connectSrc; font-src $fontSrc; img-src $imgSrc; frame-src $frameSrc;"; From c2c5dbdb6464378b35b19667191d7aa87de22de3 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 20 Dec 2019 15:48:29 +0100 Subject: [PATCH 02/55] Remove hash when joining a team with a link --- www/common/outer/team.js | 1 - www/teams/inner.js | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/www/common/outer/team.js b/www/common/outer/team.js index b7d6e3fb7..c41f728a0 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -1440,7 +1440,6 @@ define([ var json = Util.tryParse(val); if (!json) { return void cb({ error: "parseError" }); } - console.error("JSON", json); cb(json); }, { // cryptget opts network: ctx.store.network, diff --git a/www/teams/inner.js b/www/teams/inner.js index fb80caeb2..3f26874b2 100644 --- a/www/teams/inner.js +++ b/www/teams/inner.js @@ -1059,6 +1059,7 @@ define([ var hashData = Hash.parseTypeHash('invite', hash); var password = hashData.password; var seeds = InviteInner.deriveSeeds(hashData.key); + var sframeChan = common.getSframeChannel(); if (Object.keys(privateData.teams || {}).length >= Constants.MAX_TEAMS_SLOTS) { return void cb([ @@ -1146,6 +1147,7 @@ define([ return; } // No error: join successful! + sframeChan.event('EV_SET_HASH', ''); var $div = $('div.cp-team-list').empty(); refreshList(common, function (content) { $div.append(content); @@ -1170,7 +1172,6 @@ define([ nThen(function (waitFor) { // Get preview content. - var sframeChan = common.getSframeChannel(); sframeChan.query('Q_ANON_GET_PREVIEW_CONTENT', { seeds: seeds }, waitFor(function (err, json) { if (json && (json.error || !Object.keys(json).length)) { $(errorBlock).text(Messages.team_inviteInvalidLinkError).show(); From 72e308994d374a894e74c47844799279281b2c74 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 23 Dec 2019 15:46:16 +0100 Subject: [PATCH 03/55] Fix scrollbar in support's textarea --- customize.dist/src/less2/include/buttons.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/customize.dist/src/less2/include/buttons.less b/customize.dist/src/less2/include/buttons.less index f3cb50e17..fb99471b6 100644 --- a/customize.dist/src/less2/include/buttons.less +++ b/customize.dist/src/less2/include/buttons.less @@ -24,9 +24,9 @@ } textarea { - overflow: hidden; padding: 8px; &[readonly] { + overflow: hidden; resize: none; } } From 31ec784de6024a2fb10f3632ade357700e749fc6 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 23 Dec 2019 16:48:15 -0500 Subject: [PATCH 04/55] remove examples directoryr --- www/examples/index.html | 20 ----- www/examples/json/README.md | 125 ------------------------------ www/examples/json/index.html | 50 ------------ www/examples/json/main.js | 76 ------------------ www/examples/pin/index.html | 13 ---- www/examples/pin/inner.html | 8 -- www/examples/pin/main.js | 50 ------------ www/examples/read/index.html | 33 -------- www/examples/read/main.js | 32 -------- www/examples/render/index.html | 42 ---------- www/examples/render/main.js | 104 ------------------------- www/examples/render/render-sd.css | 116 --------------------------- 12 files changed, 669 deletions(-) delete mode 100644 www/examples/index.html delete mode 100644 www/examples/json/README.md delete mode 100644 www/examples/json/index.html delete mode 100644 www/examples/json/main.js delete mode 100644 www/examples/pin/index.html delete mode 100644 www/examples/pin/inner.html delete mode 100644 www/examples/pin/main.js delete mode 100644 www/examples/read/index.html delete mode 100644 www/examples/read/main.js delete mode 100644 www/examples/render/index.html delete mode 100644 www/examples/render/main.js delete mode 100644 www/examples/render/render-sd.css diff --git a/www/examples/index.html b/www/examples/index.html deleted file mode 100644 index 9730bc56b..000000000 --- a/www/examples/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - -

What can be built on top of CryptPad?

- - - diff --git a/www/examples/json/README.md b/www/examples/json/README.md deleted file mode 100644 index ec1a8f340..000000000 --- a/www/examples/json/README.md +++ /dev/null @@ -1,125 +0,0 @@ -# Realtime Lists and Maps - -Our realtime list/map API has some limitations. - -## Datatype Serialization - -Only datatypes which can be serialized via `JSON.parse(JSON.stringify(yourObject))` will be preserved. - -This means the following types can be serialized: - -1. strings -2. objects -3. arrays -4. booleans -5. numbers -6. null - -While these cannot be serialized: - -1. undefined -2. symbol - -## Object Interaction - -Only 'get' and 'set' methods are supported. -This is because we need to limit the operations we support to those supported by all browsers we might use. - -Currently that means we can't rely on `in`, `delete`, or anything other than a `get`/`set` operation to behave as expected. -Treat all other features as `Undefined Behaviour`. - -> Your mileage may vary - -`set` methods include all of the [assignment operators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Assignment_Operators#Exponentiation_assignment). - -``` -// where 'x' is the realtime object `{}` - -// assignment -x.n = 5; - -x.n += 3; -x.n++; -++x.n; - -x.a = 5; -x.b = 3; -x.a *= x.b++; -x // {a: 15, b: 4, n: 10} -``` - -Instead of `delete`, assign `undefined`. -`delete` will remove an attribute locally, but the deletion will not propogate to other clients until your next serialization. -This is potentially problematic, as it can result in poorly formed patches. - -### Object and array methods - -methods which do not directly use setters and getters can be problematic: - -`Array.push` behaves correctly, however, `Array.pop` does not. - - -## Deep Equality - -Normally in Javascript objects are passed by reference. -That means you can do things like this: - -``` -var a = {x: 5}; -var b = a; - -// true -console.log(a === b); -``` - -Using the realtime list/map API, objects are serialized, and are therefore copied by value. - -Since objects are deserialized and created on each client, you will not be able to rely on this kind of equality across objects, despite their having been created in this fashion. - -Object equality _might_ work if the comparison is performed on the same client that initially created the object, but relying on this kind of behaviour is not advisable. - -## Listeners - -You can add a listener to an attribute (via its path relative to the root realtime object). - -There are various types of listeners - -* change -* remove -* disconnect -* ready - -### Semantics - -Suppose you have a realtime object `A` containing nested structures. - -``` -{ - a: { - b: { - c: 5 - } - }, - d: { - e: [ - 1, - 4, - 9 - ] - } -} -``` - -If you want to be alerted whenever the second element in the array `e` within `d` changes, you can attach a listener like so: - -``` -A.on('change', ['d', 'e', 1], function (oldval, newval, path, rootObject) { - /* do something with these values */ - console.log("value changes from %s to %s", oldval, newval); -}); -``` - -## Known Bugs - -there is currently an issue with popping the last element of an array. - diff --git a/www/examples/json/index.html b/www/examples/json/index.html deleted file mode 100644 index 770a86929..000000000 --- a/www/examples/json/index.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - -
-

The field below behaves like a REPL, with the realtime object created by this page exposed as the value x

-

Open your browser's console to see the output.

-
-
- - diff --git a/www/examples/json/main.js b/www/examples/json/main.js deleted file mode 100644 index 434ac1a3e..000000000 --- a/www/examples/json/main.js +++ /dev/null @@ -1,76 +0,0 @@ -define([ - 'jquery', - '/api/config', - '/bower_components/chainpad-listmap/chainpad-listmap.js', - '/bower_components/chainpad-crypto/crypto.js', - '/common/cryptpad-common.js' -], function ($, Config, RtListMap, Crypto, Common) { - - var secret = Common.getSecrets(); - - var config = { - websocketURL: Config.websocketURL, - channel: secret.channel, - //cryptKey: secret.key, - data: {}, - crypto: Crypto.createEncryptor(secret.key) - }; - - var module = window.APP = {}; - - var $repl = $('[name="repl"]'); - - var setEditable = module.setEditable = function (bool) { - [$repl].forEach(function ($el) { - $el.attr('disabled', !bool); - }); - }; - - setEditable(false); - - var rt = module.rt = RtListMap.create(config); - rt.proxy.on('create', function (info) { - console.log("initializing..."); - window.location.hash = info.channel + secret.key; - }).on('ready', function () { - console.log("...your realtime object is ready"); - - rt.proxy - // on(event, path, cb) - .on('change', [], function (o, n, p) { - console.log("root change event firing for path [%s]: %s => %s", p.join(','), o, n); - }) - .on('remove', [], function (o, p) { - console.log("Removal of value [%s] at path [%s]", o, p.join(',')); - }) - .on('change', ['a', 'b', 'c'], function (o, n, p) { - console.log("Deeper change event at [%s]: %s => %s", p.join(','), o, n); - console.log("preventing propogation..."); - return false; - }) - // on(event, cb) - .on('disconnect', function () { - setEditable(false); - window.alert("Network connection lost"); - }); - - // set up user interface hooks - $repl.on('keyup', function (e) { - if (e.which === 13 /* enter keycode */) { - var value = $repl.val(); - - if (!value.trim()) { return; } - - console.log("evaluating `%s`", value); - var x = rt.proxy; - x = x; // LOL jshint says this is unused otherwise <3 - - console.log('> ', eval(value)); // jshint ignore:line - console.log(); - $repl.val(''); - } - }); - - setEditable(true); - }); -}); diff --git a/www/examples/pin/index.html b/www/examples/pin/index.html deleted file mode 100644 index e17a68143..000000000 --- a/www/examples/pin/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - CryptPad - - - - -
- -
- - diff --git a/www/examples/pin/inner.html b/www/examples/pin/inner.html deleted file mode 100644 index 9680685b7..000000000 --- a/www/examples/pin/inner.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - -

PEWPEW diff --git a/www/examples/pin/main.js b/www/examples/pin/main.js deleted file mode 100644 index a54bc8ce1..000000000 --- a/www/examples/pin/main.js +++ /dev/null @@ -1,50 +0,0 @@ -define([ - 'jquery', - '/common/cryptpad-common.js', - '/common/pinpad.js' -], function ($, Cryptpad, Pinpad) { - window.APP = { - Cryptpad: Cryptpad, - }; - - var synchronize = function (call) { - // provide a sorted list of unique channels - var list = Cryptpad.getCanonicalChannelList(); - - var localHash = call.hashChannelList(list); - var serverHash; - - call.getFileListSize(function (e, bytes) { - if (e) { return void console.error(e); } - console.log("total %sK bytes used", bytes / 1000); - }); - - call.getServerHash(function (e, hash) { - if (e) { return void console.error(e); } - serverHash = hash; - - if (serverHash === localHash) { - return console.log("all your pads are pinned. There is nothing to do"); - } - - call.reset(list, function (e, response) { - if (e) { return console.error(e); } - else { - return console.log('reset pin list. new hash is [%s]', response); - } - }); - }); - }; - - $(function () { - Cryptpad.ready(function () { - var network = Cryptpad.getNetwork(); - var proxy = Cryptpad.getStore().getProxy().proxy; - - Pinpad.create(network, proxy, function (e, call) { - if (e) { return void console.error(e); } - synchronize(call); - }); - }); - }); -}); diff --git a/www/examples/read/index.html b/www/examples/read/index.html deleted file mode 100644 index 399a43309..000000000 --- a/www/examples/read/index.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - -
- - - - - diff --git a/www/examples/read/main.js b/www/examples/read/main.js deleted file mode 100644 index 32c301032..000000000 --- a/www/examples/read/main.js +++ /dev/null @@ -1,32 +0,0 @@ -define([ - 'jquery', - '/common/cryptget.js' -], function ($, Crypt) { - var $target = $('#target'); - - var useDoc = function (err, doc) { - if (err) { return console.error(err); } - //console.log(doc); - $('#putter').val(doc); - }; - - $('#get').click(function () { - var val = $target.val(); - if (!val.trim()) { return; } - Crypt.get(val, useDoc); - }); - - $('#put').click(function () { - var hash = $target.val().trim(); - Crypt.put(hash, $('#putter').val(), function (e) { - if (e) { console.error(e); } - $('#get').click(); - }); - }); - - $('#open').click(function () { - window.open('/code/#' + $target.val()); - }); - - if (window.location.hash) { Crypt.get(void 0, useDoc); } -}); diff --git a/www/examples/render/index.html b/www/examples/render/index.html deleted file mode 100644 index 56112aab3..000000000 --- a/www/examples/render/index.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - -
-
-
diff --git a/www/examples/render/main.js b/www/examples/render/main.js deleted file mode 100644 index 83cdaa36c..000000000 --- a/www/examples/render/main.js +++ /dev/null @@ -1,104 +0,0 @@ -define([ - 'jquery', - '/api/config', - '/bower_components/chainpad-netflux/chainpad-netflux.js', - '/bower_components/chainpad-crypto/crypto.js', - '/bower_components/marked/marked.min.js', - '/bower_components/hyperjson/hyperjson.js', - '/common/cryptpad-common.js', - '/bower_components/diff-dom/diffDOM.js', -], function ($, Config, Realtime, Crypto, Marked, Hyperjson, Cryptpad) { - var DiffDom = window.diffDOM; - - var secret = Cryptpad.getSecrets(); - - // set markdown rendering options :: strip html to prevent XSS - Marked.setOptions({ - sanitize: true - }); - - var module = window.APP = { }; - - var $target = module.$target = $('#target'); - - var config = { - websocketURL: Config.websocketURL, - channel: secret.channel, - crypto: Crypto.createEncryptor(secret.key) - }; - - var draw = window.draw = (function () { - var target = $target[0], - inner = $target.find('#inner')[0]; - - if (!target) { throw new Error(); } - var DD = new DiffDom({}); - - return function (md) { - var rendered = Marked(md||""); - // make a dom - var New = $('
'+rendered+'
')[0]; - - var patches = (DD).diff(inner, New); - DD.apply(inner, patches); - return patches; - }; - }()); - - var redrawTimeout; - var lazyDraw = function (md) { - if (redrawTimeout) { clearTimeout(redrawTimeout); } - redrawTimeout = setTimeout(function () { - draw(md); - }, 450); - }; - - var initializing = true; - - config.onInit = function (info) { - window.location.hash = info.channel + secret.key; - module.realtime = info.realtime; - }; - - var getContent = function (userDoc) { - try { - var parsed = JSON.parse(userDoc); - if (typeof(parsed.content) !== 'string') { - throw new Error(); - } - return parsed.content; - } catch (err) { - return userDoc; - } - }; - - // when your editor is ready - config.onReady = function () { - console.log("Realtime is ready!"); - var userDoc = module.realtime.getUserDoc(); - lazyDraw(getContent(userDoc)); - initializing = false; - }; - - // when remote editors do things... - config.onRemote = function () { - if (initializing) { return; } - var userDoc = module.realtime.getUserDoc(); - lazyDraw(getContent(userDoc)); - }; - - config.onLocal = function () { - // we're not really expecting any local events for this editor... - /* but we might add a second pane in the future so that you don't need - a second window to edit your markdown */ - if (initializing) { return; } - var userDoc = module.realtime.getUserDoc(); - lazyDraw(userDoc); - }; - - config.onAbort = function () { - window.alert("Network Connection Lost"); - }; - - Realtime.start(config); -}); diff --git a/www/examples/render/render-sd.css b/www/examples/render/render-sd.css deleted file mode 100644 index e8fde3e71..000000000 --- a/www/examples/render/render-sd.css +++ /dev/null @@ -1,116 +0,0 @@ -html { - font-family: sans-serif; - -webkit-text-size-adjust: 100%; - -ms-text-size-adjust: 100%; - - /*background-color: #073642;*/ - color: #839496; - - font-family: 'PT Sans', sans-serif; -} -body { - background-color: #002b36; -} - - -a:focus { - outline: thin dotted; -} -a:active, -a:hover { - outline: 0; -} -h1 { - font-size: 2em; -} -b, -strong { - font-weight: bold; -} -code, -pre { - font-family: monospace, serif; - font-size: 1em; -} -pre { - white-space: pre-wrap; - word-wrap: break-word; -} -q { - quotes: "\201C" "\201D" "\2018" "\2019"; -} -img { - border: 0; -} -svg:not(:root) { - overflow: hidden; -} -table { - border-collapse: collapse; - border-spacing: 0; -} -pre, -code { - font-family: 'Inconsolata', sans-serif; -} - -h1, -h2, -h3, -h4, -h5, -h6 { - font-family: 'PT Sans Narrow', sans-serif; - font-weight: 700; -} -code { - background-color: #073642; - padding: 2px; -} -a { - color: #b58900; -} -a:visited { - color: #cb4b16; -} -a:hover { - color: #cb4b16; -} -h1 { - color: #d33682; -} -h2, -h3, -h4, -h5, -h6 { - color: #859900; -} -pre { - background-color: #002b36; - color: #839496; - border: 1pt solid #586e75; - box-shadow: 5pt 5pt 8pt #073642; -} -pre code { - background-color: #002b36; -} -h1 { - font-size: 2.8em; -} -h2 { - font-size: 2.4em; -} -h3 { - font-size: 1.8em; -} -h4 { - font-size: 1.4em; -} -h5 { - font-size: 1.3em; -} -h6 { - font-size: 1.15em; -} - From 2dfbb522b056854322f964b10e0d87a83b638608 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 23 Dec 2019 17:01:00 -0500 Subject: [PATCH 05/55] simplify server websocket configuration --- container-start.sh | 3 --- server.js | 10 ---------- www/common/outer/network-config.js | 1 - 3 files changed, 14 deletions(-) diff --git a/container-start.sh b/container-start.sh index 81338503a..9f28d0127 100755 --- a/container-start.sh +++ b/container-start.sh @@ -17,9 +17,6 @@ sedeasy() { } # Configure -[ -n "$USE_SSL" ] && echo "Using secure websockets: $USE_SSL" \ - && sedeasy "useSecureWebsockets: [^,]*," "useSecureWebsockets: ${USE_SSL}," cfg/config.js - [ -n "$STORAGE" ] && echo "Using storage adapter: $STORAGE" \ && sedeasy "storage: [^,]*," "storage: ${STORAGE}," cfg/config.js diff --git a/server.js b/server.js index c47c4e450..acee1c499 100644 --- a/server.js +++ b/server.js @@ -13,9 +13,6 @@ var nThen = require("nthen"); var config = require("./lib/load-config"); -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) { @@ -211,9 +208,6 @@ app.get('/api/config', function(req, res){ removeDonateButton: (config.removeDonateButton === true), allowSubscriptions: (config.allowSubscriptions === true), websocketPath: config.useExternalWebsocket ? undefined : config.websocketPath, - // FIXME don't send websocketURL if websocketPath is provided. deprecated. - websocketURL:'ws' + ((useSecureWebsockets) ? 's' : '') + '://' + host + ':' + - websocketPort + '/cryptpad_websocket', httpUnsafeOrigin: config.httpUnsafeOrigin.replace(/^\s*/, ''), adminEmail: config.adminEmail, adminKeys: admins, @@ -327,10 +321,6 @@ var nt = nThen(function (w) { historyKeeper = HK.create(hkConfig); }).nThen(function () { if (config.useExternalWebsocket) { return; } - if (websocketPort !== config.httpPort) { - log.debug("setting up a new websocket server"); - wsConfig = { port: websocketPort}; - } var wsSrv = new WebSocketServer(wsConfig); NetfluxSrv.run(wsSrv, config, historyKeeper); }); diff --git a/www/common/outer/network-config.js b/www/common/outer/network-config.js index 300810b97..ff4c22870 100644 --- a/www/common/outer/network-config.js +++ b/www/common/outer/network-config.js @@ -4,7 +4,6 @@ define([ var Config = {}; Config.getWebsocketURL = function (origin) { - if (!ApiConfig.websocketPath) { return ApiConfig.websocketURL; } var path = ApiConfig.websocketPath; if (/^ws{1,2}:\/\//.test(path)) { return path; } From cffb0da833b87eade74d3aa47f5747605cec04e7 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 23 Dec 2019 17:05:08 -0500 Subject: [PATCH 06/55] remove ssl support in node server. Use a reverse proxy for this. --- config/config.example.js | 9 --------- server.js | 28 +--------------------------- 2 files changed, 1 insertion(+), 36 deletions(-) diff --git a/config/config.example.js b/config/config.example.js index 779843f44..7dd49c54c 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -124,15 +124,6 @@ module.exports = { padContentSecurity: baseCSP.join('; ') + "script-src 'self' 'unsafe-eval' 'unsafe-inline'" + domain, - /* it is recommended that you serve CryptPad over https - * the filepaths below are used to configure your certificates - */ - //privKeyAndCertFiles: [ - // '/etc/apache2/ssl/my_secret.key', - // '/etc/apache2/ssl/my_public_cert.crt', - // '/etc/apache2/ssl/my_certificate_authorities_cert_chain.ca' - //], - /* Main pages * add exceptions to the router so that we can access /privacy.html * and other odd pages diff --git a/server.js b/server.js index acee1c499..1a619e63c 100644 --- a/server.js +++ b/server.js @@ -3,7 +3,6 @@ */ var Express = require('express'); var Http = require('http'); -var Https = require('https'); var Fs = require('fs'); var WebSocketServer = require('ws').Server; var NetfluxSrv = require('./node_modules/chainpad-server/NetfluxWebsocketSrv'); @@ -33,8 +32,6 @@ var Storage = require(config.storage||'./storage/file'); var app = debuggable('app', Express()); -var httpsOpts; - // mode can be FRESH (default), DEV, or PACKAGE var FRESH_KEY = ''; @@ -162,29 +159,6 @@ app.use("/customize.dist", Express.static(__dirname + '/customize.dist')); app.use(/^\/[^\/]*$/, Express.static('customize')); app.use(/^\/[^\/]*$/, Express.static('customize.dist')); -if (config.privKeyAndCertFiles) { - var privKeyAndCerts = ''; - config.privKeyAndCertFiles.forEach(function (file) { - privKeyAndCerts = privKeyAndCerts + Fs.readFileSync(file); - }); - var array = privKeyAndCerts.split('\n-----BEGIN '); - for (var i = 1; i < array.length; i++) { array[i] = '-----BEGIN ' + array[i]; } - var privKey; - for (var i = 0; i < array.length; i++) { - if (array[i].indexOf('PRIVATE KEY-----\n') !== -1) { - privKey = array[i]; - array.splice(i, 1); - break; - } - } - if (!privKey) { throw new Error("cannot find private key"); } - httpsOpts = { - cert: array.shift(), - key: privKey, - ca: array - }; -} - var admins = []; try { admins = (config.adminKeys || []).map(function (k) { @@ -244,7 +218,7 @@ app.use(function (req, res, next) { send404(res, custom_four04_path); }); -var httpServer = httpsOpts ? Https.createServer(httpsOpts, app) : Http.createServer(app); +var httpServer = Http.createServer(app); httpServer.listen(config.httpPort,config.httpAddress,function(){ var host = config.httpAddress; From 4c51d464fd7f7223c52f88c2d5ba68c7326ee587 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 23 Dec 2019 17:15:07 -0500 Subject: [PATCH 07/55] drop replify integration --- config/config.example.js | 8 -------- package-lock.json | 5 ----- package.json | 1 - rpc.js | 3 --- server.js | 23 ++--------------------- 5 files changed, 2 insertions(+), 38 deletions(-) diff --git a/config/config.example.js b/config/config.example.js index 7dd49c54c..82b919bb5 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -359,14 +359,6 @@ module.exports = { */ logFeedback: false, - /* 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" - /* ===================== * DEPRECATED * ===================== */ diff --git a/package-lock.json b/package-lock.json index 08328fcb1..cdf37370d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1069,11 +1069,6 @@ "string_decoder": "~0.10.x" } }, - "replify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/replify/-/replify-1.2.0.tgz", - "integrity": "sha1-lAFm0gfRDphhT+SSU60vCsAZ9+E=" - }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", diff --git a/package.json b/package.json index 835641d6b..be6071a2f 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "netflux-websocket": "^0.1.20", "nthen": "0.1.8", "pull-stream": "^3.6.1", - "replify": "^1.2.0", "saferphore": "0.0.1", "sortify": "^1.0.4", "stream-to-pull-stream": "^1.7.2", diff --git a/rpc.js b/rpc.js index 8c7fe6c70..8968b9a4c 100644 --- a/rpc.js +++ b/rpc.js @@ -1369,7 +1369,6 @@ type NetfluxWebsocketSrvContext_t = { */ RPC.create = function ( config /*:Config_t*/, - debuggable /*:(string, T)=>T*/, cb /*:(?Error, ?Function)=>void*/ ) { Log = config.log; @@ -1405,8 +1404,6 @@ RPC.create = function ( console.error("Can't parse admin keys. Please update or fix your config.js file!"); } - debuggable('rpc_env', Env); - var Sessions = Env.Sessions; var paths = Env.paths; var pinPath = paths.pin = keyOrDefaultString('pinPath', './pins'); diff --git a/server.js b/server.js index 1a619e63c..37cd0a00c 100644 --- a/server.js +++ b/server.js @@ -12,25 +12,10 @@ var nThen = require("nthen"); var config = require("./lib/load-config"); -// 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 = debuggable('app', Express()); +var app = Express(); // mode can be FRESH (default), DEV, or PACKAGE @@ -275,7 +260,7 @@ var nt = nThen(function (w) { if (typeof(config.rpc) !== 'string') { return; } // load pin store... var Rpc = require(config.rpc); - Rpc.create(config, debuggable, w(function (e, _rpc) { + Rpc.create(config, w(function (e, _rpc) { if (e) { w.abort(); throw e; @@ -298,7 +283,3 @@ var nt = nThen(function (w) { var wsSrv = new WebSocketServer(wsConfig); NetfluxSrv.run(wsSrv, config, historyKeeper); }); - -if (config.debugReplName) { - require('replify')({ name: config.debugReplName, app: debuggableStore }); -} From 67cde69120d2755ed463692989cb44c0d0f6f544 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 23 Dec 2019 17:28:01 -0500 Subject: [PATCH 08/55] drop support for configuration points we can't possibly support --- config/config.example.js | 29 ----------------------------- server.js | 10 +++------- 2 files changed, 3 insertions(+), 36 deletions(-) diff --git a/config/config.example.js b/config/config.example.js index 82b919bb5..1fb702519 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -272,7 +272,6 @@ module.exports = { */ openFileLimit: 2048, - /* ===================== * DATABASE VOLUMES * ===================== */ @@ -359,34 +358,6 @@ module.exports = { */ logFeedback: false, - /* ===================== - * DEPRECATED - * ===================== */ - /* - You have the option of specifying an alternative storage adaptor. - These status of these alternatives are specified in their READMEs, - which are available at the following URLs: - - mongodb: a noSQL database - https://github.com/xwiki-labs/cryptpad-mongo-store - amnesiadb: in memory storage - https://github.com/xwiki-labs/cryptpad-amnesia-store - leveldb: a simple, fast, key-value store - https://github.com/xwiki-labs/cryptpad-level-store - sql: an adaptor for a variety of sql databases via knexjs - https://github.com/xwiki-labs/cryptpad-sql-store - - For the most up to date solution, use the default storage adaptor. - */ - storage: './storage/file', - - /* CryptPad's socket server can be extended to respond to RPC calls - * you can configure it to respond to custom RPC calls if you like. - * provide the path to your RPC module here, or `false` if you would - * like to disable the RPC interface completely - */ - rpc: './rpc.js', - /* CryptPad supports verbose logging * (false by default) */ diff --git a/server.js b/server.js index 37cd0a00c..6fe11e06d 100644 --- a/server.js +++ b/server.js @@ -5,7 +5,7 @@ var Express = require('express'); var Http = require('http'); var Fs = require('fs'); var WebSocketServer = require('ws').Server; -var NetfluxSrv = require('./node_modules/chainpad-server/NetfluxWebsocketSrv'); +var NetfluxSrv = require('chainpad-server/NetfluxWebsocketSrv'); var Package = require('./package.json'); var Path = require("path"); var nThen = require("nthen"); @@ -13,7 +13,7 @@ var nThen = require("nthen"); var config = require("./lib/load-config"); // support multiple storage back ends -var Storage = require(config.storage||'./storage/file'); +var Storage = require('./storage/file'); var app = Express(); @@ -256,11 +256,7 @@ var nt = nThen(function (w) { }, 1000 * 60 * 5); // run every five minutes })); }).nThen(function (w) { - config.rpc = typeof(config.rpc) === 'undefined'? './rpc.js' : config.rpc; - if (typeof(config.rpc) !== 'string') { return; } - // load pin store... - var Rpc = require(config.rpc); - Rpc.create(config, w(function (e, _rpc) { + require("./rpc").create(config, w(function (e, _rpc) { if (e) { w.abort(); throw e; From 97bc793ca892d2c33e0aadc4f32376bf6b8c818e Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 23 Dec 2019 17:39:13 -0500 Subject: [PATCH 09/55] simplify websocket configuration even more --- server.js | 12 ++++++++---- www/common/outer/network-config.js | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/server.js b/server.js index 6fe11e06d..e086a0596 100644 --- a/server.js +++ b/server.js @@ -166,7 +166,7 @@ app.get('/api/config', function(req, res){ }, removeDonateButton: (config.removeDonateButton === true), allowSubscriptions: (config.allowSubscriptions === true), - websocketPath: config.useExternalWebsocket ? undefined : config.websocketPath, + websocketPath: config.websocketPath, httpUnsafeOrigin: config.httpUnsafeOrigin.replace(/^\s*/, ''), adminEmail: config.adminEmail, adminKeys: admins, @@ -234,7 +234,13 @@ var nt = nThen(function (w) { log = config.log = _log; })); }).nThen(function (w) { - if (config.useExternalWebsocket) { return; } + if (config.useExternalWebsocket) { + // if you plan to use an external websocket server + // then you don't need to load any API services other than the logger. + // Just abort. + w.abort(); + return; + } Storage.create(config, w(function (_store) { config.store = _store; })); @@ -264,7 +270,6 @@ var nt = nThen(function (w) { rpc = _rpc; })); }).nThen(function () { - if (config.useExternalWebsocket) { return; } var HK = require('./historyKeeper.js'); var hkConfig = { tasks: config.tasks, @@ -275,7 +280,6 @@ var nt = nThen(function (w) { }; historyKeeper = HK.create(hkConfig); }).nThen(function () { - if (config.useExternalWebsocket) { return; } var wsSrv = new WebSocketServer(wsConfig); NetfluxSrv.run(wsSrv, config, historyKeeper); }); diff --git a/www/common/outer/network-config.js b/www/common/outer/network-config.js index ff4c22870..3d3465ffe 100644 --- a/www/common/outer/network-config.js +++ b/www/common/outer/network-config.js @@ -4,7 +4,7 @@ define([ var Config = {}; Config.getWebsocketURL = function (origin) { - var path = ApiConfig.websocketPath; + var path = ApiConfig.websocketPath || '/cryptpad_websocket'; if (/^ws{1,2}:\/\//.test(path)) { return path; } var l = window.location; From a8b363dba0dcd4a3d9ee828beb5a2140671ded00 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 23 Dec 2019 18:23:50 -0500 Subject: [PATCH 10/55] drop support for 'websocketPath' and 'useExternalWebsocket' configuration points --- config/config.example.js | 14 ++++++++------ server.js | 4 ++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/config/config.example.js b/config/config.example.js index 1fb702519..2b5bc687d 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -97,15 +97,17 @@ module.exports = { httpUnsafeOrigin: domain, - /* your server's websocket url is configurable - * (default: '/cryptpad_websocket') + /* Your CryptPad server will share this value with clients + * via its /api/config endpoint. * - * websocketPath can be relative, of the form '/path/to/websocket' - * or absolute, specifying a particular URL + * If you want to host your API and asset servers on different hosts + * specify a URL for your API server websocket endpoint, like so: + * wss://api.yourdomain.com/cryptpad_websocket * - * 'wss://cryptpad.fr:3000/cryptpad_websocket' + * Otherwise, leave this commented and your clients will use the default + * websocket (wss://yourdomain.com/cryptpad_websocket) */ - websocketPath: '/cryptpad_websocket', + //externalWebsocketPath: 'wss://api.yourdomain.com/cryptpad_websocket /* CryptPad can be configured to send customized HTTP Headers * These settings may vary widely depending on your needs diff --git a/server.js b/server.js index e086a0596..8a9a98a41 100644 --- a/server.js +++ b/server.js @@ -166,7 +166,7 @@ app.get('/api/config', function(req, res){ }, removeDonateButton: (config.removeDonateButton === true), allowSubscriptions: (config.allowSubscriptions === true), - websocketPath: config.websocketPath, + websocketPath: config.externalWebsocketPath, httpUnsafeOrigin: config.httpUnsafeOrigin.replace(/^\s*/, ''), adminEmail: config.adminEmail, adminKeys: admins, @@ -234,7 +234,7 @@ var nt = nThen(function (w) { log = config.log = _log; })); }).nThen(function (w) { - if (config.useExternalWebsocket) { + if (config.externalWebsocketPath) { // if you plan to use an external websocket server // then you don't need to load any API services other than the logger. // Just abort. From 05699b2c94cc4446b26e65b322fc24d5e5eb292f Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 23 Dec 2019 18:27:11 -0500 Subject: [PATCH 11/55] unify websocket configuration points --- CHANGELOG.md | 14 ++++++++++++++ config/config.example.js | 2 +- server.js | 4 ++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea5909dab..649e18d02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +# J release (3.9.0) + +## Goals + +* simplify server configuration by dropping support for unused features + * drop `websocketPath` `websocketURL` and `useExternalWebsockets` + * use `externalWebsocketURL` to replace all of these + +## Update notes + +## Features + +## Bug fixes + # IsolobodonPortoricensis release (3.8.0) We had some trouble finding an extinct animal whose name started with "I", and we had to resort to using a scientific name. diff --git a/config/config.example.js b/config/config.example.js index 2b5bc687d..c76fe9d37 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -107,7 +107,7 @@ module.exports = { * Otherwise, leave this commented and your clients will use the default * websocket (wss://yourdomain.com/cryptpad_websocket) */ - //externalWebsocketPath: 'wss://api.yourdomain.com/cryptpad_websocket + //externalWebsocketURL: 'wss://api.yourdomain.com/cryptpad_websocket /* CryptPad can be configured to send customized HTTP Headers * These settings may vary widely depending on your needs diff --git a/server.js b/server.js index 8a9a98a41..65bf36a87 100644 --- a/server.js +++ b/server.js @@ -166,7 +166,7 @@ app.get('/api/config', function(req, res){ }, removeDonateButton: (config.removeDonateButton === true), allowSubscriptions: (config.allowSubscriptions === true), - websocketPath: config.externalWebsocketPath, + websocketPath: config.externalWebsocketURL, httpUnsafeOrigin: config.httpUnsafeOrigin.replace(/^\s*/, ''), adminEmail: config.adminEmail, adminKeys: admins, @@ -234,7 +234,7 @@ var nt = nThen(function (w) { log = config.log = _log; })); }).nThen(function (w) { - if (config.externalWebsocketPath) { + if (config.externalWebsocketURL) { // if you plan to use an external websocket server // then you don't need to load any API services other than the logger. // Just abort. From 7c9b53695b2eeff8502dfa0aa5462b31acecbbb5 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 26 Dec 2019 18:23:52 -0500 Subject: [PATCH 12/55] add a bunch of comments to all the mysterious sections of the nginx example config also remove a large, complicated, unnecessary section --- docs/example.nginx.conf | 101 ++++++++++++++++++++++++++++------------ 1 file changed, 72 insertions(+), 29 deletions(-) diff --git a/docs/example.nginx.conf b/docs/example.nginx.conf index 462b205fd..70b62b709 100644 --- a/docs/example.nginx.conf +++ b/docs/example.nginx.conf @@ -7,15 +7,29 @@ server { listen 443 ssl http2; - # XXX These two variables + # CryptPad serves static assets over these two domains. + # `main_domain` is what users will enter in their address bar. + # Privileged computation such as key management is handled in this scope + # UI content is loaded via the `sandbox_domain`. + # "Content Security Policy" headers prevent content loaded via the sandbox + # from accessing privileged information. + # These variables must be different to take advantage of CryptPad's sandboxing techniques. + # In the event of an XSS vulnerability in CryptPad's front-end code + # this will limit the amount of information accessible to attackers. set $main_domain "your-main-domain.com"; set $sandbox_domain "your-sandbox-domain.com"; - # The $api_domain and $files_domain are optional + # CryptPad's dynamic content (websocket traffic and encrypted blobs) + # can be served over separate domains. Using dedicated domains (or subdomains) + # for these purposes allows you to move them to a separate machine at a later date + # if you find that a single machine cannot handle all of your users. + # If you don't use dedicated domains, this can be the same as $main_domain + # If you do, they'll be added as exceptions to any rules which block connections to remote domains. set $api_domain "api.your-main-domain.com"; set $files_domain "files.your-main-domain.com"; - server_name $main_domain $sandbox_domain; + # nginx doesn't let you set server_name via variables, so you need to hardcode your domains here + server_name your-main-domain.com your-sandbox-domain.com; ssl_certificate /home/cryptpad/.acme.sh/your-main-domain.com/fullchain.cer; ssl_certificate_key /home/cryptpad/.acme.sh/your-main-domain.com/your-main-domain.com.key; @@ -38,33 +52,64 @@ server { index index.html; error_page 404 /customize.dist/404.html; + # any static assets loaded with "ver=" in their URL will be cached for a year if ($args ~ ver=) { set $cacheControl max-age=31536000; } # Will not set any header if it is emptystring add_header Cache-Control $cacheControl; + # CSS can be dynamically set inline, loaded from the same domain, or from $main_domain set $styleSrc "'unsafe-inline' 'self' ${main_domain}"; - set $scriptSrc "'self' ${main_domain}"; + + # connect-src restricts URLs which can be loaded using script interfaces set $connectSrc "'self' https://${main_domain} wss://${main_domain} $main_domain https://${api_domain} blob:"; + + # fonts can be loaded from data-URLs or the main domain set $fontSrc "'self' data: ${main_domain}"; + + # images can be loaded from anywhere, though we'd like to deprecate this as it allows the use of images for tracking set $imgSrc "'self' data: * blob: ${main_domain}"; + + # frame-src specifies valid sources for nested browsing contexts. + # this prevents loading any iframes from anywhere other than the sandbox domain set $frameSrc "'self' ${sandbox_domain} blob:"; + + # specifies valid sources for loading media using video or audio set $mediaSrc "'self' data: * blob: ${main_domain}"; + + # defines valid sources for webworkers and nested browser contexts + # deprecated in favour of worker-src and frame-src set $childSrc "https://${main_domain}"; + + # specifies valid sources for Worker, SharedWorker, or ServiceWorker scripts. + # supercedes child-src but is unfortunately not yet universally supported. set $workerSrc "https://${main_domain}"; + # script-src specifies valid sources for javascript, including inline handlers + set $scriptSrc "'self' ${main_domain}"; + set $unsafe 0; + # the following assets are loaded via the sandbox domain + # they unfortunately still require exceptions to the sandboxing to work correctly. if ($uri = "/pad/inner.html") { set $unsafe 1; } if ($uri = "/sheet/inner.html") { set $unsafe 1; } if ($uri = "/common/onlyoffice/web-apps/apps/spreadsheeteditor/main/index.html") { set $unsafe 1; } + + # everything except the sandbox domain is a privileged scope, as they might be used to handle keys if ($host != sandbox.cryptpad.info) { set $unsafe 0; } + + # privileged contexts allow a few more rights than unprivileged contexts, though limits are still applied if ($unsafe) { set $scriptSrc "'self' 'unsafe-eval' 'unsafe-inline' ${main_domain}"; } - add_header Content-Security-Policy "default-src 'none'; child-src $childSrc; worker-src $workerSrc; media-src $mediaSrc; style-src $styleSrc; script-src $scriptSrc; connect-src $connectSrc; font-src $fontSrc; img-src $imgSrc; frame-src $frameSrc;"; + # Finally, set all the rules you composed above. + add_header Content-Security-Policy "default-src 'none'; child-src $childSrc; worker-src $workerSrc; media-src $mediaSrc; style-src $styleSrc; script-src $scriptSrc; connect-src $connectSrc; font-src $fontSrc; img-src $imgSrc; frame-src $frameSrc;"; + # The nodejs process can handle all traffic whether accessed over websocket or as static assets + # We prefer to serve static content from nginx directly and to leave the API server to handle + # the dynamic content that only it can manage. This is primarily an optimization location ^~ /cryptpad_websocket { proxy_pass http://localhost:3000; proxy_set_header X-Real-IP $remote_addr; @@ -77,33 +122,20 @@ server { proxy_set_header Connection upgrade; } - location ^~ /datastore/ { - alias /home/cryptpad/office.cryptpad/data/datastore; - if ($request_method = 'OPTIONS') { - add_header 'Access-Control-Allow-Origin' '*'; - add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; - add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range'; - add_header 'Access-Control-Max-Age' 0; - add_header 'Content-Type' 'application/octet-stream; charset=utf-8'; - add_header 'Content-Length' 0; - return 204; - } - add_header Cache-Control max-age=0; - add_header 'Access-Control-Allow-Origin' '*'; - add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; - add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range'; - add_header 'Access-Control-Expose-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range'; - try_files $uri =404; - } - location ^~ /customize.dist/ { # This is needed in order to prevent infinite recursion between /customize/ and the root } + # try to load customizeable content via /customize/ and fall back to the default content + # located at /customize.dist/ + # This is what allows you to override behaviour. location ^~ /customize/ { rewrite ^/customize/(.*)$ $1 break; try_files /customize/$uri /customize.dist/$uri; } + # /api/config is loaded once per page load and is used to retrieve + # the caching variable which is applied to every other resource + # which is loaded during that session. location = /api/config { proxy_pass http://localhost:3000; proxy_set_header X-Real-IP $remote_addr; @@ -111,24 +143,35 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } + # encrypted blobs are immutable and are thus cached for a year location ^~ /blob/ { add_header Cache-Control max-age=31536000; try_files $uri =404; } + # the "block-store" serves encrypted payloads containing users' drive keys + # these payloads are unlocked via login credentials. They are mutable + # and are thus never cached. They're small enough that it doesn't matter, in any case. location ^~ /block/ { add_header Cache-Control max-age=0; try_files $uri =404; } - location ^~ /datastore/ { - add_header Cache-Control max-age=0; - try_files $uri =404; - } - + # This block provides an alternative means of loading content + # otherwise only served via websocket. This is solely for debugging purposes, + # and is thus not allowed by default. + #location ^~ /datastore/ { + #add_header Cache-Control max-age=0; + #try_files $uri =404; + #} + + # The nodejs server has some built-in forwarding rules to prevent + # URLs like /pad from resulting in a 404. This simply adds a trailing slash + # to a variety of applications. location ~ ^/(register|login|settings|user|pad|drive|poll|slide|code|whiteboard|file|media|profile|contacts|todo|filepicker|debug|kanban|sheet|support|admin|notifications|teams)$ { rewrite ^(.*)$ $1/ redirect; } + # Finally, serve anything the above exceptions don't govern. try_files /www/$uri /www/$uri/index.html /customize/$uri; } From 80f3175a0e870f54d7e343a0ce2a47aef02e6bd6 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 27 Dec 2019 13:05:02 -0500 Subject: [PATCH 13/55] simplify cipher list, better dhparams --- docs/example.nginx.conf | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/example.nginx.conf b/docs/example.nginx.conf index 70b62b709..b9bb71d4a 100644 --- a/docs/example.nginx.conf +++ b/docs/example.nginx.conf @@ -31,16 +31,25 @@ server { # nginx doesn't let you set server_name via variables, so you need to hardcode your domains here server_name your-main-domain.com your-sandbox-domain.com; + # You'll need to Set the path to your certificates and keys here ssl_certificate /home/cryptpad/.acme.sh/your-main-domain.com/fullchain.cer; ssl_certificate_key /home/cryptpad/.acme.sh/your-main-domain.com/your-main-domain.com.key; ssl_trusted_certificate /home/cryptpad/.acme.sh/your-main-domain.com/ca.cer; - ssl_dhparam /etc/nginx/dhparam.pem; + # diffie-hellman parameters are used to negotiate keys for your session + # generate strong parameters using the following command + ssl_dhparam /etc/nginx/dhparam.pem; # openssl dhparam -out /etc/nginx/dhparam.pem 4096 + + # Speeds things up a little bit when resuming a session ssl_session_timeout 5m; - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # omit SSLv3 because of POODLE - # ECDHE better than DHE (faster) ECDHE & DHE GCM better than CBC (attacks on AES) Everything better than SHA1 (deprecated) - ssl_ciphers 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA'; - ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:5m; + + # You'll need nginx 1.13.0 or better to support TLSv1.3 + ssl_protocols TLSv1.2 TLSv1.3; + + # https://cipherli.st/ + ssl_ciphers EECDH+AESGCM:EDH+AESGCM; + ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0 add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-XSS-Protection "1; mode=block"; From 69bfca52fa4060a50e42c04a060b4fb703a861a4 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 30 Dec 2019 12:11:44 -0500 Subject: [PATCH 14/55] nest volumes not exposed via HTTP in 'data' folder for easier migration --- Dockerfile | 3 --- config/config.example.js | 6 +++--- docker-compose.yml | 3 --- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index ae312e3f9..6cac1605c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,11 +28,8 @@ VOLUME /cryptpad/cfg VOLUME /cryptpad/datastore VOLUME /cryptpad/customize VOLUME /cryptpad/blobstage -VOLUME /cryptpad/pins -VOLUME /cryptpad/tasks VOLUME /cryptpad/block VOLUME /cryptpad/blob -VOLUME /cryptpad/blobstage VOLUME /cryptpad/data # Copy cryptpad and tini from the build container diff --git a/config/config.example.js b/config/config.example.js index c76fe9d37..9981c0626 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -299,12 +299,12 @@ module.exports = { * Pin requests are stored in a pin-store. The location of this store is * defined here. */ - pinPath: './pins', + pinPath: './data/pins', /* if you would like the list of scheduled tasks to be stored in a custom location, change the path below: */ - taskPath: './tasks', + taskPath: './data/tasks', /* if you would like users' authenticated blocks to be stored in a custom location, change the path below: @@ -319,7 +319,7 @@ module.exports = { /* CryptPad stores incomplete blobs in a 'staging' area until they are * fully uploaded. Set its location here. */ - blobStagingPath: './blobstage', + blobStagingPath: './data/blobstage', /* CryptPad supports logging events directly to the disk in a 'logs' directory * Set its location here, or set it to false (or nothing) if you'd rather not log diff --git a/docker-compose.yml b/docker-compose.yml index 73673f7dd..e0d977866 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,10 +25,7 @@ services: volumes: - ./data/files:/cryptpad/datastore:rw - ./data/customize:/cryptpad/customize:rw - - ./data/pins:/cryptpad/pins:rw - ./data/blob:/cryptpad/blob:rw - - ./data/blobstage:/cryptpad/blobstage:rw - - ./data/tasks:/cryptpad/tasks:rw - ./data/block:/cryptpad/block:rw - ./data/config:/cryptpad/cfg:rw - ./data/data:/cryptpad/data:rw From 448a5c4b2c965248fc25cb5be7aacaa879da4b84 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 30 Dec 2019 12:13:04 -0500 Subject: [PATCH 15/55] take note of config changes in CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 649e18d02..fadcdb437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ ## Update notes +* modify default config and docker config +* review websocket config changes and update accordingly +* you may want to use the updated nginx config + ## Features ## Bug fixes From 743f7264e0dc3f5eab1c9b3a6083aa7f5c19b950 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 30 Dec 2019 12:58:26 -0500 Subject: [PATCH 16/55] prevent form autofill on the team invite modal --- www/common/common-ui-elements.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 331c15fd9..6323be780 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1639,6 +1639,11 @@ define([ h('p', Messages.team_inviteLinkTitle ), linkError = h('div.alert.alert-danger.cp-teams-invite-alert', {style : 'display: none;'}), linkForm = h('div.cp-teams-invite-form', [ + // autofill: 'off' was insufficient + // adding these two fake inputs confuses firefox and prevents unwanted form autofill + h('input', { type: 'text', style: 'display: none'}), + h('input', { type: 'password', style: 'display: none'}), + linkName = h('input', { placeholder: Messages.team_inviteLinkTempName }), From 78a1c6bdbd191fce62bdcfbb8abf1b73ee8240e0 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 30 Dec 2019 12:58:26 -0500 Subject: [PATCH 17/55] prevent form autofill on the team invite modal --- www/common/common-ui-elements.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 331c15fd9..6323be780 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1639,6 +1639,11 @@ define([ h('p', Messages.team_inviteLinkTitle ), linkError = h('div.alert.alert-danger.cp-teams-invite-alert', {style : 'display: none;'}), linkForm = h('div.cp-teams-invite-form', [ + // autofill: 'off' was insufficient + // adding these two fake inputs confuses firefox and prevents unwanted form autofill + h('input', { type: 'text', style: 'display: none'}), + h('input', { type: 'password', style: 'display: none'}), + linkName = h('input', { placeholder: Messages.team_inviteLinkTempName }), From 78b54fad536c517648e6d095d75407bf13059579 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 30 Dec 2019 12:58:26 -0500 Subject: [PATCH 18/55] prevent form autofill on the team invite modal --- www/common/common-ui-elements.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 331c15fd9..6323be780 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1639,6 +1639,11 @@ define([ h('p', Messages.team_inviteLinkTitle ), linkError = h('div.alert.alert-danger.cp-teams-invite-alert', {style : 'display: none;'}), linkForm = h('div.cp-teams-invite-form', [ + // autofill: 'off' was insufficient + // adding these two fake inputs confuses firefox and prevents unwanted form autofill + h('input', { type: 'text', style: 'display: none'}), + h('input', { type: 'password', style: 'display: none'}), + linkName = h('input', { placeholder: Messages.team_inviteLinkTempName }), From 75925b6cebdbcfc3797ca75ed93d782a3d635a74 Mon Sep 17 00:00:00 2001 From: ansuz Date: Sat, 4 Jan 2020 01:39:31 -0500 Subject: [PATCH 19/55] fix blob support in example config --- docs/example.nginx.conf | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/example.nginx.conf b/docs/example.nginx.conf index b9bb71d4a..c382910b9 100644 --- a/docs/example.nginx.conf +++ b/docs/example.nginx.conf @@ -154,7 +154,23 @@ server { # encrypted blobs are immutable and are thus cached for a year location ^~ /blob/ { + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; + add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Conten +t-Type,Content-Range,Range'; + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'application/octet-stream; charset=utf-8'; + add_header 'Content-Length' 0; + return 204; + } add_header Cache-Control max-age=31536000; + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; + add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Ty +pe,Content-Range,Range'; + add_header 'Access-Control-Expose-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-T +ype,Content-Range,Range'; try_files $uri =404; } From 32bdfacd9258b5ccb5e063299f62c2b8af0ab6ca Mon Sep 17 00:00:00 2001 From: ansuz Date: Sat, 4 Jan 2020 11:56:09 -0500 Subject: [PATCH 20/55] fix incorrectly pasted line --- docs/example.nginx.conf | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/example.nginx.conf b/docs/example.nginx.conf index c382910b9..da61ce42e 100644 --- a/docs/example.nginx.conf +++ b/docs/example.nginx.conf @@ -157,8 +157,7 @@ server { if ($request_method = 'OPTIONS') { add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; - add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Conten -t-Type,Content-Range,Range'; + add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range'; add_header 'Access-Control-Max-Age' 1728000; add_header 'Content-Type' 'application/octet-stream; charset=utf-8'; add_header 'Content-Length' 0; @@ -167,10 +166,8 @@ t-Type,Content-Range,Range'; add_header Cache-Control max-age=31536000; add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; - add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Ty -pe,Content-Range,Range'; - add_header 'Access-Control-Expose-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-T -ype,Content-Range,Range'; + add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range'; + add_header 'Access-Control-Expose-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range'; try_files $uri =404; } From a714e41d498096199b95aedaa02d5df2b0baefa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20N=C3=B6thlich?= Date: Sat, 4 Jan 2020 23:58:01 +0100 Subject: [PATCH 21/55] Log errors in faulty custom config file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CryptPAD now terminates and does not fallback to default config if the custom config is faulty. The error in the custom config is logged to the terminal now. Signed-off-by: Adrian Nöthlich --- lib/load-config.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/load-config.js b/lib/load-config.js index 80f4706dc..0756c2df4 100644 --- a/lib/load-config.js +++ b/lib/load-config.js @@ -8,7 +8,14 @@ try { console.log("You can configure the administrator email (adminEmail) in your config/config.js file"); } } catch (e) { - console.log("Config not found, loading the example config. You can customize the configuration by copying config/config.example.js to " + configPath); + if (e instanceof SyntaxError) { + console.error("config/config.js is faulty. See stacktrace below for more information. Terminating. \n"); + console.error(e.name + ": " + e.message); + console.error(e.stack.split("\n\n")[0]); + process.exit(1); + } else { + console.log("Config not found, loading the example config. You can customize the configuration by copying config/config.example.js to " + configPath); + } config = require("../config/config.example"); } module.exports = config; From 080ffa09a6de9caded394de2a24c8e5b3ff25c39 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 6 Jan 2020 10:43:04 +0100 Subject: [PATCH 22/55] Fix 'team is undefined' error --- www/common/outer/team.js | 1 + 1 file changed, 1 insertion(+) diff --git a/www/common/outer/team.js b/www/common/outer/team.js index c41f728a0..759d08817 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -1035,6 +1035,7 @@ define([ var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]); if (!teamData) { return true; } var team = ctx.teams[teamId]; + if (!team) { return true; } var secret = Hash.getSecrets('team', hash || teamData.roHash, teamData.password); // Upgrade the listmap if we can From eb85a5908e800f523a2391d6ff385644e2b85024 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 6 Jan 2020 17:05:29 +0100 Subject: [PATCH 23/55] Fix pad type in search for read-only pads --- www/common/drive-ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index 3fa850b51..bc0fda97e 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -3008,7 +3008,7 @@ define([ r.paths.forEach(function (path) { if (!r.inSharedFolder && APP.hideDuplicateOwned && manager.isDuplicateOwned(path)) { return; } - var href = r.data.href; + var href = r.data.href || r.data.roHref; var parsed = Hash.parsePadUrl(href); var $table = $(''); var $icon = $('
', {'rowspan': '3', 'class': 'cp-app-drive-search-icon'}); From f590f60dcb7b800f04cea983313d0b1046f72e9f Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 6 Jan 2020 17:51:40 +0100 Subject: [PATCH 24/55] Fix transfer ownership to a team --- www/common/cryptpad-common.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index c869967af..57815b3ef 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -728,7 +728,7 @@ define([ }).nThen(function () { postMessage("SET_PAD_TITLE", { teamId: data.teamId, - href: data.href, + href: Hash.getRelativeHref(data.href), title: data.title, password: data.password, channel: secret.channel, From 4aaef2d8117822480c7a4fc51821fc62df8c2562 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 6 Jan 2020 14:29:45 -0500 Subject: [PATCH 25/55] remove use of deprecated 'new Buffer' instanciation --- historyKeeper.js | 2 +- rpc.js | 2 +- storage/file.js | 2 +- www/common/common-util.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/historyKeeper.js b/historyKeeper.js index 53ce46807..59a694e8c 100644 --- a/historyKeeper.js +++ b/historyKeeper.js @@ -284,7 +284,7 @@ module.exports.create = function (cfg) { const storeMessage = function (ctx, channel, msg, isCp, optionalMessageHash) { const id = channel.id; - const msgBin = new Buffer(msg + '\n', 'utf8'); + const msgBin = Buffer.from(msg + '\n', 'utf8'); queueStorage(id, function (next) { // Store the message first, and update the index only once it's stored. diff --git a/rpc.js b/rpc.js index 8968b9a4c..fcd85a390 100644 --- a/rpc.js +++ b/rpc.js @@ -1062,7 +1062,7 @@ var writeLoginBlock = function (Env, msg, cb) { // FIXME BLOCKS // flow is dumb and I need to guard against this which will never happen /*:: if (typeof(validatedBlock) === 'undefined') { throw new Error('should never happen'); } */ /*:: if (typeof(path) === 'undefined') { throw new Error('should never happen'); } */ - Fs.writeFile(path, new Buffer(validatedBlock), { encoding: "binary", }, function (err) { + Fs.writeFile(path, Buffer.from(validatedBlock), { encoding: "binary", }, function (err) { if (err) { return void cb(err); } cb(); }); diff --git a/storage/file.js b/storage/file.js index 402982f6b..bb65cff43 100644 --- a/storage/file.js +++ b/storage/file.js @@ -832,7 +832,7 @@ const messageBin = (env, chanName, msgBin, cb) => { // append a string to a channel's log as a new line var message = function (env, chanName, msg, cb) { - messageBin(env, chanName, new Buffer(msg + '\n', 'utf8'), cb); + messageBin(env, chanName, Buffer.from(msg + '\n', 'utf8'), cb); }; // stream messages from a channel log diff --git a/www/common/common-util.js b/www/common/common-util.js index ba9fa2f03..da83b5960 100644 --- a/www/common/common-util.js +++ b/www/common/common-util.js @@ -3,7 +3,7 @@ // polyfill for atob in case you're using this from node... window.atob = window.atob || function (str) { return Buffer.from(str, 'base64').toString('binary'); }; // jshint ignore:line - window.btoa = window.btoa || function (str) { return new Buffer(str, 'binary').toString('base64'); }; // jshint ignore:line + window.btoa = window.btoa || function (str) { return Buffer.from(str, 'binary').toString('base64'); }; // jshint ignore:line Util.slice = function (A, start, end) { return Array.prototype.slice.call(A, start, end); From 1b3c2a43bf5c0aa7505529286139bdb04aa73b43 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 6 Jan 2020 15:22:55 -0500 Subject: [PATCH 26/55] remove wss from connecSrc in example nginx config --- docs/example.nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/example.nginx.conf b/docs/example.nginx.conf index da61ce42e..02f233735 100644 --- a/docs/example.nginx.conf +++ b/docs/example.nginx.conf @@ -72,7 +72,7 @@ server { set $styleSrc "'unsafe-inline' 'self' ${main_domain}"; # connect-src restricts URLs which can be loaded using script interfaces - set $connectSrc "'self' https://${main_domain} wss://${main_domain} $main_domain https://${api_domain} blob:"; + set $connectSrc "'self' https://${main_domain} $main_domain https://${api_domain} blob:"; # fonts can be loaded from data-URLs or the main domain set $fontSrc "'self' data: ${main_domain}"; From 8dd7b88dd8813105705e51209dc7a41c8c863449 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 7 Jan 2020 08:26:58 -0500 Subject: [PATCH 27/55] WIP changelog --- CHANGELOG.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fadcdb437..8336ed704 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,14 +3,19 @@ ## Goals * simplify server configuration by dropping support for unused features - * drop `websocketPath` `websocketURL` and `useExternalWebsockets` - * use `externalWebsocketURL` to replace all of these ## Update notes * modify default config and docker config * review websocket config changes and update accordingly -* you may want to use the updated nginx config +* default config.js now nests directories not exposed by nginx in the /data/ directory + * this should not affect existing instances set up manually, though anyone running docker should review their setup before updating +* updated example nginx config uses variables for domains, simplifies sandbox configuration +* drop support for old websocket configuration style, prefer unified value + * drop `websocketPath` `websocketURL` and `useExternalWebsockets` + * use `externalWebsocketURL` to replace all of these +* drop use of `new Buffer`, use `Buffer.from` + ## Features From 944a411d42273f11eba50efb7e0d78bc839678d7 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 7 Jan 2020 11:49:54 -0500 Subject: [PATCH 28/55] bump footer string and increment version number --- customize.dist/pages.js | 2 +- package-lock.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/customize.dist/pages.js b/customize.dist/pages.js index 281f213f6..a7fcd9441 100644 --- a/customize.dist/pages.js +++ b/customize.dist/pages.js @@ -103,7 +103,7 @@ define([ ])*/ ]) ]), - h('div.cp-version-footer', "CryptPad v3.8.0 (IsolobodonPortoricensis)") + h('div.cp-version-footer', "CryptPad v3.9.0 (JamaicanMonkey)") ]); }; diff --git a/package-lock.json b/package-lock.json index cdf37370d..de127e96c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cryptpad", - "version": "3.8.0", + "version": "3.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index be6071a2f..1d676323d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cryptpad", "description": "realtime collaborative visual editor with zero knowlege server", - "version": "3.8.0", + "version": "3.9.0", "license": "AGPL-3.0+", "repository": { "type": "git", From 44a5ea39bd3afc4bbe2e0bbc6ed83ef35d2bee22 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 7 Jan 2020 13:40:54 -0500 Subject: [PATCH 29/55] update the changelog to provide a detailed account of everything this release breaks --- CHANGELOG.md | 63 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8336ed704..c5561e1af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,26 +1,65 @@ -# J release (3.9.0) +# JamaicanMonkey release (3.9.0) ## Goals -* simplify server configuration by dropping support for unused features +Over time we've added many small configuration values to CryptPad's `config/config.js`. +As the number of possible variations grew it became increasingly difficult to test the platform and to provide clear documentation. +Ultimately this has made the platform more difficult to understand and consequently to host. -## Update notes +This release features relatively few bug fixes or features. +Instead, we took the calm period of the northern winter holidays to simplify the process of running a server and to begin working on some comprehensive documentation. -* modify default config and docker config -* review websocket config changes and update accordingly -* default config.js now nests directories not exposed by nginx in the /data/ directory - * this should not affect existing instances set up manually, though anyone running docker should review their setup before updating -* updated example nginx config uses variables for domains, simplifies sandbox configuration -* drop support for old websocket configuration style, prefer unified value - * drop `websocketPath` `websocketURL` and `useExternalWebsockets` - * use `externalWebsocketURL` to replace all of these -* drop use of `new Buffer`, use `Buffer.from` +## Update notes +We have chosen to drop support for a number of parameters which we believe are not widely used. +Read the following list carefully before updating, as you could be relying on behaviour which no longer exists. + +* Due to reasons of security and performance we have long advised that administrators make their instance available only over HTTPS provided by a reverse proxy such as nginx instead of loading TLS certificates via the node process itself. We have removed the option of serving HTTPS traffic directly from node by removing all support for HTTPS in this process. +* Over the years many administrators have had to migrate their instance from one machine to another and have had difficulty identifying which directories were responsible for storing user data. We are beginning to migrate all user-generated data from the repository's root into the `data` directory as a new default, allowing for admins to migrate content by copying this single directory. + * for the time being we have not moved anything which is exposed directly over HTTPS since that complicates the upgrade process by requiring all configuration changes to be made simultaneously. + * the modifications we've made only affect the _default configuration_ provided by `config/config.example.js`, existing instances which have copied this file to `config/config.js` will not be affected. + * only the following values have been modified: + * `pinPath` + * `taskPath` + * `blobStagingPath` +* We have modified the Dockerfile volume list to reflect the changes to these default paths. If you are using docker you will have to either: + * revert their removal or + * move the affected directories into the `data` directory and update your live config file to reflect their new location +* Please note that we do our team does not use docker, that it was included in the main repository as a community contribution, and that we are not committed to supporting its configuration since we do not test it. + * Our official policy is to provide an up-to-date set of configuration files reflecting the state of our production installation on [CryptPad.fr](https://cryptpad.fr) using Debian, nginx, and systemd. + * we are actively working on improving our documentation for this particular configuration and we plan to close issues for other configurations as being outside of the project's scope. +* We've updated our example nginx configuration file, located at `cryptpad/docs/example.nginx.conf`. + * in addition to a great number of comments, it now makes use of variables configure the domains referenced by the CSP headers which are required to take advantage of all of CryptPad's security features. +* Prompted by warnings from recent nodejs versions we are updating our recommended version to v12.14.0 which is at the time of this writing the latest Long Term Support version. + * you may need to update to successfully launch your server. + * as always, we recommend using nvm to manage nodejs installation. +* We have dropped support for a number of experimental features: + * replify (which allowed admins to modify their server at runtime using a REPL connected via a named socket) + * heapdump (which provided snapshots of the server's memory if it crashed) + * configurable RPC files as a configuration parameter +* Finally, we've replaced a number of websocket configuration values (`websocketURL`, `websocketPath`, `useExternalWebsockets`, and `useSecureWebsockets`) with one optional value (`externalWebsocketURL`) in config.js + * if your instance is configured in the default manner you shouldn't actually need this value, as it will default to using `/cryptpad_websocket`. + * if you have configured your instance to serve all static assets over one domain and to host your API server on another, set `externalWebsocketURL` to `wss://your-domain.tld/cryptpad_websocket` or whatever URL will be correctly forwarded to your API server. + +Once you have reviewed your configuration files and ensured that they are correct, update to 3.9.0 with the following steps: + +1. take your server down +2. get the latest code with `git pull origin master` +3. install some required serverside dependency with `npm update` +4. (optionally) update clientside dependencies with `bower update` +5. bring your server back up ## Features +* We made some minor improvements to the process of redeeming invitation links for teams. + * invitation links can only be used once, so we remove the hash from the URL bar once you've landed on the redemption page so that reloading after redeeming doesn't indicate that you've used an expired link. + ## Bug fixes +* We noticed and fixed a style regression which incorrectly removed the scrollbar from some textareas +* We also found that it was possible to corrupt the href of an item in a team's drive if you first shared a pad with your team then transferred ownership, the link stored in the team's drive would have its domain concatenated together twice. +* The type value of read-only pads displayed as search results in user and team drives was incorrect but is now correctly inferred. + # IsolobodonPortoricensis release (3.8.0) We had some trouble finding an extinct animal whose name started with "I", and we had to resort to using a scientific name. From 488e5804e45e87c76595a7aa2dff98ba37c8646e Mon Sep 17 00:00:00 2001 From: Weblate Date: Wed, 8 Jan 2020 21:34:09 +0000 Subject: [PATCH 30/55] Translated using Weblate (Finnish) Currently translated at 62.9% (745 of 1185 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/fi/ Translated using Weblate (Finnish) Currently translated at 53.2% (631 of 1185 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/fi/ Translated using Weblate (Finnish) Currently translated at 51.6% (611 of 1185 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/fi/ Translated using Weblate (Finnish) Currently translated at 51.2% (607 of 1185 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/fi/ Translated using Weblate (Finnish) Currently translated at 44.2% (524 of 1185 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/fi/ --- www/common/translations/messages.fi.json | 374 ++++++++++++++++++++++- 1 file changed, 373 insertions(+), 1 deletion(-) diff --git a/www/common/translations/messages.fi.json b/www/common/translations/messages.fi.json index 4fa064e05..8a77fae80 100644 --- a/www/common/translations/messages.fi.json +++ b/www/common/translations/messages.fi.json @@ -377,5 +377,377 @@ "fm_padIsOwnedOther": "Tämän padin omistaa toinen käyttäjä", "fm_deletedPads": "Nämä padit eivät ole enää saatavilla palvelimella, ne on poistettu CryptDrivestasi: {0}", "fm_tags_name": "Tunnisteen nimi", - "fm_tags_used": "Käyttökertojen määrä" + "fm_tags_used": "Käyttökertojen määrä", + "fm_restoreDrive": "Palautetaan Drivesi aikaisempaan tilaan. Saadaksesi parhaimman lopputuloksen vältä muutosten tekemistä Driveesi, kunnes toimenpide on valmis.", + "fm_moveNestedSF": "Et voi siirtää jaettua kansiota toisen jaetun kansion sisään. Kansiota {0} ei siirretty.", + "fm_passwordProtected": "Salasanasuojattu", + "fc_newfolder": "Uusi kansio", + "fc_newsharedfolder": "Uusi jaettu kansio", + "fc_rename": "Nimeä uudelleen", + "fc_color": "Vaihda väriä", + "fc_open": "Avaa", + "fc_open_ro": "Avaa (vain luku)", + "fc_openInCode": "Avaa Koodi-editorissa", + "fc_expandAll": "Laajenna kaikki", + "fc_collapseAll": "Tiivistä kaikki", + "fc_delete": "Siirrä roskakoriin", + "fc_delete_owned": "Poista palvelimelta", + "fc_restore": "Palauta", + "fc_remove": "Poista CryptDrivesta", + "fc_remove_sharedfolder": "Poista", + "fc_empty": "Tyhjennä roskakori", + "fc_prop": "Ominaisuudet", + "fc_hashtag": "Tunnisteet", + "fc_sizeInKilobytes": "Koko kilotavuissa", + "fo_moveUnsortedError": "Et voi siirtää kansiota mallipohjalistaan", + "fo_existingNameError": "Valitsemasi nimi on jo käytössä kansiossa. Ole hyvä ja valitse toinen nimi.", + "fo_moveFolderToChildError": "Et voi siirtää kansiota sen alikansioon", + "fo_unableToRestore": "Tiedoston palauttaminen sen alkuperäiseen sijaintiin ei onnistunut. Voit yrittää siirtää sen toiseen sijaintiin.", + "fo_unavailableName": "Valitsemassasi sijainnissa on jo samanniminen tiedosto tai kansio. Ole hyvä ja anna elementille uusi nimi ja yritä uudelleen.", + "fs_migration": "CryptDrivesi päivitetään uuteen versioon. Nykyinen sivu täytyy ladata uudelleen.
Ole hyvä ja lataa sivu uudelleen jatkaaksesi käyttöä.", + "login_login": "Kirjaudu sisään", + "login_makeAPad": "Luo padi anonyyminä", + "login_nologin": "Selaa paikallisia padeja", + "login_register": "Rekisteröidy", + "logoutButton": "Kirjaudu ulos", + "settingsButton": "Asetukset", + "login_username": "Käyttäjänimi", + "login_password": "Salasana", + "login_confirm": "Vahvista salasanasi", + "login_remember": "Muista minut", + "login_hashing": "Salasanaasi hajautetaan, tämä saattaa kestää jonkin aikaa.", + "login_hello": "Hei {0},", + "login_helloNoName": "Hei,", + "login_accessDrive": "Käytä Driveasi", + "login_orNoLogin": "tai", + "login_noSuchUser": "VIrheellinen käyttäjätunnus tai salasana. Yritä uudelleen tai rekisteröidy", + "login_invalUser": "Käyttäjänimi vaaditaan", + "login_invalPass": "Salasana vaaditaan", + "login_unhandledError": "Tapahtui odottamaton virhe :(", + "register_importRecent": "Tuo padeja anonyymistä sessiosta", + "register_acceptTerms": "Hyväksyn käyttöehdot", + "register_passwordsDontMatch": "Salasanat eivät täsmää!", + "register_passwordTooShort": "Salasanan täytyy olla vähintään {0} merkkiä pitkä.", + "register_mustAcceptTerms": "Sinun täytyy hyväksyä käyttöehdot.", + "register_mustRememberPass": "Emme voi nollata salasanaasi, jos unohdat sen. On erittäin tärkeää, että muistat sen! Ole hyvä ja laita rasti valintaruutuun vahvistaaksesi.", + "register_whyRegister": "Miksi rekisteröityminen kannattaa?", + "register_header": "Tervetuloa CryptPadiin", + "register_explanation": "

Käydään läpi muutama perusasia:

  • Salasanasi on myös salausavain, jolla kaikki luomasi padit salataan. Jos kadotat salasanasi, emme voi millään tavalla palauttaa tietojasi.
  • Voit tuoda viimeksi katselemasi padit selaimestasi käyttäjätilillesi.
  • Jos käytät jaettua tietokonetta, täytyy sinun kirjautua ulos lopettaessasi työskentelyn - välilehden sulkeminen ei riitä.
", + "register_writtenPassword": "Olen kirjoittanut ylös käyttäjätunnukseni ja salasanani, jatka", + "register_cancel": "Takaisin", + "register_warning": "Nollatietoperiaate tarkoittaa, ettemme voi palauttaa tietojasi, jos hävität salasanasi.", + "register_alreadyRegistered": "Tämä käyttäjä on jo olemassa, haluatko kirjautua sisään?", + "register_emailWarning0": "Näyttää siltä, että lähetit sähköpostiosoitteesi käyttäjätunnuksenasi.", + "register_emailWarning1": "Voit halutessasi tehdä niin, mutta sitä ei lähetetä palvelimellemme.", + "register_emailWarning2": "Toisin kuin monissa muissa palveluissa, emme voi palauttaa salasanaasi sähköpostin avulla.", + "register_emailWarning3": "Jos ymmärrät tämän ja haluat silti käyttää sähköpostiosoitettasi käyttäjätunnuksenasi, klikkaa OK.", + "settings_cat_account": "Käyttäjätili", + "settings_cat_drive": "CryptDrive", + "settings_cat_cursor": "Kursori", + "settings_cat_code": "Koodi", + "settings_cat_pad": "Teksti", + "settings_cat_creation": "Uusi padi", + "settings_cat_subscription": "Tilaus", + "settings_title": "Asetukset", + "settings_save": "Tallenna", + "settings_backupCategory": "Varmuuskopiointi", + "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_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", + "settings_exportDescription": "Odota hetki, dokumenttejasi puretaan ja ladataan. Tämä voi viedä muutaman minuutin. Välilehden sulkeminen keskeyttää toimenpiteen.", + "settings_exportFailed": "Jos padin lataamiseen menee enemmän kuin yksi (1) minuutti, sitä ei oteta mukaan vientiin. Viennistä pois jääneiden padien linkit näytetään lopuksi.", + "settings_exportWarning": "Huomautus: tämä työkalu on beta-versiossa, ja siinä saattaa olla skaalautuvuusongelmia. Suosittelemme välilehden jättämistä aktiiviseksi paremman suorituskyvyn takaamiseksi.", + "settings_exportCancel": "Haluatko varmasti keskeyttää viennin? Seuraavalla kerralla toimenpide täytyy aloittaa alusta.", + "settings_export_reading": "Luetaan CryptDrivea...", + "settings_export_download": "Ladataan ja puretaan dokumentteja...", + "settings_export_compressing": "Pakataan tiedostoja...", + "settings_export_done": "Latauksesi on valmis!", + "settings_exportError": "Tarkastele virheitä", + "settings_exportErrorDescription": "Emme onnistuneet lisäämään seuraavia dokumentteja vientipakettiin:", + "settings_exportErrorEmpty": "Tätä dokumenttia ei voi viedä (tyhjä tai virheellinen sisältö).", + "settings_exportErrorMissing": "Tätä dokumenttia ei löydy palvelimeltamme (vanhentunut tai omistajansa poistama)", + "settings_exportErrorOther": "Dokumenttia viedessä tapahtui virhe: {0}", + "settings_resetNewTitle": "Tyhjennä CryptDrive", + "settings_resetButton": "Poista", + "settings_reset": "Poista kaikki tiedostot ja kansiot CryptDrivestasi", + "settings_resetPrompt": "Tämä toiminto poistaa kaikki padit CryptDrivestasi.
Haluatko varmasti jatkaa?
Kirjoita \"I love CryptPad\" vahvistaaksesi.", + "settings_resetDone": "Drivesi on nyt tyhjennetty!", + "settings_resetError": "Virheellinen vahvistusteksti. CryptDriveasi ei ole muutettu.", + "settings_resetTipsAction": "Nollaa", + "settings_resetTips": "Vinkit", + "settings_resetTipsButton": "Nollaa CryptDriven vinkit", + "settings_resetTipsDone": "Kaikki vinkit tulevat jälleen näkyviin.", + "settings_thumbnails": "Pienoiskuvat", + "settings_disableThumbnailsAction": "Ota pienoiskuvien luonti pois käytöstä CryptDrivessa", + "settings_disableThumbnailsDescription": "Pienoiskuvat luodaan automaattisesti uuden padin käytön yhteydessä, ja ne säilytetään selaimessasi. Voit ottaa tämän ominaisuuden pois käytöstä.", + "settings_resetThumbnailsAction": "Tyhjennä", + "settings_resetThumbnailsDescription": "Tyhjennä kaikki padien pienoiskuvat selaimesi välimuistista.", + "settings_resetThumbnailsDone": "Kaikki pienoiskuvat on poistettu.", + "settings_importTitle": "Tuo tässä selaimessa viimeksi käytetyt padit CryptDriveesi", + "settings_import": "Tuo", + "settings_importConfirm": "Haluatko varmasti tuoda tässä selaimessa viimeksi käytetyt padit käyttäjätilisi CryptDriveen?", + "settings_importDone": "Tuonti valmis", + "settings_autostoreTitle": "Padien tallennus CryptDrivessa", + "settings_autostoreHint": "AutomaattinenKaikki käyttämäsi padit tallennetaan CryptDriveesi.
Manuaalinen (kysy aina) Jos et ole vielä tallentanut padia, kysytään sinulta, haluatko tallentaa sen CryptDriveesi.
Manuaalinen (älä kysy)Padeja ei tallenneta automaattisesti CryptDriveesi. Padien tallennusmahdollisuus piilotetaan.", + "settings_autostoreYes": "Automaattinen", + "settings_autostoreNo": "Manuaalinen (älä kysy)", + "settings_autostoreMaybe": "Manuaalinen (kysy aina)", + "settings_userFeedbackTitle": "Palaute", + "settings_userFeedbackHint1": "CryptPad tarjoaa palvelimelle hyvin yksinkertaista palautetta kertoakseen meille, miten voimme kehittää käyttäjäkokemustasi. ", + "settings_userFeedbackHint2": "Padisi sisältöä ei koskaan jaeta palvelimen kanssa.", + "settings_userFeedback": "Salli käyttäjäpalaute", + "settings_deleteTitle": "Käyttäjätilin poisto", + "settings_deleteHint": "Käyttäjätilin poisto on pysyvä toimenpide. CryptDrivesi ja lista padeistasi poistetaan palvelimelta. Loput padeistasi poistetaan 90 päivän kuluttua, jos kukaan muu ei ole tallentanut niitä omaan CryptDriveensa.", + "settings_deleteButton": "Poista käyttäjätilisi", + "settings_deleteModal": "Jaa seuraavat tiedot CryptPad-instanssisi ylläpitäjän kanssa poistaaksesi tietosi palvelimelta.", + "settings_deleteConfirm": "Klikkaamalla OK käyttäjätilisi poistetaan pysyvästi. Oletko varma?", + "settings_deleted": "Käyttäjätilisi on nyt poistettu. Klikkaa OK siirtyäksesi kotisivulle.", + "settings_anonymous": "Et ole kirjautunut sisään. Nämä asetukset koskevat vain tätä selainta.", + "settings_publicSigningKey": "Julkinen salausavain", + "settings_usage": "Käyttö", + "settings_usageTitle": "Näytä kiinnitettyjen padien koko megatavuissa", + "settings_pinningNotAvailable": "Kiinnitetyt padit ovat saatavilla ainoastaan kirjautuneille käyttäjille.", + "settings_pinningError": "Jokin meni pieleen", + "settings_usageAmount": "Kiinnitetyt padisi käyttävät {0}Mt", + "settings_logoutEverywhereButton": "Kirjaudu ulos", + "settings_logoutEverywhereTitle": "Kirjaudu ulos kaikkialta", + "settings_logoutEverywhere": "Pakota uloskirjautuminen kaikista web-sessioista", + "settings_logoutEverywhereConfirm": "Oletko varma? Joudut kirjautumaan kaikilla laitteillasi uudelleen sisään.", + "settings_driveDuplicateTitle": "Omistettujen padien kaksoiskappaleet", + "settings_driveDuplicateHint": "Siirtäessäsi omistettuja padeja jaettuun kansioon omassa CryptDrivessasi säilytetään niistä kopio, jotta tiedosto pysyy hallinnassasi. Voit piilottaa tiedostojen kaksoiskappaleet. Vain jaettu versio jää näkyville, ellei sitä poisteta. Tässä tapauksessa alkuperäinen padi näytetään sen edellisessä sijainnissa.", + "settings_driveDuplicateLabel": "Piilota kaksoiskappaleet", + "settings_codeIndentation": "Koodieditorin sisennys (välilyönnit)", + "settings_codeUseTabs": "Sisennä sarkainmerkkejä käyttäen (välilyöntien sijaan)", + "settings_codeFontSize": "Koodieditorin fonttikoko", + "settings_padWidth": "Editorin maksimileveys", + "settings_padWidthHint": "Teksti-tyyppiset padit käyttävät oletusleveytenä näyttölaitteesi maksimileveyttä, mikä voi tehdä lukemisesta vaikeaa. Tästä voit pienentää editorin leveyttä.", + "settings_padWidthLabel": "Pienennä editorin leveyttä", + "settings_padSpellcheckTitle": "Oikeinkirjoituksen tarkistus", + "settings_padSpellcheckHint": "Tämä vaihtoehto ottaa käyttöön oikeinkirjoituksen tarkastuksen Teksti-tyyppisissä padeissa. Oikeinkirjoitusvirheet alleviivataan punaisella. Klikkaa virheellistä sanaa hiiren oikealla painikkeella painaessasi Ctrl- tai Meta-näppäintä pohjaan nähdäksesi korjausehdotukset.", + "settings_padSpellcheckLabel": "Ota oikeinkirjoituksen tarkastus käyttöön Teksti-muotoisissa padeissa", + "settings_creationSkip": "Ohita padin luontiruutu", + "settings_creationSkipHint": "Padien luontisivu esittää vaihtoehtoja padin luomiseen auttaakseen sinua hallitsemaan ja suojaamaan tietojasi. Jos koet sen hidastavan työskentelyäsi, voit tällä asetuksella ohittaa luontisivun ja käyttää sen sijaan yläpuolella määrittelemiäsi oletusasetuksia.", + "settings_creationSkipTrue": "Ohita", + "settings_creationSkipFalse": "Näytä", + "settings_templateSkip": "Ohita mallipohjan valinta-dialogi", + "settings_templateSkipHint": "Luodessasi uutta padia sinulta kysytään, haluatko käyttää mallipohjaa, jos sinulla on tallennettuja mallipohjia tälle padityypille. Tällä asetuksella voit valita, ettei mallipohjan valinta-dialogia näytetä ja siten mallipohjia ei koskaan käytetä.", + "settings_ownDriveTitle": "Ota käyttöön viimeisimmät tiliominaisuudet", + "settings_ownDriveHint": "Teknisistä syistä vanhemmilla käyttäjätileillä ei suoraan ole pääsyä CryptPadin uusimpiin ominaisuuksiin. Ilmainen päivitys uuteen käyttäjätiliin valmistelee CryptDrivesi tulevia ominaisuuksia varten häiritsemättä tavanomaista toimintaasi.", + "settings_ownDriveButton": "Päivitä käyttäjätilisi", + "settings_ownDriveConfirm": "Käyttäjätilin päivitykseen voi mennä jonkin aikaa. Joudut kirjautumaan uudelleen sisään kaikilla laitteillasi. Oletko varma, että haluat aloittaa päivityksen?", + "settings_ownDrivePending": "Käyttäjätiliäsi päivitetään. Ole hyvä, äläkä sulje tai lataa tätä sivua uudelleen, ennen kuin toimenpide on valmis.", + "settings_changePasswordTitle": "Vaihda salasanasi", + "settings_changePasswordHint": "Vaihda käyttäjätilisi salasana. Syötä nykyinen salasanasi ja vahvista uusi salasana kirjoittamalla se kahdesti.
Emme voi nollata unohtuneita salasanoja, joten olethan varovainen!", + "settings_changePasswordButton": "Vaihda salasana", + "settings_changePasswordCurrent": "Nykyinen salasana", + "settings_changePasswordNew": "Uusi salasana", + "settings_changePasswordNewConfirm": "Vahvista uusi salasana", + "settings_changePasswordConfirm": "Oletko varma, että haluat vaihtaa salasanasi? Joudut kirjautumaan uudelleen sisään kaikilla laitteillasi.", + "settings_changePasswordError": "Tapahtui odottamaton virhe. Jos et pääse kirjautumaan sisään tai vaihtamaan salasanaasi, ota yhteyttä CryptPad-instanssisi ylläpitäjiin.", + "settings_changePasswordPending": "Salasanaasi päivitetään. Ole hyvä äläkä sulje tai lataa tätä sivua uudelleen, ennen kuin toimenpide on valmis.", + "settings_changePasswordNewPasswordSameAsOld": "Uuden salasanasi on oltava erilainen kuin nykyinen salasana.", + "settings_cursorColorTitle": "Kursorin väri", + "settings_cursorColorHint": "Vaihda kollaboratiivisissa dokumenteissa käytettävää käyttäjääsi yhdistettyä väriä.", + "settings_cursorShareTitle": "Jaa oma kursorisijainti", + "settings_cursorShareHint": "Voit päättää, haluatko kursorisijaintisi näkyvän muille kollaboratiivisissa dokumenteissa.", + "settings_cursorShareLabel": "Jaa sijainti", + "settings_cursorShowTitle": "Näytä muiden käyttäjien kursorisijainti", + "settings_cursorShowHint": "Voit valita, haluatko nähdä muiden käyttäjien kursorit kollaboratiivisissa dokumenteissa.", + "settings_cursorShowLabel": "Näytä kursorit", + "upload_title": "Tiedostojen lataus", + "upload_type": "Tyyppi", + "upload_modal_title": "Tiedostojen latauksen asetukset", + "upload_modal_filename": "Tiedostonimi (pääte {0} lisätty automaattisesti)", + "upload_modal_owner": "Omistettu tiedosto", + "uploadFolder_modal_title": "Kansion latauksen asetukset", + "uploadFolder_modal_filesPassword": "Tiedostojen salasana", + "uploadFolder_modal_owner": "Omistetut tiedostot", + "uploadFolder_modal_forceSave": "Tallenna tiedostot CryptDriveesi", + "upload_serverError": "Palvelinvirhe: tiedoston lataus palvelimelle epäonnistui.", + "upload_uploadPending": "Edellinen latauksesi on kesken. Haluatko keskeyttää sen ja ladata uuden tiedoston?", + "upload_success": "Tiedosto ({0}) on ladattu onnistuneesti palvelimelle ja lisätty CryptDriveesi.", + "upload_notEnoughSpace": "CryptDrivessasi ei ole tarpeeksi vapaata tallennustilaa tälle tiedostolle.", + "upload_notEnoughSpaceBrief": "Tallennustila ei riitä", + "upload_tooLarge": "Tiedoston koko ylittää suurimman sallitun latauskoon.", + "upload_tooLargeBrief": "Liian suuri tiedosto", + "upload_choose": "Valitse tiedosto", + "upload_pending": "Odottaa", + "upload_cancelled": "Keskeytetty", + "upload_name": "Tiedostonimi", + "upload_size": "Koko", + "upload_progress": "Edistyminen", + "upload_mustLogin": "Kirjaudu sisään ladataksesi tiedostoja", + "upload_up": "Lataa", + "download_button": "Pura ja lataa", + "download_mt_button": "Lataa tietokoneelle", + "download_resourceNotAvailable": "Pyydetty resurssi ei ollut saatavilla... Paina Esc jatkaaksesi.", + "download_dl": "Lataa tietokoneelle", + "download_step1": "Ladataan", + "download_step2": "Puretaan", + "todo_title": "CryptTodo", + "todo_newTodoNamePlaceholder": "Kuvaile tehtävääsi...", + "todo_newTodoNameTitle": "Lisää tämä tehtävä Tehtävät-listaasi", + "todo_markAsCompleteTitle": "Merkitse tehtävä valmiiksi", + "todo_markAsIncompleteTitle": "Merkitse tehtävä keskeneräiseksi", + "todo_removeTaskTitle": "Poista tehtävä Tehtävät-listaltasi", + "pad_showToolbar": "Näytä työkalurivi", + "pad_hideToolbar": "Piilota työkalurivi", + "pad_base64": "Tämä padi sisältää tehottomasti tallennettuja kuvia. Kuvat kasvattavat padin kokoa merkittävästi ja hidastavat padin lataamista. Voit siirtää nämä tiedostot uuteen tiedostomuotoon, joka säilytetään erillisenä CryptDrivessasi. Haluatko siirtää kuvat nyt?", + "mdToolbar_button": "Näytä tai piilota Markdown-työkalupalkki", + "mdToolbar_defaultText": "Lisää tekstisi tähän", + "mdToolbar_help": "Ohje", + "mdToolbar_tutorial": "http://www.markdowntutorial.com/", + "mdToolbar_bold": "Lihavoitu", + "mdToolbar_italic": "Kursiivi", + "mdToolbar_strikethrough": "Yliviivaus", + "mdToolbar_heading": "Otsikko", + "mdToolbar_link": "Linkki", + "mdToolbar_quote": "Lainaus", + "mdToolbar_nlist": "Järjestetty lista", + "mdToolbar_list": "Luettelomerkkilista", + "mdToolbar_check": "Tehtävälista", + "mdToolbar_code": "Koodi", + "mdToolbar_toc": "Sisällysluettelo", + "home_product": "CryptPad on sisäänrakennetun tietosuojan periaatteen mukainen vaihtoehto suosituille toimisto-ohjelmistoille ja pilvipalveluille. Kaikki CryptPadiin tallennettavat tiedot salataan ennen palvelimelle lähettämistä. Kukaan (emme edes me) ei voi päästä tietoihisi ilman avaimiasi.", + "home_host": "Tämä on itsenäinen yhteisön ylläpitämä Cryptpad-instanssi. Sen lähdekoodi on saatavilla GitHubissa.", + "home_host_agpl": "Cryptpad-ohjelmisto jaellaan AGPL3-ohjelmistolisenssin ehtojen mukaisesti", + "home_ngi": "NGI Award-palkinnon voittaja", + "about_intro": "CryptPadia kehittää Pariisissa, Ranskassa ja Iasissa, Romaniassa toimivaXWiki SAS-pienyrityksen tutkimusryhmä. CryptPadin parissa työskentelee kolme ryhmän ydinjäsentä ja lisäksi joitakin avustajia XWiki SAS:n sisältä ja ulkopuolelta.", + "about_core": "Ydinkehittäjät", + "about_contributors": "Tärkeät avustajat", + "main_info": "

Luottamuksellista yhteistyötä

Jaetut dokumentit mahdollistavat ideoiden jakamisen samalla kun nollatietoperiaate-teknologia suojaa yksityisyytesi - jopa meiltä.", + "main_catch_phrase": "Pilvipalvelu nollatietoperiaatteella", + "main_footerText": "CryptPadin avulla voit nopeasti luoda kollaboratiivisia dokumentteja muistiinpanoja ja yhteistä ideointia varten.", + "footer_applications": "Sovellukset", + "footer_contact": "Ota yhteyttä", + "footer_aboutUs": "Tietoa meistä", + "about": "Tietoa meistä", + "privacy": "Yksityisyys", + "contact": "Ota yhteyttä", + "terms": "Käyttöehdot", + "blog": "Blogi", + "topbar_whatIsCryptpad": "Mikä on CryptPad", + "whatis_title": "Mikä on CryptPad", + "whatis_collaboration": "Nopeaa ja helppoa yhteistyötä", + "whatis_collaboration_p1": "CryptPadin avulla voit nopeasti luoda kollaboratiivisia dokumentteja muistiinpanoja ja yhteistä ideointia varten. Rekisteröitymällä ja kirjautumalla sisään saat mahdollisuuden ladata tiedostoja palvelimelle ja oman CryptDriven, jossa voit säilyttää kaikki padisi. Rekisteröityneet käyttäjät saavat ilmaiseksi 50 Mt tallennustilaa.", + "whatis_collaboration_p2": "Voit helposti antaa käyttöoikeuden CryptPad-dokumenttiin jakamalla sen linkin. Voit myös jakaa dokumentin linkin vain luku-tilassa, jolloin voit julkistaa yhteistyön tulokset ja muokata niitä edelleen.", + "team_inviteLinkError": "Linkin luomisessa tapahtui virhe.", + "whatis_collaboration_p3": "CKEditor:illa voit luoda yksinkertaisia muotoiltavia tekstitiedostoja sekä Markdown-tiedostoja, jotka muunnetaan tekstiä muokatessasi reaaliaikaisesti esitysmuotoon. Voit myös käyttää Kysely-sovellusta tapahtumien ajoittamiseen useiden osallistujien kanssa.", + "whatis_zeroknowledge": "Nollatietoperiaate", + "whatis_zeroknowledge_p1": "Emme halua tietää, mitä kirjoitat. Modernin kryptografian avulla voit olla varma, ettemme todellakaan tiedä siitä mitään. CryptPad käyttää 100-prosenttisesti asiakasohjelmassa tapahtuvaa salausta suojatakseen tuottamaasi sisältöä meiltä palvelimen ylläpitäjiltä.", + "whatis_zeroknowledge_p2": "Rekisteröityessäsi ja kirjautuessasi sisään käyttäjätunnuksesi ja salasanasi lasketaan salaiseksi avaimeksi scrypt-avaintenmuodostusfunktiolla. Tätä avainta, käyttäjätunnustasi ja salasanaasi ei koskaan lähetetä palvelimelle. Sen sijaan niitä käytetään asiakasohjelmassa CryptDrivesi sisällön purkamiseen. CryptDrivesi puolestaan sisältää avaimet kaikkiin padeihin, joihin sinulla on käyttöoikeus.", + "whatis_zeroknowledge_p3": "Kun jaat linkin dokumenttiin, jaat itse asiassa dokumentin käyttöön tarvittavan salausavaimen, mutta koska salausavain sisällytetään katkelmatunnisteeseen (fragment identifier), sitä ei koskaan lähetetä palvelimelle suoraan. Tutustu yksityisyydestä kertovaan blogikirjoitukseemme saadaksesi selville, mihin metadataan meillä on pääsy ja mihin taas ei.", + "whatis_drive": "CryptDriven järjestely", + "whatis_drive_p1": "Kun käytät padia CryptPadissa, lisätään se automaattisesti CryptDrivesi pääkansioon. Voit halutessasi myöhemmin järjestellä padit kansioihin tai viedä ne roskakoriin. CryptDrive antaa sinun hakea padejasi ja järjestellä niitä milloin ja miten haluat.", + "whatis_drive_p2": "Intuitiivinen raahaa ja pudota-käyttöliittymä mahdollistaa padien siirtelemisen CryptDrivessa niin, etteivät niiden linkit muutu, eivätkä padien osallistujat siten koskaan menetä käyttöoikeuttaan niihin.", + "whatis_drive_p3": "Voit myös ladata CryptDriveesi tiedostoja ja jakaa niitä kollegoidesi kanssa. Ladattuja tiedostoja voidaan järjestellä samaan tapaan kuin kollaboratiivisia padeja.", + "whatis_business": "CryptPad yrityksille", + "admin_activeSessionsTitle": "Aktiiviset yhteydet", + "admin_activeSessionsHint": "Aktiivisten WebSocket-yhteyksien määrä (ja yhdistetyt uniikit IP-osoitteet)", + "admin_activePadsTitle": "Aktiiviset padit", + "admin_activePadsHint": "Tällä hetkellä katseltavien tai muokattavien uniikkien dokumenttien määrä", + "admin_registeredTitle": "Rekisteröityneet käyttäjät", + "admin_registeredHint": "CryptPad-instanssiisi rekisteröityneiden käyttäjien määrä", + "admin_updateLimitTitle": "Päivitä käyttäjien tallennuskiintiöt", + "admin_updateLimitHint": "Käyttäjien tallennuskiintiöiden pakotettu päivitys voidaan tehdä milloin tahansa, mutta se on tarpeen ainoastaan virhetilanteissa", + "admin_updateLimitButton": "Päivitä tallennuskiintiöt", + "admin_updateLimitDone": "Päivitys onnistui", + "admin_flushCacheTitle": "Tyhjennä HTTP-välimuisti", + "notifications_cat_friends": "Kaveripyynnöt", + "notifications_cat_pads": "Kanssani jaetut", + "notifications_cat_archived": "Historia", + "notifications_dismissAll": "Hylkää kaikki", + "support_notification": "Ylläpitäjä on vastannut tukipyyntöösi", + "requestEdit_button": "Pyydä muokkausoikeutta", + "requestEdit_dialog": "Haluatko varmasti pyytää padin omistajalta muokkausoikeutta?", + "requestEdit_confirm": "{1} on pyytänyt oikeutta muokata padia {0}. Haluatko myöntää muokkausoikeuden?", + "requestEdit_fromFriend": "Olet kaveri käyttäjän {0} kanssa", + "requestEdit_fromStranger": "Et ole käyttäjän {0} kaveri", + "requestEdit_viewPad": "Avaa padi uudessa välilehdessä", + "later": "Päätä myöhemmin", + "requestEdit_request": "{1} haluaa muokata padia {0}", + "requestEdit_accepted": "{1} on myöntänyt sinulle muokkausoikeuden padiin {0}", + "requestEdit_sent": "Kaveripyyntö lähetetty", + "properties_unknownUser": "{0} tuntematon(ta) käyttäjä(ä)", + "pricing": "Hinnoittelu", + "homePage": "Kotisivu", + "features_noData": "Henkilötietoja ei tarvita", + "features_pricing": "{0}-{2}€/kk", + "features_emailRequired": "Sähköpostiosoite vaaditaan", + "owner_removeText": "Poista olemassaoleva omistaja", + "owner_removePendingText": "Peru odottava tarjous", + "owner_addText": "Tarjoa yhteisomistajuutta kaverille", + "owner_unknownUser": "Tuntematon käyttäjä", + "owner_removeButton": "Poista valitut omistajat", + "owner_removePendingButton": "Peru valitut tarjoukset", + "owner_addButton": "Tarjoa omistajuutta", + "owner_removeConfirm": "Haluatko varmasti poistaa omistajuuden valituilta käyttäjiltä? Tästä toimenpiteestä lähetetään heille ilmoitus.", + "owner_removeMeConfirm": "Olet luopumassa omistajuusoikeuksistasi. Tätä toimintoa ei voi perua. Oletko varma?", + "owner_addConfirm": "Yhteisomistajat voivat muokata sisältöä ja poistaa omistajuutesi. Oletko varma?", + "owner_openModalButton": "Hallinnoi omistajia", + "owner_add": "{0} haluaa sinut padin {1} omistajaksi. Hyväksytkö tämän?", + "owner_request": "{0} haluaa sinut {1} omistajaksi", + "owner_request_accepted": "{0} on hyväksynyt tarjouksesi {1} omistajuudesta", + "share_linkTeam": "Lisää tiimin CryptDriveen", + "team_pickFriends": "Valitse tiimiin kutsuttavat kaverit", + "team_inviteModalButton": "Kutsu", + "team_noFriend": "Sinulla ei ole vielä kavereita CryptPadissa.", + "drive_sfPassword": "Jaettu kansiosi {0} ei ole enää saatavilla. Se on joko poistettu omistajansa toimesta tai sille on asetettu uusi salasana. Voit poistaa tämän kansion CryptDrivestasi tai palauttaa käyttöoikeuden käyttämällä uutta salasanaa.", + "drive_sfPasswordError": "Väärä salasana", + "password_error_seed": "Padia ei löytynyt!
Tämä virhe voi johtua kahdesta syystä: joko padiin on lisätty tai vaihdettu salasana, tai padi on poistettu palvelimelta.", + "properties_confirmChangeFile": "Oletko varma? Käyttäjät, joilla ei ole uutta salasanaa eivät pääse enää käyttämään tiedostoa.", + "properties_confirmNewFile": "Oletko varma? Salasanan lisääminen muuttaa tämän tiedoston URL-osoitetta. Käyttäjät, joilla ei ole salasanaa eivät pääse enää käyttämään tiedostoa.", + "properties_passwordWarningFile": "Salasanan vaihto onnistui, mutta emme onnistuneet päivittämään uusia tietoja CryptDriveesi. Tiedoston vanha versio täytyy ehkä poistaa manuaalisesti.", + "properties_passwordSuccessFile": "Salasanan vaihto onnistui.", + "driveOfflineError": "Yhteytesi CryptPadiin on katkennut. Tähän padiin tehdyt muutokset eivät tallennu CryptDriveesi. Ole hyvä ja sulje kaikki CryptPad-välilehdet, ja yritä uudelleen uudessa selainikkunassa. ", + "teams_table": "Roolit", + "teams_table_generic": "Roolit ja käyttöoikeudet", + "teams_table_generic_view": "Tarkastele: käytä kansioita ja padeja vain luku-tilassa.", + "teams_table_generic_edit": "Muokkaa: luo, muokkaa ja poista kansioita ja padeja.", + "teams_table_generic_admin": "Hallitse jäseniä: kutsu ja poista jäseniä, vaihda jäsenten rooleja Ylläpitäjä-rooliin asti.", + "teams_table_generic_own": "Hallitse tiimiä: vaihda tiimin nimeä ja avatar-kuvaa, lisää tai poista Omistajia, muuta tiimin tilausta, poista tiimi.", + "teams_table_specific": "Poikkeukset", + "teams_table_specificHint": "Nämä ovat vanhempia jaettuja kansioita, joissa katselijoilla on edelleen oikeus muokata olemassaolevia padeja. Näihin kansioihin luodut tai kopioidut padit saavat oletuskäyttöoikeudet.", + "teams_table_admins": "Hallitse jäseniä", + "teams_table_owners": "Hallitse tiimiä", + "teams_table_role": "Rooli", + "pad_wordCount": "Sanamäärä: {0}", + "share_linkWarning": "Tämä linkki sisältää dokumenttisi avaimet. Linkin vastaanottajat saavat dokumenttiisi käyttöoikeudet, joita ei voi poistaa jälkikäteen.", + "share_linkPasswordAlert": "Tämä elementti on salasanasuojattu. Kun lähetät linkin, täytyy vastaanottajan syöttää salasana.", + "share_contactPasswordAlert": "Tämä elementti on salasanasuojattu. Koska jaat sen CryptPad-yhteyshenkilön kanssa, ei vastaanottajan tarvitse syöttää salasanaa.", + "share_embedPasswordAlert": "Tämä elementti on salasanasuojattu. Kun upotat tämän padin, katselijoita pyydetään syöttämään salasana.", + "passwordFaqLink": "Lue lisää salasanoista", + "share_noContactsLoggedIn": "Sinulla ei ole vielä yhtään CryptPad-yhteyshenkilöä. Jaa linkki käyttäjäprofiiliisi, jotta muut käyttäjät voivat lähettää sinulle yhteyspyyntöjä.", + "share_copyProfileLink": "Kopioi käyttäjäprofiilin linkki", + "share_noContactsNotLoggedIn": "Kirjaudu sisään tai rekisteröidy nähdäksesi olemassaolevat yhteystietosi ja lisätäksesi uusia yhteystietoja.", + "contacts_mute": "Mykistä", + "contacts_unmute": "Poista mykistys", + "contacts_manageMuted": "Hallinnoi mykistyksiä", + "contacts_mutedUsers": "Mykistetyt käyttäjätilit", + "contacts_muteInfo": "Mykistetyt käyttäjät eivät voi lähettää sinulle viestejä tai ilmoituksia.
Mykistetyt käyttäjät eivät saa tietää, että olet mykistänyt heidät. ", + "team_inviteLinkTitle": "Luo yksilöity kutsu tähän tiimiin", + "team_inviteLinkTempName": "Väliaikainen nimi (näkyvissä Odottavat kutsut-listassa)", + "team_inviteLinkSetPassword": "Suojaa linkki salasanalla (suositeltavaa)", + "team_inviteLinkNote": "Lisää henkilökohtainen viesti", + "team_inviteLinkNoteMsg": "Tämä viesti näytetään ennen vastaanottajan päätöstä tiimiin liittymisestä.", + "team_inviteLinkLoading": "Luodaan linkkiä", + "team_inviteLinkWarning": "Ensimmäinen tämän linkin avaava henkilö voi liittyä tiimiin ja katsella sen sisältöä. Ole varovainen, kun jaat sen.", + "team_inviteLinkErrorName": "Ole hyvä ja lisää kutsuttavalle henkilölle nimi. Hän voi muuttaa sitä myöhemmin. ", + "team_inviteLinkCreate": "Luo linkki", + "team_inviteLinkCopy": "Kopioi linkki", + "team_inviteFrom": "Lähettäjä:", + "team_inviteFromMsg": "{0} on kutsunut sinut liittymään tiimiin {1}", + "team_invitePleaseLogin": "Ole hyvä ja kirjaudu sisään tai rekisteröidy hyväksyäksesi kutsun.", + "team_inviteEnterPassword": "Ole hyvä ja syötä kutsun salasana jatkaaksesi.", + "team_invitePasswordLoading": "Puretaan kutsua", + "team_inviteJoin": "Liity tiimiin", + "team_inviteTitle": "Kutsu tiimiin", + "team_inviteGetData": "Haetaan tiimitietoja", + "team_cat_link": "Kutsulinkki", + "team_links": "Kutsulinkit", + "team_inviteInvalidLinkError": "Tämä kutsulinkki ei ole kelvollinen." } From a73a1ec1164c319e98ed07ff11efb8289c53fa58 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 8 Jan 2020 16:48:18 -0500 Subject: [PATCH 31/55] update changelog to note Finnish translations --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5561e1af..b249e10c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ Once you have reviewed your configuration files and ensured that they are correc * We made some minor improvements to the process of redeeming invitation links for teams. * invitation links can only be used once, so we remove the hash from the URL bar once you've landed on the redemption page so that reloading after redeeming doesn't indicate that you've used an expired link. +* [One of our Finnish-speaking contributors](https://weblate.cryptpad.fr/user/ilo/) has translated a very large amount of the platform's text in the last few weeks, making Finnish our fifth most thoroughly translated language! ## Bug fixes From 8ccafbc821c0a88d53f2cb6a3cb14c5791920975 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 9 Jan 2020 10:59:20 +0100 Subject: [PATCH 32/55] Fix tests for invite url --- www/assert/main.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/www/assert/main.js b/www/assert/main.js index 6ad05a7a2..cdde441f0 100644 --- a/www/assert/main.js +++ b/www/assert/main.js @@ -255,11 +255,11 @@ define([ }, "test support for trailing slashes in version 1 hash failed to parse"); assert(function (cb) { - var secret = Hash.parsePadUrl('/invite/#/1/ilrOtygzDVoUSRpOOJrUuQ/e8jvf36S3chzkkcaMrLSW7PPrz7VDp85lIFNI26dTmr=/'); + var secret = Hash.parsePadUrl('/invite/#/2/invite/edit/oRE0oLCtEXusRDyin7GyLGcS/p/'); var hd = secret.hashData; - cb(hd.channel === "ilrOtygzDVoUSRpOOJrUuQ" && - hd.pubkey === "e8jvf36S3chzkkcaMrLSW7PPrz7VDp85lIFNI26dTmr=" && - hd.type === 'invite'); + cb(hd.key === "oRE0oLCtEXusRDyin7GyLGcS" && + hd.password && + hd.app === 'invite'); }, "test support for invite urls"); // test support for V2 From d4d07f33326b1a63ee0f648eca2f62a1b5cda498 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 9 Jan 2020 11:20:35 +0100 Subject: [PATCH 33/55] Add support for ownerKey in the hash (version 1 and 2) --- www/assert/main.js | 26 ++++++++++++++++++++++++++ www/common/common-hash.js | 16 ++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/www/assert/main.js b/www/assert/main.js index cdde441f0..ceff9f58b 100644 --- a/www/assert/main.js +++ b/www/assert/main.js @@ -254,6 +254,32 @@ define([ !secret.hashData.present); }, "test support for trailing slashes in version 1 hash failed to parse"); + // test support for ownerKey + assert(function (cb) { + var secret = Hash.parsePadUrl('/pad/#/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI/present/uPmJDtDJ9okhdIyQ-8zphYlpaAonJDOC6MAcYY6iBwWBQr+XmrQ9uGY9WkApJTfEfAu5QcqaDCw1Ul+JXKcYkA/embed'); + return cb(secret.hashData.version === 1 && + secret.hashData.mode === "edit" && + secret.hashData.channel === "3Ujt4F2Sjnjbis6CoYWpoQ" && + secret.hashData.key === "usn4+9CqVja8Q7RZOGTfRgqI" && + secret.hashData.ownerKey === "uPmJDtDJ9okhdIyQ-8zphYlpaAonJDOC6MAcYY6iBwWBQr+XmrQ9uGY9WkApJTfEfAu5QcqaDCw1Ul+JXKcYkA" && + secret.hashData.embed && + secret.hashData.present); + }, "test support for owner key in version 1 hash failed to parse"); + assert(function (cb) { + var parsed = Hash.parsePadUrl('/pad/#/2/pad/edit/oRE0oLCtEXusRDyin7GyLGcS/p/uPmJDtDJ9okhdIyQ-8zphYlpaAonJDOC6MAcYY6iBwWBQr+XmrQ9uGY9WkApJTfEfAu5QcqaDCw1Ul+JXKcYkA/embed'); + var secret = Hash.getSecrets('pad', parsed.hash); + return cb(parsed.hashData.version === 2 && + parsed.hashData.mode === "edit" && + parsed.hashData.type === "pad" && + parsed.hashData.key === "oRE0oLCtEXusRDyin7GyLGcS" && + secret.channel === "d8d51b4aea863f3f050f47f8ad261753" && + window.nacl.util.encodeBase64(secret.keys.cryptKey) === "0Ts1M6VVEozErV2Nx/LTv6Im5SCD7io2LlhasyyBPQo=" && + secret.keys.validateKey === "f5A1FM9Gp55tnOcM75RyHD1oxBG9ZPh9WDA7qe2Fvps=" && + parsed.hashData.ownerKey === "uPmJDtDJ9okhdIyQ-8zphYlpaAonJDOC6MAcYY6iBwWBQr+XmrQ9uGY9WkApJTfEfAu5QcqaDCw1Ul+JXKcYkA" && + parsed.hashData.embed && + parsed.hashData.password); + }, "test support for owner key in version 2 hash failed to parse"); + assert(function (cb) { var secret = Hash.parsePadUrl('/invite/#/2/invite/edit/oRE0oLCtEXusRDyin7GyLGcS/p/'); var hd = secret.hashData; diff --git a/www/common/common-hash.js b/www/common/common-hash.js index 4ed1193e3..c26a097e4 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -158,9 +158,17 @@ Version 1 options = hashArr.slice(5); parsed.present = options.indexOf('present') !== -1; parsed.embed = options.indexOf('embed') !== -1; + // Check if we have an ownerKey for this pad + hashArr.some(function (data) { + if (data.length === 86) { // XXX 88 characters - 2 trailing "="... + parsed.ownerKey = data; + return true; + } + }); parsed.getHash = function (opts) { var hash = hashArr.slice(0, 5).join('/') + '/'; + if (parsed.ownerKey) { hash += parsed.ownerKey + '/'; } if (opts.embed) { hash += 'embed/'; } if (opts.present) { hash += 'present/'; } return hash; @@ -177,9 +185,17 @@ Version 1 parsed.password = options.indexOf('p') !== -1; parsed.present = options.indexOf('present') !== -1; parsed.embed = options.indexOf('embed') !== -1; + // Check if we have a ownerKey for this pad + hashArr.some(function (data) { + if (data.length === 86) { // XXX 88 characters - 2 trailing "="... + parsed.ownerKey = data; + return true; + } + }); parsed.getHash = function (opts) { var hash = hashArr.slice(0, 5).join('/') + '/'; + if (parsed.ownerKey) { hash += parsed.ownerKey + '/'; } if (parsed.password) { hash += 'p/'; } if (opts.embed) { hash += 'embed/'; } if (opts.present) { hash += 'present/'; } From 14905a5693e7c69f488fa5debec7ddb955da129c Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 9 Jan 2020 11:51:28 +0100 Subject: [PATCH 34/55] Support ownerKey in file hash --- www/common/common-hash.js | 51 +++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/www/common/common-hash.js b/www/common/common-hash.js index c26a097e4..8f6471f1b 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -15,6 +15,19 @@ var factory = function (Util, Crypto, Nacl) { .decodeUTF8(JSON.stringify(list)))); }; + // XXX move this code? + Hash.generateSignPair = function (safe) { + var ed = Nacl.sign.keyPair(); + var makeSafe = function (key) { + if (!safe) { return key; } + return Crypto.b64RemoveSlashes(key).replace(/=+$/g, ''); + }; + return { + validateKey: makeSafe(encode64(ed.publicKey)), + signKey: makeSafe(encode64(ed.secretKey)), + }; + }; + var getEditHashFromKeys = Hash.getEditHashFromKeys = function (secret) { var version = secret.version; var data = secret.keys; @@ -134,6 +147,17 @@ Version 1 /code/#/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI */ + var getOwnerKey = function (hashArr) { + var k; + // Check if we have a ownerKey for this pad + hashArr.some(function (data) { + if (data.length === 86) { // XXX 88 characters - 2 trailing "="... + k = data; + return true; + } + }); + return k; + }; var parseTypeHash = Hash.parseTypeHash = function (type, hash) { if (!hash) { return; } var options; @@ -158,17 +182,12 @@ Version 1 options = hashArr.slice(5); parsed.present = options.indexOf('present') !== -1; parsed.embed = options.indexOf('embed') !== -1; - // Check if we have an ownerKey for this pad - hashArr.some(function (data) { - if (data.length === 86) { // XXX 88 characters - 2 trailing "="... - parsed.ownerKey = data; - return true; - } - }); + parsed.ownerKey = getOwnerKey(options); parsed.getHash = function (opts) { var hash = hashArr.slice(0, 5).join('/') + '/'; - if (parsed.ownerKey) { hash += parsed.ownerKey + '/'; } + var owner = typeof(opts.ownerKey) !== "undefined" ? opts.ownerKey : parsed.ownerKey; + if (owner) { hash += owner + '/'; } if (opts.embed) { hash += 'embed/'; } if (opts.present) { hash += 'present/'; } return hash; @@ -185,17 +204,12 @@ Version 1 parsed.password = options.indexOf('p') !== -1; parsed.present = options.indexOf('present') !== -1; parsed.embed = options.indexOf('embed') !== -1; - // Check if we have a ownerKey for this pad - hashArr.some(function (data) { - if (data.length === 86) { // XXX 88 characters - 2 trailing "="... - parsed.ownerKey = data; - return true; - } - }); + parsed.ownerKey = getOwnerKey(options); parsed.getHash = function (opts) { var hash = hashArr.slice(0, 5).join('/') + '/'; - if (parsed.ownerKey) { hash += parsed.ownerKey + '/'; } + var owner = typeof(opts.ownerKey) !== "undefined" ? opts.ownerKey : parsed.ownerKey; + if (owner) { hash += owner + '/'; } if (parsed.password) { hash += 'p/'; } if (opts.embed) { hash += 'embed/'; } if (opts.present) { hash += 'present/'; } @@ -212,6 +226,8 @@ Version 1 parsed.version = 1; parsed.channel = hashArr[2].replace(/-/g, '/'); parsed.key = hashArr[3].replace(/-/g, '/'); + options = hashArr.slice(4); + parsed.ownerKey = getOwnerKey(options); return parsed; } if (hashArr[1] && hashArr[1] === '2') { // Version 2 @@ -223,9 +239,12 @@ Version 1 parsed.password = options.indexOf('p') !== -1; parsed.present = options.indexOf('present') !== -1; parsed.embed = options.indexOf('embed') !== -1; + parsed.ownerKey = getOwnerKey(options); parsed.getHash = function (opts) { var hash = hashArr.slice(0, 4).join('/') + '/'; + var owner = typeof(opts.ownerKey) !== "undefined" ? opts.ownerKey : parsed.ownerKey; + if (owner) { hash += owner + '/'; } if (parsed.password) { hash += 'p/'; } if (opts.embed) { hash += 'embed/'; } if (opts.present) { hash += 'present/'; } From 96a00f89dfa3abe9cc5580de933a34c470d7ae43 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 9 Jan 2020 15:16:07 +0100 Subject: [PATCH 35/55] Generate burn after reading link for pads --- www/assert/main.js | 8 + www/common/common-hash.js | 9 +- www/common/common-interface.js | 6 + www/common/common-ui-elements.js | 264 +++++++++++++++++++++--------- www/common/outer/invitation.js | 1 + www/common/sframe-common-outer.js | 23 ++- 6 files changed, 219 insertions(+), 92 deletions(-) diff --git a/www/assert/main.js b/www/assert/main.js index ceff9f58b..c29b3bfa3 100644 --- a/www/assert/main.js +++ b/www/assert/main.js @@ -279,6 +279,14 @@ define([ parsed.hashData.embed && parsed.hashData.password); }, "test support for owner key in version 2 hash failed to parse"); + assert(function (cb) { + var secret = Hash.parsePadUrl('/file/#/1/TRplGM-WsVkXR+LkJ0tD3D45A1YFZ-Cy/eO4RJwh8yHEEDhl1aHfuwQ2IzosPBZx-HDaWc1lW+hY=/uPmJDtDJ9okhdIyQ-8zphYlpaAonJDOC6MAcYY6iBwWBQr+XmrQ9uGY9WkApJTfEfAu5QcqaDCw1Ul+JXKcYkA/'); + return cb(secret.hashData.version === 1 && + secret.hashData.channel === "TRplGM/WsVkXR+LkJ0tD3D45A1YFZ/Cy" && + secret.hashData.key === "eO4RJwh8yHEEDhl1aHfuwQ2IzosPBZx/HDaWc1lW+hY=" && + secret.hashData.ownerKey === "uPmJDtDJ9okhdIyQ-8zphYlpaAonJDOC6MAcYY6iBwWBQr+XmrQ9uGY9WkApJTfEfAu5QcqaDCw1Ul+JXKcYkA" && + !secret.hashData.present); + }, "test support for owner key in version 1 file hash failed to parse"); assert(function (cb) { var secret = Hash.parsePadUrl('/invite/#/2/invite/edit/oRE0oLCtEXusRDyin7GyLGcS/p/'); diff --git a/www/common/common-hash.js b/www/common/common-hash.js index 8f6471f1b..902a91793 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -16,15 +16,16 @@ var factory = function (Util, Crypto, Nacl) { }; // XXX move this code? - Hash.generateSignPair = function (safe) { + Hash.generateSignPair = function () { var ed = Nacl.sign.keyPair(); var makeSafe = function (key) { - if (!safe) { return key; } return Crypto.b64RemoveSlashes(key).replace(/=+$/g, ''); }; return { - validateKey: makeSafe(encode64(ed.publicKey)), - signKey: makeSafe(encode64(ed.secretKey)), + validateKey: Hash.encodeBase64(ed.publicKey), + signKey: Hash.encodeBase64(ed.secretKey), + safeValidateKey: makeSafe(Hash.encodeBase64(ed.publicKey)), + safeSignKey: makeSafe(Hash.encodeBase64(ed.secretKey)), }; }; diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 4528b7a1b..55d7e6939 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -209,10 +209,16 @@ define([ $(title).prepend(' ').prepend(icon); } $(title).click(function () { + var old = tabs[active]; + if (old.onHide) { old.onHide(); } titles.forEach(function (t) { $(t).removeClass('alertify-tabs-active'); }); contents.forEach(function (c) { $(c).removeClass('alertify-tabs-content-active'); }); + if (tab.onShow) { + tab.onShow(); + } $(title).addClass('alertify-tabs-active'); $(content).addClass('alertify-tabs-content-active'); + active = i; }); titles.push(title); contents.push(content); diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 6323be780..d9f0af36c 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -917,60 +917,79 @@ define([ className: 'primary cp-share-with-friends', name: Messages.share_withFriends, onClick: function () { - var href = Hash.getRelativeHref(linkGetter()); - var $friends = $div.find('.cp-usergrid-user.cp-selected'); - $friends.each(function (i, el) { - var curve = $(el).attr('data-curve'); - // Check if the selected element is a friend or a team - if (curve) { // Friend - if (!curve || !friends[curve]) { return; } - var friend = friends[curve]; - if (!friend.notifications || !friend.curvePublic) { return; } - common.mailbox.sendTo("SHARE_PAD", { + var href; + NThen(function (waitFor) { + var w = waitFor(); + // linkGetter can be async if this is a burn after reading URL + var res = linkGetter({}, function (url) { + if (!url) { + waitFor.abort(); + return; + } + console.warn('BAR'); + href = url; + setTimeout(w); + }); + if (res && /^http/.test(res)) { + href = Hash.getRelativeHref(res); + setTimeout(w); + return; + } + }).nThen(function () { + var $friends = $div.find('.cp-usergrid-user.cp-selected'); + $friends.each(function (i, el) { + var curve = $(el).attr('data-curve'); + // Check if the selected element is a friend or a team + if (curve) { // Friend + if (!curve || !friends[curve]) { return; } + var friend = friends[curve]; + if (!friend.notifications || !friend.curvePublic) { return; } + common.mailbox.sendTo("SHARE_PAD", { + href: href, + password: config.password, + isTemplate: config.isTemplate, + name: myName, + title: title + }, { + channel: friend.notifications, + curvePublic: friend.curvePublic + }); + return; + } + // Team + var ed = $(el).attr('data-ed'); + var team = teams[ed]; + if (!team) { return; } + sframeChan.query('Q_STORE_IN_TEAM', { href: href, password: config.password, - isTemplate: config.isTemplate, - name: myName, - title: title - }, { - channel: friend.notifications, - curvePublic: friend.curvePublic + path: config.isTemplate ? ['template'] : undefined, + title: title, + teamId: team.id + }, function (err) { + if (err) { return void console.error(err); } }); - return; - } - // Team - var ed = $(el).attr('data-ed'); - var team = teams[ed]; - if (!team) { return; } - sframeChan.query('Q_STORE_IN_TEAM', { - href: href, - password: config.password, - path: config.isTemplate ? ['template'] : undefined, - title: title, - teamId: team.id - }, function (err) { - if (err) { return void console.error(err); } }); - }); - UI.findCancelButton().click(); - - // Update the "recently shared with" array: - // Get the selected curves - var curves = $friends.toArray().map(function (el) { - return ($(el).attr('data-curve') || '').slice(0,8); - }).filter(function (x) { return x; }); - // Prepend them to the "order" array - Array.prototype.unshift.apply(order, curves); - order = Util.deduplicateString(order); - // Make sure we don't have "old" friends and save - order = order.filter(function (curve) { - return smallCurves.indexOf(curve) !== -1; + UI.findCancelButton().click(); + + // Update the "recently shared with" array: + // Get the selected curves + var curves = $friends.toArray().map(function (el) { + return ($(el).attr('data-curve') || '').slice(0,8); + }).filter(function (x) { return x; }); + // Prepend them to the "order" array + Array.prototype.unshift.apply(order, curves); + order = Util.deduplicateString(order); + // Make sure we don't have "old" friends and save + order = order.filter(function (curve) { + return smallCurves.indexOf(curve) !== -1; + }); + common.setAttribute(['general', 'share-friends'], order); + if (onShare) { + onShare.fire(); + } }); - common.setAttribute(['general', 'share-friends'], order); - if (onShare) { - onShare.fire(); - } }, keys: [13] }; @@ -1049,6 +1068,29 @@ define([ } }; + var makeBurnAfterReadingUrl = function (common, href, channel, cb) { + var keyPair = Hash.generateSignPair(); + var parsed = Hash.parsePadUrl(href); + console.error(href, parsed); + var newHref = parsed.getUrl({ + ownerKey: keyPair.safeSignKey + }); + var sframeChan = common.getSframeChannel(); + NThen(function (waitFor) { + sframeChan.query('Q_SET_PAD_METADATA', { + channel: channel, + command: 'ADD_OWNERS', + value: [keyPair.validateKey] + }, waitFor(function (err) { + if (err) { + waitFor.abort(); + UI.warn(Messages.error); + } + })); + }).nThen(function () { + cb(newHref); + }); + }; UIElements.createShareModal = function (config) { var origin = config.origin; var pathname = config.pathname; @@ -1078,6 +1120,7 @@ define([ var parsed = Hash.parsePadUrl(pathname); var canPresent = ['code', 'slide'].indexOf(parsed.type) !== -1; + var burnAfterReading; var rights = h('div.msg.cp-inline-radio-group', [ h('label', Messages.share_linkAccess), h('div.radio-group',[ @@ -1086,9 +1129,33 @@ define([ canPresent ? UI.createRadio('accessRights', 'cp-share-present', Messages.share_linkPresent, false, { mark: {tabindex:1} }) : undefined, UI.createRadio('accessRights', 'cp-share-editable-true', - Messages.share_linkEdit, false, { mark: {tabindex:1} })]) + Messages.share_linkEdit, false, { mark: {tabindex:1} })]), + burnAfterReading = hashes.viewHash ? UI.createRadio('accessRights', 'cp-share-bar', + 'BAR', false, { mark: {tabindex:1}, label: {style: "display: none;"} }) : undefined // XXX ]); + // Burn after reading + // Check if we are an owner of this pad. If we are, we can show the burn after reading option. + // When BAR is selected, display a red message indicating the consequence and add + // the options to generate the BAR url + var barAlert = h('div.alert.alert-danger.cp-alertify-bar-selected', { + style: 'display: none;' + }, " You have set this pad to self-destruct. Once a recipient opens this pad, it will be permanently deleted from the server. "); // XXX + var channel = Hash.getSecrets('pad', hash, config.password).channel; + common.getPadMetadata({ + channel: channel + }, function (obj) { + if (!obj || obj.error) { return; } + var priv = common.getMetadataMgr().getPrivateData(); + // Not an owner: don't display the burn after reading option + if (!Array.isArray(obj.owners) || obj.owners.indexOf(priv.edPublic) === -1) { + $(burnAfterReading).remove(); + return; + } + // When the burn after reading option is selected, transform the modal buttons + $(burnAfterReading).show(); + }); + var $rights = $(rights); var saveValue = function () { @@ -1100,13 +1167,25 @@ define([ }); }; - var getLinkValue = function (initValue) { + var burnAfterReadingUrl; + + var getLinkValue = function (initValue, cb) { var val = initValue || {}; var edit = val.edit !== undefined ? val.edit : Util.isChecked($rights.find('#cp-share-editable-true')); var embed = val.embed; var present = val.present !== undefined ? val.present : Util.isChecked($rights.find('#cp-share-present')); + var burnAfterReading = Util.isChecked($rights.find('#cp-share-bar')); + if (burnAfterReading && !burnAfterReadingUrl) { + if (cb) { // Called from the contacts tab, "share" button + var barHref = origin + pathname + '#' + (hashes.viewHash || hashes.editHash); + return makeBurnAfterReadingUrl(common, barHref, channel, function (url) { + cb(url); + }); + } + return 'XXX Click on the button below to generate a link'; // XXX + } var hash = (!hashes.viewHash || (edit && hashes.editHash)) ? hashes.editHash : hashes.viewHash; - var href = origin + pathname + '#' + hash; + var href = burnAfterReading ? burnAfterReadingUrl : (origin + pathname + '#' + hash); var parsed = Hash.parsePadUrl(href); return origin + parsed.getUrl({embed: embed, present: present}); }; @@ -1160,8 +1239,8 @@ define([ }); }); - - + + linkContent.push($(barAlert).clone()[0]); // Burn after reading var link = h('div.cp-share-modal', linkContent); var $link = $(link); @@ -1169,7 +1248,7 @@ define([ var linkButtons = [ makeCancelButton(), !config.sharedFolder && { - className: 'secondary', + className: 'secondary cp-nobar', name: Messages.share_linkOpen, onClick: function () { saveValue(); @@ -1180,9 +1259,8 @@ define([ return true; }, keys: [[13, 'ctrl']] - }, - { - className: 'primary', + }, { + className: 'primary cp-nobar', name: Messages.share_linkCopy, onClick: function () { saveValue(); @@ -1193,26 +1271,26 @@ define([ if (success) { UI.log(Messages.shareSuccess); } }, keys: [13] + }, { + className: 'primary cp-bar', + name: 'GENERATE LINK', + onClick: function () { + var barHref = origin + pathname + '#' + (hashes.viewHash || hashes.editHash); + makeBurnAfterReadingUrl(common, barHref, channel, function (url) { + burnAfterReadingUrl = url; + $rights.find('input[type="radio"]').trigger('change'); + }); + return true; + }, + keys: [] } ]; - // update values for link preview when radio btns change - $link.find('#cp-share-link-preview').val(getLinkValue()); - $rights.find('input[type="radio"]').on('change', function () { - $link.find('#cp-share-link-preview').val(getLinkValue({ - embed: Util.isChecked($link.find('#cp-share-embed')) - })); - }); - $link.find('input[type="checkbox"]').on('change', function () { - $link.find('#cp-share-link-preview').val(getLinkValue({ - embed: Util.isChecked($link.find('#cp-share-embed')) - })); - }); - var frameLink = UI.dialog.customModal(link, { buttons: linkButtons, onClose: config.onClose, }); + $(frameLink).find('.cp-bar').hide(); // Share with contacts tab @@ -1240,6 +1318,7 @@ define([ ])); } + $(contactsContent).append($(barAlert).clone()); // Burn after reading var contactButtons = friendsObject.buttons; contactButtons.unshift(makeCancelButton()); @@ -1282,21 +1361,52 @@ define([ keys: [13] }]; + var onShowEmbed = function () { + $rights.find('#cp-share-bar').closest('label').hide(); + $rights.find('input[type="radio"]:enabled').first().prop('checked', 'checked'); + $rights.find('input[type="radio"]').trigger('change'); + }; + var embed = h('div.cp-share-modal', embedContent); var $embed = $(embed); - // update values for link preview when radio btns change + var frameEmbed = UI.dialog.customModal(embed, { + buttons: embedButtons, + onClose: config.onClose, + }); + + // update values for link and embed preview when radio btns change $embed.find('#cp-embed-link-preview').val(getEmbedValue()); + $link.find('#cp-share-link-preview').val(getLinkValue()); $rights.find('input[type="radio"]').on('change', function () { + $link.find('#cp-share-link-preview').val(getLinkValue({ + embed: Util.isChecked($link.find('#cp-share-embed')) + })); + // Hide or show the burn after reading alert + if (Util.isChecked($rights.find('#cp-share-bar'))) { + $('.cp-alertify-bar-selected').show(); + // Show burn after reading button + $('.alertify').find('.cp-bar').show(); + $('.alertify').find('.cp-nobar').hide(); + return; + } $embed.find('#cp-embed-link-preview').val(getEmbedValue()); + // Hide burn after reading button + $('.alertify').find('.cp-nobar').show(); + $('.alertify').find('.cp-bar').hide(); + $('.cp-alertify-bar-selected').hide(); }); - - var frameEmbed = UI.dialog.customModal(embed, { - buttons: embedButtons, - onClose: config.onClose, + $link.find('input[type="checkbox"]').on('change', function () { + $link.find('#cp-share-link-preview').val(getLinkValue({ + embed: Util.isChecked($link.find('#cp-share-embed')) + })); }); + // Create modal + var resetTab = function () { + $rights.find('label.cp-radio').show(); + }; var tabs = [{ title: Messages.share_contactCategory, icon: "fa fa-address-book", @@ -1310,7 +1420,9 @@ define([ }, { title: Messages.share_embedCategory, icon: "fa fa-code", - content: frameEmbed + content: frameEmbed, + onShow: onShowEmbed, + onHide: resetTab }]; if (typeof(AppConfig.customizeShareOptions) === 'function') { AppConfig.customizeShareOptions(hashes, tabs, { diff --git a/www/common/outer/invitation.js b/www/common/outer/invitation.js index 603cae3c1..d84d7a81f 100644 --- a/www/common/outer/invitation.js +++ b/www/common/outer/invitation.js @@ -17,6 +17,7 @@ var factory = function (Util, Cred, Nacl) { }; }; + // XXX move this function? Invite.generateSignPair = function () { var ed = Nacl.sign.keyPair(); return { diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 05cafbdec..9308f05cb 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -507,6 +507,17 @@ define([ } }); + sframeChan.on('Q_GET_PAD_METADATA', function (data, cb) { + if (!data || !data.channel) { + data = { + channel: secret.channel + }; + } + Cryptpad.getPadMetadata(data, cb); + }); + sframeChan.on('Q_SET_PAD_METADATA', function (data, cb) { + Cryptpad.setPadMetadata(data, cb); + }); }; addCommonRpc(sframeChan); @@ -1170,18 +1181,6 @@ define([ }); }); - sframeChan.on('Q_GET_PAD_METADATA', function (data, cb) { - if (!data || !data.channel) { - data = { - channel: secret.channel - }; - } - Cryptpad.getPadMetadata(data, cb); - }); - sframeChan.on('Q_SET_PAD_METADATA', function (data, cb) { - Cryptpad.setPadMetadata(data, cb); - }); - if (cfg.messaging) { Notifier.getPermission(); From 9ee9e4608716af91a655dcf02469484517aeb9a8 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 9 Jan 2020 17:30:15 +0100 Subject: [PATCH 36/55] Receiving a burn after reading URL --- customize.dist/src/less2/include/toolbar.less | 6 +++ www/code/inner.js | 1 + www/common/common-ui-elements.js | 25 +++++++++++ www/common/cryptpad-common.js | 4 ++ www/common/onlyoffice/inner.js | 1 + www/common/outer/async-store.js | 45 ++++++++++++++----- www/common/outer/store-rpc.js | 1 + www/common/sframe-common-outer.js | 15 +++++++ www/common/sframe-common.js | 8 ++++ www/kanban/inner.js | 2 + www/pad/inner.js | 1 + www/poll/inner.js | 1 + www/slide/inner.js | 1 + www/whiteboard/inner.js | 1 + 14 files changed, 100 insertions(+), 12 deletions(-) diff --git a/customize.dist/src/less2/include/toolbar.less b/customize.dist/src/less2/include/toolbar.less index 0df5823a0..1d1ca0c47 100644 --- a/customize.dist/src/less2/include/toolbar.less +++ b/customize.dist/src/less2/include/toolbar.less @@ -97,6 +97,12 @@ .ckeditor_fix(); + .cp-burn-after-reading { + text-align: center; + font-size: @colortheme_app-font-size !important; + margin: 0 !important; + } + .cp-markdown-toolbar { height: @toolbar_line-height; background-color: @toolbar-bg-color-l20; diff --git a/www/code/inner.js b/www/code/inner.js index e2ed086de..e933bd4bd 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -98,6 +98,7 @@ define([ }; var mkHelpMenu = function (framework) { var $codeMirrorContainer = $('#cp-app-code-container'); + $codeMirrorContainer.prepend(framework._.sfCommon.getBurnAfterReadingWarning()); var helpMenu = framework._.sfCommon.createHelpMenu(['text', 'code']); $codeMirrorContainer.prepend(helpMenu.menu); diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index d9f0af36c..1964ed570 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -3978,6 +3978,7 @@ define([ UIElements.onServerError = function (common, err, toolbar, cb) { if (["EDELETED", "EEXPIRED"].indexOf(err.type) === -1) { return; } + var priv = common.getMetadataMgr().getPrivateData(); var msg = err.type; if (err.type === 'EEXPIRED') { msg = Messages.expiredError; @@ -3985,6 +3986,7 @@ define([ msg += Messages.errorCopy; } } else if (err.type === 'EDELETED') { + if (priv.burnAfterReading) { return void cb(); } msg = Messages.deletedError; if (err.loaded) { msg += Messages.errorCopy; @@ -4034,6 +4036,26 @@ define([ $password.find('.cp-password-input').focus(); }; + UIElements.displayBurnAfterReadingPage = function (common, cb) { + var info = h('p.cp-password-info', 'XXX Burn after reading'); // XXX + var button = h('button', 'Proceed'); // XXX + + $(button).on('click', function () { + cb(); + }); + + var block = h('div#cp-loading-burn-after-reading', [ + info, + button + ]); + UI.errorLoadingScreen(block); + }; + UIElements.getBurnAfterReadingWarning = function (common) { + var priv = common.getMetadataMgr().getPrivateData(); + if (!priv.burnAfterReading) { return; } + return h('div.alert.alert-danger.cp-burn-after-reading', 'Pewpewpew'); // XXX + }; + var crowdfundingState = false; UIElements.displayCrowdfunding = function (common) { if (crowdfundingState) { return; } @@ -4091,6 +4113,9 @@ define([ if (data && data.stored) { return; } // We won't display the popup for dropped files var priv = common.getMetadataMgr().getPrivateData(); + // This pad will be deleted automatically, it shouldn't be stored + if (priv.burnAfterReading) { return; } + var typeMsg = priv.pathname.indexOf('/file/') !== -1 ? Messages.autostore_file : priv.pathname.indexOf('/drive/') !== -1 ? Messages.autostore_sf : Messages.autostore_pad; diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 57815b3ef..a0057c59a 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -847,6 +847,10 @@ define([ postMessage('GET_PAD_METADATA', data, cb); }; + common.burnPad = function (data) { + postMessage('BURN_PAD', data); + }; + common.changePadPassword = function (Crypt, Crypto, data, cb) { var href = data.href; var newPassword = data.password; diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index 0958b024d..a40ccccae 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -926,6 +926,7 @@ define([ $rightside.append($forget); var helpMenu = common.createHelpMenu(['beta', 'oo']); + $('#cp-app-oo-editor').prepend(common.getBurnAfterReadingWarning()); $('#cp-app-oo-editor').prepend(helpMenu.menu); toolbar.$drawer.append(helpMenu.button); diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 870a539e7..1d1f26ba3 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -9,6 +9,7 @@ define([ '/common/common-feedback.js', '/common/common-realtime.js', '/common/common-messaging.js', + '/common/pinpad.js', '/common/outer/sharedfolder.js', '/common/outer/cursor.js', '/common/outer/onlyoffice.js', @@ -26,7 +27,7 @@ define([ '/bower_components/nthen/index.js', '/bower_components/saferphore/index.js', ], function (Sortify, UserObject, ProxyManager, Migrate, Hash, Util, Constants, Feedback, - Realtime, Messaging, + Realtime, Messaging, Pinpad, SF, Cursor, OnlyOffice, Mailbox, Profile, Team, Messenger, NetConfig, AppConfig, Crypto, ChainPad, CpNetflux, Listmap, nThen, Saferphore) { @@ -409,19 +410,17 @@ define([ var initRpc = function (clientId, data, cb) { if (!store.loggedIn) { return cb(); } if (store.rpc) { return void cb(account); } - require(['/common/pinpad.js'], function (Pinpad) { - Pinpad.create(store.network, store.proxy, function (e, call) { - if (e) { return void cb({error: e}); } + Pinpad.create(store.network, store.proxy, function (e, call) { + if (e) { return void cb({error: e}); } - store.rpc = call; + store.rpc = call; - Store.getPinLimit(null, null, function (obj) { - if (obj.error) { console.error(obj.error); } - account.limit = obj.limit; - account.plan = obj.plan; - account.note = obj.note; - cb(obj); - }); + Store.getPinLimit(null, null, function (obj) { + if (obj.error) { console.error(obj.error); } + account.limit = obj.limit; + account.plan = obj.plan; + account.note = obj.note; + cb(obj); }); }); }; @@ -1653,6 +1652,28 @@ define([ cb(); }; + // Delete a pad received with a burn after reading URL + Store.burnPad = function (clientId, data, cb) { + var channel = data.channel; + var ownerKey = Crypto.b64AddSlashes(data.ownerKey || ''); + if (!channel || !ownerKey) { return void console.error("Can't delete BAR pad"); } + try { + var signKey = Hash.decodeBase64(ownerKey); + var pair = Crypto.Nacl.sign.keyPair.fromSecretKey(signKey); + Pinpad.create(store.network, { + edPublic: Hash.encodeBase64(pair.publicKey), + edPrivate: Hash.encodeBase64(pair.secretKey) + }, function (e, rpc) { + if (e) { return void console.error(e); } + rpc.removeOwnedChannel(channel, function (err) { + if (err) { console.error(err); } + }); + }); + } catch (e) { + console.error(e); + } + }; + // Fetch the latest version of the metadata on the server and return it. // If the pad is stored in our drive, update the local values of "owners" and "expire" Store.getPadMetadata = function (clientId, data, cb) { diff --git a/www/common/outer/store-rpc.js b/www/common/outer/store-rpc.js index 49250582f..41963402b 100644 --- a/www/common/outer/store-rpc.js +++ b/www/common/outer/store-rpc.js @@ -80,6 +80,7 @@ define([ IS_NEW_CHANNEL: Store.isNewChannel, REQUEST_PAD_ACCESS: Store.requestPadAccess, GIVE_PAD_ACCESS: Store.givePadAccess, + BURN_PAD: Store.burnPad, GET_PAD_METADATA: Store.getPadMetadata, SET_PAD_METADATA: Store.setPadMetadata, CHANGE_PAD_PASSWORD_PIN: Store.changePadPasswordPin, diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 9308f05cb..c19e9e344 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -323,6 +323,7 @@ define([ } Utils.crypto = Utils.Crypto.createEncryptor(Utils.secret.keys); var parsed = Utils.Hash.parsePadUrl(window.location.href); + var burnAfterReading = parsed && parsed.hashData && parsed.hashData.ownerKey; if (!parsed.type) { throw new Error(); } var defaultTitle = Utils.UserObject.getDefaultName(parsed); var edPublic, curvePublic, notifications, isTemplate; @@ -376,6 +377,7 @@ define([ fromFileData: Cryptpad.fromFileData ? { title: Cryptpad.fromFileData.title } : undefined, + burnAfterReading: burnAfterReading, storeInTeam: Cryptpad.initialTeam || (Cryptpad.initialPath ? -1 : undefined) }; if (window.CryptPad_newSharedFolder) { @@ -1235,6 +1237,14 @@ define([ window.location.hash = hash; }; + if (burnAfterReading) { + Cryptpad.padRpc.onReadyEvent.reg(function () { + Cryptpad.burnPad({ + channel: secret.channel, + ownerKey: burnAfterReading + }); + }); + } var cpNfCfg = { sframeChan: sframeChan, channel: secret.channel, @@ -1358,12 +1368,17 @@ define([ }); }); + sframeChan.on('EV_BURN_AFTER_READING', function () { + startRealtime(); + }); + sframeChan.ready(); Utils.Feedback.reportAppUsage(); if (!realtime && !Test.testing) { return; } if (isNewFile && cfg.useCreationScreen && !Test.testing) { return; } + if (burnAfterReading) { return; } //if (isNewFile && Utils.LocalStore.isLoggedIn() // && AppConfig.displayCreationScreen && cfg.useCreationScreen) { return; } diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index f8bbb205b..9e51d46de 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -96,6 +96,7 @@ define([ funcs.createMarkdownToolbar = callWithCommon(UIElements.createMarkdownToolbar); funcs.createHelpMenu = callWithCommon(UIElements.createHelpMenu); funcs.getPadCreationScreen = callWithCommon(UIElements.getPadCreationScreen); + funcs.getBurnAfterReadingWarning = callWithCommon(UIElements.getBurnAfterReadingWarning); funcs.createNewPadModal = callWithCommon(UIElements.createNewPadModal); funcs.onServerError = callWithCommon(UIElements.onServerError); funcs.importMediaTagMenu = callWithCommon(UIElements.importMediaTagMenu); @@ -300,6 +301,13 @@ define([ } // If we display the pad creation screen, it will handle deleted pads directly funcs.getPadCreationScreen(c, config, waitFor()); + return; + } + if (priv.burnAfterReading) { + UIElements.displayBurnAfterReadingPage(funcs, waitFor(function () { + UI.addLoadingScreen(); + ctx.sframeChan.event('EV_BURN_AFTER_READING'); + })); } }; funcs.createPad = function (cfg, cb) { diff --git a/www/kanban/inner.js b/www/kanban/inner.js index 5a9e10f47..621d56cae 100644 --- a/www/kanban/inner.js +++ b/www/kanban/inner.js @@ -350,6 +350,8 @@ define([ var mkHelpMenu = function (framework) { var $toolbarContainer = $('#cp-app-kanban-container'); + $toolbarContainer.prepend(framework._.sfCommon.getBurnAfterReadingWarning()); + var helpMenu = framework._.sfCommon.createHelpMenu(['kanban']); $toolbarContainer.prepend(helpMenu.menu); diff --git a/www/pad/inner.js b/www/pad/inner.js index 5db93837c..78f027182 100644 --- a/www/pad/inner.js +++ b/www/pad/inner.js @@ -190,6 +190,7 @@ define([ var mkHelpMenu = function (framework) { var $toolbarContainer = $('.cke_toolbox_main'); + $toolbarContainer.before(framework._.sfCommon.getBurnAfterReadingWarning()); var helpMenu = framework._.sfCommon.createHelpMenu(['text', 'pad']); $toolbarContainer.before(helpMenu.menu); diff --git a/www/poll/inner.js b/www/poll/inner.js index ba8fd7d65..7420f5c09 100644 --- a/www/poll/inner.js +++ b/www/poll/inner.js @@ -1187,6 +1187,7 @@ define([ $drawer.append($export); var helpMenu = common.createHelpMenu(['poll']); + $('#cp-app-poll-form').prepend(common.getBurnAfterReadingWarning()); $('#cp-app-poll-form').prepend(helpMenu.menu); $drawer.append(helpMenu.button); diff --git a/www/slide/inner.js b/www/slide/inner.js index 6d97bf8e5..5736828d5 100644 --- a/www/slide/inner.js +++ b/www/slide/inner.js @@ -410,6 +410,7 @@ define([ var mkHelpMenu = function (framework) { var $codeMirrorContainer = $('#cp-app-slide-editor-container'); + $codeMirrorContainer.prepend(framework._.sfCommon.getBurnAfterReadingWarning()); var helpMenu = framework._.sfCommon.createHelpMenu(['text', 'slide']); $codeMirrorContainer.prepend(helpMenu.menu); diff --git a/www/whiteboard/inner.js b/www/whiteboard/inner.js index a90dd4187..77057f505 100644 --- a/www/whiteboard/inner.js +++ b/www/whiteboard/inner.js @@ -270,6 +270,7 @@ define([ var mkHelpMenu = function (framework) { var $appContainer = $('#cp-app-whiteboard-container'); + $appContainer.prepend(framework._.sfCommon.getBurnAfterReadingWarning()); var helpMenu = framework._.sfCommon.createHelpMenu(['whiteboard']); $appContainer.prepend(helpMenu.menu); framework._.toolbar.$drawer.append(helpMenu.button); From 1bf48a5a8ccfadef44b6900584c534aa87383f2e Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 9 Jan 2020 17:30:53 +0100 Subject: [PATCH 37/55] lint compliance --- www/common/outer/async-store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 1d1f26ba3..e39a2f980 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -1653,7 +1653,7 @@ define([ }; // Delete a pad received with a burn after reading URL - Store.burnPad = function (clientId, data, cb) { + Store.burnPad = function (clientId, data) { var channel = data.channel; var ownerKey = Crypto.b64AddSlashes(data.ownerKey || ''); if (!channel || !ownerKey) { return void console.error("Can't delete BAR pad"); } From a3b3a9e4fbc7cee388385bcdf47ff06b2ad59eab Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 9 Jan 2020 17:47:10 +0100 Subject: [PATCH 38/55] Fix button not updating when generating a BAR url --- www/common/common-ui-elements.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 1964ed570..415390909 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1383,7 +1383,7 @@ define([ embed: Util.isChecked($link.find('#cp-share-embed')) })); // Hide or show the burn after reading alert - if (Util.isChecked($rights.find('#cp-share-bar'))) { + if (Util.isChecked($rights.find('#cp-share-bar')) && !burnAfterReadingUrl) { $('.cp-alertify-bar-selected').show(); // Show burn after reading button $('.alertify').find('.cp-bar').show(); From 5ee12f8da7c4c98b31e474146da9ed003899fd07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Benqu=C3=A9?= Date: Fri, 10 Jan 2020 13:07:31 +0000 Subject: [PATCH 39/55] hard coded keys for testing --- www/common/common-ui-elements.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 415390909..310c7172e 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1131,7 +1131,7 @@ define([ UI.createRadio('accessRights', 'cp-share-editable-true', Messages.share_linkEdit, false, { mark: {tabindex:1} })]), burnAfterReading = hashes.viewHash ? UI.createRadio('accessRights', 'cp-share-bar', - 'BAR', false, { mark: {tabindex:1}, label: {style: "display: none;"} }) : undefined // XXX + 'View once and self-destruct', false, { mark: {tabindex:1}, label: {style: "display: none;"} }) : undefined // XXX KEY ]); // Burn after reading @@ -1140,7 +1140,7 @@ define([ // the options to generate the BAR url var barAlert = h('div.alert.alert-danger.cp-alertify-bar-selected', { style: 'display: none;' - }, " You have set this pad to self-destruct. Once a recipient opens this pad, it will be permanently deleted from the server. "); // XXX + }, " You have set this pad to self-destruct. Once a recipient opens this pad, it will be permanently deleted from the server. "); // XXX KEY var channel = Hash.getSecrets('pad', hash, config.password).channel; common.getPadMetadata({ channel: channel @@ -1182,7 +1182,7 @@ define([ cb(url); }); } - return 'XXX Click on the button below to generate a link'; // XXX + return 'Click on the button below to generate a link'; // XXX KEY } var hash = (!hashes.viewHash || (edit && hashes.editHash)) ? hashes.editHash : hashes.viewHash; var href = burnAfterReading ? burnAfterReadingUrl : (origin + pathname + '#' + hash); @@ -4037,8 +4037,8 @@ define([ }; UIElements.displayBurnAfterReadingPage = function (common, cb) { - var info = h('p.cp-password-info', 'XXX Burn after reading'); // XXX - var button = h('button', 'Proceed'); // XXX + var info = h('p.cp-password-info', 'This document will self-destruct as soon as you open it. It will be removed form the server, once you close this window you will not be able to access it again. If you are not ready to proceed you can close this window and come back later. '); // XXX KEY + var button = h('button.primary', 'Proceed'); // XXX KEY $(button).on('click', function () { cb(); @@ -4053,7 +4053,7 @@ define([ UIElements.getBurnAfterReadingWarning = function (common) { var priv = common.getMetadataMgr().getPrivateData(); if (!priv.burnAfterReading) { return; } - return h('div.alert.alert-danger.cp-burn-after-reading', 'Pewpewpew'); // XXX + return h('div.alert.alert-danger.cp-burn-after-reading', 'This pad has been deleted from the server, once you close this window you will not be able to access it again.'); // XXX KEY }; var crowdfundingState = false; From 7042b9c2d73a1b9df98b5e48ec8a5767ae9f88ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Benqu=C3=A9?= Date: Fri, 10 Jan 2020 13:07:52 +0000 Subject: [PATCH 40/55] style loading screen message --- customize.dist/loading.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/customize.dist/loading.js b/customize.dist/loading.js index 600f3e8d4..10db051ff 100644 --- a/customize.dist/loading.js +++ b/customize.dist/loading.js @@ -77,7 +77,7 @@ define([], function () { background: #FFF; padding: 20px; width: 100%; - color: #000; + color: #3F4141; text-align: center; display: none; } @@ -94,6 +94,9 @@ define([], function () { text-align: left; margin-bottom: 15px; } +p.cp-password-info{ + text-align: left; +} #cp-loading-password-prompt .cp-password-form { display: flex; justify-content: space-around; @@ -201,6 +204,19 @@ define([], function () { animation-timing-function: cubic-bezier(.6,0.15,0.4,0.85); } +button.primary{ + border: 1px solid #4591c4; + padding: 8px 12px; + text-transform: uppercase; + background-color: #4591c4; + color: white; + font-weight: bold; +} + +button.primary:hover{ + background-color: rgb(52, 118, 162); +} + */}).toString().slice(14, -3); var urlArgs = window.location.href.replace(/^.*\?([^\?]*)$/, function (all, x) { return x; }); var elem = document.createElement('div'); From f1d1690cf88fb5139be50cc6071dd2f7a5c6629b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Benqu=C3=A9?= Date: Fri, 10 Jan 2020 13:11:16 +0000 Subject: [PATCH 41/55] access rights buttons --- customize.dist/src/less2/include/modals-ui-elements.less | 3 +++ 1 file changed, 3 insertions(+) diff --git a/customize.dist/src/less2/include/modals-ui-elements.less b/customize.dist/src/less2/include/modals-ui-elements.less index 289cc4e69..cee774a98 100644 --- a/customize.dist/src/less2/include/modals-ui-elements.less +++ b/customize.dist/src/less2/include/modals-ui-elements.less @@ -14,6 +14,9 @@ .radio-group { display: flex; flex-direction: row; + &:not(:last-child){ + margin-bottom: 8px; + } .cp-radio { margin-right: 30px; } From bdd338902b6b27b5ee45b75715024a03db80cba8 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 10 Jan 2020 15:53:02 +0100 Subject: [PATCH 42/55] Hide radio buttons in share modal when no contacts --- www/common/common-ui-elements.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 310c7172e..41b929034 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1322,7 +1322,13 @@ define([ var contactButtons = friendsObject.buttons; contactButtons.unshift(makeCancelButton()); - + + var onShowContacts = function () { + if (!hasFriends) { + $rights.hide(); + } + }; + var frameContacts = UI.dialog.customModal(contactsContent, { buttons: contactButtons, onClose: config.onClose, @@ -1405,13 +1411,16 @@ define([ // Create modal var resetTab = function () { + $rights.show(); $rights.find('label.cp-radio').show(); }; var tabs = [{ title: Messages.share_contactCategory, icon: "fa fa-address-book", content: frameContacts, - active: hasFriends + active: hasFriends, + onShow: onShowContacts, + onHide: resetTab }, { title: Messages.share_linkCategory, icon: "fa fa-link", From 9f8f6399d82254be38586143b84ae2d9763468c3 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 10 Jan 2020 16:16:11 +0100 Subject: [PATCH 43/55] Add finnish translation --- customize.dist/messages.js | 1 + 1 file changed, 1 insertion(+) diff --git a/customize.dist/messages.js b/customize.dist/messages.js index 829cc4f23..45dd2fac2 100755 --- a/customize.dist/messages.js +++ b/customize.dist/messages.js @@ -5,6 +5,7 @@ var map = { 'de': 'Deutsch', 'el': 'Ελληνικά', 'es': 'Español', + 'fi': 'Suomalainen', 'fr': 'Français', 'it': 'Italiano', 'nb': 'Norwegian Bokmål', From b5b3e99e56af8bfaa31ff82582146eeea4bfd38c Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 10 Jan 2020 12:29:57 -0500 Subject: [PATCH 44/55] use correct function signature for Pinned.load --- scripts/check-account-deletion.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check-account-deletion.js b/scripts/check-account-deletion.js index cf271a1da..0532e69ed 100644 --- a/scripts/check-account-deletion.js +++ b/scripts/check-account-deletion.js @@ -41,7 +41,7 @@ nThen((waitFor) => { pinned = Pins.calculateFromLog(content.toString('utf8'), f); })); }).nThen((waitFor) => { - Pinned.load(waitFor((d) => { + Pinned.load(waitFor((err, d) => { data = Object.keys(d); }), { exclude: [edPublic + '.ndjson'] From ddb204def4b792491b26fef6afe716736068dd1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Benqu=C3=A9?= Date: Mon, 13 Jan 2020 10:32:22 +0000 Subject: [PATCH 45/55] Change temporary text --- www/common/common-ui-elements.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 41b929034..90418a694 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -4047,7 +4047,7 @@ define([ UIElements.displayBurnAfterReadingPage = function (common, cb) { var info = h('p.cp-password-info', 'This document will self-destruct as soon as you open it. It will be removed form the server, once you close this window you will not be able to access it again. If you are not ready to proceed you can close this window and come back later. '); // XXX KEY - var button = h('button.primary', 'Proceed'); // XXX KEY + var button = h('button.primary', 'view and delete'); // XXX KEY $(button).on('click', function () { cb(); From e3aa814c0af1debd69239615dde630325cd0c608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Benqu=C3=A9?= Date: Mon, 13 Jan 2020 10:53:48 +0000 Subject: [PATCH 46/55] style of loading screen messages and password form --- customize.dist/loading.js | 42 +++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/customize.dist/loading.js b/customize.dist/loading.js index 10db051ff..b4c3c4a3a 100644 --- a/customize.dist/loading.js +++ b/customize.dist/loading.js @@ -13,7 +13,8 @@ define([], function () { right: 0px; background: linear-gradient(to right, #326599 0%, #326599 50%, #4591c4 50%, #4591c4 100%); color: #fafafa; - font-size: 1.5em; + font-size: 1.3em; + line-height: 120%; opacity: 1; display: flex; flex-flow: column; @@ -78,12 +79,10 @@ define([], function () { padding: 20px; width: 100%; color: #3F4141; - text-align: center; + text-align: left; display: none; } -#cp-loading-password-prompt { - font-size: 18px; -} + #cp-loading-password-prompt .cp-password-error { color: white; background: #9e0000; @@ -99,22 +98,43 @@ p.cp-password-info{ } #cp-loading-password-prompt .cp-password-form { display: flex; - justify-content: space-around; flex-wrap: wrap; } -#cp-loading-password-prompt .cp-password-form button, -#cp-loading-password-prompt .cp-password-form .cp-password-input { +#cp-loading-password-prompt .cp-password-form button{ background-color: #4591c4; color: white; border: 1px solid #4591c4; } + +.cp-password-input{ + border: 1px solid #4591c4; + background-color: white; + border-radius 0; +} + +.cp-password-form button{ + padding: 8px 12px; + font-weight: bold; + text-transform: uppercase; +} + +#cp-loading-password-prompt .cp-password-form{ + width: 100%; +} + #cp-loading-password-prompt .cp-password-form .cp-password-container { flex-shrink: 1; min-width: 0; } + +#cp-loading-password-prompt .cp-password-form .cp-password-container .cp-password-reveal{ + color: #4591c4; + padding: 0px 24px; +} + #cp-loading-password-prompt .cp-password-form input { flex: 1; - padding: 0 5px; + padding: 12px; min-width: 0; text-overflow: ellipsis; } @@ -122,7 +142,7 @@ p.cp-password-info{ background-color: #326599; } #cp-loading-password-prompt ::placeholder { - color: #d9d9d9; + color: #999999; opacity: 1; } #cp-loading-password-prompt :-ms-input-placeholder { @@ -157,7 +177,7 @@ p.cp-password-info{ background: #222; color: #fafafa; text-align: center; - font-size: 1.5em; + font-size: 1.3em; opacity: 0.7; font-family: 'Open Sans', 'Helvetica Neue', sans-serif; padding: 15px; From ba43bb9f07abc0fc6e9cbf0248be690874ee7134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Benqu=C3=A9?= Date: Mon, 13 Jan 2020 12:05:41 +0000 Subject: [PATCH 47/55] adjust font size of password --- customize.dist/loading.js | 1 + 1 file changed, 1 insertion(+) diff --git a/customize.dist/loading.js b/customize.dist/loading.js index b4c3c4a3a..09d2060dc 100644 --- a/customize.dist/loading.js +++ b/customize.dist/loading.js @@ -107,6 +107,7 @@ p.cp-password-info{ } .cp-password-input{ + font-size:16px; border: 1px solid #4591c4; background-color: white; border-radius 0; From 09da8ac6a2fccc80de1cad078f32eca9d65fdc9e Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 13 Jan 2020 14:52:46 +0100 Subject: [PATCH 48/55] Warn the owners when deleting a BAR pad --- www/common/outer/async-store.js | 49 +++++++++++++++++++++++++++++-- www/common/sframe-common-outer.js | 2 ++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index e39a2f980..f733e3adf 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -1653,6 +1653,45 @@ define([ }; // Delete a pad received with a burn after reading URL + + var notifyOwnerPadRemoved = function (data, obj) { + var channel = data.channel; + var href = data.href; + var parsed = Hash.parsePadUrl(href); + var secret = Hash.getSecrets(parsed.type, parsed.hash, data.password); + if (obj && obj.error) { return; } + if (!obj.mailbox) { return; } + + // Decrypt the mailbox + var crypto = Crypto.createEncryptor(secret.keys); + var m = []; + try { + if (typeof (obj.mailbox) === "string") { + m.push(crypto.decrypt(obj.mailbox, true, true)); + } else { + Object.keys(obj.mailbox).forEach(function (k) { + m.push(crypto.decrypt(obj.mailbox[k], true, true)); + }); + } + } catch (e) { + console.error(e); + } + // Tell all the owners that the pad was deleted from the server + var curvePublic = store.proxy.curvePublic; + var myData = Messaging.createData(store.proxy, false); + m.forEach(function (obj) { + var mb = JSON.parse(obj); + if (mb.curvePublic === curvePublic) { return; } + store.mailbox.sendTo('OWNED_PAD_REMOVED', { + channel: channel, + user: myData + }, { + channel: mb.notifications, + curvePublic: mb.curvePublic + }, function () {}); + }); + }; + Store.burnPad = function (clientId, data) { var channel = data.channel; var ownerKey = Crypto.b64AddSlashes(data.ownerKey || ''); @@ -1665,8 +1704,14 @@ define([ edPrivate: Hash.encodeBase64(pair.secretKey) }, function (e, rpc) { if (e) { return void console.error(e); } - rpc.removeOwnedChannel(channel, function (err) { - if (err) { console.error(err); } + Store.getPadMetadata(null, { + channel: channel + }, function (md) { + rpc.removeOwnedChannel(channel, function (err) { + if (err) { return void console.error(err); } + // Notify owners that the pad was removed + notifyOwnerPadRemoved(data, md); + }); }); }); } catch (e) { diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index c19e9e344..d0a84816a 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -1240,6 +1240,8 @@ define([ if (burnAfterReading) { Cryptpad.padRpc.onReadyEvent.reg(function () { Cryptpad.burnPad({ + password: password, + href: window.location.href, channel: secret.channel, ownerKey: burnAfterReading }); From 1e6e9fd288835fe43b3717c4c7ab322c5f1e76ba Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 13 Jan 2020 15:58:46 +0100 Subject: [PATCH 49/55] Remove deleted pad from the drive --- www/common/outer/async-store.js | 5 +++++ www/common/outer/mailbox-handlers.js | 25 +++++++++++++++++++++++++ www/common/outer/mailbox.js | 1 + www/common/proxy-manager.js | 4 ++++ 4 files changed, 35 insertions(+) diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index f733e3adf..681f1d575 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -2175,6 +2175,11 @@ define([ updateMetadata: function () { broadcast([], "UPDATE_METADATA"); }, + updateDrive: function () { + sendDriveEvent('DRIVE_CHANGE', { + path: ['drive', 'filesData'] + }); + }, pinPads: function (data, cb) { Store.pinPads(null, data, cb); }, }, waitFor, function (ev, data, clients, _cb) { var cb = Util.once(_cb || function () {}); diff --git a/www/common/outer/mailbox-handlers.js b/www/common/outer/mailbox-handlers.js index 6e10cd5a2..253157361 100644 --- a/www/common/outer/mailbox-handlers.js +++ b/www/common/outer/mailbox-handlers.js @@ -482,6 +482,31 @@ define([ cb(true); }; + handlers['OWNED_PAD_REMOVED'] = function (ctx, box, data, cb) { + var msg = data.msg; + var content = msg.content; + + if (msg.author !== content.user.curvePublic) { return void cb(true); } + if (!content.channel) { + console.log('Remove invalid notification'); + return void cb(true); + } + + var channel = content.channel; + var res = ctx.store.manager.findChannel(channel); + + res.forEach(function (obj) { + var paths = ctx.store.manager.findFile(obj.id); + ctx.store.manager.delete({ + paths: paths + }, function () { + ctx.updateDrive(); + }); + }); + + cb(true); + }; + return { diff --git a/www/common/outer/mailbox.js b/www/common/outer/mailbox.js index 1def668e1..b84e98dd7 100644 --- a/www/common/outer/mailbox.js +++ b/www/common/outer/mailbox.js @@ -422,6 +422,7 @@ proxy.mailboxes = { store: store, pinPads: cfg.pinPads, updateMetadata: cfg.updateMetadata, + updateDrive: cfg.updateDrive, emit: emit, clients: [], boxes: {}, diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index 1bd88e90b..796c52898 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -119,6 +119,7 @@ define([ // If it's not a shared folder, check the pads if (!data) { data = Env.user.userObject.getFileData(id, editable); } ret.push({ + id: id, data: data, userObject: Env.user.userObject }); @@ -126,6 +127,7 @@ define([ Object.keys(Env.folders).forEach(function (fId) { Env.folders[fId].userObject.findChannels([channel]).forEach(function (id) { ret.push({ + id: id, fId: fId, data: Env.folders[fId].userObject.getFileData(id, editable), userObject: Env.folders[fId].userObject @@ -1095,9 +1097,11 @@ define([ // Store getChannelsList: callWithEnv(getChannelsList), addPad: callWithEnv(addPad), + delete: callWithEnv(_delete), // Tools findChannel: callWithEnv(findChannel), findHref: callWithEnv(findHref), + findFile: callWithEnv(findFile), getEditHash: callWithEnv(getEditHash), user: Env.user, folders: Env.folders From df03ddcca0b1351fe40ccea494fc390dd914f71d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Benqu=C3=A9?= Date: Mon, 13 Jan 2020 15:26:57 +0000 Subject: [PATCH 50/55] temporary translation keys --- www/common/common-ui-elements.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 90418a694..89ad52835 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1130,8 +1130,8 @@ define([ Messages.share_linkPresent, false, { mark: {tabindex:1} }) : undefined, UI.createRadio('accessRights', 'cp-share-editable-true', Messages.share_linkEdit, false, { mark: {tabindex:1} })]), - burnAfterReading = hashes.viewHash ? UI.createRadio('accessRights', 'cp-share-bar', - 'View once and self-destruct', false, { mark: {tabindex:1}, label: {style: "display: none;"} }) : undefined // XXX KEY + burnAfterReading = hashes.viewHash ? UI.createRadio('accessRights', 'cp-share-bar', Messages.burnAfterReading_linkBurnAfterReading || + 'View once and self-destruct', false, { mark: {tabindex:1}, label: {style: "display: none;"} }) : undefined // XXX temp KEY ]); // Burn after reading @@ -1140,7 +1140,7 @@ define([ // the options to generate the BAR url var barAlert = h('div.alert.alert-danger.cp-alertify-bar-selected', { style: 'display: none;' - }, " You have set this pad to self-destruct. Once a recipient opens this pad, it will be permanently deleted from the server. "); // XXX KEY + }, Messages.burnAfterReading_warningLink || " You have set this pad to self-destruct. Once a recipient opens this pad, it will be permanently deleted from the server."); // XXX temp KEY var channel = Hash.getSecrets('pad', hash, config.password).channel; common.getPadMetadata({ channel: channel @@ -1182,7 +1182,7 @@ define([ cb(url); }); } - return 'Click on the button below to generate a link'; // XXX KEY + return Messages.burnAfterReading_generateLink || 'Click on the button below to generate a link'; // XXX temp KEY } var hash = (!hashes.viewHash || (edit && hashes.editHash)) ? hashes.editHash : hashes.viewHash; var href = burnAfterReading ? burnAfterReadingUrl : (origin + pathname + '#' + hash); @@ -4046,8 +4046,8 @@ define([ }; UIElements.displayBurnAfterReadingPage = function (common, cb) { - var info = h('p.cp-password-info', 'This document will self-destruct as soon as you open it. It will be removed form the server, once you close this window you will not be able to access it again. If you are not ready to proceed you can close this window and come back later. '); // XXX KEY - var button = h('button.primary', 'view and delete'); // XXX KEY + var info = h('p.cp-password-info', Messages.burnAfterReading_warning || 'This document will self-destruct as soon as you open it. It will be removed form the server, once you close this window you will not be able to access it again. If you are not ready to proceed you can close this window and come back later. '); // XXX temp KEY + var button = h('button.primary', Messages.burnAfterReading_proceed || 'view and delete'); // XXX temp KEY $(button).on('click', function () { cb(); @@ -4062,7 +4062,7 @@ define([ UIElements.getBurnAfterReadingWarning = function (common) { var priv = common.getMetadataMgr().getPrivateData(); if (!priv.burnAfterReading) { return; } - return h('div.alert.alert-danger.cp-burn-after-reading', 'This pad has been deleted from the server, once you close this window you will not be able to access it again.'); // XXX KEY + return h('div.alert.alert-danger.cp-burn-after-reading', Messages.burnAfterReading_warningDeleted || 'This pad has been deleted from the server, once you close this window you will not be able to access it again.'); // XXX temp KEY }; var crowdfundingState = false; From 73af078e4abeef087deba5a3d087872391576c1f Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 13 Jan 2020 16:43:44 +0100 Subject: [PATCH 51/55] Hide share modal when pad is deleted --- www/common/common-ui-elements.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 41b929034..0ab7586e1 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -4001,6 +4001,8 @@ define([ msg += Messages.errorCopy; } } + var sframeChan = common.getSframeChannel(); + sframeChan.event('EV_SHARE_OPEN', {hidden: true}); if (toolbar && typeof toolbar.deleted === "function") { toolbar.deleted(); } UI.errorLoadingScreen(msg, true, true); (cb || function () {})(); From d02092eb76e74b1467116776a3486f05c7ea0e7d Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 13 Jan 2020 17:06:49 +0100 Subject: [PATCH 52/55] Fix cache and storage issues in share and filepicker iframes --- www/common/sframe-common-outer.js | 4 ++-- www/filepicker/main.js | 2 +- www/share/main.js | 17 ++++++++++++++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index d0a84816a..f53a6b496 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -81,8 +81,8 @@ define([ }); localStorage.CRYPTPAD_URLARGS = ApiConfig.requireConf.urlArgs; } - var cache = {}; - var localStore = {}; + var cache = window.cpCache = {}; + var localStore = window.localStore = {}; Object.keys(localStorage).forEach(function (k) { if (k.indexOf('CRYPTPAD_CACHE|') === 0) { cache[k.slice(('CRYPTPAD_CACHE|').length)] = localStorage[k]; diff --git a/www/filepicker/main.js b/www/filepicker/main.js index d6017c2dd..cada394ba 100644 --- a/www/filepicker/main.js +++ b/www/filepicker/main.js @@ -58,7 +58,7 @@ define([ // Remove the listener once we've received the READY message window.removeEventListener('message', whenReady); // Answer with the requested data - postMsg(JSON.stringify({ txid: data.txid, language: Cryptpad.getLanguage() })); + postMsg(JSON.stringify({ txid: data.txid, language: Cryptpad.getLanguage(), localStore: window.localStore, cache: window.cpCache })); // Then start the channel window.addEventListener('message', function (msg) { diff --git a/www/share/main.js b/www/share/main.js index 43e514f58..28cc1a882 100644 --- a/www/share/main.js +++ b/www/share/main.js @@ -60,7 +60,7 @@ define([ // Remove the listener once we've received the READY message window.removeEventListener('message', whenReady); // Answer with the requested data - postMsg(JSON.stringify({ txid: data.txid, language: Cryptpad.getLanguage() })); + postMsg(JSON.stringify({ txid: data.txid, language: Cryptpad.getLanguage(), localStore: window.localStore, cache: window.cpCache })); // Then start the channel window.addEventListener('message', function (msg) { @@ -105,6 +105,21 @@ define([ config.addCommonRpc(sframeChan); + sframeChan.on('EV_CACHE_PUT', function (x) { + Object.keys(x).forEach(function (k) { + localStorage['CRYPTPAD_CACHE|' + k] = x[k]; + }); + }); + sframeChan.on('EV_LOCALSTORE_PUT', function (x) { + Object.keys(x).forEach(function (k) { + if (typeof(x[k]) === "undefined") { + delete localStorage['CRYPTPAD_STORE|' + k]; + return; + } + localStorage['CRYPTPAD_STORE|' + k] = x[k]; + }); + }); + sframeChan.on('Q_GET_FILES_LIST', function (types, cb) { Cryptpad.getSecureFilesList(types, function (err, data) { cb({ From 9e9a2dafaac893a0627b36912b63e61aafc31cca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Benqu=C3=A9?= Date: Mon, 13 Jan 2020 16:29:09 +0000 Subject: [PATCH 53/55] fix warning paragraph spacing --- customize.dist/loading.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/customize.dist/loading.js b/customize.dist/loading.js index 09d2060dc..e20e79438 100644 --- a/customize.dist/loading.js +++ b/customize.dist/loading.js @@ -83,7 +83,7 @@ define([], function () { display: none; } -#cp-loading-password-prompt .cp-password-error { +#cp-loading-password-prompt p.cp-password-error { color: white; background: #9e0000; padding: 5px; @@ -93,6 +93,10 @@ define([], function () { text-align: left; margin-bottom: 15px; } +#cp-loading-burn-after-reading .cp-password-info { + margin-bottom: 15px; +} + p.cp-password-info{ text-align: left; } From 9e1f49c1779d1e66b3a9c09a6e54fd0acd42a827 Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 15 Jan 2020 15:33:52 +0100 Subject: [PATCH 54/55] Enable spreadsheets for anonymous users --- www/common/application_config_internal.js | 2 +- www/common/onlyoffice/inner.js | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/www/common/application_config_internal.js b/www/common/application_config_internal.js index d4c1954e4..900a9ec28 100644 --- a/www/common/application_config_internal.js +++ b/www/common/application_config_internal.js @@ -20,7 +20,7 @@ define(function() { * users and these users will be redirected to the login page if they still try to access * the app */ - config.registeredOnlyTypes = ['file', 'contacts', 'oodoc', 'ooslide', 'sheet', 'notifications']; + config.registeredOnlyTypes = ['file', 'contacts', 'oodoc', 'ooslide', 'notifications']; /* CryptPad is available is multiple languages, but only English and French are maintained * by the developers. The other languages may be outdated, and any missing string for a langauge diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index a40ccccae..c22ca019c 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -584,7 +584,9 @@ define([ ooChannel.cpIndex++; ooChannel.lastHash = hash; // Check if a checkpoint is needed - if (ooChannel.cpIndex % CHECKPOINT_INTERVAL === 0) { + var lastCp = getLastCp(); + if (common.isLoggedIn() && (ooChannel.cpIndex % CHECKPOINT_INTERVAL === 0 || + (ooChannel.cpIndex - lastCp.index) > CHECKPOINT_INTERVAL)) { makeCheckpoint(); } // Remove my lock @@ -662,7 +664,7 @@ define([ var startOO = function (blob, file) { if (APP.ooconfig) { return void console.error('already started'); } var url = URL.createObjectURL(blob); - var lock = readOnly || !common.isLoggedIn(); + var lock = readOnly;// || !common.isLoggedIn(); // Config APP.ooconfig = { From b80d3cf24f232f33e448e849df9daa444b07f832 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 16 Jan 2020 14:52:04 +0100 Subject: [PATCH 55/55] Ability to provide a dropdown container to a text input. Applied to framework export. --- .../src/less2/include/alertify.less | 3 ++ customize.dist/src/less2/include/buttons.less | 17 ++++++++++ .../src/less2/include/dropdown.less | 3 +- www/common/common-interface.js | 15 +++++++- www/common/common-ui-elements.js | 11 ++++-- www/common/sframe-app-framework.js | 34 ++++++++++++++++++- 6 files changed, 76 insertions(+), 7 deletions(-) diff --git a/customize.dist/src/less2/include/alertify.less b/customize.dist/src/less2/include/alertify.less index a2b787e1c..83c9068eb 100644 --- a/customize.dist/src/less2/include/alertify.less +++ b/customize.dist/src/less2/include/alertify.less @@ -168,6 +168,9 @@ margin-bottom: 0; } } + .cp-alertify-type-container { + overflow: visible !important; + } .alertify-tabs { max-height: 100%; display: flex; diff --git a/customize.dist/src/less2/include/buttons.less b/customize.dist/src/less2/include/buttons.less index fb99471b6..ad6aaf9cc 100644 --- a/customize.dist/src/less2/include/buttons.less +++ b/customize.dist/src/less2/include/buttons.less @@ -23,6 +23,23 @@ } } + div.cp-alertify-type { + display: flex; + input { + margin: 0; + flex: 1; + min-width: 0; + } + span { + button { + margin: 0; + height: 100%; + margin-left: -1px; + text-transform: unset !important; + } + } + } + textarea { padding: 8px; &[readonly] { diff --git a/customize.dist/src/less2/include/dropdown.less b/customize.dist/src/less2/include/dropdown.less index 10044528e..271603216 100644 --- a/customize.dist/src/less2/include/dropdown.less +++ b/customize.dist/src/less2/include/dropdown.less @@ -17,8 +17,7 @@ button { .fa-caret-down { - margin-right: 0px; - margin-left: 5px; + margin-right: 1em !important; } * { .tools_unselectable(); diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 55d7e6939..d4b3059b4 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -161,6 +161,17 @@ define([ return h('p.msg', h('input', attrs)); }; + dialog.textTypeInput = function (dropdown) { + var attrs = { + type: 'text', + 'class': 'cp-text-type-input', + }; + return h('p.msg.cp-alertify-type-container', h('div.cp-alertify-type', [ + h('input', attrs), + dropdown // must be a "span" + ])); + }; + dialog.nav = function (content) { return h('nav', content || [ dialog.cancelButton(), @@ -186,6 +197,7 @@ define([ }); }; return $frame.click(function (e) { + $frame.find('.cp-dropdown-content').hide(); e.stopPropagation(); })[0]; }; @@ -480,7 +492,8 @@ define([ cb = cb || function () {}; opt = opt || {}; - var inputBlock = opt.password ? UI.passwordInput() : dialog.textInput(); + var inputBlock = opt.password ? UI.passwordInput() : + (opt.typeInput ? dialog.textTypeInput(opt.typeInput) : dialog.textInput()); var input = $(inputBlock).is('input') ? inputBlock : $(inputBlock).find('input')[0]; input.value = typeof(def) === 'string'? def: ''; diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index a195a1ba9..eb032bfaf 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -2896,9 +2896,11 @@ define([ var $button = $('