diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d333700b..e667c55b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,38 @@ +# 4.6.0 + +## Goals + +* work on polls/surveys +* stabilize and implement tests + +## Update notes + +* checkup/server/config + * test for anti-FLoC header + * add anti-FloC header to server so the default dev server passes all tests + * update NGINX example to avoid duplicated headers + * simplify dev server headers + * adjust table borders for dark/light mode + * say what headers are wrong +* rename exported object in `application_config_internal.js` to avoid copypasta errors +* `AppConfig.disableAnonymousPadCreation = false;` + * note that it's only enforced client-side +* update lodash devDependency + +## Features + +* generate `supportMailbox` keys via the admin panel + * document this +* markdown preview + * code blocks are full width + +## Bug fixes + +* fix opening links from temporary shared folders on iphone or other contexts that do not support shared workers +* show "features" instead of "pricing" in static pages' footer when premium subscriptions are not available +* use preferred 12/24h time format in date picker +* add error handling to admin panel decree RPCs + # 4.5.0 ## Goals diff --git a/config/config.example.js b/config/config.example.js index f53f3bb5b..d05b985af 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -122,19 +122,6 @@ module.exports = { ], */ - /* CryptPad's administration panel includes a "support" tab - * wherein administrators with a secret key can view messages - * sent from users via the encrypted forms on the /support/ page - * - * To enable this functionality: - * run `node ./scripts/generate-admin-keys.js` - * save the public key in your config in the value below - * add the private key via the admin panel - * and back it up in a secure manner - * - */ - // supportMailboxPublicKey: "", - /* We're very proud that CryptPad is available to the public as free software! * We do, however, still need to pay our bills as we develop the platform. * @@ -147,11 +134,6 @@ module.exports = { */ //removeDonateButton: false, - /* CryptPad will display a point of contact for your instance on its contact page - * (/contact.html) if you provide it below. - */ - adminEmail: 'i.did.not.read.my.config@cryptpad.fr', - /* * By default, CryptPad contacts one of our servers once a day. * This check-in will also send some very basic information about your instance including its diff --git a/customize.dist/pages.js b/customize.dist/pages.js index e3ae2b83e..aaf7c60b3 100644 --- a/customize.dist/pages.js +++ b/customize.dist/pages.js @@ -43,6 +43,17 @@ define([ return Pages.externalLink(el, Pages.localizeDocsLink(href)); }; + var accounts = Pages.accounts = { + donateURL: AppConfig.donateURL || "https://opencollective.com/cryptpad/", + upgradeURL: AppConfig.upgradeURL + }; + + Pages.areSubscriptionsAllowed = function () { + try { + return ApiConfig.allowSubscriptions && accounts.upgradeURL && !ApiConfig.restrictRegistration; + } catch (err) { return void console.error(err); } + }; + var languageSelector = function () { var options = []; var languages = Msg._languages; @@ -94,7 +105,7 @@ define([ var imprintUrl = AppConfig.imprint && (typeof(AppConfig.imprint) === "boolean" ? '/imprint.html' : AppConfig.imprint); - Pages.versionString = "v4.5.0"; + Pages.versionString = "v4.6.0"; // used for the about menu @@ -133,7 +144,7 @@ define([ footerCol('footer_product', [ footLink('/what-is-cryptpad.html', 'topbar_whatIsCryptpad'), Pages.docsLink, - footLink('/features.html', 'pricing'), + footLink('/features.html', Pages.areSubscriptionsAllowed()? 'pricing': 'features'), // Messages.pricing, Messages.features Pages.githubLink, footLink('https://opencollective.com/cryptpad/contribute/', 'footer_donate'), ]), diff --git a/customize.dist/pages/features.js b/customize.dist/pages/features.js index ee82d5d67..1dfd2417f 100644 --- a/customize.dist/pages/features.js +++ b/customize.dist/pages/features.js @@ -8,10 +8,7 @@ define([ '/api/config', '/common/common-ui-elements.js', ], function ($, h, Msg, AppConfig, LocalStore, Pages, Config, UIElements) { - var accounts = { - donateURL: AppConfig.donateURL || "https://opencollective.com/cryptpad/", - upgradeURL: AppConfig.upgradeURL - }; + var accounts = Pages.accounts; return function () { Msg.features_f_apps_note = AppConfig.availablePadTypes.map(function (app) { @@ -145,10 +142,11 @@ define([ ]), ]), ]); - var availableFeatures = - (Config.allowSubscriptions && accounts.upgradeURL && !Config.restrictRegistration) ? - [anonymousFeatures, registeredFeatures, premiumFeatures] : - [anonymousFeatures, registeredFeatures]; + var availableFeatures = [ + anonymousFeatures, + registeredFeatures, + Pages.areSubscriptionsAllowed() ? premiumFeatures: undefined, + ]; return h('div#cp-main', [ Pages.infopageTopbar(), diff --git a/customize.dist/src/less2/include/markdown.less b/customize.dist/src/less2/include/markdown.less index 717b49990..3943f128d 100644 --- a/customize.dist/src/less2/include/markdown.less +++ b/customize.dist/src/less2/include/markdown.less @@ -194,7 +194,7 @@ pre > code { display: block; position: relative; - width: 90%; + width: 100%; margin: auto; padding: 5px; overflow-x: auto; diff --git a/docs/example.nginx.conf b/docs/example.nginx.conf index 78e4f30ce..a51a1ecaa 100644 --- a/docs/example.nginx.conf +++ b/docs/example.nginx.conf @@ -167,6 +167,13 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # These settings prevent both NGINX and the API server + # from setting the same headers and creating duplicates + proxy_hide_header Cross-Origin-Resource-Policy; + add_header Cross-Origin-Resource-Policy cross-origin; + proxy_hide_header Cross-Origin-Embedder-Policy; + add_header Cross-Origin-Embedder-Policy require-corp; } # encrypted blobs are immutable and are thus cached for a year diff --git a/lib/commands/admin-rpc.js b/lib/commands/admin-rpc.js index 32338b772..9ab86bdf2 100644 --- a/lib/commands/admin-rpc.js +++ b/lib/commands/admin-rpc.js @@ -348,6 +348,9 @@ var commands = { CLEAR_CACHED_CHANNEL_INDEX: clearChannelIndex, GET_CACHED_CHANNEL_INDEX: getChannelIndex, + // TODO implement admin historyTrim + // TODO implement kick from channel + // TODO implement force-disconnect user(s)? CLEAR_CACHED_CHANNEL_METADATA: clearChannelMetadata, GET_CACHED_CHANNEL_METADATA: getChannelMetadata, diff --git a/lib/defaults.js b/lib/defaults.js index 3d5e74576..635e155be 100644 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -48,6 +48,7 @@ Default.httpHeaders = function () { "X-XSS-Protection": "1; mode=block", "X-Content-Type-Options": "nosniff", "Access-Control-Allow-Origin": "*", + "Permissions-policy":"interest-cohort=()" }; }; Default.mainPages = function () { diff --git a/lib/hk-util.js b/lib/hk-util.js index 7c244c174..5a96970d0 100644 --- a/lib/hk-util.js +++ b/lib/hk-util.js @@ -701,6 +701,7 @@ const handleGetHistory = function (Env, Server, seq, userId, parsed) { } if (msgCount === 0 && !metadata_cache[channelName] && Server.channelContainsUser(channelName, userId)) { + // TODO this might be a good place to reject channel creation by anonymous users handleFirstMessage(Env, channelName, metadata); Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(metadata)]); } diff --git a/package-lock.json b/package-lock.json index 75b03cfe1..b1d794abe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cryptpad", - "version": "4.5.0", + "version": "4.6.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 3314ffd17..8526afc8a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cryptpad", "description": "realtime collaborative visual editor with zero knowlege server", - "version": "4.5.0", + "version": "4.6.0", "license": "AGPL-3.0+", "repository": { "type": "git", diff --git a/server.js b/server.js index 3a71f83b8..1824cf59c 100644 --- a/server.js +++ b/server.js @@ -108,28 +108,21 @@ var setHeaders = (function () { // apply a bunch of cross-origin headers for XLSX export in FF and printing elsewhere applyHeaderMap(res, { "Cross-Origin-Opener-Policy": /^\/sheet\//.test(req.url)? 'same-origin': '', - "Cross-Origin-Embedder-Policy": 'require-corp', }); if (Env.NO_SANDBOX) { // handles correct configuration for local development // https://stackoverflow.com/questions/11531121/add-duplicate-http-response-headers-in-nodejs applyHeaderMap(res, { "Cross-Origin-Resource-Policy": 'cross-origin', + "Cross-Origin-Embedder-Policy": 'require-corp', }); } - // Don't set CSP headers on /api/config because they aren't necessary and they cause problems + // Don't set CSP headers on /api/ endpoints + // because they aren't necessary and they cause problems // when duplicated by NGINX in production environments - if (/^\/api\/(broadcast|config)/.test(req.url)) { - /* - if (Env.NO_SANDBOX) { - applyHeaderMap(res, { - "Cross-Origin-Resource-Policy": 'cross-origin', - }); - } - */ - return; - } + if (/^\/api\/(broadcast|config)/.test(req.url)) { return; } + applyHeaderMap(res, { "Cross-Origin-Resource-Policy": 'cross-origin', }); diff --git a/www/admin/inner.js b/www/admin/inner.js index ffc9a561c..3956d7e50 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -16,6 +16,7 @@ define([ '/support/ui.js', '/lib/datepicker/flatpickr.js', + '/bower_components/tweetnacl/nacl-fast.min.js', 'css!/lib/datepicker/flatpickr.min.css', 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', @@ -44,6 +45,7 @@ define([ 'instanceStatus': {} }; + var Nacl = window.nacl; var common; var sFrameChan; @@ -54,6 +56,7 @@ define([ 'cp-admin-archive', 'cp-admin-unarchive', 'cp-admin-registration', + 'cp-admin-email' ], 'quota': [ // Msg.admin_cat_quota 'cp-admin-defaultlimit', @@ -71,7 +74,8 @@ define([ ], 'support': [ // Msg.admin_cat_support 'cp-admin-support-list', - 'cp-admin-support-init' + 'cp-admin-support-init', + 'cp-admin-support-priv', ], 'broadcast': [ // Msg.admin_cat_broadcast 'cp-admin-maintenance', @@ -267,8 +271,11 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ADMIN_DECREE', data: ['RESTRICT_REGISTRATION', [val]] - }, function (e) { - if (e) { UI.warn(Messages.error); console.error(e); } + }, function (e, response) { + if (e || response.error) { + UI.warn(Messages.error); + console.error(e, response); + } APP.updateStatus(function () { spinner.done(); state = APP.instanceStatus.restrictRegistration; @@ -282,6 +289,45 @@ define([ return $div; }; + create['email'] = function () { + var key = 'email'; + var $div = makeBlock(key, true); // Msg.admin_emailHint, Msg.admin_emailTitle, Msg.admin_emailButton + var $button = $div.find('button'); + + var input = h('input', { + type: 'email', + value: ApiConfig.adminEmail || '' + }); + var $input = $(input); + var innerDiv = h('div.cp-admin-setlimit-form', input); + var spinner = UI.makeSpinner($(innerDiv)); + + $button.click(function () { + if (!$input.val()) { return; } + spinner.spin(); + $button.attr('disabled', 'disabled'); + sFrameChan.query('Q_ADMIN_RPC', { + cmd: 'ADMIN_DECREE', + data: ['SET_ADMIN_EMAIL', [$input.val()]] + }, function (e, response) { + $button.removeAttr('disabled'); + if (e || response.error) { + UI.warn(Messages.error); + $input.val(''); + console.error(e, response); + spinner.hide(); + return; + } + spinner.done(); + UI.log(Messages.saved); + }); + }); + + $button.before(innerDiv); + + return $div; + }; + var getPrettySize = UIElements.prettySize; create['defaultlimit'] = function () { @@ -316,8 +362,11 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ADMIN_DECREE', data: ['UPDATE_DEFAULT_STORAGE', data] - }, function (e) { - if (e) { UI.warn(Messages.error); return void console.error(e); } + }, function (e, response) { + if (e || response.error) { + UI.warn(Messages.error); + return void console.error(e, response); + } var limit = getPrettySize(l); $div.find('.cp-admin-defaultlimit-value').text(Messages._getKey('admin_limit', [limit])); }); @@ -448,8 +497,12 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ADMIN_DECREE', data: ['RM_QUOTA', data] - }, function (e) { - if (e) { UI.warn(Messages.error); console.error(e); } + }, function (e, response) { + if (e || response.error) { + UI.warn(Messages.error); + console.error(e, response); + return; + } APP.refreshLimits(); $key.val(''); }); @@ -462,8 +515,12 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ADMIN_DECREE', data: ['SET_QUOTA', data] - }, function (e) { - if (e) { UI.warn(Messages.error); console.error(e); } + }, function (e, response) { + if (e || response.error) { + UI.warn(Messages.error); + console.error(e, response); + return; + } APP.refreshLimits(); $key.val(''); }); @@ -637,8 +694,13 @@ define([ }; var supportKey = ApiConfig.supportMailbox; + var checkAdminKey = function (priv) { + if (!supportKey) { return; } + return Hash.checkBoxKeyPair(priv, supportKey); + }; + create['support-list'] = function () { - if (!supportKey || !APP.privateKey) { return; } + if (!supportKey || !APP.privateKey || !checkAdminKey(APP.privateKey)) { return; } var $container = makeBlock('support-list'); // Msg.admin_supportListHint, .admin_supportListTitle var $div = $(h('div.cp-support-container')).appendTo($container); @@ -898,16 +960,63 @@ define([ return $container; }; + create['support-priv'] = function () { + if (!supportKey || !APP.privateKey || !checkAdminKey(APP.privateKey)) { return; } - var checkAdminKey = function (priv) { - if (!supportKey) { return; } - return Hash.checkBoxKeyPair(priv, supportKey); + var $div = makeBlock('support-priv', true); // Msg.admin_supportPrivHint, .admin_supportPrivTitle, .admin_supportPrivButton + var $button = $div.find('button').click(function () { + $button.remove(); + var $selectable = $(UI.dialog.selectable(APP.privateKey)).css({ 'max-width': '28em' }); + $div.append($selectable); + }); + return $div; }; - create['support-init'] = function () { var $div = makeBlock('support-init'); // Msg.admin_supportInitHint, .admin_supportInitTitle if (!supportKey) { - $div.append(h('p', Messages.admin_supportInitHelp)); + (function () { + $div.append(h('p', Messages.admin_supportInitHelp)); + var button = h('button.btn.btn-primary', Messages.admin_supportInitGenerate); + var $button = $(button).appendTo($div); + $div.append($button); + var spinner = UI.makeSpinner($div); + $button.click(function () { + spinner.spin(); + $button.attr('disabled', 'disabled'); + var keyPair = Nacl.box.keyPair(); + var pub = Nacl.util.encodeBase64(keyPair.publicKey); + var priv = Nacl.util.encodeBase64(keyPair.secretKey); + // Store the private key first. It won't be used until the decree is accepted. + sFrameChan.query("Q_ADMIN_MAILBOX", priv, function (err, obj) { + if (err || (obj && obj.error)) { + console.error(err || obj.error); + UI.warn(Messages.error); + spinner.hide(); + return; + } + // Then send the decree + sFrameChan.query('Q_ADMIN_RPC', { + cmd: 'ADMIN_DECREE', + data: ['SET_SUPPORT_MAILBOX', [pub]] + }, function (e, response) { + $button.removeAttr('disabled'); + if (e || response.error) { + UI.warn(Messages.error); + console.error(e, response); + spinner.hide(); + return; + } + spinner.done(); + UI.log(Messages.saved); + supportKey = pub; + APP.privateKey = priv; + $('.cp-admin-support-init').hide(); + APP.$rightside.append(create['support-list']()); + APP.$rightside.append(create['support-priv']()); + }); + }); + }); + })(); return $div; } if (!APP.privateKey || !checkAdminKey(APP.privateKey)) { @@ -937,6 +1046,7 @@ define([ APP.privateKey = key; $('.cp-admin-support-init').hide(); APP.$rightside.append(create['support-list']()); + APP.$rightside.append(create['support-priv']()); }); }); return $div; @@ -1030,9 +1140,10 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ADMIN_DECREE', data: ['SET_LAST_BROADCAST_HASH', [lastHash]] - }, function (e) { - if (e) { - console.error(e); + }, function (e, response) { + if (e || response.error) { + UI.warn(Messages.error); + console.error(e, response); return; } console.log('lastBroadcastHash updated'); @@ -1299,19 +1410,23 @@ define([ var $start = $(start); var $end = $(end); var is24h = false; + var dateFormat = "Y-m-d H:i"; try { is24h = !new Intl.DateTimeFormat(navigator.language, { hour: 'numeric' }).format(0).match(/AM/); } catch (e) {} + if (!is24h) { dateFormat = "Y-m-d h:i K"; } var endPickr = Flatpickr(end, { enableTime: true, time_24hr: is24h, + dateFormat: dateFormat, minDate: new Date() }); Flatpickr(start, { enableTime: true, time_24hr: is24h, minDate: new Date(), + dateFormat: dateFormat, onChange: function () { endPickr.set('minDate', new Date($start.val())); } @@ -1336,9 +1451,10 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ADMIN_DECREE', data: ['SET_MAINTENANCE', [data]] - }, function (e) { - if (e) { - UI.warn(Messages.error); console.error(e); + }, function (e, response) { + if (e || response.error) { + UI.warn(Messages.error); + console.error(e, response); $button.prop('disabled', ''); return; } @@ -1430,10 +1546,11 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ADMIN_DECREE', data: ['SET_SURVEY_URL', [data]] - }, function (e) { - if (e) { + }, function (e, response) { + if (e || response.error) { $button.prop('disabled', ''); - UI.warn(Messages.error); console.error(e); + UI.warn(Messages.error); + console.error(e, response); return; } // Maintenance applied, send notification @@ -1529,11 +1646,12 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'GET_WORKER_PROFILES', }, function (e, data) { - if (e) { return void console.error(e); } + if (e || data.error) { + UI.warn(Messages.error); + return void console.error(e, data); + } //console.info(data); $div.find("table").remove(); - - process(data); $div.append(table); }); diff --git a/www/checkup/app-checkup.less b/www/checkup/app-checkup.less index 40e6a1add..a475d5983 100644 --- a/www/checkup/app-checkup.less +++ b/www/checkup/app-checkup.less @@ -20,7 +20,7 @@ html, body { } .pending { - border: 1px solid white; + border: 1px solid @cryptpad_text_col; .fa { margin-right: 20px; } @@ -45,7 +45,7 @@ html, body { table { td { padding: 5px; - border: 1px solid white; + border: 1px solid @cryptpad_text_col; } } diff --git a/www/checkup/main.js b/www/checkup/main.js index aeb8a4239..2beee1229 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -50,6 +50,10 @@ define([ ]); }; + var cacheBuster = function (url) { + return url + '?test=' + (+new Date()); + }; + var trimmedSafe = trimSlashes(ApiConfig.httpSafeOrigin); var trimmedUnsafe = trimSlashes(ApiConfig.httpUnsafeOrigin); @@ -117,7 +121,7 @@ define([ var checkAvailability = function (url, cb) { $.ajax({ - url: url, + url: cacheBuster(url), data: {}, complete: function (xhr) { cb(xhr.status === 200); @@ -169,10 +173,13 @@ define([ }).nThen(function () { // Iframe is loaded clearTimeout(to); + console.log("removing sandbox iframe"); + $('iframe#sbox-iframe').remove(); cb(true); }); }); + var shared_websocket; // Test Websocket var evWSError = Util.mkEvent(true); assert(function (_cb, msg) { @@ -185,6 +192,7 @@ define([ })); var ws = new WebSocket(NetConfig.getWebsocketURL()); + shared_websocket = ws; var to = setTimeout(function () { console.error('Websocket TIMEOUT'); evWSError.fire(); @@ -203,6 +211,7 @@ define([ }); // Test login block + var shared_realtime; assert(function (_cb, msg) { var websocketErr = "No WebSocket available"; var cb = Util.once(Util.both(_cb, function (status) { @@ -237,7 +246,7 @@ define([ var blockUrl = Login.Block.getBlockUrl(opt.blockKeys); var blockRequest = Login.Block.serialize("{}", opt.blockKeys); var removeRequest = Login.Block.remove(opt.blockKeys); - console.log('Test block URL:', blockUrl); + console.warn('Testing block URL (%s). One 404 is normal.', blockUrl); var userHash = '/2/drive/edit/000000000000000000000000'; var secret = Hash.getSecrets('drive', userHash); @@ -264,7 +273,7 @@ define([ console.error("Can't create new channel. This may also be a websocket issue."); return void cb(false); } - RT = rt; + shared_realtime = RT = rt; var proxy = rt.proxy; proxy.edPublic = opt.edPublic; proxy.edPrivate = opt.edPrivate; @@ -330,14 +339,13 @@ define([ }).nThen(function () { cb(true); }); - }); var sheetURL = '/common/onlyoffice/v4/web-apps/apps/spreadsheeteditor/main/index.html'; assert(function (cb, msg) { msg.innerText = "Missing HTTP headers required for .xlsx export from sheets. "; - var url = sheetURL; + var url = cacheBuster(sheetURL); var expect = { 'cross-origin-resource-policy': 'cross-origin', 'cross-origin-embedder-policy': 'require-corp', @@ -366,37 +374,12 @@ define([ }); assert(function (cb, msg) { - msg = msg; - return void cb(true); - /* - msg.appendChild(h('span', [ - "The spreadsheet editor's code was not served with the required Content-Security Policy headers. ", - "This is most often caused by incorrectly configured sandbox parameters (", - h('code', 'httpUnsafeOrigin'), - ' and ', - h('code', 'httpSafeOrigin'), - ' in ', - CONFIG_PATH, - "), or settings in your reverse proxy's configuration which don't match your application server's config. ", - RESTART_WARNING(), - ])); - - $.ajax(sheetURL, { + msg.innerText = "Missing HTTP header required to disable Google's Floc."; + $.ajax('/?'+ (+new Date()), { complete: function (xhr) { - var csp = xhr.getResponseHeader('Content-Security-Policy'); - if (!/unsafe\-eval/.test(csp)) { - // OnlyOffice requires unsafe-eval - console.error('CSP', csp); - return cb("expected 'unsafe-eval'"); - } - if (!/unsafe\-inline/.test(csp)) { - // OnlyOffice also requires unsafe-inline - console.error('CSP', csp); - return cb("expected 'unsafe-inline'"); - } - cb(true); + cb(xhr.getResponseHeader('permissions-policy') === 'interest-cohort=()'); }, - }); */ + }); }); assert(function (cb, msg) { @@ -407,17 +390,20 @@ define([ "Your browser console may provide more details as to why this resource could not be loaded. ", ])); - $.ajax('/api/broadcast', { + $.ajax(cacheBuster('/api/broadcast'), { dataType: 'text', complete: function (xhr) { - console.log(xhr); cb(xhr.status === 200); }, }); }); - var checkAPIHeaders = function (url, cb) { - $.ajax(url, { + var code = function (content) { + return h('code', content); + }; + + var checkAPIHeaders = function (url, msg, cb) { + $.ajax(cacheBuster(url), { dataType: 'text', complete: function (xhr) { var allHeaders = xhr.getAllResponseHeaders(); @@ -436,15 +422,31 @@ define([ var expect = { 'cross-origin-resource-policy': 'cross-origin', + 'cross-origin-embedder-policy': 'require-corp', }; - var incorrect = Object.keys(expect).some(function (k) { + var incorrect = false; + + Object.keys(expect).forEach(function (k) { var response = xhr.getResponseHeader(k); - if (response !== expect[k]) { - return true; + var expected = expect[k]; + if (response !== expected) { + incorrect = true; + msg.appendChild(h('p', [ + 'The ', + code(k), + ' header for ', + code(url), + " is '", + code(response), + "' instead of '", + code(expected), + "' as expected.", + ])); + } }); - if (duplicated || incorrect) { console.error(allHeaders); } + if (duplicated || incorrect) { console.debug(allHeaders); } cb(!duplicated && !incorrect); }, }); @@ -455,13 +457,13 @@ define([ assert(function (cb, msg) { var url = '/api/config'; msg.innerText = url + INCORRECT_HEADER_TEXT; - checkAPIHeaders(url, cb); + checkAPIHeaders(url, msg, cb); }); assert(function (cb, msg) { var url = '/api/broadcast'; msg.innerText = url + INCORRECT_HEADER_TEXT; - checkAPIHeaders(url, cb); + checkAPIHeaders(url, msg, cb); }); var setWarningClass = function (msg) { @@ -479,8 +481,9 @@ define([ 'This instance does not provide a valid ', h('code', 'adminEmail'), ' which can make it difficult to contact its adminstrator to report vulnerabilities or abusive content.', - ' This can be configured in ', CONFIG_PATH(), '. ', - RESTART_WARNING(), + " This can be configured on your instance's admin panel. Use the provided ", + code("Flush cache'"), + " button for this change to take effect for all users.", ])); cb(email); }); @@ -517,6 +520,156 @@ define([ cb(false); }); + var response = Util.response(function (err) { + console.error('SANDBOX_ERROR', err); + }); + + var sandboxIframe = h('iframe', { + class: 'sandbox-test', + src: cacheBuster(trimmedSafe + '/checkup/sandbox/index.html'), + }); + document.body.appendChild(sandboxIframe); + + var sandboxIframeReady = Util.mkEvent(true); + setTimeout(function () { + sandboxIframeReady.fire("TIMEOUT"); + }, 10 * 1000); + + var postMessage = function (content, cb) { + try { + var txid = Util.uid(); + content.txid = txid; + response.expect(txid, cb, 15000); + sandboxIframe.contentWindow.postMessage(JSON.stringify(content), '*'); + } catch (err) { + console.error(err); + } + }; + + var deferredPostMessage = function (content, _cb) { + var cb = Util.once(Util.mkAsync(_cb)); + nThen(function (w) { + sandboxIframeReady.reg(w(function (err) { + if (!err) { return; } + w.abort(); + cb(err); + })); + }).nThen(function () { + postMessage(content, cb); + }); + }; + + window.addEventListener('message', function (event) { + try { + var msg = JSON.parse(event.data); + if (msg.command === 'READY') { return void sandboxIframeReady.fire(); } + if (msg.q === "READY") { return; } // ignore messages from the usual sandboxed iframe + var txid = msg.txid; + if (!txid) { return console.log("no handler for ", txid); } + response.handle(txid, msg.content); + } catch (err) { + console.error(event); + console.error(err); + } + }); + + var parseCSP = function (CSP) { + //console.error(CSP); + var CSP_headers = {}; + CSP.split(";") + .forEach(function (rule) { + rule = (rule || "").trim(); + if (!rule) { return; } + var parts = rule.split(/\s/); + var first = parts[0]; + var rest = rule.slice(first.length + 1); + CSP_headers[first] = rest; + //console.error(rule.trim()); + //console.info("[%s] '%s'", first, rest); + }); + return CSP_headers; + }; + + var hasUnsafeEval = function (CSP_headers) { + return /unsafe\-eval/.test(CSP_headers['script-src']); + }; + + var hasUnsafeInline = function (CSP_headers) { + return /unsafe\-inline/.test(CSP_headers['script-src']); + }; + + var hasOnlyOfficeHeaders = function (CSP_headers) { + if (!hasUnsafeEval(CSP_headers)) { + console.error("NO_UNSAFE_EVAL"); + console.log(CSP_headers); + return false; + } + if (!hasUnsafeInline(CSP_headers)) { + console.error("NO_UNSAFE_INLINE"); + return void false; + } + return true; + }; + + var CSP_WARNING = function (url) { + return h('span', [ + code(url), + ' does not have the required ', + code("'content-security-policy'"), + ' headers set. This is most often related to incorrectly configured sandbox domains or reverse proxies.', + ]); + }; + + assert(function (_cb, msg) { + var url = '/sheet/inner.html'; + var cb = Util.once(Util.mkAsync(_cb)); + msg.appendChild(CSP_WARNING(url)); + deferredPostMessage({ + command: 'GET_HEADER', + content: { + url: url, + header: 'content-security-policy', + }, + }, function (content) { + var CSP_headers = parseCSP(content); + cb(hasOnlyOfficeHeaders(CSP_headers)); + }); + }); + + assert(function (cb, msg) { + var url = '/common/onlyoffice/v4/web-apps/apps/spreadsheeteditor/main/index.html'; + msg.appendChild(CSP_WARNING(url)); + deferredPostMessage({ + command: 'GET_HEADER', + content: { + url: url, + header: 'content-security-policy', + }, + }, function (content) { + var CSP_headers = parseCSP(content); + cb(hasOnlyOfficeHeaders(CSP_headers)); + }); + }); + + assert(function (cb, msg) { + var url = '/sheet/inner.html'; + msg.appendChild(h('span', [ + code(url), + ' does not have the required ', + code("'cross-origin-opener-policy'"), + ' headers set.', + ])); + deferredPostMessage({ + command: 'GET_HEADER', + content: { + url: url, + header: 'cross-origin-opener-policy', + }, + }, function (content) { + cb(content === 'same-origin'); + }); + }); + if (false) { assert(function (cb, msg) { msg.innerText = 'fake test to simulate failure'; @@ -583,6 +736,14 @@ define([ $progress.remove(); $('body').prepend(report); + try { + console.log('closing shared websocket'); + shared_websocket.close(); + } catch (err) { console.error(err); } + try { + console.log('closing shared realtime'); + shared_realtime.network.disconnect(); + } catch (err) { console.error(err); } }, function (i, total) { console.log('test '+ i +' completed'); completed++; diff --git a/www/checkup/sandbox/index.html b/www/checkup/sandbox/index.html new file mode 100644 index 000000000..04a9502d3 --- /dev/null +++ b/www/checkup/sandbox/index.html @@ -0,0 +1,11 @@ + + + + + + + + +
+ + diff --git a/www/checkup/sandbox/main.js b/www/checkup/sandbox/main.js new file mode 100644 index 000000000..7ddfb07f8 --- /dev/null +++ b/www/checkup/sandbox/main.js @@ -0,0 +1,50 @@ +define([ + 'jquery', + + '/bower_components/tweetnacl/nacl-fast.min.js', + 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', + 'less!/checkup/app-checkup.less', +], function ($) { + var postMessage = function (content) { + window.parent.postMessage(JSON.stringify(content), '*'); + }; + postMessage({ command: "READY", }); + var getHeaders = function (url, cb) { + $.ajax(url + "?test=" + (+new Date()), { + dataType: 'text', + complete: function (xhr) { + var allHeaders = xhr.getAllResponseHeaders(); + return void cb(void 0, allHeaders, xhr); + }, + }); + }; + var COMMANDS = {}; + COMMANDS.GET_HEADER = function (content, cb) { + var url = content.url; + getHeaders(url, function (err, headers, xhr) { + cb(xhr.getResponseHeader(content.header)); + }); + }; + + window.addEventListener("message", function (event) { + if (event && event.data) { + try { + //console.log(JSON.parse(event.data)); + var msg = JSON.parse(event.data); + var command = msg.command; + var txid = msg.txid; + COMMANDS[command](msg.content, function (response) { + // postMessage with same txid + postMessage({ + txid: txid, + content: response, + }); + }); + } catch (err) { + console.error(err); + } + } else { + console.error(event); + } + }); +}); diff --git a/www/common/application_config_internal.js b/www/common/application_config_internal.js index c7b66c1e5..dcfb3872b 100644 --- a/www/common/application_config_internal.js +++ b/www/common/application_config_internal.js @@ -4,14 +4,14 @@ * file (make a copy from /customize.dist/application_config.js) */ define(function() { - var config = {}; + var AppConfig = {}; /* Select the buttons displayed on the main page to create new collaborative sessions. * Removing apps from the list will prevent users from accessing them. They will instead be * redirected to the drive. * You should never remove the drive from this list. */ - config.availablePadTypes = ['drive', 'teams', 'pad', 'sheet', 'code', 'slide', 'poll', 'kanban', 'whiteboard', + AppConfig.availablePadTypes = ['drive', 'teams', 'pad', 'sheet', 'code', 'slide', 'poll', 'kanban', 'whiteboard', /*'doc', 'presentation',*/ 'file', /*'todo',*/ 'contacts' /*, 'calendar' */]; /* The registered only types are apps restricted to registered users. * You should never remove apps from this list unless you know what you're doing. The apps @@ -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', 'notifications', 'support']; + AppConfig.registeredOnlyTypes = ['file', 'contacts', 'notifications', 'support']; /* 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 @@ -30,37 +30,37 @@ define(function() { * can be found at the top of the file `/customize.dist/messages.js`. The list should only * contain languages code ('en', 'fr', 'de', 'pt-br', etc.), not their full name. */ - //config.availableLanguages = ['en', 'fr', 'de']; + //AppConfig.availableLanguages = ['en', 'fr', 'de']; /* You can display a link to the imprint (legal notice) of your website in the static pages * footer. To do so, you can either set the following value to `true` and create an imprint.html page * in the `customize` directory. You can also set it to an absolute URL if your imprint page already exists. */ - config.imprint = false; - // config.imprint = true; - // config.imprint = 'https://xwiki.com/en/company/legal-notice'; + AppConfig.imprint = false; + // AppConfig.imprint = true; + // AppConfig.imprint = 'https://xwiki.com/en/company/legal-notice'; /* You can display a link to your own privacy policy in the static pages footer. * To do so, set the following value to the absolute URL of your privacy policy. */ - // config.privacy = 'https://xwiki.com/en/company/PrivacyPolicy'; + // AppConfig.privacy = 'https://xwiki.com/en/company/PrivacyPolicy'; /* We (the project's developers) include the ability to display a 'Roadmap' in static pages footer. * This is disabled by default. * We use this to publish the project's development roadmap, but you can use it however you like. * To do so, set the following value to an absolute URL. */ - //config.roadmap = 'https://cryptpad.fr/kanban/#/2/kanban/view/PLM0C3tFWvYhd+EPzXrbT+NxB76Z5DtZhAA5W5hG9wo/'; + //AppConfig.roadmap = 'https://cryptpad.fr/kanban/#/2/kanban/view/PLM0C3tFWvYhd+EPzXrbT+NxB76Z5DtZhAA5W5hG9wo/'; /* Cryptpad apps use a common API to display notifications to users * by default, notifications are hidden after 5 seconds * You can change their duration here (measured in milliseconds) */ - config.notificationTimeout = 5000; - config.disableUserlistNotifications = false; + AppConfig.notificationTimeout = 5000; + AppConfig.disableUserlistNotifications = false; // Update the default colors available in the whiteboard application - config.whiteboardPalette = [ + AppConfig.whiteboardPalette = [ '#000000', // black '#FFFFFF', // white '#848484', // grey @@ -82,14 +82,14 @@ define(function() { // Background color in the apps with centered content: // - file app in view mode // - rich text app when editor's width reduced in settings - config.appBackgroundColor = '#666'; + AppConfig.appBackgroundColor = '#666'; // Set enableTemplates to false to remove the button allowing users to save a pad as a template // and remove the template category in CryptDrive - config.enableTemplates = true; + AppConfig.enableTemplates = true; // Set enableHistory to false to remove the "History" button in all the apps. - config.enableHistory = true; + AppConfig.enableHistory = true; /* user passwords are hashed with scrypt, and salted with their username. this value will be appended to the username, causing the resulting hash @@ -101,15 +101,15 @@ define(function() { created. Changing it at a later time will break logins for all existing users. */ - config.loginSalt = ''; - config.minimumPasswordLength = 8; + AppConfig.loginSalt = ''; + AppConfig.minimumPasswordLength = 8; // Amount of time (ms) before aborting the session when the algorithm cannot synchronize the pad - config.badStateTimeout = 30000; + AppConfig.badStateTimeout = 30000; // Customize the icon used for each application. // You can update the colors by making a copy of /customize.dist/src/less2/include/colortheme.less - config.applicationsIcon = { + AppConfig.applicationsIcon = { file: 'cptools-file', fileupload: 'cptools-file-upload', folderupload: 'cptools-folder-upload', @@ -130,50 +130,49 @@ define(function() { // Ability to create owned pads and expiring pads through a new pad creation screen. // The new screen can be disabled by the users in their settings page - config.displayCreationScreen = true; + AppConfig.displayCreationScreen = true; // Prevent anonymous users from storing pads in their drive - config.disableAnonymousStore = false; + // NOTE: this is only enforced client-side as the server does not distinguish between users drives and pads + AppConfig.disableAnonymousStore = false; + // Prevent anonymous users from creating new pads (they can still access and edit existing ones) + // NOTE: this is only enforced client-side and will not prevent malicious clients from storing data + AppConfig.disableAnonymousPadCreation = false; // Hide the usage bar in settings and drive - //config.hideUsageBar = true; + //AppConfig.hideUsageBar = true; // Disable feedback for all the users and hide the settings part about feedback - //config.disableFeedback = true; - - // Add new options in the share modal (extend an existing tab or add a new tab). - // More info about how to use it on the wiki: - // https://github.com/xwiki-labs/cryptpad/wiki/Application-config#configcustomizeshareoptions - //config.customizeShareOptions = function (hashes, tabs, config) {}; + //AppConfig.disableFeedback = true; // Add code to be executed on every page before loading the user object. `isLoggedIn` (bool) is // indicating if the user is registered or anonymous. Here you can change the way anonymous users // work in CryptPad, use an external SSO or even force registration // *NOTE*: You have to call the `callback` function to continue the loading process - //config.beforeLogin = function(isLoggedIn, callback) {}; + //AppConfig.beforeLogin = function(isLoggedIn, callback) {}; // Add code to be executed on every page after the user object is loaded (also work for // unregistered users). This allows you to interact with your users' drive // *NOTE*: You have to call the `callback` function to continue the loading process - //config.afterLogin = function(api, callback) {}; + //AppConfig.afterLogin = function(api, callback) {}; // Disabling the profile app allows you to import the profile informations (display name, avatar) // from an external source and make sure the users can't change them from CryptPad. - // You can use config.afterLogin to import these values in the users' drive. - //config.disableProfile = true; + // You can use AppConfig.afterLogin to import these values in the users' drive. + //AppConfig.disableProfile = true; // Disable the use of webworkers and sharedworkers in CryptPad. // Workers allow us to run the websockets connection and open the user drive in a separate thread. // SharedWorkers allow us to load only one websocket and one user drive for all the browser tabs, // making it much faster to open new tabs. - config.disableWorkers = false; + AppConfig.disableWorkers = false; // Teams are always loaded during the initial loading screen (for the first tab only if // SharedWorkers are available). Allowing users to be members of multiple teams can // make them have a very slow loading time. To avoid impacting the user experience // significantly, we're limiting the number of teams per user to 3 by default. // You can change this value here. - //config.maxTeamsSlots = 5; + //AppConfig.maxTeamsSlots = 5; // Each team is considered as a registered user by the server. Users and teams are indistinguishable // in the database so teams will offer the same storage limits as users by default. @@ -181,7 +180,7 @@ define(function() { // We're limiting the number of teams each user is able to own to 1 in order to make sure // users don't use "fake" teams (1 member) just to increase their storage limit. // You can change the value here. - // config.maxOwnedTeams = 5; + // AppConfig.maxOwnedTeams = 5; // The userlist displayed in collaborative documents is stored alongside the document data. // Everytime someone with edit rights joins a document or modify their user data (display @@ -192,14 +191,14 @@ define(function() { // position of other users' cursor. You can configure the number of user from which the session // will enter into degraded mode. A big number may result in collaborative edition being broken, // but this number depends on the network and CPU performances of each user's device. - config.degradedLimit = 8; + AppConfig.degradedLimit = 8; // In "legacy" mode, one-time users were always creating an "anonymous" drive when visiting CryptPad // in which they could store their pads. The new "driveless" mode allow users to open an existing // pad without creating a drive in the background. The drive will only be created if they visit // a different page (Drive, Settings, etc.) or try to create a new pad themselves. You can disable // the driveless mode by changing the following value to "false" - config.allowDrivelessMode = true; + AppConfig.allowDrivelessMode = true; - return config; + return AppConfig; }); diff --git a/www/common/pdfjs/build/pdf.worker.js b/www/common/pdfjs/build/pdf.worker.js index 753d5c5d2..2e1bb365e 100644 --- a/www/common/pdfjs/build/pdf.worker.js +++ b/www/common/pdfjs/build/pdf.worker.js @@ -5355,10 +5355,12 @@ var PDFFunction = function PDFFunctionClosure() { var domain = IR[1]; var range = IR[2]; var code = IR[3]; +/* var compiled = new PostScriptCompiler().compile(code, domain, range); if (compiled) { return new Function('src', 'srcOffset', 'dest', 'destOffset', compiled); } +*/ (0, _util.info)('Unable to compile PS function'); var numOutputs = range.length >> 1; var numInputs = domain.length >> 1; @@ -38545,4 +38547,4 @@ if (typeof PDFJS === 'undefined' || !PDFJS.compatibilityChecked) { /***/ }) /******/ ]); }); -//# sourceMappingURL=pdf.worker.js.map \ No newline at end of file +//# sourceMappingURL=pdf.worker.js.map diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index 70c239d67..227656040 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -448,6 +448,9 @@ define([ } }; funcs.createPad = function (cfg, cb) { + if (AppConfig.disableAnonymousPadCreation && !funcs.isLoggedIn()) { + return void UI.errorLoadingScreen(Messages.mustLogin); + } ctx.sframeChan.query("Q_CREATE_PAD", { owned: cfg.owned, expire: cfg.expire, diff --git a/www/lib/calendar/date-picker.js b/www/lib/calendar/date-picker.js index bc42dbbeb..d66e98c3d 100644 --- a/www/lib/calendar/date-picker.js +++ b/www/lib/calendar/date-picker.js @@ -9,14 +9,17 @@ define([ var end = cfg.endpicker; var is24h = false + var dateFormat = "Y-m-d H:i"; try { is24h = !new Intl.DateTimeFormat(navigator.language, { hour: 'numeric' }).format(0).match(/AM/); } catch (e) {} + if (!is24h) { dateFormat = "Y-m-d h:i K"; } var e = $(end.input)[0]; var endPickr = Flatpickr(e, { enableTime: true, time_24hr: is24h, + dateFormat: dateFormat, minDate: start.date }); endPickr.setDate(end.date); @@ -25,6 +28,7 @@ define([ var startPickr = Flatpickr(s, { enableTime: true, time_24hr: is24h, + dateFormat: dateFormat, onChange: function () { endPickr.set('minDate', startPickr.parseDate(s.value)); }