From de820dc0d19c7179d59a14ed8d253f9aa03fbbc8 Mon Sep 17 00:00:00 2001 From: yflory <yann.flory@xwiki.com> Date: Mon, 17 Feb 2020 14:07:29 +0100 Subject: [PATCH] Split properties modal into 2 modals --- www/common/common-ui-elements.js | 646 ++---------------------------- www/common/drive-ui.js | 97 +++-- www/common/inner/access.js | 658 +++++++++++++++++++++++++++++++ 3 files changed, 740 insertions(+), 661 deletions(-) create mode 100644 www/common/inner/access.js diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 8aba07c18..67c4518fa 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -100,588 +100,8 @@ define([ }; }; - var createOwnerModal = function (common, data) { - var friends = common.getFriends(true); - var sframeChan = common.getSframeChannel(); - var priv = common.getMetadataMgr().getPrivateData(); - var user = common.getMetadataMgr().getUserData(); - var edPublic = priv.edPublic; - var channel = data.channel; - var owners = data.owners || []; - var pending_owners = data.pending_owners || []; - var teams = priv.teams; - var teamOwner = data.teamId; - - var redrawAll = function () {}; - - var div1 = h('div.cp-usergrid-user.cp-share-column.cp-ownership'); - var div2 = h('div.cp-usergrid-user.cp-share-column.cp-ownership'); - var $div1 = $(div1); - var $div2 = $(div2); - - // Remove owner column - var drawRemove = function (pending) { - var _owners = {}; - var o = (pending ? pending_owners : owners) || []; - o.forEach(function (ed) { - var f; - Object.keys(friends).some(function (c) { - if (friends[c].edPublic === ed) { - f = friends[c]; - return true; - } - }); - Object.keys(teams).some(function (id) { - if (teams[id].edPublic === ed) { - f = teams[id]; - f.teamId = id; - } - }); - if (ed === edPublic) { - f = f || user; - if (f.name) { f.edPublic = edPublic; } - } - _owners[ed] = f || { - displayName: Messages._getKey('owner_unknownUser', [ed]), - edPublic: ed, - }; - }); - var msg = pending ? Messages.owner_removePendingText - : Messages.owner_removeText; - var removeCol = UIElements.getUserGrid(msg, { - common: common, - large: true, - data: _owners, - noFilter: true - }, function () { - }); - var $div = $(removeCol.div); - // When clicking on the remove button, we check the selected users. - // If you try to remove yourself, we'll display an additional warning message - var btnMsg = pending ? Messages.owner_removePendingButton : Messages.owner_removeButton; - var removeButton = h('button.no-margin', btnMsg); - $(removeButton).click(function () { - // Check selection - var $sel = $div.find('.cp-usergrid-user.cp-selected'); - var sel = $sel.toArray(); - if (!sel.length) { return; } - var me = false; - var toRemove = sel.map(function (el) { - var ed = $(el).attr('data-ed'); - if (!ed) { return; } - if (teamOwner && teams[teamOwner] && teams[teamOwner].edPublic === ed) { me = true; } - if (ed === edPublic && !teamOwner) { me = true; } - return ed; - }).filter(function (x) { return x; }); - NThen(function (waitFor) { - var msg = me ? Messages.owner_removeMeConfirm : Messages.owner_removeConfirm; - UI.confirm(msg, waitFor(function (yes) { - if (!yes) { - waitFor.abort(); - return; - } - })); - }).nThen(function (waitFor) { - // Send the command - sframeChan.query('Q_SET_PAD_METADATA', { - channel: channel, - command: pending ? 'RM_PENDING_OWNERS' : 'RM_OWNERS', - value: toRemove, - teamId: teamOwner - }, waitFor(function (err, res) { - err = err || (res && res.error); - if (err) { - waitFor.abort(); - redrawAll(); - var text = err === "INSUFFICIENT_PERMISSIONS" ? Messages.fm_forbidden - : Messages.error; - return void UI.warn(text); - } - UI.log(Messages.saved); - })); - }).nThen(function (waitFor) { - sel.forEach(function (el) { - var curve = $(el).attr('data-curve'); - var friend = curve === user.curvePublic ? user : friends[curve]; - if (!friend) { return; } - common.mailbox.sendTo("RM_OWNER", { - channel: channel, - title: data.title, - pending: pending - }, { - channel: friend.notifications, - curvePublic: friend.curvePublic - }, waitFor()); - }); - }).nThen(function () { - redrawAll(); - }); - }); - $div.append(h('p', removeButton)); - return $div; - }; - - // Add owners column - var drawAdd = function () { - var $div = $(h('div.cp-share-column')); - var _friends = JSON.parse(JSON.stringify(friends)); - Object.keys(_friends).forEach(function (curve) { - if (owners.indexOf(_friends[curve].edPublic) !== -1 || - pending_owners.indexOf(_friends[curve].edPublic) !== -1 || - !_friends[curve].notifications) { - delete _friends[curve]; - } - }); - var addCol = UIElements.getUserGrid(Messages.owner_addText, { - common: common, - large: true, - data: _friends - }, function () { - //console.log(arguments); - }); - $div.append(addCol.div); - - var teamsData = Util.tryParse(JSON.stringify(priv.teams)) || {}; - Object.keys(teamsData).forEach(function (id) { - var t = teamsData[id]; - t.teamId = id; - if (owners.indexOf(t.edPublic) !== -1 || pending_owners.indexOf(t.edPublic) !== -1) { - delete teamsData[id]; - } - }); - var teamsList = UIElements.getUserGrid(Messages.owner_addTeamText, { - common: common, - large: true, - noFilter: true, - data: teamsData - }, function () {}); - $div.append(teamsList.div); - - // When clicking on the add button, we get the selected users. - var addButton = h('button.no-margin', Messages.owner_addButton); - $(addButton).click(function () { - // Check selection - var $sel = $div.find('.cp-usergrid-user.cp-selected'); - var sel = $sel.toArray(); - if (!sel.length) { return; } - var toAdd = sel.map(function (el) { - var curve = $(el).attr('data-curve'); - // If the pad is woned by a team, we can transfer ownership to ourselves - if (curve === user.curvePublic && teamOwner) { return priv.edPublic; } - var friend = friends[curve]; - if (!friend) { return; } - return friend.edPublic; - }).filter(function (x) { return x; }); - var toAddTeams = sel.map(function (el) { - var team = teamsData[$(el).attr('data-teamid')]; - if (!team || !team.edPublic) { return; } - return { - edPublic: team.edPublic, - id: $(el).attr('data-teamid') - }; - }).filter(function (x) { return x; }); - - NThen(function (waitFor) { - var msg = Messages.owner_addConfirm; - UI.confirm(msg, waitFor(function (yes) { - if (!yes) { - waitFor.abort(); - return; - } - })); - }).nThen(function (waitFor) { - // Add one of our teams as an owner - if (toAddTeams.length) { - // Send the command - sframeChan.query('Q_SET_PAD_METADATA', { - channel: channel, - command: 'ADD_OWNERS', - value: toAddTeams.map(function (obj) { return obj.edPublic; }), - teamId: teamOwner - }, waitFor(function (err, res) { - err = err || (res && res.error); - if (err) { - waitFor.abort(); - redrawAll(); - var text = err === "INSUFFICIENT_PERMISSIONS" ? - Messages.fm_forbidden : Messages.error; - return void UI.warn(text); - } - var isTemplate = priv.isTemplate || data.isTemplate; - toAddTeams.forEach(function (obj) { - sframeChan.query('Q_STORE_IN_TEAM', { - href: data.href || data.rohref, - password: data.password, - path: isTemplate ? ['template'] : undefined, - title: data.title || '', - teamId: obj.id - }, waitFor(function (err) { - if (err) { return void console.error(err); } - })); - }); - })); - } - }).nThen(function (waitFor) { - // Offer ownership to a friend - if (toAdd.length) { - // Send the command - sframeChan.query('Q_SET_PAD_METADATA', { - channel: channel, - command: 'ADD_PENDING_OWNERS', - value: toAdd, - teamId: teamOwner - }, waitFor(function (err, res) { - err = err || (res && res.error); - if (err) { - waitFor.abort(); - redrawAll(); - var text = err === "INSUFFICIENT_PERMISSIONS" ? Messages.fm_forbidden - : Messages.error; - return void UI.warn(text); - } - })); - } - }).nThen(function (waitFor) { - sel.forEach(function (el) { - var curve = $(el).attr('data-curve'); - var friend = curve === user.curvePublic ? user : friends[curve]; - if (!friend) { return; } - common.mailbox.sendTo("ADD_OWNER", { - channel: channel, - href: data.href, - password: data.password, - title: data.title - }, { - channel: friend.notifications, - curvePublic: friend.curvePublic - }, waitFor()); - }); - }).nThen(function () { - redrawAll(); - UI.log(Messages.saved); - }); - }); - $div.append(h('p', addButton)); - return $div; - }; - - redrawAll = function (md) { - var todo = function (obj) { - if (obj && obj.error) { return; } - owners = obj.owners || []; - pending_owners = obj.pending_owners || []; - $div1.empty(); - $div2.empty(); - $div1.append(drawRemove(false)).append(drawRemove(true)); - $div2.append(drawAdd()); - }; - - if (md) { return void todo(md); } - common.getPadMetadata({ - channel: data.channel - }, todo); - }; - - $div1.append(drawRemove(false)).append(drawRemove(true)); - $div2.append(drawAdd()); - - var handler = sframeChan.on('EV_RT_METADATA', function (md) { - if (!$div1.length) { - return void handler.stop(); - } - owners = md.owners || []; - pending_owners = md.pending_owners || []; - redrawAll(md); - }); - - // Create modal - var link = h('div.cp-share-columns', [ - div1, - div2 - /*drawRemove()[0], - drawAdd()[0]*/ - ]); - var linkButtons = [{ - className: 'cancel', - name: Messages.filePicker_close, - onClick: function () {}, - keys: [27] - }]; - return UI.dialog.customModal(link, {buttons: linkButtons}); - }; - - var getRightsProperties = function (common, data, cb) { - var $div = $('<div>'); - if (!data) { return void cb(void 0, $div); } - - var draw = function () { - var $d = $('<div>'); - var priv = common.getMetadataMgr().getPrivateData(); - var user = common.getMetadataMgr().getUserData(); - var edPublic = priv.edPublic; - var owned = false; - var _owners = {}; - if (data.owners && data.owners.length) { - if (data.owners.indexOf(edPublic) !== -1) { - owned = true; - } else { - Object.keys(priv.teams || {}).some(function (id) { - var team = priv.teams[id] || {}; - if (team.viewer) { return; } - if (data.owners.indexOf(team.edPublic) === -1) { return; } - owned = id; - return true; - }); - } - var strangers = 0; - data.owners.forEach(function (ed) { - // If a friend is an owner, add their name to the list - // otherwise, increment the list of strangers - - // Our edPublic? print "Yourself" - if (ed === edPublic) { - _owners[ed] = { - selected: true, - name: user.name, - avatar: user.avatar - }; - return; - } - // One of our teams? print the team name - if (Object.keys(priv.teams || {}).some(function (id) { - var team = priv.teams[id] || {}; - if (team.edPublic !== ed) { return; } - _owners[ed] = { - name: team.name, - avatar: team.avatar - }; - return true; - })) { - return; - } - // One of our friends? print the friend name - if (Object.keys(priv.friends || {}).some(function (c) { - var friend = priv.friends[c] || {}; - if (friend.edPublic !== ed || c === 'me') { return; } - _owners[friend.edPublic] = { - name: friend.displayName, - avatar: friend.avatar - }; - return true; - })) { - return; - } - // Otherwise it's a stranger - strangers++; - }); - if (strangers) { - _owners['stangers'] = { - name: Messages._getKey('properties_unknownUser', [strangers]), - }; - } - } - var _ownersGrid = UIElements.getUserGrid(Messages.creation_owners, { - common: common, - noSelect: true, - data: _owners, - large: true - }, function () {}); - if (_ownersGrid && Object.keys(_owners).length) { - $d.append(_ownersGrid.div); - } else { - $d.append([ - h('label', Messages.creation_owners), - ]); - $d.append(UI.dialog.selectable(Messages.creation_noOwner, { - id: 'cp-app-prop-owners', - })); - - } - - var parsed; - if (data.href || data.roHref) { - parsed = Hash.parsePadUrl(data.href || data.roHref); - } - if (owned && parsed.hashData.type === 'pad') { - var manageOwners = h('button.no-margin', Messages.owner_openModalButton); - $(manageOwners).click(function () { - data.teamId = typeof(owned) !== "boolean" ? owned : undefined; - var modal = createOwnerModal(common, data); - UI.openCustomModal(modal, { - wide: true, - }); - }); - $d.append(h('p', manageOwners)); - } - - if (!data.noExpiration) { - var expire = Messages.creation_expireFalse; - if (data.expire && typeof (data.expire) === "number") { - expire = new Date(data.expire).toLocaleString(); - } - $('<label>', {'for': 'cp-app-prop-expire'}).text(Messages.creation_expiration) - .appendTo($d); - $d.append(UI.dialog.selectable(expire, { - id: 'cp-app-prop-expire', - })); - } - - if (!data.noPassword) { - var hasPassword = data.password; - var $pwLabel = $('<label>', {'for': 'cp-app-prop-password'}).text(Messages.creation_passwordValue) - .hide().appendTo($d); - var password = UI.passwordInput({ - id: 'cp-app-prop-password', - readonly: 'readonly' - }); - var $password = $(password).hide(); - var $pwInput = $password.find('.cp-password-input'); - $pwInput.val(data.password).click(function () { - $pwInput[0].select(); - }); - $d.append(password); - - if (hasPassword) { - $pwLabel.show(); - $password.css('display', 'flex'); - } - - // In the properties, we should have the edit href if we know it. - // We should know it because the pad is stored, but it's better to check... - if (!data.noEditPassword && owned && data.href) { // 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'; - - var changePwTitle = Messages.properties_changePassword; - var changePwConfirm = isFile ? Messages.properties_confirmChangeFile : Messages.properties_confirmChange; - if (!hasPassword) { - changePwTitle = Messages.properties_addPassword; - changePwConfirm = isFile ? Messages.properties_confirmNewFile : Messages.properties_confirmNew; - } - $('<label>', {'for': 'cp-app-prop-change-password'}) - .text(changePwTitle).appendTo($d); - var newPassword = UI.passwordInput({ - id: 'cp-app-prop-change-password', - style: 'flex: 1;' - }); - var passwordOk = h('button', Messages.properties_changePasswordButton); - var changePass = h('span.cp-password-change-container', [ - newPassword, - passwordOk - ]); - var pLocked = false; - $(passwordOk).click(function () { - var newPass = $(newPassword).find('input').val(); - if (data.password === newPass || - (!data.password && !newPass)) { - return void UI.alert(Messages.properties_passwordSame); - } - if (pLocked) { return; } - pLocked = true; - 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' : - (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 - // * display upload progress - var onPending; - var onProgress; - if (isFile) { - onPending = sframeChan.on('Q_BLOB_PASSWORD_CHANGE_PENDING', function (data, cb) { - onPending.stop(); - UI.confirm(Messages.upload_uploadPending, function (yes) { - cb({cancel: yes}); - }); - }); - onProgress = sframeChan.on('EV_BLOB_PASSWORD_CHANGE_PROGRESS', function (data) { - if (typeof (data) !== "number") { return; } - var p = Math.round(data); - $(passwordOk).text(p + '%'); - }); - } - - sframeChan.query(q, { - teamId: typeof(owned) !== "boolean" ? owned : undefined, - href: data.href, - password: newPass - }, function (err, data) { - $(passwordOk).text(Messages.properties_changePasswordButton); - pLocked = false; - if (err || data.error) { - console.error(err || data.error); - return void UI.alert(Messages.properties_passwordError); - } - UI.findOKButton().click(); - - $pwLabel.show(); - $password.css('display', 'flex'); - $pwInput.val(newPass); - - // If the current document is a file or if we're changing the password from a drive, - // we don't have to reload the page at the end. - // Tell the user the password change was successful and abort - if (isFile || priv.app !== parsed.type) { - if (onProgress && onProgress.stop) { onProgress.stop(); } - $(passwordOk).text(Messages.properties_changePasswordButton); - var alertMsg = data.warning ? Messages.properties_passwordWarningFile - : Messages.properties_passwordSuccessFile; - return void UI.alert(alertMsg, undefined, {force: true}); - } - - // Pad password changed: update the href - // Use hidden hash if needed (we're an owner of this pad so we know it is stored) - var useUnsafe = Util.find(priv, ['settings', 'security', 'unsafeLinks']); - var href = (priv.readOnly && data.roHref) ? data.roHref : data.href; - if (useUnsafe === false) { - var newParsed = Hash.parsePadUrl(href); - var newSecret = Hash.getSecrets(newParsed.type, newParsed.hash, newPass); - var newHash = Hash.getHiddenHashFromKeys(parsed.type, newSecret, {}); - href = Hash.hashToHref(newHash, parsed.type); - } - - if (data.warning) { - return void UI.alert(Messages.properties_passwordWarning, function () { - common.gotoURL(href); - }, {force: true}); - } - return void UI.alert(Messages.properties_passwordSuccess, function () { - if (!isSharedFolder) { - common.gotoURL(href); - } - }, {force: true}); - }); - }); - }); - $d.append(changePass); - } - } - return $d; - }; - - var sframeChan = common.getSframeChannel(); - var handler = sframeChan.on('EV_RT_METADATA', function (md) { - if (!$div.length) { - handler.stop(); - return; - } - md = JSON.parse(JSON.stringify(md)); - data.owners = md.owners; - data.expire = md.expire; - data.pending_owners = md.pending_owners; - $div.empty(); - $div.append(draw()); - }); - $div.append(draw()); - - cb(void 0, $div); - }; - var getPadProperties = function (common, data, cb) { + var getPadProperties = function (common, data, opts, cb) { + opts = opts || {}; var $d = $('<div>'); if (!data) { return void cb(void 0, $d); } @@ -695,7 +115,7 @@ define([ })); } - if (data.roHref) { + if (data.roHref && !opts.noReadOnly) { $('<label>', {'for': 'cp-app-prop-rolink'}).text(Messages.viewShare).appendTo($d); $d.append(UI.dialog.selectable(data.roHref, { id: 'cp-app-prop-rolink', @@ -839,7 +259,8 @@ define([ }; - var getPropertiesData = function (common, cb) { + var getPropertiesData = function (common, opts, cb) { + opts = opts || {}; var data = {}; NThen(function (waitFor) { var base = common.getMetadataMgr().getPrivateData().origin; @@ -855,46 +276,33 @@ define([ Util.extend(data, val); if (data.href) { data.href = base + data.href; } if (data.roHref) { data.roHref = base + data.roHref; } - })); - common.getPadMetadata(null, waitFor(function (obj) { - if (obj && obj.error) { return; } - data.owners = obj.owners; - data.expire = obj.expire; - data.pending_owners = obj.pending_owners; - })); + }), opts.href); }).nThen(function () { cb(void 0, data); }); }; - UIElements.getProperties = function (common, data, cb) { - var c1; - var c2; - var button = [{ - className: 'primary', - name: Messages.okButton, - onClick: function () {}, - keys: [13] - }]; + UIElements.getProperties = function (common, opts, cb) { + var data; + var content; NThen(function (waitFor) { - getPadProperties(common, data, waitFor(function (e, c) { - c1 = UI.dialog.customModal(c[0], { - buttons: button - }); + getPropertiesData(common, opts, waitFor(function (e, _data) { + if (e) { + waitFor.abort(); + return void cb(e); + } + data = _data; })); - getRightsProperties(common, data, waitFor(function (e, c) { - c2 = UI.dialog.customModal(c[0], { - buttons: button - }); + }).nThen(function (waitFor) { + getPadProperties(common, data, opts, waitFor(function (e, c) { + if (e) { + waitFor.abort(); + return void cb(e); + } + content = c[0]; })); }).nThen(function () { - var tabs = UI.dialog.tabs([{ - title: Messages.fc_prop, - content: c1 - }, { - title: Messages.creation_propertiesTitle, - content: c2 - }]); - cb (void 0, $(tabs)); + var modal = UI.alert(content); + cb (void 0, modal); }); }; @@ -2382,12 +1790,8 @@ define([ if (!data) { return void UI.alert(Messages.autostore_notAvailable); } - getPropertiesData(common, function (e, data) { + UIElements.getProperties(common, {}, function (e) { if (e) { return void console.error(e); } - UIElements.getProperties(common, data, function (e, $prop) { - if (e) { return void console.error(e); } - UI.openCustomModal($prop[0]); - }); }); }); }); diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index 37a1ad7e6..ce3299af5 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -8,6 +8,9 @@ define([ '/common/common-interface.js', '/common/common-constants.js', '/common/common-feedback.js', + + '/common/inner/access.js', + '/bower_components/nthen/index.js', '/common/hyperscript.js', '/common/proxy-manager.js', @@ -23,6 +26,7 @@ define([ UI, Constants, Feedback, + Access, nThen, h, ProxyManager, @@ -79,6 +83,7 @@ define([ var faColor = 'cptools-palette'; var faTrash = 'fa-trash'; var faDelete = 'fa-eraser'; + var faAccess = 'fa-unlock-alt'; var faProperties = 'fa-info-circle'; var faTags = 'fa-hashtag'; var faUploadFiles = 'cptools-file-upload'; @@ -448,6 +453,10 @@ define([ 'data-icon': faDelete, }, Messages.fc_remove_sharedfolder)), $separator.clone()[0], + h('li', h('a.cp-app-drive-context-access.dropdown-item', { + 'tabindex': '-1', + 'data-icon': faAccess, + }, "ACCESS")), // XXX h('li', h('a.cp-app-drive-context-properties.dropdown-item', { 'tabindex': '-1', 'data-icon': faProperties, @@ -1203,7 +1212,7 @@ define([ hide.push('savelocal'); hide.push('openro'); hide.push('openincode'); - hide.push('properties'); + hide.push('properties', 'access'); hide.push('hashtag'); } // If we're in the trash, hide restore and properties for non-root elements @@ -1233,7 +1242,7 @@ define([ }); if (paths.length > 1) { hide.push('restore'); - hide.push('properties'); + hide.push('properties', 'access'); hide.push('rename'); hide.push('openparent'); hide.push('hashtag'); @@ -1259,10 +1268,10 @@ define([ case 'tree': show = ['open', 'openro', 'openincode', 'expandall', 'collapseall', 'color', 'download', 'share', 'savelocal', 'rename', 'delete', - 'deleteowned', 'removesf', 'properties', 'hashtag']; + 'deleteowned', 'removesf', 'properties', 'access', 'hashtag']; break; case 'default': - show = ['open', 'openro', 'share', 'openparent', 'delete', 'deleteowned', 'properties', 'hashtag']; + show = ['open', 'openro', 'share', 'openparent', 'delete', 'deleteowned', 'properties', 'access', 'hashtag']; break; case 'trashtree': { show = ['empty']; @@ -3074,9 +3083,8 @@ define([ }).appendTo($openDir); } $('<a>').text(Messages.fc_prop).click(function () { - APP.getProperties(r.id, function (e, $prop) { + APP.getProperties(r.id, function (e) { if (e) { return void logError(e); } - UI.alert($prop[0], undefined, true); }); }).appendTo($openDir); } @@ -3824,12 +3832,11 @@ define([ } }; - var getProperties = APP.getProperties = function (el, cb) { + APP.getProperties = function (el, cb) { if (!manager.isFile(el) && !manager.isSharedFolder(el)) { return void cb('NOT_FILE'); } //var ro = manager.isReadOnlyFile(el); - var base = APP.origin; var data; if (manager.isSharedFolder(el)) { data = JSON.parse(JSON.stringify(manager.getSharedFolderData(el))); @@ -3838,42 +3845,42 @@ define([ } if (!data || !(data.href || data.roHref)) { return void cb('INVALID_FILE'); } - if (data.href) { - data.href = base + data.href; - } - if (data.roHref) { - data.roHref = base + data.roHref; - } - - if (currentPath[0] === TEMPLATE) { - data.isTemplate = true; - } + var opts = {}; + opts.href = Hash.getRelativeHref(data.href || data.roHref); if (manager.isSharedFolder(el)) { var ro = folders[el] && folders[el].version >= 2; - if (!ro) { delete data.roHref; } - //data.noPassword = true; - //data.noEditPassword = true; - data.noExpiration = true; - // this is here to allow users to check the channel id of a shared folder - // we should remove it at some point - data.sharedFolder = true; + if (!ro) { opts.noReadOnly = true; } + } + UIElements.getProperties(common, opts, cb); + }; + APP.getAccess = function (el, cb) { + if (!manager.isFile(el) && !manager.isSharedFolder(el)) { + return void cb('NOT_FILE'); + } + var data; + if (manager.isSharedFolder(el)) { + data = JSON.parse(JSON.stringify(manager.getSharedFolderData(el))); + } else { + data = JSON.parse(JSON.stringify(manager.getFileData(el))); + } + if (!data || !(data.href || data.roHref)) { return void cb('INVALID_FILE'); } + + var opts = {}; + opts.href = Hash.getRelativeHref(data.href || data.roHref); + opts.channel = data.channel; + + // Transfer ownership: templates are stored as templates for other users/teams + if (currentPath[0] === TEMPLATE) { + opts.isTemplate = true; } - if ((manager.isFile(el) && data.roHref) || manager.isSharedFolder(el)) { // Only for pads! - sframeChan.query('Q_GET_PAD_METADATA', { - channel: data.channel - }, function (err, val) { - if (!err && !(val && val.error)) { - data.owners = val.owners; - data.expire = val.expire; - data.pending_owners = val.pending_owners; - } - UIElements.getProperties(common, data, cb); - }); - return; + // Shared folders: no expiration date + if (manager.isSharedFolder(el)) { + opts.noExpiration = true; } - UIElements.getProperties(common, data, cb); + + Access.getAccessModal(common, opts, cb); }; if (!APP.loggedIn) { @@ -4218,9 +4225,19 @@ define([ // ANON_SHARED_FOLDER el = manager.find(paths[0].path.slice(1), APP.newSharedFolder); } - getProperties(el, function (e, $prop) { + APP.getProperties(el, function (e) { + if (e) { return void logError(e); } + }); + } + else if ($this.hasClass("cp-app-drive-context-access")) { + if (paths.length !== 1) { return; } + el = manager.find(paths[0].path); + if (paths[0].path[0] === SHARED_FOLDER && APP.newSharedFolder) { + // ANON_SHARED_FOLDER + el = manager.find(paths[0].path.slice(1), APP.newSharedFolder); + } + APP.getAccess(el, function (e) { if (e) { return void logError(e); } - UI.openCustomModal($prop[0]); }); } else if ($this.hasClass("cp-app-drive-context-hashtag")) { diff --git a/www/common/inner/access.js b/www/common/inner/access.js new file mode 100644 index 000000000..139a52d64 --- /dev/null +++ b/www/common/inner/access.js @@ -0,0 +1,658 @@ +define([ + 'jquery', + '/common/common-util.js', + '/common/common-hash.js', + '/common/common-interface.js', + '/common/common-ui-elements.js', + '/common/hyperscript.js', + '/customize/messages.js', + '/bower_components/nthen/index.js', +], function ($, Util, Hash, UI, UIElements, h, + Messages, nThen) { +console.log(UI, UIElements, h); + + var Access = {}; + + var createOwnerModal = function (common, data) { + var friends = common.getFriends(true); + var sframeChan = common.getSframeChannel(); + var priv = common.getMetadataMgr().getPrivateData(); + var user = common.getMetadataMgr().getUserData(); + var edPublic = priv.edPublic; + var channel = data.channel; + var owners = data.owners || []; + var pending_owners = data.pending_owners || []; + var teams = priv.teams; + var teamOwner = data.teamId; + + var redrawAll = function () {}; + + var div1 = h('div.cp-usergrid-user.cp-share-column.cp-ownership'); + var div2 = h('div.cp-usergrid-user.cp-share-column.cp-ownership'); + var $div1 = $(div1); + var $div2 = $(div2); + + // Remove owner column + var drawRemove = function (pending) { + var _owners = {}; + var o = (pending ? pending_owners : owners) || []; + o.forEach(function (ed) { + var f; + Object.keys(friends).some(function (c) { + if (friends[c].edPublic === ed) { + f = friends[c]; + return true; + } + }); + Object.keys(teams).some(function (id) { + if (teams[id].edPublic === ed) { + f = teams[id]; + f.teamId = id; + } + }); + if (ed === edPublic) { + f = f || user; + if (f.name) { f.edPublic = edPublic; } + } + _owners[ed] = f || { + displayName: Messages._getKey('owner_unknownUser', [ed]), + edPublic: ed, + }; + }); + var msg = pending ? Messages.owner_removePendingText + : Messages.owner_removeText; + var removeCol = UIElements.getUserGrid(msg, { + common: common, + large: true, + data: _owners, + noFilter: true + }, function () { + }); + var $div = $(removeCol.div); + // When clicking on the remove button, we check the selected users. + // If you try to remove yourself, we'll display an additional warning message + var btnMsg = pending ? Messages.owner_removePendingButton : Messages.owner_removeButton; + var removeButton = h('button.no-margin', btnMsg); + $(removeButton).click(function () { + // Check selection + var $sel = $div.find('.cp-usergrid-user.cp-selected'); + var sel = $sel.toArray(); + if (!sel.length) { return; } + var me = false; + var toRemove = sel.map(function (el) { + var ed = $(el).attr('data-ed'); + if (!ed) { return; } + if (teamOwner && teams[teamOwner] && teams[teamOwner].edPublic === ed) { me = true; } + if (ed === edPublic && !teamOwner) { me = true; } + return ed; + }).filter(function (x) { return x; }); + nThen(function (waitFor) { + var msg = me ? Messages.owner_removeMeConfirm : Messages.owner_removeConfirm; + UI.confirm(msg, waitFor(function (yes) { + if (!yes) { + waitFor.abort(); + return; + } + })); + }).nThen(function (waitFor) { + // Send the command + sframeChan.query('Q_SET_PAD_METADATA', { + channel: channel, + command: pending ? 'RM_PENDING_OWNERS' : 'RM_OWNERS', + value: toRemove, + teamId: teamOwner + }, waitFor(function (err, res) { + err = err || (res && res.error); + if (err) { + waitFor.abort(); + redrawAll(); + var text = err === "INSUFFICIENT_PERMISSIONS" ? Messages.fm_forbidden + : Messages.error; + return void UI.warn(text); + } + UI.log(Messages.saved); + })); + }).nThen(function (waitFor) { + sel.forEach(function (el) { + var curve = $(el).attr('data-curve'); + var friend = curve === user.curvePublic ? user : friends[curve]; + if (!friend) { return; } + common.mailbox.sendTo("RM_OWNER", { + channel: channel, + title: data.title, + pending: pending + }, { + channel: friend.notifications, + curvePublic: friend.curvePublic + }, waitFor()); + }); + }).nThen(function () { + redrawAll(); + }); + }); + $div.append(h('p', removeButton)); + return $div; + }; + + // Add owners column + var drawAdd = function () { + var $div = $(h('div.cp-share-column')); + var _friends = JSON.parse(JSON.stringify(friends)); + Object.keys(_friends).forEach(function (curve) { + if (owners.indexOf(_friends[curve].edPublic) !== -1 || + pending_owners.indexOf(_friends[curve].edPublic) !== -1 || + !_friends[curve].notifications) { + delete _friends[curve]; + } + }); + var addCol = UIElements.getUserGrid(Messages.owner_addText, { + common: common, + large: true, + data: _friends + }, function () { + //console.log(arguments); + }); + $div.append(addCol.div); + + var teamsData = Util.tryParse(JSON.stringify(priv.teams)) || {}; + Object.keys(teamsData).forEach(function (id) { + var t = teamsData[id]; + t.teamId = id; + if (owners.indexOf(t.edPublic) !== -1 || pending_owners.indexOf(t.edPublic) !== -1) { + delete teamsData[id]; + } + }); + var teamsList = UIElements.getUserGrid(Messages.owner_addTeamText, { + common: common, + large: true, + noFilter: true, + data: teamsData + }, function () {}); + $div.append(teamsList.div); + + // When clicking on the add button, we get the selected users. + var addButton = h('button.no-margin', Messages.owner_addButton); + $(addButton).click(function () { + // Check selection + var $sel = $div.find('.cp-usergrid-user.cp-selected'); + var sel = $sel.toArray(); + if (!sel.length) { return; } + var toAdd = sel.map(function (el) { + var curve = $(el).attr('data-curve'); + // If the pad is woned by a team, we can transfer ownership to ourselves + if (curve === user.curvePublic && teamOwner) { return priv.edPublic; } + var friend = friends[curve]; + if (!friend) { return; } + return friend.edPublic; + }).filter(function (x) { return x; }); + var toAddTeams = sel.map(function (el) { + var team = teamsData[$(el).attr('data-teamid')]; + if (!team || !team.edPublic) { return; } + return { + edPublic: team.edPublic, + id: $(el).attr('data-teamid') + }; + }).filter(function (x) { return x; }); + + nThen(function (waitFor) { + var msg = Messages.owner_addConfirm; + UI.confirm(msg, waitFor(function (yes) { + if (!yes) { + waitFor.abort(); + return; + } + })); + }).nThen(function (waitFor) { + // Add one of our teams as an owner + if (toAddTeams.length) { + // Send the command + sframeChan.query('Q_SET_PAD_METADATA', { + channel: channel, + command: 'ADD_OWNERS', + value: toAddTeams.map(function (obj) { return obj.edPublic; }), + teamId: teamOwner + }, waitFor(function (err, res) { + err = err || (res && res.error); + if (err) { + waitFor.abort(); + redrawAll(); + var text = err === "INSUFFICIENT_PERMISSIONS" ? + Messages.fm_forbidden : Messages.error; + return void UI.warn(text); + } + var isTemplate = priv.isTemplate || data.isTemplate; + toAddTeams.forEach(function (obj) { + sframeChan.query('Q_STORE_IN_TEAM', { + href: data.href || data.rohref, + password: data.password, + path: isTemplate ? ['template'] : undefined, + title: data.title || '', + teamId: obj.id + }, waitFor(function (err) { + if (err) { return void console.error(err); } + })); + }); + })); + } + }).nThen(function (waitFor) { + // Offer ownership to a friend + if (toAdd.length) { + // Send the command + sframeChan.query('Q_SET_PAD_METADATA', { + channel: channel, + command: 'ADD_PENDING_OWNERS', + value: toAdd, + teamId: teamOwner + }, waitFor(function (err, res) { + err = err || (res && res.error); + if (err) { + waitFor.abort(); + redrawAll(); + var text = err === "INSUFFICIENT_PERMISSIONS" ? Messages.fm_forbidden + : Messages.error; + return void UI.warn(text); + } + })); + } + }).nThen(function (waitFor) { + sel.forEach(function (el) { + var curve = $(el).attr('data-curve'); + var friend = curve === user.curvePublic ? user : friends[curve]; + if (!friend) { return; } + common.mailbox.sendTo("ADD_OWNER", { + channel: channel, + href: data.href, + password: data.password, + title: data.title + }, { + channel: friend.notifications, + curvePublic: friend.curvePublic + }, waitFor()); + }); + }).nThen(function () { + redrawAll(); + UI.log(Messages.saved); + }); + }); + $div.append(h('p', addButton)); + return $div; + }; + + redrawAll = function (md) { + var todo = function (obj) { + if (obj && obj.error) { return; } + owners = obj.owners || []; + pending_owners = obj.pending_owners || []; + $div1.empty(); + $div2.empty(); + $div1.append(drawRemove(false)).append(drawRemove(true)); + $div2.append(drawAdd()); + }; + + if (md) { return void todo(md); } + common.getPadMetadata({ + channel: data.channel + }, todo); + }; + + $div1.append(drawRemove(false)).append(drawRemove(true)); + $div2.append(drawAdd()); + + var handler = sframeChan.on('EV_RT_METADATA', function (md) { + if (!$div1.length) { + return void handler.stop(); + } + owners = md.owners || []; + pending_owners = md.pending_owners || []; + redrawAll(md); + }); + + // Create modal + var link = h('div.cp-share-columns', [ + div1, + div2 + /*drawRemove()[0], + drawAdd()[0]*/ + ]); + var linkButtons = [{ + className: 'cancel', + name: Messages.filePicker_close, + onClick: function () {}, + keys: [27] + }]; + return UI.dialog.customModal(link, {buttons: linkButtons}); + }; + + var getRightsProperties = function (common, data, cb) { + var $div = $('<div>'); + if (!data) { return void cb(void 0, $div); } + + var draw = function () { + var $d = $('<div>'); + var priv = common.getMetadataMgr().getPrivateData(); + var user = common.getMetadataMgr().getUserData(); + var edPublic = priv.edPublic; + var owned = false; + var _owners = {}; + if (data.owners && data.owners.length) { + if (data.owners.indexOf(edPublic) !== -1) { + owned = true; + } else { + Object.keys(priv.teams || {}).some(function (id) { + var team = priv.teams[id] || {}; + if (team.viewer) { return; } + if (data.owners.indexOf(team.edPublic) === -1) { return; } + owned = id; + return true; + }); + } + var strangers = 0; + data.owners.forEach(function (ed) { + // If a friend is an owner, add their name to the list + // otherwise, increment the list of strangers + + // Our edPublic? print "Yourself" + if (ed === edPublic) { + _owners[ed] = { + selected: true, + name: user.name, + avatar: user.avatar + }; + return; + } + // One of our teams? print the team name + if (Object.keys(priv.teams || {}).some(function (id) { + var team = priv.teams[id] || {}; + if (team.edPublic !== ed) { return; } + _owners[ed] = { + name: team.name, + avatar: team.avatar + }; + return true; + })) { + return; + } + // One of our friends? print the friend name + if (Object.keys(priv.friends || {}).some(function (c) { + var friend = priv.friends[c] || {}; + if (friend.edPublic !== ed || c === 'me') { return; } + _owners[friend.edPublic] = { + name: friend.displayName, + avatar: friend.avatar + }; + return true; + })) { + return; + } + // Otherwise it's a stranger + strangers++; + }); + if (strangers) { + _owners['stangers'] = { + name: Messages._getKey('properties_unknownUser', [strangers]), + }; + } + } + var _ownersGrid = UIElements.getUserGrid(Messages.creation_owners, { + common: common, + noSelect: true, + data: _owners, + large: true + }, function () {}); + if (_ownersGrid && Object.keys(_owners).length) { + $d.append(_ownersGrid.div); + } else { + $d.append([ + h('label', Messages.creation_owners), + ]); + $d.append(UI.dialog.selectable(Messages.creation_noOwner, { + id: 'cp-app-prop-owners', + })); + + } + + var parsed; + if (data.href || data.roHref) { + parsed = Hash.parsePadUrl(data.href || data.roHref); + } + if (owned && parsed.hashData.type === 'pad') { + var manageOwners = h('button.no-margin', Messages.owner_openModalButton); + $(manageOwners).click(function () { + data.teamId = typeof(owned) !== "boolean" ? owned : undefined; + var modal = createOwnerModal(common, data); + UI.openCustomModal(modal, { + wide: true, + }); + }); + $d.append(h('p', manageOwners)); + } + + if (!data.noExpiration) { + var expire = Messages.creation_expireFalse; + if (data.expire && typeof (data.expire) === "number") { + expire = new Date(data.expire).toLocaleString(); + } + $('<label>', {'for': 'cp-app-prop-expire'}).text(Messages.creation_expiration) + .appendTo($d); + $d.append(UI.dialog.selectable(expire, { + id: 'cp-app-prop-expire', + })); + } + + if (!data.noPassword) { + var hasPassword = data.password; + var $pwLabel = $('<label>', {'for': 'cp-app-prop-password'}).text(Messages.creation_passwordValue) + .hide().appendTo($d); + var password = UI.passwordInput({ + id: 'cp-app-prop-password', + readonly: 'readonly' + }); + var $password = $(password).hide(); + var $pwInput = $password.find('.cp-password-input'); + $pwInput.val(data.password).click(function () { + $pwInput[0].select(); + }); + $d.append(password); + + if (hasPassword) { + $pwLabel.show(); + $password.css('display', 'flex'); + } + + // In the properties, we should have the edit href if we know it. + // We should know it because the pad is stored, but it's better to check... + if (!data.noEditPassword && owned && data.href) { // 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'; + + var changePwTitle = Messages.properties_changePassword; + var changePwConfirm = isFile ? Messages.properties_confirmChangeFile : Messages.properties_confirmChange; + if (!hasPassword) { + changePwTitle = Messages.properties_addPassword; + changePwConfirm = isFile ? Messages.properties_confirmNewFile : Messages.properties_confirmNew; + } + $('<label>', {'for': 'cp-app-prop-change-password'}) + .text(changePwTitle).appendTo($d); + var newPassword = UI.passwordInput({ + id: 'cp-app-prop-change-password', + style: 'flex: 1;' + }); + var passwordOk = h('button', Messages.properties_changePasswordButton); + var changePass = h('span.cp-password-change-container', [ + newPassword, + passwordOk + ]); + var pLocked = false; + $(passwordOk).click(function () { + var newPass = $(newPassword).find('input').val(); + if (data.password === newPass || + (!data.password && !newPass)) { + return void UI.alert(Messages.properties_passwordSame); + } + if (pLocked) { return; } + pLocked = true; + 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' : + (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 + // * display upload progress + var onPending; + var onProgress; + if (isFile) { + onPending = sframeChan.on('Q_BLOB_PASSWORD_CHANGE_PENDING', function (data, cb) { + onPending.stop(); + UI.confirm(Messages.upload_uploadPending, function (yes) { + cb({cancel: yes}); + }); + }); + onProgress = sframeChan.on('EV_BLOB_PASSWORD_CHANGE_PROGRESS', function (data) { + if (typeof (data) !== "number") { return; } + var p = Math.round(data); + $(passwordOk).text(p + '%'); + }); + } + + sframeChan.query(q, { + teamId: typeof(owned) !== "boolean" ? owned : undefined, + href: data.href, + password: newPass + }, function (err, data) { + $(passwordOk).text(Messages.properties_changePasswordButton); + pLocked = false; + if (err || data.error) { + console.error(err || data.error); + return void UI.alert(Messages.properties_passwordError); + } + UI.findOKButton().click(); + + $pwLabel.show(); + $password.css('display', 'flex'); + $pwInput.val(newPass); + + // If the current document is a file or if we're changing the password from a drive, + // we don't have to reload the page at the end. + // Tell the user the password change was successful and abort + if (isFile || priv.app !== parsed.type) { + if (onProgress && onProgress.stop) { onProgress.stop(); } + $(passwordOk).text(Messages.properties_changePasswordButton); + var alertMsg = data.warning ? Messages.properties_passwordWarningFile + : Messages.properties_passwordSuccessFile; + return void UI.alert(alertMsg, undefined, {force: true}); + } + + // Pad password changed: update the href + // Use hidden hash if needed (we're an owner of this pad so we know it is stored) + var useUnsafe = Util.find(priv, ['settings', 'security', 'unsafeLinks']); + var href = (priv.readOnly && data.roHref) ? data.roHref : data.href; + if (useUnsafe === false) { + var newParsed = Hash.parsePadUrl(href); + var newSecret = Hash.getSecrets(newParsed.type, newParsed.hash, newPass); + var newHash = Hash.getHiddenHashFromKeys(parsed.type, newSecret, {}); + href = Hash.hashToHref(newHash, parsed.type); + } + + if (data.warning) { + return void UI.alert(Messages.properties_passwordWarning, function () { + common.gotoURL(href); + }, {force: true}); + } + return void UI.alert(Messages.properties_passwordSuccess, function () { + if (!isSharedFolder) { + common.gotoURL(href); + } + }, {force: true}); + }); + }); + }); + $d.append(changePass); + } + } + return $d; + }; + + var sframeChan = common.getSframeChannel(); + var handler = sframeChan.on('EV_RT_METADATA', function (md) { + if (!$div.length) { + handler.stop(); + return; + } + md = JSON.parse(JSON.stringify(md)); + data.owners = md.owners; + data.expire = md.expire; + data.pending_owners = md.pending_owners; + $div.empty(); + $div.append(draw()); + }); + $div.append(draw()); + + cb(void 0, $div); + }; + + var getAccessData = function (common, opts, _cb) { + var cb = Util.once(Util.mkAsync(_cb)); + opts = opts || {}; + var data = {}; + nThen(function (waitFor) { + var base = common.getMetadataMgr().getPrivateData().origin; + common.getPadAttribute('', waitFor(function (err, val) { + if (err || !val) { + waitFor.abort(); + return void cb(err || 'EEMPTY'); + } + if (!val.fileType) { + delete val.owners; + delete val.expire; + } + Util.extend(data, val); + if (data.href) { data.href = base + data.href; } + if (data.roHref) { data.roHref = base + data.roHref; } + }), opts.href); + + // If this is a file, don't try to look for metadata + if (opts.channel && opts.channel.length > 34) { return; } + common.getPadMetadata({ + channel: opts.channel // optional, fallback to current pad + }, waitFor(function (obj) { + if (obj && obj.error) { console.error(obj.error); return; } + data.owners = obj.owners; + data.expire = obj.expire; + data.pending_owners = obj.pending_owners; + })); + }).nThen(function () { + cb(void 0, data); + }); + }; + Access.getAccessModal = function (common, opts, cb) { + var data; + var content; + nThen(function (waitFor) { + getAccessData(common, opts, waitFor(function (e, _data) { + if (e) { + waitFor.abort(); + return void cb(e); + } + data = _data; + })); + }).nThen(function (waitFor) { + getRightsProperties(common, data, waitFor(function (e, c) { + if (e) { + waitFor.abort(); + return void cb(e); + } + content = c[0]; + })); + }).nThen(function () { + var modal = UI.alert(content); + cb (void 0, modal); + }); + }; + + return Access; +});