// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors // // SPDX-License-Identifier: AGPL-3.0-or-later define([ 'jquery', '/api/config', '/api/broadcast', '/common/common-util.js', '/common/common-hash.js', '/common/common-language.js', '/common/common-interface.js', '/common/common-constants.js', '/common/common-feedback.js', '/common/hyperscript.js', '/common/clipboard.js', '/customize/messages.js', '/customize/application_config.js', '/customize/pages.js', '/components/nthen/index.js', '/common/inner/invitation.js', '/common/visible.js', '/common/pad-types.js', 'css!/customize/fonts/cptools/style.css', ], function ($, Config, Broadcast, Util, Hash, Language, UI, Constants, Feedback, h, Clipboard, Messages, AppConfig, Pages, NThen, InviteInner, Visible, PadTypes) { var UIElements = {}; var urlArgs = Config.requireConf.urlArgs; UIElements.getSvgLogo = function () { var svg = (function(){/* */}).toString().slice(14,-3); return svg; }; UIElements.prettySize = function (bytes) { var unit = Util.magnitudeOfBytes(bytes); if (unit === 'GB') { return Messages._getKey('formattedGB', [ Util.bytesToGigabytes(bytes)]); } else if (unit === 'MB') { return Messages._getKey('formattedMB', [ Util.bytesToMegabytes(bytes)]); } else { return Messages._getKey('formattedKB', [ Util.bytesToKilobytes(bytes)]); } }; UIElements.updateTags = function (common, hrefs) { var existing, tags; var allTags = {}; if (!hrefs || typeof (hrefs) === "string") { hrefs = [hrefs]; } NThen(function(waitFor) { common.getSframeChannel().query("Q_GET_ALL_TAGS", null, waitFor(function(err, res) { if (err || res.error) { return void console.error(err || res.error); } existing = Object.keys(res.tags).sort(); })); }).nThen(function (waitFor) { hrefs.forEach(function (href) { common.getPadAttribute('tags', waitFor(function (err, res) { if (err) { if (err === 'NO_ENTRY') { UI.alert(Messages.tags_noentry); } waitFor.abort(); return void console.error(err); } allTags[href] = res || []; if (tags) { // Intersect with tags from previous pads tags = (res || []).filter(function (tag) { return tags.indexOf(tag) !== -1; }); } else { tags = res || []; } }), href); }); }).nThen(function () { UI.dialog.tagPrompt(tags, existing, function (newTags) { if (!Array.isArray(newTags)) { return; } var added = []; var removed = []; newTags.forEach(function (tag) { if (tags.indexOf(tag) === -1) { added.push(tag); } }); tags.forEach(function (tag) { if (newTags.indexOf(tag) === -1) { removed.push(tag); } }); var update = function (oldTags) { Array.prototype.push.apply(oldTags, added); removed.forEach(function (tag) { var idx = oldTags.indexOf(tag); oldTags.splice(idx, 1); }); }; hrefs.forEach(function (href) { var oldTags = allTags[href] || []; update(oldTags); common.setPadAttribute('tags', Util.deduplicateString(oldTags), null, href); }); }); }); }; var dcAlert; UIElements.disconnectAlert = function () { if (dcAlert && $(dcAlert.element).length) { return; } dcAlert = UI.alert(Messages.common_connectionLost, undefined, true); }; UIElements.reconnectAlert = function () { if (!dcAlert) { return; } if (!dcAlert.delete) { dcAlert = undefined; return; } dcAlert.delete(); dcAlert = undefined; }; var importContent = UIElements.importContent = function (type, f, cfg) { return function (_file) { var todo = function (file) { var reader = new FileReader(); var parsed = file && file.name && /.+\.([^.]+)$/.exec(file.name); var ext = parsed && parsed[1]; reader.onload = function (e) { f(e.target.result, file, ext); }; if (cfg && cfg.binary && cfg.binary.indexOf(ext) !== -1) { reader.readAsArrayBuffer(file, type); } else { reader.readAsText(file, type); } }; if (_file) { return void todo(_file); } var $files = $('', {type:"file"}); if (cfg && cfg.accept) { $files.attr('accept', cfg.accept); } $files.click(); $files.on('change', function (e) { var file = e.target.files[0]; todo(file); }); }; }; UIElements.getUserGrid = function (label, config, onSelect) { var common = config.common; var users = config.data; if (!users) { return; } var icons = Object.keys(users).map(function (key, i) { var data = users[key]; var name = UI.getDisplayName(data.displayName || data.name); var avatar = h('span.cp-usergrid-avatar.cp-avatar', { 'aria-hidden': true, }); common.displayAvatar($(avatar), data.avatar, name, Util.noop, data.uid); var removeBtn, el; if (config.remove) { removeBtn = h('span.fa.fa-times'); $(removeBtn).attr('tabindex', '0'); $(removeBtn).on('click keydown', function(event) { if (event.type === 'click' || (event.type === 'keydown' && event.key === 'Enter')) { event.preventDefault(); config.remove(el); } }); } el = h('div.cp-usergrid-user'+(data.selected?'.cp-selected':'')+(config.large?'.large':''), { 'data-ed': data.edPublic, 'data-teamid': data.teamId, 'data-curve': data.curvePublic || '', 'data-name': name.toLowerCase(), 'data-order': i, 'tabindex': config.noSelect ? '-1' : '0', style: 'order:'+i+';' },[ avatar, h('span.cp-usergrid-user-name', name), data.notRemovable ? undefined : removeBtn ]); return el; }).filter(function (x) { return x; }); var noOthers = icons.length === 0 ? '.cp-usergrid-empty' : ''; var classes = noOthers + (config.large?'.large':'') + (config.list?'.list':''); var inputFilter = h('input', { placeholder: Messages.share_filterFriend }); var div = h('div.cp-usergrid-container' + classes, [ label ? h('label', label) : undefined, h('div.cp-usergrid-filter', (config.noFilter || config.noSelect) ? undefined : [ inputFilter ]), ]); var $div = $(div); // Hide friends when they are filtered using the text input var redraw = function () { var name = $(inputFilter).val().trim().replace(/"/g, '').toLowerCase(); $div.find('.cp-usergrid-user').show(); if (name) { $div.find('.cp-usergrid-user:not(.cp-selected):not([data-name*="'+name+'"])').hide(); } }; $(inputFilter).on('keydown keyup change', redraw); $(div).append(h('div.cp-usergrid-grid', icons)); if (!config.noSelect) { $div.on('click', '.cp-usergrid-user', function () { var sel = $(this).hasClass('cp-selected'); if (!sel) { $(this).addClass('cp-selected'); } else { var order = $(this).attr('data-order'); order = order ? 'order:'+order : ''; $(this).removeClass('cp-selected').attr('style', order); } onSelect(); }); $div.on('keydown', '.cp-usergrid-user', function (e) { if (e.which === 13) { e.preventDefault(); e.stopPropagation(); $(this).trigger('click'); } }); } return { icons: icons, div: div }; }; UIElements.noContactsMessage = function (common) { var metadataMgr = common.getMetadataMgr(); var data = metadataMgr.getUserData(); var origin = metadataMgr.getPrivateData().origin; if (common.isLoggedIn()) { return { content: h('p', Messages.share_noContactsLoggedIn), buttons: [{ className: 'secondary', name: Messages.share_copyProfileLink, onClick: function () { var profile = data.profile ? (origin + '/profile/#' + data.profile) : ''; Clipboard.copy(profile, (err) => { if (!err) { UI.log(Messages.shareSuccess); } }); }, keys: [13] }] }; } else { return { content: h('p', Messages.share_noContactsNotLoggedIn), buttons: [{ className: 'secondary', name: Messages.login_register, onClick: function () { common.setLoginRedirect('register'); } }, { className: 'secondary', name: Messages.login_login, onClick: function () { common.setLoginRedirect('login'); } }] }; } }; UIElements.createInviteTeamModal = function (config) { var common = config.common; var hasFriends = Object.keys(config.friends || {}).length !== 0; var privateData = common.getMetadataMgr().getPrivateData(); var team = privateData.teams[config.teamId]; if (!team) { return void UI.warn(Messages.error); } var origin = privateData.origin; var module = config.module || common.makeUniversal('team'); // Invite contacts var $div; var refreshButton = function () { if (!$div) { return; } var $modal = $div.closest('.alertify'); var $nav = $modal.find('nav'); var $btn = $nav.find('button.primary'); var selected = $div.find('.cp-usergrid-user.cp-selected').length; if (selected) { $btn.prop('disabled', ''); } else { $btn.prop('disabled', 'disabled'); } }; var getContacts = function () { var list = UIElements.getUserGrid(Messages.team_pickFriends, { common: common, data: config.friends, large: true }, refreshButton); var div = h('div.contains-nav'); var $div = $(div); $div.append(list.div); var contactsButtons = [{ className: 'primary', name: Messages.team_inviteModalButton, onClick: function () { var $sel = $div.find('.cp-usergrid-user.cp-selected'); var sel = $sel.toArray(); if (!sel.length) { return; } sel.forEach(function (el) { var curve = $(el).attr('data-curve'); module.execCommand('INVITE_TO_TEAM', { teamId: config.teamId, user: config.friends[curve] }, function (obj) { if (obj && obj.error) { console.error(obj.error); return UI.warn(Messages.error); } }); }); }, keys: [13] }]; return { content: div, buttons: contactsButtons }; }; var friendsObject = hasFriends ? getContacts() : UIElements.noContactsMessage(common); var friendsList = friendsObject.content; var contactsButtons = friendsObject.buttons; contactsButtons.unshift({ className: 'cancel', name: Messages.cancel, onClick: function () {}, keys: [27] }); var contactsContent = h('div.cp-share-modal', [ friendsList ]); var frameContacts = UI.dialog.customModal(contactsContent, { buttons: contactsButtons, }); var linkName, linkPassword, linkMessage, linkError; var linkForm, linkSpin, linkResult, linkUses, linkRole; var linkWarning; // Invite from link var dismissButton = h('span.fa.fa-times'); var roleViewer = UI.createRadio('cp-team-role', 'cp-team-role-viewer', Messages.team_viewers, true, { input: { value: 'VIEWER' }, }); var roleMember = UI.createRadio('cp-team-role', 'cp-team-role-member', Messages.team_members, false, { input: { value: 'MEMBER' }, }); var linkContent = h('div.cp-share-modal', [ h('p', Messages.team_inviteLinkTitle ), linkError = h('div.alert.alert-danger.cp-teams-invite-alert', {style : 'display: none;'}), linkForm = h('div.cp-teams-invite-form', [ // autofill: 'off' was insufficient // adding these two fake inputs confuses firefox and prevents unwanted form autofill h('input', { type: 'text', style: 'display: none'}), h('input', { type: 'password', style: 'display: none'}), linkName = h('input', { placeholder: Messages.team_inviteLinkTempName }), h('br'), h('div.cp-teams-invite-block', [ h('span', Messages.team_inviteLinkSetPassword), h('a.cp-teams-help.fa.fa-question-circle', { href: Pages.localizeDocsLink('https://docs.cryptpad.org/en/user_guide/security.html#passwords-for-documents-and-folders'), target: "_blank", 'data-tippy-placement': "right" }) ]), linkPassword = UI.passwordInput({ id: 'cp-teams-invite-password', placeholder: Messages.login_password }), h('div.cp-teams-invite-block', h('span', Messages.team_inviteLinkNote) ), linkMessage = h('textarea.cp-teams-invite-message', { placeholder: Messages.team_inviteLinkNoteMsg, rows: 3 }), linkRole = h('div.cp-teams-invite-block.cp-teams-invite-role', h('span', Messages.team_inviteRole), roleViewer, roleMember ), h('div.cp-teams-invite-block.cp-teams-invite-uses', linkUses = h('input', { type: 'number', min: 0, max: 999, value: 1 }), h('span', Messages.team_inviteUses) ), ]), linkSpin = h('div.cp-teams-invite-spinner', { style: 'display: none;' }, [ h('i.fa.fa-spinner.fa-spin'), h('span', Messages.team_inviteLinkLoading) ]), linkResult = h('div', { style: 'display: none;' }, h('textarea', { readonly: 'readonly' })), linkWarning = h('div.cp-teams-invite-alert.alert.alert-warning.dismissable', { style: "display: none;" }, [ h('span.cp-inline-alert-text', Messages.team_inviteLinkWarning), dismissButton ]) ]); $(linkUses).on('change keyup', function(e) { if (e.target.value === '') { e.target.value = 0; } }); $(linkMessage).keydown(function (e) { if (e.which === 13) { e.stopPropagation(); } }); var localStore = window.cryptpadStore; localStore.get('hide-alert-teamInvite', function (val) { if (val === '1') { return; } $(linkWarning).css('display', 'flex'); $(dismissButton).on('click', function () { localStore.put('hide-alert-teamInvite', '1'); $(linkWarning).remove(); }); }); var $linkContent = $(linkContent); var href; var process = function () { var $nav = $linkContent.closest('.alertify').find('nav'); $(linkError).text('').hide(); var name = $(linkName).val(); var uses = Number($(linkUses).val()); if (isNaN(uses) || !uses) { uses = -1; } var role = $(linkRole).find("input[name='cp-team-role']:checked").val() || 'VIEWER'; var pw = $(linkPassword).find('input').val(); var msg = $(linkMessage).val(); var hash = Hash.createRandomHash('invite', pw); var hashData = Hash.parseTypeHash('invite', hash); href = origin + '/teams/#' + hash; if (!name || !name.trim()) { $(linkError).text(Messages.team_inviteLinkErrorName).show(); return true; } var seeds = InviteInner.deriveSeeds(hashData.key); var salt = InviteInner.deriveSalt(pw, AppConfig.loginSalt); var bytes64; NThen(function (waitFor) { $(linkForm).hide(); $(linkSpin).show(); $nav.find('button.cp-teams-invite-create').hide(); $nav.find('button.cp-teams-invite-copy').show(); setTimeout(waitFor(), 150); }).nThen(function (waitFor) { InviteInner.deriveBytes(seeds.scrypt, salt, waitFor(function (_bytes) { bytes64 = _bytes; })); }).nThen(function (waitFor) { module.execCommand('CREATE_INVITE_LINK', { name: name, password: pw, message: msg, bytes64: bytes64, hash: hash, teamId: config.teamId, seeds: seeds, role: role, uses: uses }, waitFor(function (obj) { if (obj && obj.error) { waitFor.abort(); $(linkSpin).hide(); $(linkForm).show(); $nav.find('button.cp-teams-invite-create').show(); $nav.find('button.cp-teams-invite-copy').hide(); return void $(linkError).text(Messages.team_inviteLinkError).show(); } // Display result here $(linkSpin).hide(); $(linkResult).show().find('textarea').text(href); $nav.find('button.cp-teams-invite-copy').prop('disabled', ''); })); }); return true; }; var linkButtons = [{ className: 'cancel', name: Messages.cancel, onClick: function () {}, keys: [27] }, { className: 'primary cp-teams-invite-create', name: Messages.team_inviteLinkCreate, onClick: function () { return process(); }, keys: [] }, { className: 'primary cp-teams-invite-copy', name: Messages.team_inviteLinkCopy, onClick: function () { if (!href) { return; } Clipboard.copy(href, (err) => { if (!err) { UI.log(Messages.shareSuccess); } }); }, keys: [] }]; var frameLink = UI.dialog.customModal(linkContent, { buttons: linkButtons, }); $(frameLink).find('.cp-teams-invite-copy').prop('disabled', 'disabled').hide(); // Create modal var tabs = [{ title: Messages.share_contactCategory, icon: "fa fa-address-book", content: frameContacts, active: hasFriends }, { title: Messages.share_linkCategory, icon: "fa fa-link", content: frameLink, active: !hasFriends }]; var modal = UI.dialog.tabs(tabs); UI.openCustomModal(modal); }; UIElements.openDirectlyConfirmation = function (common, cb) { cb = cb || Util.noop; UI.confirm(h('p', Messages.ui_openDirectly), yes => { if (!yes) { return void cb(yes); } common.openDirectly(); cb(yes); }); }; UIElements.getEntryFromButton = function ($button) { if (!$button || !$button.length) { return; } let $icon = $button.find('> i'); let attributes = {}; let btnClass = $button.attr('class'); let btnId = $button.attr('id'); let btnTitle = $button.attr('title'); if (btnClass) { attributes['class'] = btnClass; } if (btnId) { attributes['id'] = btnId; } if (btnTitle && !attributes.title) { attributes['title'] = btnTitle; } return UIElements.createDropdownEntry({ tag: 'a', attributes: attributes, content: [ h('i',{ 'class': $icon.attr('class') }), h('span', $button.text()) ], action: function () { $button.click(); return true; } }); }; UIElements.createButton = function (common, type, rightside, data, callback) { var AppConfig = common.getAppConfig(); var button; var sframeChan = common.getSframeChannel(); var appType = (common.getMetadataMgr().getMetadata().type || 'pad').toUpperCase(); data = data || {}; if (!callback && data.callback) { callback = data.callback; } switch (type) { case 'export': button = $('