diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b6a59683..ea1d3ced2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ * reminders in calendars * import/export * include LICENSE for ical.js + * translations + * out of BETA + * available from user admin menu * use a specific version of bootstrap-tokenfield in bower.json * don't create readmes * support displaying a roadmap in static pages' footer @@ -18,6 +21,12 @@ * lock sheets faster when applying checkpoints * guard against undefined checkpoints * don't spam users with prompts to checkpoints when they can't +* decrees + * SET_ADMIN_EMAIL + * SET_SUPPORT_MAILBOX +* Add DAPSI to our sponsor list +* checkup + * check for duplicate or incorrect headers # 4.4.0 diff --git a/lib/commands/core.js b/lib/commands/core.js index 42ed0455b..a0be7cd67 100644 --- a/lib/commands/core.js +++ b/lib/commands/core.js @@ -13,6 +13,10 @@ Core.isValidId = function (chan) { [32, 33, 48].indexOf(chan.length) > -1; }; +Core.isValidPublicKey = function (owner) { + return typeof(owner) === 'string' && owner.length === 44; +}; + var makeToken = Core.makeToken = function () { return Number(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)) .toString(16); diff --git a/lib/decrees.js b/lib/decrees.js index fca8fb331..eed840057 100644 --- a/lib/decrees.js +++ b/lib/decrees.js @@ -1,4 +1,5 @@ var Decrees = module.exports; +var Core = require("./commands/core"); /* Admin decrees which modify global server state @@ -29,6 +30,10 @@ SET_LAST_BROADCAST_HASH SET_SURVEY_URL SET_MAINTENANCE +// EASIER CONFIG +SET_ADMIN_EMAIL +SET_SUPPORT_MAILBOX + NOT IMPLEMENTED: // RESTRICTED REGISTRATION @@ -37,9 +42,11 @@ REVOKE_INVITE REDEEM_INVITE // 2.0 -Env.adminEmail -Env.supportMailbox Env.DEV_MODE || Env.FRESH_MODE, + +ADD_ADMIN_KEY +RM_ADMIN_KEY + */ var commands = {}; @@ -88,6 +95,20 @@ var isNonNegativeNumber = function (n) { }; */ +var default_validator = function () { return true; }; +var makeGenericSetter = function (attr, validator) { + validator = validator || default_validator; + return function (Env, args) { + if (!validator(args)) { + throw new Error("INVALID_ARGS"); + } + var value = args[0]; + if (value === Env[attr]) { return false; } + Env[attr] = value; + return true; + }; +}; + var isInteger = function (n) { return !(typeof(n) !== 'number' || isNaN(n) || (n % 1) !== 0); }; @@ -97,15 +118,7 @@ var args_isInteger = function (args) { }; var makeIntegerSetter = function (attr) { - return function (Env, args) { - if (!args_isInteger(args)) { - throw new Error('INVALID_ARGS'); - } - var integer = args[0]; - if (integer === Env[attr]) { return false; } - Env[attr] = integer; - return true; - }; + return makeGenericSetter(attr, args_isInteger); }; // CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_MAX_UPLOAD_SIZE', [50 * 1024 * 1024]]], console.log) @@ -130,6 +143,14 @@ var args_isString = function (args) { return Array.isArray(args) && typeof(args[0]) === "string"; }; +// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_ADMIN_EMAIL', ['admin@website.tld']]], console.log) +commands.SET_ADMIN_EMAIL = makeGenericSetter('adminEmail', args_isString); + +// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_SUPPORT_MAILBOX', ["Tdz6+fE9N9XXBY93rW5qeNa/k27yd40c0vq7EJyt7jA="]]], console.log) +commands.SET_SUPPORT_MAILBOX = makeGenericSetter('supportMailbox', function (args) { + return args_isString(args) && Core.isValidPublicKey(args[0]); +}); + // Maintenance: Empty string or an object with a start and end time var isNumber = function (value) { return typeof(value) === "number" && !isNaN(value); diff --git a/lib/metadata.js b/lib/metadata.js index 97f2e484a..d320a5c9b 100644 --- a/lib/metadata.js +++ b/lib/metadata.js @@ -1,4 +1,5 @@ var Meta = module.exports; +var Core = require("./commands/core"); var deduplicate = require("./common-util").deduplicateString; @@ -35,9 +36,7 @@ the owners field is guaranteed to exist. var commands = {}; -var isValidPublicKey = function (owner) { - return typeof(owner) === 'string' && owner.length === 44; -}; +var isValidPublicKey = Core.isValidPublicKey; // isValidPublicKey is a better indication of what the above function does // I'm preserving this function name in case we ever want to expand its diff --git a/server.js b/server.js index 4a107b785..978070b01 100644 --- a/server.js +++ b/server.js @@ -105,13 +105,15 @@ var setHeaders = (function () { } if (Object.keys(headers).length) { return function (req, res) { + // apply a bunch of cross-origin headers for XLSX export in FF and printing elsewhere applyHeaderMap(res, { "Cross-Origin-Opener-Policy": /^\/sheet\//.test(req.url)? 'same-origin': '', "Cross-Origin-Embedder-Policy": 'require-corp', }); - if (Env.NO_SANDBOX) { + 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', }); @@ -120,11 +122,13 @@ var setHeaders = (function () { // Don't set CSP headers on /api/config 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) { + /* + if (Env.NO_SANDBOX) { applyHeaderMap(res, { "Cross-Origin-Resource-Policy": 'cross-origin', }); } + */ return; } applyHeaderMap(res, { diff --git a/www/checkup/main.js b/www/checkup/main.js index f02b11c9e..f8b7e5612 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -365,7 +365,7 @@ define([ }); assert(function (cb, msg) { - msg = msg; // XXX + msg = msg; return void cb(true); /* msg.appendChild(h('span', [ @@ -415,6 +415,58 @@ define([ }); }); + var checkAPIHeaders = function (url, cb) { + $.ajax(url, { + dataType: 'text', + complete: function (xhr) { + var allHeaders = xhr.getAllResponseHeaders(); + console.error(allHeaders); + + var headers = {}; + + var duplicated = allHeaders.split('\n').some(function (header) { + var duplicate; + header.replace(/([^:]+):(.*)/, function (all, type, value) { + type = type.trim(); + if (typeof(headers[type]) !== 'undefined') { + duplicate = true; + } + headers[type] = value.trim(); + }); + return duplicate; + }); + + if (duplicated) { return void cb(false); } + + var expect = { + 'cross-origin-resource-policy': 'cross-origin', + }; + var incorrect = Object.keys(expect).some(function (k) { + var response = xhr.getResponseHeader(k); + if (response !== expect[k]) { + return true; + } + }); + + cb(!incorrect); + }, + }); + }; + + var INCORRECT_HEADER_TEXT = ' was served with duplicated or incorrect headers. Compare your reverse-proxy configuration against the provided example.'; + + assert(function (cb, msg) { + var url = '/api/config'; + msg.innerText = url + INCORRECT_HEADER_TEXT; + checkAPIHeaders(url, cb); + }); + + assert(function (cb, msg) { + var url = '/api/broadcast'; + msg.innerText = url + INCORRECT_HEADER_TEXT; + checkAPIHeaders(url, cb); + }); + var row = function (cells) { return h('tr', cells.map(function (cell) { return h('td', cell); diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 5ddc48641..a89092e55 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -798,7 +798,6 @@ define([ ])).click(common.prepareFeedback(type)).click(function () { $(button).hide(); common.getSframeChannel().query("Q_AUTOSTORE_STORE", null, function (err, obj) { - waitingForStoringCb = false; var error = err || (obj && obj.error); if (error) { $(button).show(); diff --git a/www/common/diffMarked.js b/www/common/diffMarked.js index f2fb97f24..cb95f37aa 100644 --- a/www/common/diffMarked.js +++ b/www/common/diffMarked.js @@ -40,6 +40,7 @@ define([ Mermaid = _Mermaid; Mermaid.initialize({ gantt: { axisFormat: '%m-%d', }, + flowchart: { htmlLabels: false, }, theme: (window.CryptPad_theme === 'dark') ? 'dark' : 'default', "themeCSS": mermaidThemeCSS, }); diff --git a/www/common/inner/common-mediatag.js b/www/common/inner/common-mediatag.js index 775f1f34a..9f7506d61 100644 --- a/www/common/inner/common-mediatag.js +++ b/www/common/inner/common-mediatag.js @@ -386,11 +386,11 @@ define([ 'tabindex': '-1', 'data-icon': "fa-eye", }, Messages.pad_mediatagPreview)), - h('li.cp-svg', h('a.cp-app-code-context-openin.dropdown-item', { + h('li', h('a.cp-app-code-context-openin.dropdown-item', { 'tabindex': '-1', 'data-icon': "fa-external-link", }, Messages.pad_mediatagOpen)), - h('li.cp-svg', h('a.cp-app-code-context-share.dropdown-item', { + h('li', h('a.cp-app-code-context-share.dropdown-item', { 'tabindex': '-1', 'data-icon': "fa-shhare-alt", }, Messages.pad_mediatagShare)), @@ -398,7 +398,7 @@ define([ 'tabindex': '-1', 'data-icon': "fa-cloud-upload", }, Messages.pad_mediatagImport)), - h('li', h('a.cp-app-code-context-download.dropdown-item', { + h('li.cp-svg', h('a.cp-app-code-context-download.dropdown-item', { 'tabindex': '-1', 'data-icon': "fa-download", }, Messages.download_mt_button)), @@ -429,6 +429,52 @@ define([ common.importMediaTag($mt); } else if ($this.hasClass("cp-app-code-context-download")) { + if ($mt.is('pre.mermaid') || $mt.is('pre.markmap')) { + (function () { + var name = 'image.svg'; // XXX + var svg = $mt.find('svg')[0].cloneNode(true); + $(svg).attr('xmlns', 'http://www.w3.org/2000/svg').attr('width', $mt.width()).attr('height', $mt.height()); + $(svg).find('foreignObject').each(function (i, el) { + var $el = $(el); + $el.find('br').after('\n'); + $el.find('br').remove(); + var t = $el[0].innerText || $el[0].textContent; + t.split('\n').forEach(function (text, i) { + var dy = (i+1)+'em'; + $el.after(h('text', {y:0, dy:dy, style: ''}, text)); + }); + $el.remove(); + }); + var html = svg.outerHTML; + html = html.replace('
', '
'); + var b = new Blob([html], { type: 'image/svg+xml' }); + window.saveAs(b, name); + })(); + return; + } + if ($mt.is('pre.mathjax')) { + (function () { + var name = 'image.png'; // XXX + var svg = $mt.find('> span > svg')[0]; + var clone = svg.cloneNode(true); + var html = clone.outerHTML; + var b = new Blob([html], { type: 'image/svg+xml' }); + var blobURL = URL.createObjectURL(b); + var i = new Image(); + i.onload = function () { + var canvas = document.createElement('canvas'); + canvas.width = i.width; + canvas.height = i.height; + var context = canvas.getContext('2d'); + context.drawImage(i, 0, 0, i.width, i.height); + canvas.toBlob(function (blob) { + window.saveAs(blob, name); + }); + }; + i.src = blobURL; + })(); + return; + } var media = Util.find($mt, [0, '_mediaObject']); if (!media) { return void console.error('no media'); } if (!media.complete) { return void UI.warn(Messages.mediatag_notReady); }