diff --git a/customize.dist/src/less2/include/alertify.less b/customize.dist/src/less2/include/alertify.less index 9cd904c45..4bd5e298b 100644 --- a/customize.dist/src/less2/include/alertify.less +++ b/customize.dist/src/less2/include/alertify.less @@ -159,6 +159,9 @@ margin-bottom: @alertify_padding-base; margin: 0; overflow: auto; + :last-child { + margin-bottom: 0; + } } .alertify-tabs { max-height: 100%; @@ -222,14 +225,22 @@ background-color: @alertify-input-fg; color: @cryptpad_text_col; border: 1px solid @alertify-input-bg; - margin-bottom: 15px; + margin-bottom: @alertify_padding-base; width: 100%; font-size: 100%; padding: @alertify_padding-base; &[readonly] { background-color: @alertify-light-bg; color: @cryptpad_text_col; - border-color: @alertify-input-fg; + border-color: @alertify-light-bg; + } + } + + textarea { + overflow: hidden; + padding: 8px; + &[readonly] { + resize: none; } } @@ -509,5 +520,33 @@ overflow-x: auto; } } + // Bootstrap Alerts + .alert { + margin: 0px 0px @alertify_padding-base 0px; + font-size: 12px; + padding: 5px; + border-radius: 0px; + i { + margin-right: 10px; + } + &.alert-primary { + background-color: @alertify-base; + color: @alertify-fg; + border-color: @alertify-fg; + a { + color: @alertify-fg; + text-decoration: underline; + } + } + &.dismissable { + display: flex; + align-items: center; + span.fa-times { + font-size: @colortheme_app-font-size; + margin-left: 20px; + cursor: pointer; + } + } + } } diff --git a/customize.dist/src/less2/include/avatar.less b/customize.dist/src/less2/include/avatar.less index 87b4a3a32..c85c34877 100644 --- a/customize.dist/src/less2/include/avatar.less +++ b/customize.dist/src/less2/include/avatar.less @@ -1,9 +1,12 @@ @import (reference) "./tools.less"; +@import (reference) "./colortheme-all.less"; .avatar_vars( @width: 30px ) { @avatar-width: @width; @avatar-font-size: @width / 1.2; + @avatar-default-bg: #D9D8D8; + @avatar-default-fg: darken(@avatar-default-bg, 40%); } .avatar_main(@width: 30px) { --LessLoader_require: LessLoader_currentFile(); @@ -30,16 +33,16 @@ justify-content: center; align-items: center; - border-radius: 4px; overflow: hidden; box-sizing: content-box; } .cp-avatar-default { .tools_unselectable(); - background: white; - color: black; + background: @avatar-default-bg; + color: @avatar-default-fg; font-size: @avatar-font-size; font-size: var(--avatar-font-size); + text-transform: capitalize; } media-tag { min-height: @avatar-width; diff --git a/customize.dist/src/less2/include/modals-ui-elements.less b/customize.dist/src/less2/include/modals-ui-elements.less index 6e3921781..21707eab8 100644 --- a/customize.dist/src/less2/include/modals-ui-elements.less +++ b/customize.dist/src/less2/include/modals-ui-elements.less @@ -1,12 +1,16 @@ @import (reference) "./colortheme-all.less"; - +@import (reference) "./variables.less"; .modals-ui-elements_main() { --LessLoader_require: LessLoader_currentFile(); } & { + .cp-spacer { + height: @variables_padding; + } // Share modal .msg.cp-inline-radio-group { overflow: unset !important; + padding: 0px @variables_padding; .radio-group { display: flex; flex-direction: row; diff --git a/customize.dist/src/less2/include/password-input.less b/customize.dist/src/less2/include/password-input.less index 79824bac7..0f476390a 100644 --- a/customize.dist/src/less2/include/password-input.less +++ b/customize.dist/src/less2/include/password-input.less @@ -1,3 +1,4 @@ +@import (reference) "./colortheme-all.less"; .password_main() { --LessLoader_require: LessLoader_currentFile(); } @@ -17,7 +18,7 @@ justify-content: center; cursor: pointer; &:hover { - background-color: rgba(0,0,0,0.1); + color: darken(@colortheme_alertify-primary, 10%); } } } diff --git a/customize.dist/src/less2/include/usergrid.less b/customize.dist/src/less2/include/usergrid.less index 4ff807638..6ba8c2d07 100644 --- a/customize.dist/src/less2/include/usergrid.less +++ b/customize.dist/src/less2/include/usergrid.less @@ -6,16 +6,18 @@ --LessLoader_require: LessLoader_currentFile(); }; & { + .cp-usergrid-container { + margin-bottom: 12px !important; // even when last child of .msg .cp-usergrid-grid { display: flex; flex-wrap: wrap; + margin: -3px; margin-bottom: 6px; - } - &:not(.large) { - .cp-usergrid-grid { - margin: -3px; - margin-bottom: 6px; + max-height: 130px; + overflow-y: auto; + @media screen and (max-height: 515px) { + max-height: unset; // remove double scrollbar } } &.cp-usergrid-empty { @@ -28,17 +30,22 @@ input { flex: 1; min-width: 0; + margin: 0; margin-bottom: 0 !important; + height: 38px; &::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */ color: @cryptpad_color_grey; opacity: 1; /* Firefox */ } } - margin-bottom: 15px; + margin-bottom: 10px; &:empty { margin: 0; display: none; } + button:last-child { + margin-right: 0px !important; + } } .cp-usergrid-user { width: 70px; @@ -58,33 +65,48 @@ background-color: @colortheme_alertify-primary; color: @colortheme_alertify-primary-text; order: -1 !important; + .cp-usergrid-avatar { + media-tag, .cp-avatar-default { + opacity: 0.7; + } + } } .cp-usergrid-user-avatar { min-height: 40px; } + .cp-usergrid-user-name { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; width: 100%; text-align: center; - line-height: 18px; + line-height: 20px; + flex: 1; } - border: 1px solid @colortheme_alertify-primary; &:not(.large) { .avatar_main(40px); } &.large { .avatar_main(25px); - width: 140px; + width: 145px; height: 35px; flex-flow: row; - margin: 0; - margin-right: 15px; - margin-bottom: 1px; - &:nth-child(3n) { - margin-right: 0; + margin: 3px; + flex-basis: calc(33.3333333% - 6px); + flex-shrink: 1; + min-width: 0; + .cp-usergrid-user-name { + margin-left: 5px; + text-align: left; + line-height: 150%; + color: @cryptpad_text_col; + } + } + &.cp-selected { + .cp-usergrid-user-name { + color: @colortheme_alertify-primary-text; } } } diff --git a/lib/batch-read.js b/lib/batch-read.js index 3e729e66d..2852a0579 100644 --- a/lib/batch-read.js +++ b/lib/batch-read.js @@ -8,6 +8,8 @@ If the result of IO or computation is requested while an identical request is already in progress, wait until the first one completes and provide its result to every routine that requested it. +Asynchrony is guaranteed. + ## Usage Provide: @@ -51,11 +53,12 @@ module.exports = function (/* task */) { var args = Array.prototype.slice.call(arguments); //if (map[id] && map[id].length > 1) { console.log("BATCH-READ DID ITS JOB for [%s][%s]", task, id); } - - map[id].forEach(function (h) { - h.apply(null, args); + setTimeout(function () { + map[id].forEach(function (h) { + h.apply(null, args); + }); + delete map[id]; }); - delete map[id]; }); }; }; diff --git a/lib/write-queue.js b/lib/write-queue.js index c1b64ebaf..c82b12b3b 100644 --- a/lib/write-queue.js +++ b/lib/write-queue.js @@ -4,7 +4,7 @@ q(id, function (next) { // whatever you need to do.... // when you're done - next(); + next(); // guaranteed to be asynchronous :D }); */ @@ -16,9 +16,11 @@ module.exports = function () { var map = {}; var next = function (id) { - if (map[id] && map[id].length === 0) { return void delete map[id]; } - var task = map[id].shift(); - task(fix1(next, id)); + setTimeout(function () { + if (map[id] && map[id].length === 0) { return void delete map[id]; } + var task = map[id].shift(); + task(fix1(next, id)); + }); }; return function (id, task) { diff --git a/www/auth/main.js b/www/auth/main.js index 7930e96e0..1b3667377 100644 --- a/www/auth/main.js +++ b/www/auth/main.js @@ -1,12 +1,17 @@ define([ 'jquery', - '/common/cryptpad-common.js', + '/common/cryptget.js', + '/common/pinpad.js', '/common/common-constants.js', '/common/outer/local-store.js', + '/common/outer/login-block.js', + '/common/outer/network-config.js', + '/customize/login.js', '/common/test.js', '/bower_components/nthen/index.js', + '/bower_components/netflux-websocket/netflux-client.js', '/bower_components/tweetnacl/nacl-fast.min.js' -], function ($, Cryptpad, Constants, LocalStore, Test, nThen) { +], function ($, Crypt, Pinpad, Constants, LocalStore, Block, NetConfig, Login, Test, nThen, Netflux) { var Nacl = window.nacl; var signMsg = function (msg, privKey) { @@ -27,9 +32,57 @@ define([ sessionStorage[Constants.userHashKey]; var proxy; + var rpc; + var network; + var rpcError; + + var loadProxy = function (hash) { + nThen(function (waitFor) { + var wsUrl = NetConfig.getWebsocketURL(); + var w = waitFor(); + Netflux.connect(wsUrl).then(function (_network) { + network = _network; + w(); + }, function (err) { + rpcError = err; + console.error(err); + }); + }).nThen(function (waitFor) { + Crypt.get(hash, waitFor(function (err, val) { + if (err) { + waitFor.abort(); + console.error(err); + return; + } + try { + var parsed = JSON.parse(val); + proxy = parsed; + } catch (e) { + console.log("Can't parse user drive", e); + } + }), { + network: network + }); + }).nThen(function (waitFor) { + if (!network) { return void waitFor.abort(); } + Pinpad.create(network, proxy, waitFor(function (e, call) { + if (e) { + rpcError = e; + return void waitFor.abort(); + } + rpc = call; + })); + }).nThen(function () { + Test(function () { + // This is only here to maybe trigger an error. + window.drive = proxy['drive']; + Test.passed(); + }); + }); + }; var whenReady = function (cb) { - if (proxy) { return void cb(); } + if (proxy && (rpc || rpcError)) { return void cb(); } console.log('CryptPad not ready...'); setTimeout(function () { whenReady(cb); @@ -45,6 +98,17 @@ define([ console.log('CP receiving', data); if (data.cmd === 'PING') { ret.res = 'PONG'; + } else if (data.cmd === 'LOGIN') { + Login.loginOrRegister(data.data.name, data.data.password, false, false, function (err) { + if (err) { + ret.error = 'LOGIN_ERROR'; + srcWindow.postMessage(JSON.stringify(ret), domain); + return; + } + loadProxy(LocalStore.getUserHash()); + srcWindow.postMessage(JSON.stringify(ret), domain); + }); + return; } else if (data.cmd === 'SIGN') { if (!AUTHORIZED_DOMAINS.filter(function (x) { return x.test(domain); }).length) { ret.error = "UNAUTH_DOMAIN"; @@ -63,7 +127,16 @@ define([ } } else if (data.cmd === 'UPDATE_LIMIT') { return void whenReady(function () { - Cryptpad.updatePinLimit(function (e, limit, plan, note) { + if (rpcError) { + // Tell the user on accounts that there was an issue and they need to wait maximum 24h or contact an admin + ret.warning = true; + srcWindow.postMessage(JSON.stringify(ret), domain); + return; + } + rpc.updatePinLimits(function (e, limit, plan, note) { + if (e) { + ret.warning = true; + } ret.res = [limit, plan, note]; srcWindow.postMessage(JSON.stringify(ret), domain); }); @@ -74,18 +147,8 @@ define([ srcWindow.postMessage(JSON.stringify(ret), domain); }); - nThen(function (waitFor) { - Cryptpad.ready(waitFor()); - }).nThen(function (waitFor) { - Cryptpad.getUserObject(null, waitFor(function (obj) { - proxy = obj; - })); - }).nThen(function () { - console.log('IFRAME READY'); - Test(function () { - // This is only here to maybe trigger an error. - window.drive = proxy['drive']; - Test.passed(); - }); - }); + var userHash = LocalStore.getUserHash(); + if (userHash) { + loadProxy(userHash); + } }); diff --git a/www/code/inner.js b/www/code/inner.js index 7c5e64c4f..e2ed086de 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -131,7 +131,7 @@ define([ if (['markdown', 'gfm'].indexOf(CodeMirror.highlightMode) === -1) { return; } if (!$previewButton.is('.cp-toolbar-button-active')) { return; } forceDrawPreview(); - }, 150); + }, 400); var previewTo; $previewButton.click(function () { diff --git a/www/code/mermaid.css b/www/code/mermaid.css index 769933f0a..60c366bb6 100644 --- a/www/code/mermaid.css +++ b/www/code/mermaid.css @@ -121,6 +121,10 @@ text.actor { font-size: 11px; text-height: 14px; } +.sectionTitle, .titleText { + font-weight: bold; +} + /* Grid and axis */ .grid .tick { stroke: lightgrey; diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 8efc7397a..8dc14cc8c 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -127,6 +127,18 @@ define([ return input; }; + dialog.selectableArea = function (value, opt) { + var attrs = merge({ + readonly: 'readonly', + }, opt); + + var input = h('textarea', attrs); + $(input).val(value).click(function () { + input.select(); + }); + return input; + }; + dialog.okButton = function (content, classString) { var sel = typeof(classString) === 'string'? 'button.ok.' + classString:'button.ok.primary'; return h(sel, { tabindex: '2', }, content || Messages.okButton); diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index e7d107e36..fb60f2751 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -147,6 +147,7 @@ define([ : Messages.owner_removeText; var removeCol = UIElements.getUserGrid(msg, { common: common, + large: true, data: _owners, noFilter: true }, function () { @@ -238,6 +239,7 @@ define([ }); var addCol = UIElements.getUserGrid(Messages.owner_addText, { common: common, + large: true, data: _friends }, function () { //console.log(arguments); @@ -254,6 +256,7 @@ define([ }); var teamsList = UIElements.getUserGrid(Messages.owner_addTeamText, { common: common, + large: true, noFilter: true, data: teamsData }, function () {}); @@ -551,9 +554,10 @@ define([ $d.append(password); } - if (!data.noEditPassword && owned && parsed.type !== "sheet") { // FIXME SHEET fix password change for sheets + if (!data.noEditPassword && owned) { // FIXME SHEET fix password change for sheets var sframeChan = common.getSframeChannel(); + var isOO = parsed.type === 'sheet'; var isFile = parsed.hashData.type === 'file'; var isSharedFolder = parsed.type === 'drive'; @@ -586,7 +590,8 @@ define([ UI.confirm(changePwConfirm, function (yes) { if (!yes) { pLocked = false; return; } $(passwordOk).html('').append(h('span.fa.fa-spinner.fa-spin', {style: 'margin-left: 0'})); - var q = isFile ? 'Q_BLOB_PASSWORD_CHANGE' : 'Q_PAD_PASSWORD_CHANGE'; + var q = isFile ? 'Q_BLOB_PASSWORD_CHANGE' : + (isOO ? 'Q_OO_PASSWORD_CHANGE' : 'Q_PAD_PASSWORD_CHANGE'); // If this is a file password change, register to the upload events: // * if there is a pending upload, ask if we shoudl interrupt @@ -737,12 +742,22 @@ define([ UIElements.getProperties = function (common, data, cb) { var c1; var c2; + var button = [{ + className: 'primary', + name: Messages.okButton, + onClick: function () {}, + keys: [13] + }]; NThen(function (waitFor) { getPadProperties(common, data, waitFor(function (e, c) { - c1 = c[0]; + c1 = UI.dialog.customModal(c[0], { + buttons: button + }); })); getRightsProperties(common, data, waitFor(function (e, c) { - c2 = c[0]; + c2 = UI.dialog.customModal(c[0], { + buttons: button + }); })); }).nThen(function () { var tabs = UI.dialog.tabs([{ @@ -782,8 +797,6 @@ define([ var noOthers = icons.length === 0 ? '.cp-usergrid-empty' : ''; - var buttonSelect = h('button', Messages.share_selectAll); - var buttonDeselect = h('button', Messages.share_deselectAll); var inputFilter = h('input', { placeholder: Messages.share_filterFriend }); @@ -791,9 +804,7 @@ define([ var div = h('div.cp-usergrid-container' + noOthers + (config.large?'.large':''), [ label ? h('label', label) : undefined, h('div.cp-usergrid-filter', (config.noFilter || config.noSelect) ? undefined : [ - inputFilter, - buttonSelect, - buttonDeselect + inputFilter ]), ]); var $div = $(div); @@ -806,23 +817,8 @@ define([ $div.find('.cp-usergrid-user:not(.cp-selected):not([data-name*="'+name+'"])').hide(); } }; - $(inputFilter).on('keydown keyup change', redraw); - $(buttonSelect).click(function () { - $div.find('.cp-usergrid-user:not(.cp-selected):visible').addClass('cp-selected'); - onSelect(); - }); - $(buttonDeselect).click(function () { - $div.find('.cp-usergrid-user.cp-selected').removeClass('cp-selected').each(function (i, el) { - var order = $(el).attr('data-order'); - if (!order) { return; } - $(el).attr('style', 'order:'+order); - }); - redraw(); - onSelect(); - }); - $(div).append(h('div.cp-usergrid-grid', icons)); if (!config.noSelect) { $div.on('click', '.cp-usergrid-user', function () { @@ -883,7 +879,8 @@ define([ var friendsList = UIElements.getUserGrid(null, { common: common, data: friends, - noFilter: false + noFilter: false, + large: true }, refreshButtons); var friendDiv = friendsList.div; $div.append(friendDiv); @@ -909,6 +906,7 @@ define([ var teamsList = UIElements.getUserGrid(Messages.share_linkTeam, { common: common, noFilter: true, + large: true, data: teams }, refreshButtons); $div.append(teamsList.div); @@ -1057,12 +1055,29 @@ define([ if (!hashes || (!hashes.editHash && !hashes.viewHash)) { return; } + // check if the pad is password protected + var hash = hashes.editHash || hashes.viewHash; + var href = origin + pathname + '#' + hash; + var parsedHref = Hash.parsePadUrl(href); + var hasPassword = parsedHref.hashData.password; + + var makeFaqLink = function () { + var link = h('span', [ + h('i.fa.fa-question-circle'), + h('a', {href: '#'}, Messages.passwordFaqLink) + ]); + $(link).click(function () { + common.openURL(config.origin + "/faq.html#security-pad_password"); + }); + return link; + }; + + var parsed = Hash.parsePadUrl(pathname); var canPresent = ['code', 'slide'].indexOf(parsed.type) !== -1; var rights = h('div.msg.cp-inline-radio-group', [ h('label', Messages.share_linkAccess), - h('br'), h('div.radio-group',[ UI.createRadio('accessRights', 'cp-share-editable-false', Messages.share_linkView, true, { mark: {tabindex:1} }), @@ -1109,9 +1124,42 @@ define([ h('br'), ] : [ UI.createCheckbox('cp-share-embed', Messages.share_linkEmbed, false, { mark: {tabindex:1} }), - h('br'), ]; - linkContent.push(UI.dialog.selectable('', { id: 'cp-share-link-preview', tabindex: 1 })); + linkContent.push(h('div.cp-spacer')); + linkContent.push(UI.dialog.selectableArea('', { id: 'cp-share-link-preview', tabindex: 1, rows:3})); + + // Show alert if the pad is password protected + if (hasPassword) { + linkContent.push(h('div.alert.alert-primary', [ + h('i.fa.fa-lock'), + Messages.share_linkPasswordAlert, h('br'), + makeFaqLink() + ])); + } + + // warning about sharing links + var localStore = window.cryptpadStore; + var dismissButton = h('span.fa.fa-times'); + var shareLinkWarning = h('div.alert.alert-warning.dismissable', + { style: 'display: none;' }, + [ + h('span.cp-inline-alert-text', Messages.share_linkWarning), + dismissButton + ]); + linkContent.push(shareLinkWarning); + + localStore.get('hide-alert-shareLinkWarning', function (val) { + if (val === '1') { return; } + $(shareLinkWarning).show(); + + $(dismissButton).on('click', function () { + localStore.put('hide-alert-shareLinkWarning', '1'); + $(shareLinkWarning).remove(); + }); + + }); + + var link = h('div.cp-share-modal', linkContent); var $link = $(link); @@ -1177,7 +1225,19 @@ define([ // XXX Don't display access rights if no contacts var contactsContent = h('div.cp-share-modal'); - $(contactsContent).append(friendsList); + var $contactsContent = $(contactsContent); + + $contactsContent.append(friendsList); + + // Show alert if the pad is password protected + if (hasPassword) { + $contactsContent.append(h('div.alert.alert-primary', [ + h('i.fa.fa-unlock'), + Messages.share_contactPasswordAlert, h('br'), + makeFaqLink() + ])); + } + var contactButtons = friendsObject.buttons; contactButtons.unshift(makeCancelButton()); @@ -1196,9 +1256,18 @@ define([ }; var embedContent = [ h('p', Messages.viewEmbedTag), - h('br'), - UI.dialog.selectable(getEmbedValue(), { id: 'cp-embed-link-preview', tabindex: 1 }) + UI.dialog.selectableArea(getEmbedValue(), { id: 'cp-embed-link-preview', tabindex: 1, rows: 3}) ]; + + // Show alert if the pad is password protected + if (hasPassword) { + embedContent.push(h('div.alert.alert-primary', [ + h('i.fa.fa-lock'), ' ', + Messages.share_embedPasswordAlert, h('br'), + makeFaqLink() + ])); + } + var embedButtons = [ makeCancelButton(), { className: 'primary', @@ -1303,6 +1372,21 @@ define([ if (!hashes.fileHash) { throw new Error("You must provide a file hash"); } var url = origin + pathname + '#' + hashes.fileHash; + // check if the file is password protected + var parsedHref = Hash.parsePadUrl(url); + var hasPassword = parsedHref.hashData.password; + + var makeFaqLink = function () { + var link = h('span', [ + h('i.fa.fa-question-circle'), + h('a', {href: '#'}, Messages.passwordFaqLink) + ]); + $(link).click(function () { + common.openURL(config.origin + "/faq.html#security-pad_password"); + }); + return link; + }; + var getLinkValue = function () { return url; }; var makeCancelButton = function() { @@ -1314,9 +1398,40 @@ define([ // Share link tab var linkContent = [ - UI.dialog.selectable(getLinkValue(), { id: 'cp-share-link-preview', tabindex: 1 }) + UI.dialog.selectableArea(getLinkValue(), { id: 'cp-share-link-preview', tabindex: 1, rows:2 }) ]; + // Show alert if the pad is password protected + if (hasPassword) { + linkContent.push(h('div.alert.alert-primary', [ + h('i.fa.fa-lock'), + Messages.share_linkPasswordAlert, h('br'), + makeFaqLink() + ])); + } + + // warning about sharing links + var localStore = window.cryptpadStore; + var dismissButton = h('span.fa.fa-times'); + var shareLinkWarning = h('div.alert.alert-warning.dismissable', + { style: 'display: none;' }, + [ + h('span.cp-inline-alert-text', Messages.share_linkWarning), + dismissButton + ]); + linkContent.push(shareLinkWarning); + + localStore.get('hide-alert-shareLinkWarning', function (val) { + if (val === '1') { return; } + $(shareLinkWarning).show(); + + $(dismissButton).on('click', function () { + localStore.put('hide-alert-shareLinkWarning', '1'); + $(shareLinkWarning).remove(); + }); + + }); + var link = h('div.cp-share-modal', linkContent); var linkButtons = [ @@ -1346,7 +1461,17 @@ define([ var friendsList = friendsObject.content; var contactsContent = h('div.cp-share-modal'); - $(contactsContent).append(friendsList); + var $contactsContent = $(contactsContent); + $contactsContent.append(friendsList); + + // Show alert if the pad is password protected + if (hasPassword) { + $contactsContent.append(h('div.alert.alert-primary', [ + h('i.fa.fa-unlock'), + Messages.share_contactPasswordAlert, h('br'), + makeFaqLink() + ])); + } var contactButtons = friendsObject.buttons; contactButtons.unshift(makeCancelButton()); @@ -1360,12 +1485,20 @@ define([ // Embed tab var embed = h('div.cp-share-modal', [ h('p', Messages.fileEmbedScript), - h('br'), UI.dialog.selectable(common.getMediatagScript()), h('p', Messages.fileEmbedTag), - h('br'), UI.dialog.selectable(common.getMediatagFromHref(fileData)), ]); + + // Show alert if the pad is password protected + if (hasPassword) { + embed.append(h('div.alert.alert-primary', [ + h('i.fa.fa-lock'), ' ', + Messages.share_embedPasswordAlert, h('br'), + makeFaqLink() + ])); + } + var embedButtons = [{ className: 'cancel', name: Messages.cancel, @@ -1800,7 +1933,7 @@ define([ if (e) { return void console.error(e); } UIElements.getProperties(common, data, function (e, $prop) { if (e) { return void console.error(e); } - UI.alert($prop[0], undefined, true); + UI.openCustomModal($prop[0]); }); }); }); diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 25a64404d..40db70457 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -1155,6 +1155,242 @@ define([ }); }; + common.changeOOPassword = function (data, _cb) { + var cb = Util.once(Util.mkAsync(_cb)); + var href = data.href; + var newPassword = data.password; + var teamId = data.teamId; + if (!href) { return void cb({ error: 'EINVAL_HREF' }); } + var parsed = Hash.parsePadUrl(href); + if (!parsed.hash) { return void cb({ error: 'EINVAL_HREF' }); } + if (parsed.type !== 'sheet') { return void cb({ error: 'EINVAL_TYPE' }); } + + var warning = false; + var newHash, newRoHref; + var oldSecret; + var oldMetadata; + var oldRtChannel; + var privateData; + var padData; + + var newSecret; + if (parsed.hashData.version >= 2) { + newSecret = Hash.getSecrets(parsed.type, parsed.hash, newPassword); + if (!(newSecret.keys && newSecret.keys.editKeyStr)) { + return void cb({error: 'EAUTH'}); + } + newHash = Hash.getEditHashFromKeys(newSecret); + } + var newHref = '/' + parsed.type + '/#' + newHash; + var newRtChannel = Hash.createChannelId(); + + var Crypt, Crypto; + var cryptgetVal; + var optsPut = { + password: newPassword, + metadata: { + validateKey: newSecret.keys.validateKey + }, + }; + + Nthen(function (waitFor) { + common.getPadAttribute('', waitFor(function (err, _data) { + padData = _data; + }), href); + }).nThen(function (waitFor) { + oldSecret = Hash.getSecrets(parsed.type, parsed.hash, padData.password); + + require([ + '/common/cryptget.js', + '/bower_components/chainpad-crypto/crypto.js', + ], waitFor(function (_Crypt, _Crypto) { + Crypt = _Crypt; + Crypto = _Crypto; + })); + + common.getPadMetadata({channel: oldSecret.channel}, waitFor(function (metadata) { + oldMetadata = metadata; + })); + common.getMetadata(waitFor(function (err, data) { + if (err) { + waitFor.abort(); + return void cb({ error: err }); + } + privateData = data.priv; + })); + }).nThen(function (waitFor) { + // Check if we're allowed to change the password + var owners = oldMetadata.owners; + optsPut.metadata.owners = owners; + var edPublic = teamId ? (privateData.teams[teamId] || {}).edPublic : privateData.edPublic; + var isOwner = Array.isArray(owners) && edPublic && owners.indexOf(edPublic) !== -1; + if (!isOwner) { + // We're not an owner, we shouldn't be able to change the password! + waitFor.abort(); + return void cb({ error: 'EPERM' }); + } + + var mailbox = oldMetadata.mailbox; + if (mailbox) { + // Create the encryptors to be able to decrypt and re-encrypt the mailboxes + var oldCrypto = Crypto.createEncryptor(oldSecret.keys); + var newCrypto = Crypto.createEncryptor(newSecret.keys); + + var m; + if (typeof(mailbox) === "string") { + try { + m = newCrypto.encrypt(oldCrypto.decrypt(mailbox, true, true)); + } catch (e) {} + } else if (mailbox && typeof(mailbox) === "object") { + m = {}; + Object.keys(mailbox).forEach(function (ed) { + try { + m[ed] = newCrypto.encrypt(oldCrypto.decrypt(mailbox[ed], true, true)); + } catch (e) { + console.error(e); + } + }); + } + optsPut.metadata.mailbox = m; + } + + var expire = oldMetadata.expire; + if (expire) { + optsPut.metadata.expire = (expire - (+new Date())) / 1000; // Lifetime in seconds + } + + // Get last cp (cryptget) + Crypt.get(parsed.hash, waitFor(function (err, val) { + if (err) { + waitFor.abort(); + return void cb({ error: err }); + } + try { + cryptgetVal = JSON.parse(val); + if (!cryptgetVal.content) { + waitFor.abort(); + return void cb({ error: 'INVALID_CONTENT' }); + } + } catch (e) { + waitFor.abort(); + return void cb({ error: 'CANT_PARSE' }); + } + }), { + password: padData.password + }); + }).nThen(function (waitFor) { + // Re-encrypt rtchannel + oldRtChannel = Util.find(cryptgetVal, ['content', 'channel']); + var newCrypto = Crypto.createEncryptor(newSecret.keys); + var oldCrypto = Crypto.createEncryptor(oldSecret.keys); + var cps = Util.find(cryptgetVal, ['content', 'hashes']); + var l = Object.keys(cps).length; + var lastCp = l ? cps[l] : {}; + cryptgetVal.content.hashes = {}; + common.getHistory({ + channel: oldRtChannel, + lastKnownHash: lastCp.hash + }, waitFor(function (obj) { + if (obj && obj.error) { + waitFor.abort(); + console.error(obj); + return void cb(obj.error); + } + var msgs = obj; + var newHistory = msgs.map(function (str) { + try { + var d = oldCrypto.decrypt(str, true, true); + return newCrypto.encrypt(d); + } catch (e) { + console.log(e); + waitFor.abort(); + return void cb({error: e}); + } + }); + // Update last knwon hash in cryptgetVal + if (lastCp) { + lastCp.hash = newHistory[0].slice(0, 64); + lastCp.index = 50; + cryptgetVal.content.hashes[1] = lastCp; + } + common.onlyoffice.execCommand({ + cmd: 'REENCRYPT', + data: { + channel: newRtChannel, + msgs: newHistory, + metadata: optsPut.metadata + } + }, waitFor(function (obj) { + if (obj && obj.error) { + waitFor.abort(); + console.warn(obj); + return void cb(obj.error); + } + })); + })); + }).nThen(function (waitFor) { + // The new rt channel is ready + // The blob uses its own encryption and doesn't need to be reencrypted + cryptgetVal.content.channel = newRtChannel; + Crypt.put(newHash, JSON.stringify(cryptgetVal), waitFor(function (err) { + if (err) { + waitFor.abort(); + return void cb({ error: err }); + } + }), optsPut); + }).nThen(function (waitFor) { + pad.leavePad({ + channel: oldSecret.channel + }, waitFor()); + pad.onDisconnectEvent.fire(true); + }).nThen(function (waitFor) { + // Set the new password to our pad data + common.setPadAttribute('password', newPassword, waitFor(function (err) { + if (err) { warning = true; } + }), href); + common.setPadAttribute('channel', newSecret.channel, waitFor(function (err) { + if (err) { warning = true; } + }), href); + common.setPadAttribute('rtChannel', newRtChannel, waitFor(function (err) { + if (err) { warning = true; } + }), href); + var viewHash = Hash.getViewHashFromKeys(newSecret); + newRoHref = '/' + parsed.type + '/#' + viewHash; + common.setPadAttribute('roHref', newRoHref, waitFor(function (err) { + if (err) { warning = true; } + }), href); + + if (parsed.hashData.password && newPassword) { return; } // same hash + common.setPadAttribute('href', newHref, waitFor(function (err) { + if (err) { warning = true; } + }), href); + }).nThen(function (waitFor) { + // delete the old pad + common.removeOwnedChannel({ + channel: oldSecret.channel, + teamId: teamId + }, waitFor(function (obj) { + if (obj && obj.error) { + waitFor.abort(); + console.info(obj); + return void cb(obj.error); + } + common.removeOwnedChannel({ + channel: oldRtChannel, + teamId: teamId + }, waitFor()); + })); + }).nThen(function () { + cb({ + warning: warning, + hash: newHash, + href: newHref, + roHref: newRoHref + }); + }); + }; + + common.changeUserPassword = function (Crypt, edPublic, data, cb) { if (!edPublic) { return void cb({ @@ -1350,6 +1586,9 @@ define([ common.getFullHistory = function (data, cb) { postMessage("GET_FULL_HISTORY", data, cb, {timeout: 180000}); }; + common.getHistory = function (data, cb) { + postMessage("GET_HISTORY", data, cb, {timeout: 180000}); + }; common.getHistoryRange = function (data, cb) { postMessage("GET_HISTORY_RANGE", data, cb); }; diff --git a/www/common/diffMarked.js b/www/common/diffMarked.js index 5817e5ac9..0f92897bc 100644 --- a/www/common/diffMarked.js +++ b/www/common/diffMarked.js @@ -197,7 +197,6 @@ define([ 'APPLET', 'VIDEO', // privacy implications of videos are the same as images 'AUDIO', // same with audio - 'SVG' ]; var unsafeTag = function (info) { /*if (info.node && $(info.node).parents('media-tag').length) { @@ -307,8 +306,39 @@ define([ var Dom = domFromHTML($('
').append($div).html()); $content[0].normalize(); - $content.find('pre.mermaid[data-processed="true"]').remove(); + + var mermaid_source = []; + var mermaid_cache = {}; + + // iterate over the unrendered mermaid inputs, caching their source as you go + $(newDomFixed).find('pre.mermaid').each(function (index, el) { + if (el.childNodes.length === 1 && el.childNodes[0].nodeType === 3) { + var src = el.childNodes[0].wholeText; + el.setAttribute('mermaid-source', src); + mermaid_source[index] = src; + } + }); + + // iterate over rendered mermaid charts + $content.find('pre.mermaid:not([processed="true"])').each(function (index, el) { + // retrieve the attached source code which it was drawn + var src = el.getAttribute('mermaid-source'); + + // check if that source exists in the set of charts which are about to be rendered + if (mermaid_source.indexOf(src) === -1) { + // if it's not, then you can remove it + if (el.parentNode && el.parentNode.children.length) { + el.parentNode.removeChild(el); + } + } else if (el.childNodes.length === 1 && el.childNodes[0].nodeType !== 3) { + // otherwise, confirm that the content of the rendered chart is not a text node + // and keep a copy of it + mermaid_cache[src] = el.childNodes[0]; + } + }); + var oldDom = domFromHTML($content[0].outerHTML); + var patch = makeDiff(oldDom, Dom, id); if (typeof(patch) === 'string') { throw new Error(patch); @@ -348,8 +378,32 @@ define([ var target = document.getElementById($a.attr('data-href')); if (target) { target.scrollIntoView(); } }); + + // loop over mermaid elements in the rendered content + $content.find('pre.mermaid').each(function (index, el) { + // since you've simply drawn the content that was supplied via markdown + // you can assume that the index of your rendered charts matches that + // of those in the markdown source. + var src = mermaid_source[index]; + el.setAttribute('mermaid-source', src); + var cached = mermaid_cache[src]; + + // check if you had cached a pre-rendered instance of the supplied source + if (typeof(cached) !== 'object') { return; } + + // if there's a cached rendering, empty out the contained source code + // which would otherwise be drawn again. + // apparently this is the fastest way to empty out an element + while (el.firstChild) { el.removeChild(el.firstChild); } //el.innerHTML = ''; + // insert the cached graph + el.appendChild(cached); + // and set a flag indicating that this graph need not be reprocessed + el.setAttribute('data-processed', true); + }); + try { - Mermaid.init(); + // finally, draw any graphs which have changed and were thus not cached + Mermaid.init(undefined, $content.find('pre.mermaid:not([data-processed="true"])')); } catch (e) { console.error(e); } } }; diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index 2f7da5886..3fa850b51 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -4187,7 +4187,7 @@ define([ } getProperties(el, function (e, $prop) { if (e) { return void logError(e); } - UI.alert($prop[0], undefined, true); + UI.openCustomModal($prop[0]); }); } else if ($this.hasClass("cp-app-drive-context-hashtag")) { diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 0cf362e5d..c5b6f6a42 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -1693,7 +1693,7 @@ define([ // GET_FULL_HISTORY from sframe-common-outer Store.getFullHistory = function (clientId, data, cb) { var network = store.network; - var hkn = network.historyKeeper; + var hk = network.historyKeeper; //var crypto = Crypto.createEncryptor(data.keys); // Get the history messages and send them to the iframe var parse = function (msg) { @@ -1708,8 +1708,10 @@ define([ var onMsg = function (msg) { if (completed) { return; } var parsed = parse(msg); + if (!parsed) { return; } if (parsed[0] === 'FULL_HISTORY_END') { cb(msgs); + network.off('message', onMsg); completed = true; return; } @@ -1726,12 +1728,69 @@ define([ } }; network.on('message', onMsg); - network.sendto(hkn, JSON.stringify(['GET_FULL_HISTORY', data.channel, data.validateKey])); + network.sendto(hk, JSON.stringify(['GET_FULL_HISTORY', data.channel, data.validateKey])); + }; + + Store.getHistory = function (clientId, data, cb) { + var network = store.network; + var hk = network.historyKeeper; + + var parse = function (msg) { + try { + return JSON.parse(msg); + } catch (e) { + return null; + } + }; + + var msgs = []; + var completed = false; + var onMsg = function (msg, sender) { + if (completed) { return; } + if (sender !== hk) { return; } + var parsed = parse(msg); + if (!parsed) { return; } + + // Ignore the metadata message + if (parsed.validateKey && parsed.channel) { return; } + if (parsed.error && parsed.channel) { + if (parsed.channel === data.channel) { + network.off('message', onMsg); + completed = true; + cb({error: parsed.error}); + } + return; + } + + // End of history: cb + if (parsed.state === 1 && parsed.channel) { + if (parsed.channel !== data.channel) { return; } + cb(msgs); + network.off('message', onMsg); + completed = true; + return; + } + + msg = parsed[4]; + // Keep only the history for our channel + if (parsed[3] !== data.channel) { return; } + if (msg) { + msg = msg.replace(/cp\|(([A-Za-z0-9+\/=]+)\|)?/, ''); + msgs.push(msg); + } + }; + network.on('message', onMsg); + + var cfg = { + lastKnownHash: data.lastKnownHash + }; + var msg = ['GET_HISTORY', data.channel, cfg]; + network.sendto(hk, JSON.stringify(msg)); }; Store.getHistoryRange = function (clientId, data, cb) { var network = store.network; - var hkn = network.historyKeeper; + var hk = network.historyKeeper; var parse = function (msg) { try { return JSON.parse(msg); @@ -1779,7 +1838,7 @@ define([ }; network.on('message', onMsg); - network.sendto(hkn, JSON.stringify(['GET_HISTORY_RANGE', data.channel, { + network.sendto(hk, JSON.stringify(['GET_HISTORY_RANGE', data.channel, { from: data.lastKnownHash, cpCount: 2, txid: txid diff --git a/www/common/outer/onlyoffice.js b/www/common/outer/onlyoffice.js index 1dc010ba4..9c1863bc6 100644 --- a/www/common/outer/onlyoffice.js +++ b/www/common/outer/onlyoffice.js @@ -200,6 +200,41 @@ define([ })); }; + var reencrypt = function (ctx, data, cId, cb) { + var channel = data.channel; + var network = ctx.store.network; + + var onOpen = function (wc) { + var hk = network.historyKeeper; + var cfg = { + metadata: data.metadata + }; + var msg = ['GET_HISTORY', wc.id, cfg]; + network.sendto(hk, JSON.stringify(msg)); + data.msgs.forEach(function (msg) { + wc.bcast(msg); + }); + wc.leave(); + cb(); + }; + + ctx.store.anon_rpc.send("IS_NEW_CHANNEL", channel, function (e, response) { + if (e) { return void cb({error: e}); } + var isNew; + if (response && response.length && typeof(response[0]) === 'boolean') { + isNew = response[0]; + } else { + cb({error: 'INVALID_RESPONSE'}); + } + if (!isNew) { return void cb({error: 'EEXISTS'}); } + + // Channel is new: we can push our reencrypted history + network.join(channel).then(onOpen, function (err) { + return void cb({error: err}); + }); + }); + }; + var leaveChannel = function (ctx, padChan) { // Leave channel and prevent reconnect when we leave a pad Object.keys(ctx.channels).some(function (ooChan) { @@ -267,6 +302,9 @@ define([ if (cmd === 'OPEN_CHANNEL') { return void openChannel(ctx, data, clientId, cb); } + if (cmd === 'REENCRYPT') { + return void reencrypt(ctx, data, clientId, cb); + } }; return oo; diff --git a/www/common/outer/store-rpc.js b/www/common/outer/store-rpc.js index 2ef490876..12b5ff6e5 100644 --- a/www/common/outer/store-rpc.js +++ b/www/common/outer/store-rpc.js @@ -73,6 +73,7 @@ define([ JOIN_PAD: Store.joinPad, LEAVE_PAD: Store.leavePad, GET_FULL_HISTORY: Store.getFullHistory, + GET_HISTORY: Store.getHistory, GET_HISTORY_RANGE: Store.getHistoryRange, IS_NEW_CHANNEL: Store.isNewChannel, REQUEST_PAD_ACCESS: Store.requestPadAccess, diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 0aa79cfd1..82b9bf2e7 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -493,6 +493,20 @@ define([ Cryptpad.storeInTeam(data, cb); }); + sframeChan.on('EV_GOTO_URL', function (url) { + if (url) { + window.location.href = url; + } else { + window.location.reload(); + } + }); + + sframeChan.on('EV_OPEN_URL', function (url) { + if (url) { + window.open(url); + } + }); + }; addCommonRpc(sframeChan); @@ -956,20 +970,6 @@ define([ }); }); - sframeChan.on('EV_GOTO_URL', function (url) { - if (url) { - window.location.href = url; - } else { - window.location.reload(); - } - }); - - sframeChan.on('EV_OPEN_URL', function (url) { - if (url) { - window.open(url); - } - }); - sframeChan.on('Q_PIN_GET_USAGE', function (teamId, cb) { Cryptpad.isOverPinLimit(teamId, function (err, overLimit, data) { cb({ @@ -1008,6 +1008,11 @@ define([ }, cb); }); + sframeChan.on('Q_OO_PASSWORD_CHANGE', function (data, cb) { + data.href = data.href || window.location.href; + Cryptpad.changeOOPassword(data, cb); + }); + sframeChan.on('Q_PAD_PASSWORD_CHANGE', function (data, cb) { data.href = data.href || window.location.href; Cryptpad.changePadPassword(Cryptget, Crypto, data, cb); diff --git a/www/common/toolbar3.js b/www/common/toolbar3.js index 224f7eeb6..b6cb3c125 100644 --- a/www/common/toolbar3.js +++ b/www/common/toolbar3.js @@ -533,7 +533,7 @@ MessengerUI, Messages) { Common.getSframeChannel().event('EV_SHARE_OPEN', { hidden: true }); - $shareBlock.click(function () { + $shareBlock.click(function () { var title = (config.title && config.title.getTitle && config.title.getTitle()) || (config.title && config.title.defaultName) || ""; diff --git a/www/kanban/inner.js b/www/kanban/inner.js index c2d845557..035794b2a 100644 --- a/www/kanban/inner.js +++ b/www/kanban/inner.js @@ -286,7 +286,7 @@ define([ kanban.inEditMode = true; // create a form to enter element var boardId = $(el.parentNode.parentNode).attr("data-id"); - var $item = $('
', {'class': 'kanban-item'}); + var $item = $('
', {'class': 'kanban-item new-item'}); var $input = getInput().val(name).appendTo($item); kanban.addForm(boardId, $item[0]); $input.focus(); diff --git a/www/kanban/jkanban.js b/www/kanban/jkanban.js index 5ef939f1b..f1f70955a 100644 --- a/www/kanban/jkanban.js +++ b/www/kanban/jkanban.js @@ -147,9 +147,13 @@ self.drake = self.dragula(self.boardContainer, { moves: function (el, source, handle, sibling) { if (self.options.readOnly) { return false; } + if (el.classList.contains('new-item')) { return false; } return handle.classList.contains('kanban-item'); }, accepts: function (el, target, source, sibling) { + if (sibling === null) { + return false; + } if (self.options.readOnly) { return false; } return true; }, diff --git a/www/settings/inner.js b/www/settings/inner.js index d67f62818..7c9dd0630 100644 --- a/www/settings/inner.js +++ b/www/settings/inner.js @@ -51,6 +51,7 @@ define([ 'cp-settings-info-block', 'cp-settings-displayname', 'cp-settings-language-selector', + 'cp-settings-resettips', 'cp-settings-logout-everywhere', 'cp-settings-autostore', 'cp-settings-userfeedback', @@ -67,7 +68,6 @@ define([ ], 'drive': [ 'cp-settings-drive-duplicate', - 'cp-settings-resettips', 'cp-settings-thumbnails', 'cp-settings-drive-backup', 'cp-settings-drive-import-local', @@ -835,7 +835,7 @@ define([ var localStore = window.cryptpadStore; $button.click(function () { Object.keys(localStore.store).forEach(function (k) { - if(k.slice(0, 9) === "hide-info") { + if(/^(hide-(info|alert))/.test(k)) { localStore.put(k, null); } });