From 99c0580895c266dcd7a6953bc5e89dafacd040ea Mon Sep 17 00:00:00 2001 From: pierre-lehnen-rc <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Tue, 14 Apr 2020 15:14:54 -0300 Subject: [PATCH 01/29] [FIX] LDAP users lose session on refresh (#17302) --- app/ldap/server/loginHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/ldap/server/loginHandler.js b/app/ldap/server/loginHandler.js index c563ea950c99..f4c3901c16ca 100644 --- a/app/ldap/server/loginHandler.js +++ b/app/ldap/server/loginHandler.js @@ -158,7 +158,7 @@ callbacks.add('beforeValidateLogin', (login) => { return login; } - if (login.type === 'ldap') { + if (login.type === 'ldap' || login.type === 'resume') { return login; } From b425642c4e7db8b81b4f171b0b0b3f38c4d1824a Mon Sep 17 00:00:00 2001 From: Renato Becker Date: Tue, 14 Apr 2020 11:36:14 -0300 Subject: [PATCH 02/29] [FIX] Omnichannel SMS / WhatsApp integration errors due to missing location data (#17288) --- app/livechat/imports/server/rest/sms.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/livechat/imports/server/rest/sms.js b/app/livechat/imports/server/rest/sms.js index 5f600a0de73c..edebecb583a7 100644 --- a/app/livechat/imports/server/rest/sms.js +++ b/app/livechat/imports/server/rest/sms.js @@ -40,7 +40,7 @@ const defineVisitor = (smsNumber) => { }; const normalizeLocationSharing = (payload) => { - const { extra: { fromLatitude: latitude, fromLongitude: longitude } } = payload; + const { extra: { fromLatitude: latitude, fromLongitude: longitude } = { } } = payload; if (!latitude || !longitude) { return; } From 26bf32fa1db7db240f48e76f3d2898fd121c14fa Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 14 Apr 2020 10:37:47 -0300 Subject: [PATCH 03/29] [FIX] Discussions created from inside DMs were not working (#17282) --- .../server/methods/createDiscussion.js | 4 +- app/lib/lib/roomTypes/public.js | 4 + app/utils/lib/RoomTypeConfig.js | 4 + server/startup/migrations/index.js | 1 + server/startup/migrations/v183.js | 94 +++++++++++++++++++ 5 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 server/startup/migrations/v183.js diff --git a/app/discussion/server/methods/createDiscussion.js b/app/discussion/server/methods/createDiscussion.js index b15a057adedb..e4287b86a45e 100644 --- a/app/discussion/server/methods/createDiscussion.js +++ b/app/discussion/server/methods/createDiscussion.js @@ -5,6 +5,7 @@ import { hasAtLeastOnePermission, canAccessRoom } from '../../../authorization/s import { Messages, Rooms } from '../../../models/server'; import { createRoom, addUserToRoom, sendMessage, attachMessage } from '../../../lib/server'; import { settings } from '../../../settings/server'; +import { roomTypes } from '../../../utils/server'; const getParentRoom = (rid) => { const room = Rooms.findOne(rid); @@ -86,7 +87,8 @@ const create = ({ prid, pmid, t_name, reply, users }) => { // auto invite the replied message owner const invitedUsers = message ? [message.u.username, ...users] : users; - const discussion = createRoom(p_room.t, name, user.username, [...new Set(invitedUsers)], false, { + const type = roomTypes.getConfig(p_room.t).getDiscussionType(); + const discussion = createRoom(type, name, user.username, [...new Set(invitedUsers)], false, { fname: t_name, description: message.msg, // TODO discussions remove topic: p_room.name, // TODO discussions remove diff --git a/app/lib/lib/roomTypes/public.js b/app/lib/lib/roomTypes/public.js index 11d688731eff..8732e6d13d9c 100644 --- a/app/lib/lib/roomTypes/public.js +++ b/app/lib/lib/roomTypes/public.js @@ -133,4 +133,8 @@ export class PublicRoomType extends RoomTypeConfig { return getAvatarURL({ username: `@${ this.roomName(roomData) }` }); } + + getDiscussionType() { + return 'c'; + } } diff --git a/app/utils/lib/RoomTypeConfig.js b/app/utils/lib/RoomTypeConfig.js index da578a82402e..bc40944c6b3a 100644 --- a/app/utils/lib/RoomTypeConfig.js +++ b/app/utils/lib/RoomTypeConfig.js @@ -299,4 +299,8 @@ export class RoomTypeConfig { openCustomProfileTab() { return false; } + + getDiscussionType() { + return 'p'; + } } diff --git a/server/startup/migrations/index.js b/server/startup/migrations/index.js index 60ee4afe9979..948bc590af93 100644 --- a/server/startup/migrations/index.js +++ b/server/startup/migrations/index.js @@ -180,4 +180,5 @@ import './v179'; import './v180'; import './v181'; import './v182'; +import './v183'; import './xrun'; diff --git a/server/startup/migrations/v183.js b/server/startup/migrations/v183.js new file mode 100644 index 000000000000..fd242bd4d9ef --- /dev/null +++ b/server/startup/migrations/v183.js @@ -0,0 +1,94 @@ +import { Random } from 'meteor/random'; + +import { Migrations } from '../../../app/migrations'; +import { Rooms, Messages, Subscriptions, Uploads } from '../../../app/models/server'; + +const unifyRooms = (room) => { + // verify if other DM already exists + const other = Rooms.findOne({ + _id: { $ne: room._id }, + t: 'd', + uids: room.uids, + }); + + // we need to at least change the _id of the current room, so remove it + Rooms.remove({ _id: room._id }); + + const newId = (other && other._id) || Random.id(); + + if (!other) { + // create the same room with different _id + Rooms.insert({ + ...room, + _id: newId, + }); + + // update subscription to point to new room _id + Subscriptions.update({ rid: room._id }, { + $set: { + rid: newId, + }, + }, { multi: true }); + + return newId; + } + + // the other room exists already, so just remove the subscription of the wrong room + Subscriptions.remove({ rid: room._id }); + + return newId; +}; + +const fixSelfDMs = () => { + Rooms.find({ + t: 'd', + uids: { $size: 1 }, + }).forEach((room) => { + if (!Array.isArray(room.uids) || room._id !== room.uids[0]) { + return; + } + + const correctId = unifyRooms(room); + + // move things to correct room + Messages.update({ rid: room._id }, { + $set: { + rid: correctId, + }, + }, { multi: true }); + Uploads.update({ rid: room._id }, { + $set: { + rid: correctId, + }, + }, { multi: true }); + }); +}; + +const fixDiscussions = () => { + Rooms.find({ t: 'd', prid: { $exists: true } }, { fields: { _id: 1 } }).forEach(({ _id }) => { + const { u } = Messages.findOne({ drid: _id }, { fields: { u: 1 } }) || {}; + + Rooms.update({ _id }, { + $set: { + t: 'p', + name: Random.id(), + u, + ro: false, + default: false, + sysMes: true, + }, + $unset: { + usernames: 1, + uids: 1, + }, + }); + }); +}; + +Migrations.add({ + version: 183, + up() { + fixDiscussions(); + fixSelfDMs(); + }, +}); From 78bf1fc69b6ddf81601191fb161e5cb67077c90c Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 14 Apr 2020 00:57:18 -0300 Subject: [PATCH 04/29] [FIX] Directory default tab (#17283) --- .../components/Directory/DirectoryTable.js | 22 +++++++++---------- .../views/app/components/Directory/index.js | 8 ++++--- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/app/ui/client/views/app/components/Directory/DirectoryTable.js b/app/ui/client/views/app/components/Directory/DirectoryTable.js index 09925c92e407..96c73164102c 100644 --- a/app/ui/client/views/app/components/Directory/DirectoryTable.js +++ b/app/ui/client/views/app/components/Directory/DirectoryTable.js @@ -27,23 +27,23 @@ export function Markdown({ children, ...props }) { const LoadingRow = ({ cols }) => - - - - - - - - - + + + + + + + - + { Array.from({ length: cols - 1 }, (_, i) => )} ; +const style = { minHeight: '40px' }; + export function DirectoryTable({ data = {}, renderRow, @@ -79,7 +79,7 @@ export function DirectoryTable({ return <> - + } onChange={handleChange} value={text} /> {channels && !channels.length diff --git a/app/ui/client/views/app/components/Directory/index.js b/app/ui/client/views/app/components/Directory/index.js index 5d56e61265a5..fa19dd1834fd 100644 --- a/app/ui/client/views/app/components/Directory/index.js +++ b/app/ui/client/views/app/components/Directory/index.js @@ -13,6 +13,8 @@ const avatarBase = { baseUrl: '/avatar/' }; export function DirectoryPage() { const t = useTranslation(); + const defaultTab = useSetting('Accounts_Directory_DefaultView'); + const federationEnabled = useSetting('FEDERATION_Enabled'); const tab = useRouteParameter('tab'); @@ -22,13 +24,13 @@ export function DirectoryPage() { useEffect(() => { if (!tab || (tab === 'external' && !federationEnabled)) { - return goToDirectory.replacingState({ tab: 'channels' }); + return goToDirectory.replacingState({ tab: defaultTab }); } - }, [tab, federationEnabled]); + }, [tab, federationEnabled, defaultTab]); return - + {t('Channels')} {t('Users')} { federationEnabled && {t('External_Users')} } From 4eb84e4dc5254ed9eb43d26aafc0ec22996734de Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 13 Apr 2020 23:30:23 -0300 Subject: [PATCH 05/29] [FIX] Avatar on sidebar when showing real names (#17286) --- app/lib/lib/roomTypes/direct.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/lib/lib/roomTypes/direct.js b/app/lib/lib/roomTypes/direct.js index 1a36927ba561..28295be6bcde 100644 --- a/app/lib/lib/roomTypes/direct.js +++ b/app/lib/lib/roomTypes/direct.js @@ -186,16 +186,23 @@ export class DirectMessageRoomType extends RoomTypeConfig { } getAvatarPath(roomData, subData) { + if (!roomData && !subData) { + return ''; + } + if (this.isGroupChat(roomData)) { return getAvatarURL({ username: roomData.uids.length + roomData.usernames.join() }); } - if (roomData) { - return getUserAvatarURL(roomData.name || this.roomName(roomData)); + const sub = subData || Subscriptions.findOne({ rid: roomData._id }, { fields: { name: 1 } }); + + if (sub && sub.name) { + return getUserAvatarURL(sub.name); } - const sub = subData || Subscriptions.findOne({ rid: roomData._id }, { fields: { name: 1 } }); - return getUserAvatarURL(sub.name || this.roomName(roomData)); + if (roomData) { + return getUserAvatarURL(roomData.name || this.roomName(roomData)); // rooms should have no name for direct messages... + } } includeInDashboard() { From 1769fcbdfc4e62bf857d61eed416d775784c6e77 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Mon, 13 Apr 2020 22:15:28 -0300 Subject: [PATCH 06/29] Update Apps-Engine to stable version (#17287) --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 54b8e0fd8dae..de56102c8bf1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2849,9 +2849,9 @@ } }, "@rocket.chat/apps-engine": { - "version": "1.13.0-beta.3003", - "resolved": "https://registry.npmjs.org/@rocket.chat/apps-engine/-/apps-engine-1.13.0-beta.3003.tgz", - "integrity": "sha512-RGegax9zaDzQta7U1v05JYUgJUhrGlYBYioMdCJRvKs6NrmJNbpdeJ6A0Bu6pNKF9ppMkG17hfxI6xT0MoSd0w==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@rocket.chat/apps-engine/-/apps-engine-1.13.0.tgz", + "integrity": "sha512-gU72qk3xhk5UYGmgsp4VLnOOYeAcsc4O8K2rYiLfs1EpBA1tNY+/YtR6Crl5usdU7sRSHUq4Y86pAchepT6aJQ==", "requires": { "adm-zip": "^0.4.9", "cryptiles": "^4.1.3", diff --git a/package.json b/package.json index 390186fce3fa..fe44b7d47cda 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "@nivo/heatmap": "^0.61.0", "@nivo/line": "^0.61.1", "@nivo/pie": "^0.61.1", - "@rocket.chat/apps-engine": "^1.13.0-beta.3003", + "@rocket.chat/apps-engine": "^1.13.0", "@rocket.chat/fuselage": "^0.7.1", "@rocket.chat/fuselage-hooks": "^0.7.1", "@rocket.chat/fuselage-polyfills": "^0.7.1", From af0111609977ec9d7477178f135ee1fb5779fa45 Mon Sep 17 00:00:00 2001 From: pierre-lehnen-rc <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Mon, 13 Apr 2020 11:28:53 -0300 Subject: [PATCH 07/29] [FIX] 404 error when clicking an username (#17275) --- app/ui/client/views/app/room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/ui/client/views/app/room.js b/app/ui/client/views/app/room.js index 188132a18ccf..8ea44dd8d5d1 100644 --- a/app/ui/client/views/app/room.js +++ b/app/ui/client/views/app/room.js @@ -83,7 +83,7 @@ const openProfileTabOrOpenDM = (e, instance, username) => { } if (result && result.rid) { - FlowRouter.go('direct', { username }, FlowRouter.current().queryParams); + FlowRouter.go('direct', { rid: result.rid }, FlowRouter.current().queryParams); } }); } else { From c6fa109b251e5526b0edf507ff3e09e920667492 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 14 Apr 2020 11:32:18 -0300 Subject: [PATCH 08/29] [FIX] User search on directory not working correctly (#17299) --- app/lib/server/startup/settings.js | 3 +-- app/models/server/models/Users.js | 11 +++-------- server/startup/migrations/v183.js | 13 ++++++++++++- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/app/lib/server/startup/settings.js b/app/lib/server/startup/settings.js index f067b03c12b3..33bf91e477af 100644 --- a/app/lib/server/startup/settings.js +++ b/app/lib/server/startup/settings.js @@ -97,9 +97,8 @@ settings.addGroup('Accounts', function() { type: 'boolean', public: true, }); - this.add('Accounts_SearchFields', '', { + this.add('Accounts_SearchFields', 'username, name, bio', { type: 'string', - public: true, }); this.add('Accounts_Directory_DefaultView', 'channels', { type: 'select', diff --git a/app/models/server/models/Users.js b/app/models/server/models/Users.js index 6930ec7a2b3a..d3139dc097f9 100644 --- a/app/models/server/models/Users.js +++ b/app/models/server/models/Users.js @@ -30,7 +30,7 @@ export class Users extends Base { this.tryEnsureIndex({ roles: 1 }, { sparse: 1 }); this.tryEnsureIndex({ name: 1 }); - this.tryEnsureIndex({ name: 'text', username: 'text', bio: 'text' }, { default_language: 'none', language_override: 'documentLanguage' }); + this.tryEnsureIndex({ bio: 1 }); this.tryEnsureIndex({ createdAt: 1 }); this.tryEnsureIndex({ lastLogin: 1 }); this.tryEnsureIndex({ status: 1 }); @@ -641,10 +641,7 @@ export class Users extends Base { const searchFields = forcedSearchFields || settings.get('Accounts_SearchFields').trim().split(','); const orStmt = _.reduce(searchFields, function(acc, el) { - el = el.trim(); - if (el && !['name', 'username', 'bio'].includes(el)) { - acc.push({ [el]: termRegex }); - } + acc.push({ [el.trim()]: termRegex }); return acc; }, []); @@ -652,10 +649,8 @@ export class Users extends Base { $and: [ { active: true, - $or: [{ - $text: { $search: searchTerm }, - }, ...orStmt], username: { $exists: true, $nin: exceptions }, + $or: orStmt, }, ...extraQuery, ], diff --git a/server/startup/migrations/v183.js b/server/startup/migrations/v183.js index fd242bd4d9ef..df71bd061730 100644 --- a/server/startup/migrations/v183.js +++ b/server/startup/migrations/v183.js @@ -1,7 +1,7 @@ import { Random } from 'meteor/random'; import { Migrations } from '../../../app/migrations'; -import { Rooms, Messages, Subscriptions, Uploads } from '../../../app/models/server'; +import { Rooms, Messages, Subscriptions, Uploads, Settings, Users } from '../../../app/models/server'; const unifyRooms = (room) => { // verify if other DM already exists @@ -85,10 +85,21 @@ const fixDiscussions = () => { }); }; +const fixUserSearch = () => { + const setting = Settings.findOneById('Accounts_SearchFields', { fields: { value: 1 } }); + const value = setting?.value?.trim(); + if (value === '' || value === 'username, name') { + Settings.updateValueById('Accounts_SearchFields', 'username, name, bio'); + } + + Users.tryDropIndex('name_text_username_text_bio_text'); +}; + Migrations.add({ version: 183, up() { fixDiscussions(); fixSelfDMs(); + fixUserSearch(); }, }); From e12a3e60018ba7779cb27e298f3adfd588af4a0b Mon Sep 17 00:00:00 2001 From: pierre-lehnen-rc <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Tue, 14 Apr 2020 17:00:02 -0300 Subject: [PATCH 09/29] [FIX] SAML assertion signature enforcement (#17278) --- .../server/saml_rocketchat.js | 15 ++++++ app/meteor-accounts-saml/server/saml_utils.js | 48 +++++++++++++++---- packages/rocketchat-i18n/i18n/en.i18n.json | 6 +++ server/startup/migrations/index.js | 1 + server/startup/migrations/v184.js | 16 +++++++ 5 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 server/startup/migrations/v184.js diff --git a/app/meteor-accounts-saml/server/saml_rocketchat.js b/app/meteor-accounts-saml/server/saml_rocketchat.js index c64e00ef5919..b8c788399527 100644 --- a/app/meteor-accounts-saml/server/saml_rocketchat.js +++ b/app/meteor-accounts-saml/server/saml_rocketchat.js @@ -62,6 +62,19 @@ Meteor.methods({ multiline: true, i18nLabel: 'SAML_Custom_Public_Cert', }); + settings.add(`SAML_Custom_${ name }_signature_validation_type`, 'All', { + type: 'select', + values: [ + { key: 'Response', i18nLabel: 'SAML_Custom_signature_validation_response' }, + { key: 'Assertion', i18nLabel: 'SAML_Custom_signature_validation_assertion' }, + { key: 'Either', i18nLabel: 'SAML_Custom_signature_validation_either' }, + { key: 'All', i18nLabel: 'SAML_Custom_signature_validation_all' }, + ], + group: 'SAML', + section: name, + i18nLabel: 'SAML_Custom_signature_validation_type', + i18nDescription: 'SAML_Custom_signature_validation_type_description', + }); settings.add(`SAML_Custom_${ name }_private_key`, '', { type: 'string', group: 'SAML', @@ -238,6 +251,7 @@ const getSamlConfigs = function(service) { // People often overlook the instruction to remove the header and footer of the certificate on this specific setting, so let's do it for them. cert: normalizeCert(settings.get(`${ service.key }_cert`)), }, + signatureValidationType: settings.get(`${ service.key }_signature_validation_type`), userDataFieldMap: settings.get(`${ service.key }_user_data_fieldmap`), allowedClockDrift: settings.get(`${ service.key }_allowed_clock_drift`), }; @@ -290,6 +304,7 @@ const configureSamlService = function(samlConfigs) { roleAttributeName: samlConfigs.roleAttributeName, roleAttributeSync: samlConfigs.roleAttributeSync, allowedClockDrift: samlConfigs.allowedClockDrift, + signatureValidationType: samlConfigs.signatureValidationType, }; }; diff --git a/app/meteor-accounts-saml/server/saml_utils.js b/app/meteor-accounts-saml/server/saml_utils.js index 5c68621d281b..532af1f00fa4 100644 --- a/app/meteor-accounts-saml/server/saml_utils.js +++ b/app/meteor-accounts-saml/server/saml_utils.js @@ -347,6 +347,10 @@ SAML.prototype.validateSignatureChildren = function(xml, cert, parent) { signature = sign; } + if (!signature) { + return false; + } + return this.validateSignature(xml, cert, signature); }; @@ -564,19 +568,43 @@ SAML.prototype.verifySignatures = function(response, assertion, xml) { return; } - debugLog('Verify Document Signature'); - if (!this.validateResponseSignature(xml, this.options.cert, response)) { - debugLog('Document Signature WRONG'); - throw new Error('Invalid Signature'); + const signatureType = this.options.signatureValidationType; + + const checkEither = signatureType === 'Either'; + const checkResponse = signatureType === 'Response' || signatureType === 'All' || checkEither; + const checkAssertion = signatureType === 'Assertion' || signatureType === 'All' || checkEither; + let anyValidSignature = false; + + if (checkResponse) { + debugLog('Verify Document Signature'); + if (!this.validateResponseSignature(xml, this.options.cert, response)) { + if (!checkEither) { + debugLog('Document Signature WRONG'); + throw new Error('Invalid Signature'); + } + } else { + anyValidSignature = true; + } + debugLog('Document Signature OK'); + } + + if (checkAssertion) { + debugLog('Verify Assertion Signature'); + if (!this.validateAssertionSignature(xml, this.options.cert, assertion)) { + if (!checkEither) { + debugLog('Assertion Signature WRONG'); + throw new Error('Invalid Assertion signature'); + } + } else { + anyValidSignature = true; + } + debugLog('Assertion Signature OK'); } - debugLog('Document Signature OK'); - debugLog('Verify Assertion Signature'); - if (!this.validateAssertionSignature(xml, this.options.cert, assertion)) { - debugLog('Assertion Signature WRONG'); - throw new Error('Invalid Assertion signature'); + if (checkEither && !anyValidSignature) { + debugLog('No Valid Signature'); + throw new Error('No valid SAML Signature found'); } - debugLog('Assertion Signature OK'); }; SAML.prototype.getSubject = function(assertion) { diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index bef4385c60a0..22e2b31e14be 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2930,6 +2930,12 @@ "SAML_Custom_Private_Key": "Private Key Contents", "SAML_Custom_Provider": "Custom Provider", "SAML_Custom_EMail_Field": "E-Mail field name", + "SAML_Custom_signature_validation_all": "Validate All Signatures", + "SAML_Custom_signature_validation_assertion": "Validate Assertion Signature", + "SAML_Custom_signature_validation_either": "Validate Either Signature", + "SAML_Custom_signature_validation_response": "Validate Response Signature", + "SAML_Custom_signature_validation_type": "Signature Validation Type", + "SAML_Custom_signature_validation_type_description": "This setting will be ignored if not Custom Certificate is provided.", "SAML_Custom_user_data_fieldmap": "User Data Field Map", "SAML_Custom_user_data_fieldmap_description": "Configure how user account fields (like email) are populated from a record in SAML (once found).
As an example, `{\"cn\":\"name\", \"mail\":\"email\"}` will choose a person's human readable name from the cn attribute, and their email from the mail attribute.
Available fields in Rocket.Chat: `name`, `email` and `username`, everything else will be saved as `customFields`.
You can also use a regex to get the field value, like this: `{\"NameID\": { \"field\": \"username\", \"regex\": \"(.*)@.+$\"}, \"email\": \"email\"}`", "SAML_Custom_Username_Field": "Username field name", diff --git a/server/startup/migrations/index.js b/server/startup/migrations/index.js index 948bc590af93..1085c8ea77df 100644 --- a/server/startup/migrations/index.js +++ b/server/startup/migrations/index.js @@ -181,4 +181,5 @@ import './v180'; import './v181'; import './v182'; import './v183'; +import './v184'; import './xrun'; diff --git a/server/startup/migrations/v184.js b/server/startup/migrations/v184.js new file mode 100644 index 000000000000..033cc48f4a68 --- /dev/null +++ b/server/startup/migrations/v184.js @@ -0,0 +1,16 @@ +import { Migrations } from '../../../app/migrations'; +import { Settings } from '../../../app/models/server'; + +Migrations.add({ + version: 184, + up() { + // Set SAML signature validation type to 'Either' + Settings.upsert({ + _id: 'SAML_Custom_Default_signature_validation_type', + }, { + $set: { + value: 'Either', + }, + }); + }, +}); From fc8eb5ab574a080deec9e91e1c4cb805be754eaa Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 14 Apr 2020 18:56:27 -0300 Subject: [PATCH 10/29] Bump version to 3.1.1 --- .docker/Dockerfile.rhel | 2 +- .github/history.json | 95 +++++++++++++++++++++++++++++++++++++++ HISTORY.md | 44 ++++++++++++++++++ app/utils/rocketchat.info | 2 +- package.json | 2 +- 5 files changed, 142 insertions(+), 3 deletions(-) diff --git a/.docker/Dockerfile.rhel b/.docker/Dockerfile.rhel index e0084ee1a1b6..87cd8371574b 100644 --- a/.docker/Dockerfile.rhel +++ b/.docker/Dockerfile.rhel @@ -1,6 +1,6 @@ FROM registry.access.redhat.com/rhscl/nodejs-8-rhel7 -ENV RC_VERSION 3.1.0 +ENV RC_VERSION 3.1.1 MAINTAINER buildmaster@rocket.chat diff --git a/.github/history.json b/.github/history.json index a77a23b8ba67..0a3e4abf62ba 100644 --- a/.github/history.json +++ b/.github/history.json @@ -42397,6 +42397,101 @@ ] } ] + }, + "3.1.1": { + "node_version": "12.16.1", + "npm_version": "6.13.4", + "mongo_versions": [ + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "17278", + "title": "[FIX] SAML assertion signature enforcement", + "userLogin": "pierre-lehnen-rc", + "milestone": "3.1.1", + "contributors": [ + "pierre-lehnen-rc", + "sampaiodiego" + ] + }, + { + "pr": "17299", + "title": "[FIX] User search on directory not working correctly", + "userLogin": "rodrigok", + "milestone": "3.1.1", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "17275", + "title": "[FIX] 404 error when clicking an username", + "userLogin": "pierre-lehnen-rc", + "milestone": "3.1.1", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "17287", + "title": "Update Apps-Engine to stable version", + "userLogin": "d-gubert", + "milestone": "3.1.1", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "17286", + "title": "[FIX] Avatar on sidebar when showing real names", + "userLogin": "ggazzo", + "milestone": "3.1.1", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "17283", + "title": "[FIX] Directory default tab", + "userLogin": "ggazzo", + "milestone": "3.1.1", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "17282", + "title": "[FIX] Discussions created from inside DMs were not working and some errors accessing recently created rooms", + "userLogin": "rodrigok", + "milestone": "3.1.1", + "contributors": [ + "rodrigok", + "sampaiodiego" + ] + }, + { + "pr": "17288", + "title": " [FIX] Omnichannel SMS / WhatsApp integration errors due to missing location data", + "userLogin": "renatobecker", + "milestone": "3.1.1", + "contributors": [ + "renatobecker", + "web-flow" + ] + }, + { + "pr": "17302", + "title": "[FIX] LDAP users lose session on refresh", + "userLogin": "pierre-lehnen-rc", + "milestone": "3.1.1", + "contributors": [ + "pierre-lehnen-rc" + ] + } + ] } } } \ No newline at end of file diff --git a/HISTORY.md b/HISTORY.md index 81183dfee7ec..16782fa4ff99 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,4 +1,48 @@ +# 3.1.1 +`2020-04-14 · 8 🐛 · 1 🔍 · 6 👩‍💻👨‍💻` + +### Engine versions +- Node: `12.16.1` +- NPM: `6.13.4` +- MongoDB: `3.4, 3.6, 4.0` + +### 🐛 Bug fixes + + +- SAML assertion signature enforcement ([#17278](https://github.com/RocketChat/Rocket.Chat/pull/17278)) + +- User search on directory not working correctly ([#17299](https://github.com/RocketChat/Rocket.Chat/pull/17299)) + +- 404 error when clicking an username ([#17275](https://github.com/RocketChat/Rocket.Chat/pull/17275)) + +- Avatar on sidebar when showing real names ([#17286](https://github.com/RocketChat/Rocket.Chat/pull/17286)) + +- Directory default tab ([#17283](https://github.com/RocketChat/Rocket.Chat/pull/17283)) + +- Discussions created from inside DMs were not working and some errors accessing recently created rooms ([#17282](https://github.com/RocketChat/Rocket.Chat/pull/17282)) + +- Omnichannel SMS / WhatsApp integration errors due to missing location data ([#17288](https://github.com/RocketChat/Rocket.Chat/pull/17288)) + +- LDAP users lose session on refresh ([#17302](https://github.com/RocketChat/Rocket.Chat/pull/17302)) + +
+🔍 Minor changes + + +- Update Apps-Engine to stable version ([#17287](https://github.com/RocketChat/Rocket.Chat/pull/17287)) + +
+ +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@d-gubert](https://github.com/d-gubert) +- [@ggazzo](https://github.com/ggazzo) +- [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) +- [@renatobecker](https://github.com/renatobecker) +- [@rodrigok](https://github.com/rodrigok) +- [@sampaiodiego](https://github.com/sampaiodiego) + # 3.1.0 `2020-04-09 · 23 🎉 · 22 🚀 · 71 🐛 · 86 🔍 · 41 👩‍💻👨‍💻` diff --git a/app/utils/rocketchat.info b/app/utils/rocketchat.info index 8d691dd7f54b..67bc530e7a14 100644 --- a/app/utils/rocketchat.info +++ b/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "3.1.0" + "version": "3.1.1" } diff --git a/package.json b/package.json index fe44b7d47cda..508a8cd18525 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "Rocket.Chat", "description": "The Ultimate Open Source WebChat Platform", - "version": "3.1.0", + "version": "3.1.1", "author": { "name": "Rocket.Chat", "url": "https://rocket.chat/" From da41b61d06c56150b72ac3c172b9f23172685686 Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Mon, 20 Apr 2020 17:01:51 -0300 Subject: [PATCH 11/29] [FIX] Remove properties from users.info response (#17238) --- app/api/server/v1/users.js | 14 ++----- app/lib/server/functions/getFullUserData.js | 22 ++++++++-- app/models/server/models/Users.js | 12 ++++++ tests/end-to-end/api/01-users.js | 45 ++++++++++++++++++++- 4 files changed, 77 insertions(+), 16 deletions(-) diff --git a/app/api/server/v1/users.js b/app/api/server/v1/users.js index 4b861eb11407..0598d8a430a2 100644 --- a/app/api/server/v1/users.js +++ b/app/api/server/v1/users.js @@ -16,7 +16,7 @@ import { setUserAvatar, saveCustomFields, } from '../../../lib'; -import { getFullUserData, getFullUserDataById } from '../../../lib/server/functions/getFullUserData'; +import { getFullUserDataByIdOrUsername } from '../../../lib/server/functions/getFullUserData'; import { API } from '../api'; import { setStatusText } from '../../../lib/server'; import { findUsersToAutocomplete } from '../lib/users'; @@ -180,20 +180,12 @@ API.v1.addRoute('users.info', { authRequired: true }, { get() { const { username, userId } = this.requestParams(); const { fields } = this.parseJsonQuery(); - const params = { - userId: this.userId, - filter: username, - limit: 1, - }; - const result = userId - ? getFullUserDataById({ userId: this.userId, filterId: userId }) - : getFullUserData(params); + const user = getFullUserDataByIdOrUsername({ userId: this.userId, filterId: userId, filterUsername: username }); - if (!result || result.count() !== 1) { + if (!user) { return API.v1.failure('User not found.'); } - const [user] = result.fetch(); const myself = user._id === this.userId; if (fields.userRooms === 1 && (myself || hasPermission(this.userId, 'view-other-user-channels'))) { user.rooms = Subscriptions.findByUserId(user._id, { diff --git a/app/lib/server/functions/getFullUserData.js b/app/lib/server/functions/getFullUserData.js index d3892bca9c51..cb75e4fa37ed 100644 --- a/app/lib/server/functions/getFullUserData.js +++ b/app/lib/server/functions/getFullUserData.js @@ -64,16 +64,32 @@ const getFields = (canViewAllInfo) => ({ ...getCustomFields(canViewAllInfo), }); -export function getFullUserDataById({ userId, filterId }) { - const canViewAllInfo = userId === filterId || hasPermission(userId, 'view-full-other-user-info'); +const removePasswordInfo = (user) => { + if (user && user.services) { + delete user.services.password; + delete user.services.email; + delete user.services.resume; + delete user.services.emailCode; + delete user.services.cloud; + delete user.services.email2fa; + delete user.services.totp; + } + return user; +}; + +export function getFullUserDataByIdOrUsername({ userId, filterId, filterUsername }) { + const caller = Users.findOneById(userId, { fields: { username: 1 } }); + const myself = userId === filterId || filterUsername === caller.username; + const canViewAllInfo = myself || hasPermission(userId, 'view-full-other-user-info'); const fields = getFields(canViewAllInfo); const options = { fields, }; + const user = Users.findOneByIdOrUsername(filterId || filterUsername, options); - return Users.findById(filterId, options); + return myself ? user : removePasswordInfo(user); } export const getFullUserData = function({ userId, filter, limit: l }) { diff --git a/app/models/server/models/Users.js b/app/models/server/models/Users.js index d3139dc097f9..96f522c9c8d1 100644 --- a/app/models/server/models/Users.js +++ b/app/models/server/models/Users.js @@ -549,6 +549,18 @@ export class Users extends Base { return this.findOne(query, options); } + findOneByIdOrUsername(idOrUsername, options) { + const query = { + $or: [{ + _id: idOrUsername, + }, { + username: idOrUsername, + }], + }; + + return this.findOne(query, options); + } + // FIND findByIds(users, options) { const query = { _id: { $in: users } }; diff --git a/tests/end-to-end/api/01-users.js b/tests/end-to-end/api/01-users.js index 56de4a6a2db6..982e8b966540 100644 --- a/tests/end-to-end/api/01-users.js +++ b/tests/end-to-end/api/01-users.js @@ -196,7 +196,10 @@ describe('[Users]', function() { }); describe('[/users.info]', () => { - after(() => updatePermission('view-other-user-channels', ['admin'])); + after(() => { + updatePermission('view-other-user-channels', ['admin']); + updatePermission('view-full-other-user-info', ['admin']); + }); it('should return an error when the user does not exist', (done) => { request.get(api('users.info')) @@ -212,7 +215,6 @@ describe('[Users]', function() { }) .end(done); }); - it('should query information about a user by userId', (done) => { request.get(api('users.info')) .set(credentials) @@ -293,6 +295,45 @@ describe('[Users]', function() { .end(done); }); }); + it('should NOT return some services fields when request to another user\'s info even if the user has the necessary permission', (done) => { + updatePermission('view-full-other-user-info', ['admin']).then(() => { + request.get(api('users.info')) + .set(credentials) + .query({ + userId: targetUser._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.not.have.nested.property('user.services.emailCode'); + expect(res.body).to.not.have.nested.property('user.services.cloud'); + expect(res.body).to.not.have.nested.property('user.services.email2fa'); + expect(res.body).to.not.have.nested.property('user.services.totp'); + expect(res.body).to.not.have.nested.property('user.services.password'); + expect(res.body).to.not.have.nested.property('user.services.email'); + expect(res.body).to.not.have.nested.property('user.services.resume'); + }) + .end(done); + }); + }); + it('should return all services fields when request for myself data even without privileged permission', (done) => { + updatePermission('view-full-other-user-info', []).then(() => { + request.get(api('users.info')) + .set(credentials) + .query({ + userId: credentials['X-User-Id'], + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('user.services.password'); + expect(res.body).to.have.nested.property('user.services.resume'); + }) + .end(done); + }); + }); }); describe('[/users.getPresence]', () => { it('should query a user\'s presence by userId', (done) => { From fc409700a3636c422c5be03f4657391e7f92f9c6 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 20 Apr 2020 19:30:45 -0300 Subject: [PATCH 12/29] [FIX] Prevent user from getting stuck on login, if there is some bad fname (#17331) --- app/ui-cached-collection/client/models/CachedCollection.js | 2 +- app/ui-sidenav/client/roomList.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/ui-cached-collection/client/models/CachedCollection.js b/app/ui-cached-collection/client/models/CachedCollection.js index 59929c54ce0f..8450ee6b879d 100644 --- a/app/ui-cached-collection/client/models/CachedCollection.js +++ b/app/ui-cached-collection/client/models/CachedCollection.js @@ -129,7 +129,7 @@ export class CachedCollection extends EventEmitter { userRelated = true, listenChangesForLoggedUsersOnly = false, useSync = true, - version = 9, + version = 10, maxCacheTime = 60 * 60 * 24 * 30, onSyncData = (/* action, record */) => {}, }) { diff --git a/app/ui-sidenav/client/roomList.js b/app/ui-sidenav/client/roomList.js index f6a062df8d7d..ad77a5654566 100644 --- a/app/ui-sidenav/client/roomList.js +++ b/app/ui-sidenav/client/roomList.js @@ -136,8 +136,8 @@ const getLowerCaseNames = (room, nameDefault = '', fnameDefault = '') => { const name = room.name || nameDefault; const fname = room.fname || fnameDefault || name; return { - lowerCaseName: name.toLowerCase(), - lowerCaseFName: fname.toLowerCase(), + lowerCaseName: String(name).toLowerCase(), + lowerCaseFName: String(fname).toLowerCase(), }; }; From 896bf5698d029f51ca6a0e4a80cfd224cca6c4f8 Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Mon, 20 Apr 2020 21:27:13 -0300 Subject: [PATCH 13/29] [NEW] [ENTERPRISE] Allows to set a group of departments accepted for forwarding chats (#17335) --- .../views/app/tabbar/visitorForward.html | 55 +++++++++---------- .../client/views/app/tabbar/visitorForward.js | 13 +++-- .../imports/server/rest/departments.js | 21 ++++++- app/livechat/server/api/lib/departments.js | 9 +++ app/livechat/server/index.js | 1 + app/livechat/server/lib/Helper.js | 2 +- app/livechat/server/lib/Livechat.js | 8 ++- .../getDepartmentForwardRestrictions.js | 13 +++++ app/utils/client/index.js | 2 +- app/utils/client/lib/RestApiClient.js | 5 ++ .../livechatDepartmentCustomFieldsForm.html | 40 ++++++++++++-- .../livechatDepartmentCustomFieldsForm.js | 37 ++++++++++++- .../server/hooks/afterRemoveDepartment.js | 10 ++++ .../hooks/beforeForwardRoomToDepartment.js | 26 +++++++++ .../onLoadForwardDepartmentRestrictions.js | 17 ++++++ ee/app/livechat-enterprise/server/index.js | 3 + .../server/models/LivechatDepartment.js | 7 +++ ee/i18n/en.i18n.json | 3 + ee/i18n/pt-BR.i18n.json | 3 + 19 files changed, 233 insertions(+), 42 deletions(-) create mode 100644 app/livechat/server/methods/getDepartmentForwardRestrictions.js create mode 100644 ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.js create mode 100644 ee/app/livechat-enterprise/server/hooks/beforeForwardRoomToDepartment.js create mode 100644 ee/app/livechat-enterprise/server/hooks/onLoadForwardDepartmentRestrictions.js diff --git a/app/livechat/client/views/app/tabbar/visitorForward.html b/app/livechat/client/views/app/tabbar/visitorForward.html index 4b41fb133faa..45c909a10ded 100644 --- a/app/livechat/client/views/app/tabbar/visitorForward.html +++ b/app/livechat/client/views/app/tabbar/visitorForward.html @@ -9,35 +9,34 @@

{{_ "Forward_chat"}}

{{/with}}
- {{#if hasDepartments}} -
- -
- {{> livechatAutocompleteUser - onClickTag=onClickTagDepartment - list=selectedDepartments - onSelect=onSelectDepartments - collection='CachedDepartmentList' - endpoint='livechat/department.autocomplete' - field='name' - sort='name' - icon="queue" - label="Enter_a_department_name" - placeholder="Enter_a_department_name" - name="department" - noMatchTemplate="userSearchEmpty" - templateItem="popupList_item_channel" - template="roomSearch" - noMatchTemplate="roomSearchEmpty" - modifier=departmentModifier - conditions=departmentConditions - }} -
-
-
- {{_ "or"}} +
+ +
+ {{> livechatAutocompleteUser + onClickTag=onClickTagDepartment + list=selectedDepartments + onSelect=onSelectDepartments + collection='CachedDepartmentList' + endpoint='livechat/department.autocomplete' + field='name' + sort='name' + icon="queue" + label="Enter_a_department_name" + placeholder="Enter_a_department_name" + name="department" + noMatchTemplate="userSearchEmpty" + templateItem="popupList_item_channel" + template="roomSearch" + noMatchTemplate="roomSearchEmpty" + modifier=departmentModifier + conditions=departmentConditions + }}
- {{/if}} +
+
+ {{_ "or"}} +
+
diff --git a/app/livechat/client/views/app/tabbar/visitorForward.js b/app/livechat/client/views/app/tabbar/visitorForward.js index 313b87e867da..84860188cf99 100644 --- a/app/livechat/client/views/app/tabbar/visitorForward.js +++ b/app/livechat/client/views/app/tabbar/visitorForward.js @@ -13,9 +13,6 @@ Template.visitorForward.helpers({ visitor() { return Template.instance().visitor.get(); }, - hasDepartments() { - return Template.instance().departments.get().filter((department) => department.enabled === true).length > 0; - }, agentName() { return this.name || this.username; }, @@ -56,7 +53,8 @@ Template.visitorForward.helpers({ return Template.instance().onSelectDepartments; }, departmentConditions() { - return { enabled: true, numAgents: { $gt: 0 } }; + const departmentForwardRestrictions = Template.instance().departmentForwardRestrictions.get(); + return { enabled: true, numAgents: { $gt: 0 }, ...departmentForwardRestrictions }; }, }); @@ -66,6 +64,7 @@ Template.visitorForward.onCreated(async function() { this.departments = new ReactiveVar([]); this.selectedAgents = new ReactiveVar([]); this.selectedDepartments = new ReactiveVar([]); + this.departmentForwardRestrictions = new ReactiveVar({}); this.onSelectDepartments = ({ item: department }) => { department.text = department.name; @@ -92,6 +91,12 @@ Template.visitorForward.onCreated(async function() { this.autorun(() => { this.room.set(ChatRoom.findOne({ _id: Template.currentData().roomId })); + const { departmentId } = this.room.get(); + if (departmentId) { + Meteor.call('livechat:getDepartmentForwardRestrictions', departmentId, (err, result) => { + this.departmentForwardRestrictions.set(result); + }); + } }); const { departments } = await APIClient.v1.get('livechat/department'); diff --git a/app/livechat/imports/server/rest/departments.js b/app/livechat/imports/server/rest/departments.js index 315fbcb30772..8a255185f86f 100644 --- a/app/livechat/imports/server/rest/departments.js +++ b/app/livechat/imports/server/rest/departments.js @@ -4,7 +4,7 @@ import { API } from '../../../../api'; import { hasPermission } from '../../../../authorization'; import { LivechatDepartment, LivechatDepartmentAgents } from '../../../../models'; import { Livechat } from '../../../server/lib/Livechat'; -import { findDepartments, findDepartmentById, findDepartmentsToAutocomplete } from '../../../server/api/lib/departments'; +import { findDepartments, findDepartmentById, findDepartmentsToAutocomplete, findDepartmentsBetweenIds } from '../../../server/api/lib/departments'; API.v1.addRoute('livechat/department', { authRequired: true }, { get() { @@ -147,3 +147,22 @@ API.v1.addRoute('livechat/department.autocomplete', { authRequired: true }, { }))); }, }); + +API.v1.addRoute('livechat/department.listByIds', { authRequired: true }, { + get() { + const { ids } = this.queryParams; + const { fields } = this.parseJsonQuery(); + if (!ids) { + return API.v1.failure('The \'ids\' param is required'); + } + if (!Array.isArray(ids)) { + return API.v1.failure('The \'ids\' param must be an array'); + } + + return API.v1.success(Promise.await(findDepartmentsBetweenIds({ + uid: this.userId, + ids, + fields, + }))); + }, +}); diff --git a/app/livechat/server/api/lib/departments.js b/app/livechat/server/api/lib/departments.js index b9b73947937a..ef008ae6fd15 100644 --- a/app/livechat/server/api/lib/departments.js +++ b/app/livechat/server/api/lib/departments.js @@ -67,3 +67,12 @@ export async function findDepartmentsToAutocomplete({ uid, selector }) { items, }; } + +export async function findDepartmentsBetweenIds({ uid, ids, fields }) { + if (!await hasPermissionAsync(uid, 'view-livechat-departments') && !await hasPermissionAsync(uid, 'view-l-room')) { + throw new Error('error-not-authorized'); + } + + const departments = await LivechatDepartment.findInIds(ids, fields).toArray(); + return { departments }; +} diff --git a/app/livechat/server/index.js b/app/livechat/server/index.js index fe61cd6f5825..f6deeb0f8933 100644 --- a/app/livechat/server/index.js +++ b/app/livechat/server/index.js @@ -69,6 +69,7 @@ import './methods/saveOfficeHours'; import './methods/sendTranscript'; import './methods/getFirstRoomMessage'; import './methods/getTagsList'; +import './methods/getDepartmentForwardRestrictions'; import './lib/Analytics'; import './lib/QueueManager'; import './lib/OfficeClock'; diff --git a/app/livechat/server/lib/Helper.js b/app/livechat/server/lib/Helper.js index a29b47eade1e..b2b47dad6b9c 100644 --- a/app/livechat/server/lib/Helper.js +++ b/app/livechat/server/lib/Helper.js @@ -224,7 +224,7 @@ export const forwardRoomToDepartment = async (room, guest, transferData) => { if (!room || !room.open) { return false; } - + callbacks.run('livechat.beforeForwardRoomToDepartment', { room, transferData }); const { _id: rid, servedBy: oldServedBy, departmentId: oldDepartmentId } = room; const inquiry = LivechatInquiry.findOneByRoomId(rid); diff --git a/app/livechat/server/lib/Livechat.js b/app/livechat/server/lib/Livechat.js index b808af5db862..323dd80b8a15 100644 --- a/app/livechat/server/lib/Livechat.js +++ b/app/livechat/server/lib/Livechat.js @@ -888,7 +888,13 @@ export const Livechat = { throw new Meteor.Error('department-not-found', 'Department not found', { method: 'livechat:removeDepartment' }); } - return LivechatDepartment.removeById(_id); + const ret = LivechatDepartment.removeById(_id); + if (ret) { + Meteor.defer(() => { + callbacks.run('livechat.afterRemoveDepartment', department); + }); + } + return ret; }, showConnecting() { diff --git a/app/livechat/server/methods/getDepartmentForwardRestrictions.js b/app/livechat/server/methods/getDepartmentForwardRestrictions.js new file mode 100644 index 000000000000..e08adc9792ba --- /dev/null +++ b/app/livechat/server/methods/getDepartmentForwardRestrictions.js @@ -0,0 +1,13 @@ +import { Meteor } from 'meteor/meteor'; + +import { callbacks } from '../../../callbacks'; + +Meteor.methods({ + 'livechat:getDepartmentForwardRestrictions'(departmentId) { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:getDepartmentForwardRestrictions' }); + } + + return callbacks.run('livechat.onLoadForwardDepartmentRestrictions', departmentId); + }, +}); diff --git a/app/utils/client/index.js b/app/utils/client/index.js index 6b2060f5d0e2..8f536315da1d 100644 --- a/app/utils/client/index.js +++ b/app/utils/client/index.js @@ -17,7 +17,7 @@ export { getURL } from '../lib/getURL'; export { getValidRoomName } from '../lib/getValidRoomName'; export { placeholders } from '../lib/placeholders'; export { templateVarHandler } from '../lib/templateVarHandler'; -export { APIClient } from './lib/RestApiClient'; +export { APIClient, mountArrayQueryParameters } from './lib/RestApiClient'; export { canDeleteMessage } from './lib/canDeleteMessage'; export { mime } from '../lib/mimeTypes'; export { secondsToHHMMSS } from '../lib/timeConverter'; diff --git a/app/utils/client/lib/RestApiClient.js b/app/utils/client/lib/RestApiClient.js index 50eba6f56ae1..511c9eb988ee 100644 --- a/app/utils/client/lib/RestApiClient.js +++ b/app/utils/client/lib/RestApiClient.js @@ -4,6 +4,11 @@ import { Accounts } from 'meteor/accounts-base'; import { baseURI } from './baseuri'; import { process2faReturn } from '../../../2fa/client/callWithTwoFactorRequired'; +export const mountArrayQueryParameters = (label, array) => array.reduce((acc, item) => { + acc += `${ label }[]=${ item }&`; + return acc; +}, ''); + export const APIClient = { delete(endpoint, params) { return APIClient._jqueryCall('DELETE', endpoint, params); diff --git a/ee/app/livechat-enterprise/client/views/app/customTemplates/livechatDepartmentCustomFieldsForm.html b/ee/app/livechat-enterprise/client/views/app/customTemplates/livechatDepartmentCustomFieldsForm.html index c0cd15e90ca8..264078cc53d5 100644 --- a/ee/app/livechat-enterprise/client/views/app/customTemplates/livechatDepartmentCustomFieldsForm.html +++ b/ee/app/livechat-enterprise/client/views/app/customTemplates/livechatDepartmentCustomFieldsForm.html @@ -2,25 +2,55 @@
- +
- +
- +
- +
- +
+ {{> livechatAutocompleteUser + onClickTag=onClickTagDepartment + list=selectedDepartments + onSelect=onSelectDepartments + collection='CachedDepartmentList' + endpoint='livechat/department.autocomplete' + field='name' + sort='name' + label="List_of_departments_for_forward" + placeholder="Enter_a_department_name" + name="department" + icon="queue" + noMatchTemplate="userSearchEmpty" + templateItem="popupList_item_channel" + template="roomSearch" + noMatchTemplate="roomSearchEmpty" + modifier=departmentModifier + showLabel=true + exceptions=exceptionsDepartments + }} + +
+ {{{_ "List_of_departments_for_forward_description"}}} +
+
+ \ No newline at end of file diff --git a/ee/app/livechat-enterprise/client/views/app/customTemplates/livechatDepartmentCustomFieldsForm.js b/ee/app/livechat-enterprise/client/views/app/customTemplates/livechatDepartmentCustomFieldsForm.js index 71ffdfde7384..a1851d16cea3 100644 --- a/ee/app/livechat-enterprise/client/views/app/customTemplates/livechatDepartmentCustomFieldsForm.js +++ b/ee/app/livechat-enterprise/client/views/app/customTemplates/livechatDepartmentCustomFieldsForm.js @@ -1,23 +1,58 @@ import { ReactiveVar } from 'meteor/reactive-var'; import { Template } from 'meteor/templating'; -import { APIClient } from '../../../../../../../app/utils/client'; +import { APIClient, mountArrayQueryParameters } from '../../../../../../../app/utils/client'; import './livechatDepartmentCustomFieldsForm.html'; Template.livechatDepartmentCustomFieldsForm.helpers({ department() { return Template.instance().department.get(); }, + departmentModifier() { + return (filter, text = '') => { + const f = filter.get(); + return `${ f.length === 0 ? text : text.replace(new RegExp(filter.get(), 'i'), (part) => `${ part }`) }`; + }; + }, + onClickTagDepartment() { + return Template.instance().onClickTagDepartment; + }, + selectedDepartments() { + return Template.instance().selectedDepartments.get(); + }, + selectedDepartmentsIds() { + return Template.instance().selectedDepartments.get().map((dept) => dept._id); + }, + onSelectDepartments() { + return Template.instance().onSelectDepartments; + }, + exceptionsDepartments() { + const department = Template.instance().department.get(); + return [department && department._id, ...Template.instance().selectedDepartments.get().map((dept) => dept._id)]; + }, }); Template.livechatDepartmentCustomFieldsForm.onCreated(function() { + this.selectedDepartments = new ReactiveVar([]); const { id: _id, department: contextDepartment } = this.data; this.department = new ReactiveVar(contextDepartment); + this.onSelectDepartments = ({ item: department }) => { + department.text = department.name; + this.selectedDepartments.set(this.selectedDepartments.get().concat(department)); + }; + + this.onClickTagDepartment = (department) => { + this.selectedDepartments.set(this.selectedDepartments.get().filter((dept) => dept._id !== department._id)); + }; if (!contextDepartment && _id) { this.autorun(async () => { const { department } = await APIClient.v1.get(`livechat/department/${ _id }`); + if (department.departmentsAllowedToForward) { + const { departments } = await APIClient.v1.get(`livechat/department.listByIds?${ mountArrayQueryParameters('ids', department.departmentsAllowedToForward) }&fields=${ JSON.stringify({ fields: { name: 1 } }) }`); + this.selectedDepartments.set(departments.map((dept) => ({ _id: dept._id, text: dept.name }))); + } this.department.set(department); }); } diff --git a/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.js b/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.js new file mode 100644 index 000000000000..4a101a0787fb --- /dev/null +++ b/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.js @@ -0,0 +1,10 @@ +import { callbacks } from '../../../../../app/callbacks'; +import { LivechatDepartment } from '../../../../../app/models/server'; + +callbacks.add('livechat.afterRemoveDepartment', (department) => { + if (!department) { + return department; + } + LivechatDepartment.removeDepartmentFromForwardListById(department._id); + return department; +}, callbacks.priority.HIGH, 'livechat-after-remove-department'); diff --git a/ee/app/livechat-enterprise/server/hooks/beforeForwardRoomToDepartment.js b/ee/app/livechat-enterprise/server/hooks/beforeForwardRoomToDepartment.js new file mode 100644 index 000000000000..b0bf3f95d1f5 --- /dev/null +++ b/ee/app/livechat-enterprise/server/hooks/beforeForwardRoomToDepartment.js @@ -0,0 +1,26 @@ +import { Meteor } from 'meteor/meteor'; + +import { callbacks } from '../../../../../app/callbacks'; +import { LivechatDepartment } from '../../../../../app/models/server'; + +callbacks.add('livechat.beforeForwardRoomToDepartment', (options) => { + const { room, transferData } = options; + if (!room || !transferData) { + return options; + } + const { departmentId } = room; + if (!departmentId) { + return options; + } + const { department: departmentToTransfer } = transferData; + const currentDepartment = LivechatDepartment.findOneById(departmentId); + if (!currentDepartment) { + return options; + } + const { departmentsAllowedToForward } = currentDepartment; + const isAllowedToTransfer = !departmentsAllowedToForward || (Array.isArray(departmentsAllowedToForward) && departmentsAllowedToForward.includes(departmentToTransfer._id)); + if (isAllowedToTransfer) { + return options; + } + throw new Meteor.Error('error-forwarding-department-target-not-allowed', 'The forwarding to the target department is not allowed.'); +}, callbacks.priority.HIGH, 'livechat-before-forward-room-to-department'); diff --git a/ee/app/livechat-enterprise/server/hooks/onLoadForwardDepartmentRestrictions.js b/ee/app/livechat-enterprise/server/hooks/onLoadForwardDepartmentRestrictions.js new file mode 100644 index 000000000000..538ce151b30c --- /dev/null +++ b/ee/app/livechat-enterprise/server/hooks/onLoadForwardDepartmentRestrictions.js @@ -0,0 +1,17 @@ +import { callbacks } from '../../../../../app/callbacks'; +import { LivechatDepartment } from '../../../../../app/models/server'; + +callbacks.add('livechat.onLoadForwardDepartmentRestrictions', (departmentId) => { + if (!departmentId) { + return {}; + } + const department = LivechatDepartment.findOneById(departmentId, { fields: { departmentsAllowedToForward: 1 } }); + if (!department) { + return {}; + } + const { departmentsAllowedToForward } = department; + if (!departmentsAllowedToForward) { + return {}; + } + return { _id: { $in: departmentsAllowedToForward } }; +}, callbacks.priority.MEDIUM, 'livechat-on-load-forward-department-restrictions'); diff --git a/ee/app/livechat-enterprise/server/index.js b/ee/app/livechat-enterprise/server/index.js index e15a5bef4edc..297e6d1962b5 100644 --- a/ee/app/livechat-enterprise/server/index.js +++ b/ee/app/livechat-enterprise/server/index.js @@ -4,6 +4,9 @@ import './hooks/addDepartmentAncestors'; import './hooks/afterForwardChatToDepartment'; import './hooks/beforeListTags'; import './hooks/setPredictedVisitorAbandonmentTime'; +import './hooks/beforeForwardRoomToDepartment'; +import './hooks/afterRemoveDepartment'; +import './hooks/onLoadForwardDepartmentRestrictions'; import './methods/addMonitor'; import './methods/getUnitsFromUserRoles'; import './methods/removeMonitor'; diff --git a/ee/app/models/server/models/LivechatDepartment.js b/ee/app/models/server/models/LivechatDepartment.js index d54c0db5d65f..2e4785282d91 100644 --- a/ee/app/models/server/models/LivechatDepartment.js +++ b/ee/app/models/server/models/LivechatDepartment.js @@ -32,6 +32,9 @@ overwriteClassOnLicense('livechat-enterprise', LivechatDepartment, { if (args.length > 2 && !args[1].type) { args[1].type = 'd'; } + if (args[1] && args[1].departmentsAllowedToForward) { + args[1].departmentsAllowedToForward = args[1].departmentsAllowedToForward.split(','); + } return originalFn.apply(this, args); }, @@ -49,4 +52,8 @@ overwriteClassOnLicense('livechat-enterprise', LivechatDepartment, { }, }); +LivechatDepartment.prototype.removeDepartmentFromForwardListById = function(_id) { + return this.update({ departmentsAllowedToForward: _id }, { $pull: { departmentsAllowedToForward: _id } }, { multi: true }); +}; + export default LivechatDepartment; diff --git a/ee/i18n/en.i18n.json b/ee/i18n/en.i18n.json index f9d3d4bd7b09..4d15e29fa152 100644 --- a/ee/i18n/en.i18n.json +++ b/ee/i18n/en.i18n.json @@ -13,6 +13,7 @@ "Enter_a_custom_message": "Enter a custom message", "Enterprise_License": "Enterprise License", "Enterprise_License_Description": "If your workspace is registered and license is provided by Rocket.Chat Cloud you don't need to manually update the license here.", + "error-forwarding-department-target-not-allowed": "The forwarding to the target department is not allowed.", "Failed_to_add_monitor": "Failed to add monitor", "Invalid Canned Response": "Invalid Canned Response", "Invalid_Department": "Invalid Department", @@ -25,6 +26,8 @@ "LDAP_Roles_To_Rocket_Chat_Roles_Description": "Role mapping in object format where the object key must be the LDAP role and the object value must be an array of RC roles. Example: { 'ldapRole': ['rcRole', 'anotherRCRole'] }", "LDAP_Validate_Roles_For_Each_Login": "Validate mapping for each login", "LDAP_Validate_Roles_For_Each_Login_Description": "If the validation should occurs for each login (Be careful with this setting because it will overwrite the user roles in each login, otherwise this will be validated only at the moment of user creation).", + "List_of_departments_for_forward": "List of departments allowed for forwarding (Optional)", + "List_of_departments_for_forward_description": "Allow to set a restricted list of departments that can receive chats from this department", "Livechat_abandoned_rooms_closed_custom_message": "Custom message when room is automatically closed by visitor inactivity", "Livechat_Monitors": "Monitors", "Livechat_monitors": "Livechat monitors", diff --git a/ee/i18n/pt-BR.i18n.json b/ee/i18n/pt-BR.i18n.json index 58d93186aaf0..7ec29f0cbd8b 100644 --- a/ee/i18n/pt-BR.i18n.json +++ b/ee/i18n/pt-BR.i18n.json @@ -9,6 +9,7 @@ "Enter_a_custom_message": "Digite uma mensagem customizada", "Enterprise_License": "Licença Enterprise", "Enterprise_License_Description": "Se você registrou seu workspace e a licença foi fornecida pelo Rocket.Chat Cloud você não precisa atualizar a licença manualmente aqui.", + "error-forwarding-department-target-not-allowed": "O encaminhamento para o departamento selecionado não é permitido.", "Invalid_Department": "Departamento inválido", "LDAP_Default_Role_To_User": "Papel padrão para o usuário", "LDAP_Default_Role_To_User_Description": "Papel padrão par ser aplicado ao usuário, caso ele tenha algum papel do LDAP que não esteja mapeado.", @@ -19,6 +20,8 @@ "LDAP_Roles_To_Rocket_Chat_Roles_Description": "Mapeamento dos papéis que deve ser em formato de objeto onde a chave do objeto precisa ser o nome do papel LDAP e o valor deve ser um array de papéis Rocket.Chat. Exemplo: { 'ldapRole': ['rcRole', 'anotherRCRole'] }", "LDAP_Validate_Roles_For_Each_Login": "Validar o mapeamento em cada login", "LDAP_Validate_Roles_For_Each_Login_Description": " Se a validação deve ser feita a cada login(Tenha cuidado com essa configuração, pois ela vai sobrescrever os papéis de usuário a cada login, caso esteja desabilitado, a validação será feita apenas no momento da criação do usuário).", + "List_of_departments_for_forward": "Lista de departamentos permitidos para o encaminhamento(Opcional).", + "List_of_departments_for_forward_description": "Permite definir uma lista restrita de departamentos que podem receber conversas desse departamento.", "Livechat_abandoned_rooms_closed_custom_message": "Mensagem customizada para usar quando a sala for automaticamente fechada por abandono do visitante", "Livechat_Monitors": "Monitores", "Livechat_monitors": "Monitores de Livechat", From 545e03a07d023a1a359779c9287212e525c9fc66 Mon Sep 17 00:00:00 2001 From: Hullen Gonzales Date: Mon, 20 Apr 2020 21:51:17 -0300 Subject: [PATCH 14/29] [FIX] Show active admin and user account menu item (#17047) --- app/apps/client/admin/helpers.js | 2 +- app/apps/client/admin/marketplace.js | 2 +- app/cloud/client/index.js | 4 ++-- app/federation/client/admin/dashboard.js | 2 +- app/livechat/client/views/sideNav/livechatFlex.js | 3 +++ app/theme/client/imports/components/sidebar/sidebar-item.css | 4 ++++ app/theme/client/imports/general/variables.css | 1 + app/ui-account/client/accountFlex.js | 3 +++ app/ui-admin/client/adminFlex.html | 2 +- app/ui-admin/client/adminFlex.js | 5 +++++ private/client/imports/general/variables.css | 1 + 11 files changed, 23 insertions(+), 6 deletions(-) diff --git a/app/apps/client/admin/helpers.js b/app/apps/client/admin/helpers.js index ca04c715fbc5..f78b4c661599 100644 --- a/app/apps/client/admin/helpers.js +++ b/app/apps/client/admin/helpers.js @@ -47,7 +47,7 @@ const promptCloudLogin = () => { html: false, }, (confirmed) => { if (confirmed) { - FlowRouter.go('cloud-config'); + FlowRouter.go('cloud'); } }); }; diff --git a/app/apps/client/admin/marketplace.js b/app/apps/client/admin/marketplace.js index 0907080453d2..495bda2a90ac 100644 --- a/app/apps/client/admin/marketplace.js +++ b/app/apps/client/admin/marketplace.js @@ -254,7 +254,7 @@ Template.marketplace.helpers({ Template.marketplace.events({ 'click .js-cloud-login'() { - FlowRouter.go('cloud-config'); + FlowRouter.go('cloud'); }, 'submit .js-search-form'(event) { event.stopPropagation(); diff --git a/app/cloud/client/index.js b/app/cloud/client/index.js index 6504dcc0e8e7..0c8025d22dbd 100644 --- a/app/cloud/client/index.js +++ b/app/cloud/client/index.js @@ -8,7 +8,7 @@ import { registerAdminRoute, registerAdminSidebarItem } from '../../ui-admin/cli import { hasAtLeastOnePermission } from '../../authorization'; registerAdminRoute('/cloud', { - name: 'cloud-config', + name: 'cloud', async action() { await import('./admin'); BlazeLayout.render('main', { center: 'cloud', old: true }); @@ -25,7 +25,7 @@ registerAdminRoute('/cloud/oauth-callback', { registerAdminSidebarItem({ icon: 'cloud-plus', - href: 'admin/cloud', + href: 'cloud', i18nLabel: 'Connectivity_Services', permissionGranted() { return hasAtLeastOnePermission(['manage-cloud']); diff --git a/app/federation/client/admin/dashboard.js b/app/federation/client/admin/dashboard.js index a675b11e2d68..3d68975bd35b 100644 --- a/app/federation/client/admin/dashboard.js +++ b/app/federation/client/admin/dashboard.js @@ -77,7 +77,7 @@ registerAdminRoute('/federation-dashboard', { registerAdminSidebarItem({ icon: 'discover', - href: 'admin/federation-dashboard', + href: 'federation-dashboard', i18nLabel: 'Federation Dashboard', permissionGranted() { return hasRole(Meteor.userId(), 'admin'); diff --git a/app/livechat/client/views/sideNav/livechatFlex.js b/app/livechat/client/views/sideNav/livechatFlex.js index a7dc9cb909d9..f57b76c313df 100644 --- a/app/livechat/client/views/sideNav/livechatFlex.js +++ b/app/livechat/client/views/sideNav/livechatFlex.js @@ -1,4 +1,5 @@ import { Template } from 'meteor/templating'; +import { FlowRouter } from 'meteor/kadira:flow-router'; import { SideNav, Layout } from '../../../../ui-utils'; import { t } from '../../../../utils'; @@ -8,11 +9,13 @@ import { sidebarItems } from './livechatSideNavItems'; Template.livechatFlex.helpers({ menuItem(name, icon, section) { + const routeName = FlowRouter.getRouteName(); return { name: t(name), icon, pathSection: section, darken: true, + active: section === routeName, }; }, embeddedVersion() { diff --git a/app/theme/client/imports/components/sidebar/sidebar-item.css b/app/theme/client/imports/components/sidebar/sidebar-item.css index b74a84800690..125ed7283f45 100644 --- a/app/theme/client/imports/components/sidebar/sidebar-item.css +++ b/app/theme/client/imports/components/sidebar/sidebar-item.css @@ -5,6 +5,10 @@ background-color: var(--sidebar-background-light-hover); } + &--active { + background-color: var(--sidebar-background-light-active); + } + &__picture { color: inherit; } diff --git a/app/theme/client/imports/general/variables.css b/app/theme/client/imports/general/variables.css index b2a0f27e476d..2cfb61fd812f 100644 --- a/app/theme/client/imports/general/variables.css +++ b/app/theme/client/imports/general/variables.css @@ -181,6 +181,7 @@ --sidebar-background-hover: var(--rc-color-primary-dark); --sidebar-background-light: var(--rc-color-primary-lightest); --sidebar-background-light-hover: var(--rc-color-primary-light); + --sidebar-background-light-active: var(--rc-color-primary-light-medium); --sidebar-default-padding: 24px; --sidebar-small-default-padding: 16px; --sidebar-extra-small-default-padding: 12px; diff --git a/app/ui-account/client/accountFlex.js b/app/ui-account/client/accountFlex.js index 56187884ed67..fe118a1c0d2a 100644 --- a/app/ui-account/client/accountFlex.js +++ b/app/ui-account/client/accountFlex.js @@ -1,4 +1,5 @@ import { Template } from 'meteor/templating'; +import { FlowRouter } from 'meteor/kadira:flow-router'; import { settings } from '../../settings'; import { hasAllPermission } from '../../authorization'; @@ -32,12 +33,14 @@ Template.accountFlex.helpers({ return settings.get('Webdav_Integration_Enabled'); }, menuItem(name, icon, section, group) { + const routeParam = FlowRouter.getParam('group'); return { name: t(name), icon, pathSection: section, pathGroup: group, darken: true, + active: group === routeParam, }; }, embeddedVersion() { diff --git a/app/ui-admin/client/adminFlex.html b/app/ui-admin/client/adminFlex.html index 140889c1fab2..35ef735fee26 100644 --- a/app/ui-admin/client/adminFlex.html +++ b/app/ui-admin/client/adminFlex.html @@ -8,7 +8,7 @@

{{_ "Administration"}}

    - {{> sidebarItem menuItem "Info" "info-circled" "admin" "info" }} + {{> sidebarItem menuItem "Info" "info-circled" "admin-info" "" }} {{#if hasPermission 'run-import'}} {{> sidebarItem menuItem "Import" "import" "admin-import" "" }} diff --git a/app/ui-admin/client/adminFlex.js b/app/ui-admin/client/adminFlex.js index 6571d708d47c..dc9dfb51039f 100644 --- a/app/ui-admin/client/adminFlex.js +++ b/app/ui-admin/client/adminFlex.js @@ -3,6 +3,7 @@ import s from 'underscore.string'; import { ReactiveVar } from 'meteor/reactive-var'; import { Template } from 'meteor/templating'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import { FlowRouter } from 'meteor/kadira:flow-router'; import { settings } from '../../settings'; import { menu, SideNav, Layout } from '../../ui-utils/client'; @@ -69,6 +70,9 @@ Template.adminFlex.helpers({ return getSidebarItems(); }, menuItem(name, icon, section, group) { + const routeParam = FlowRouter.getParam('group'); + const routeName = FlowRouter.getRouteName(); + return { name: t(name), icon, @@ -76,6 +80,7 @@ Template.adminFlex.helpers({ pathGroup: group, darken: true, isLightSidebar: true, + active: (routeParam && routeParam === group) || (routeName !== 'admin' && routeName === section), }; }, embeddedVersion() { diff --git a/private/client/imports/general/variables.css b/private/client/imports/general/variables.css index cf80aae7fd6a..92c5124c4d22 100644 --- a/private/client/imports/general/variables.css +++ b/private/client/imports/general/variables.css @@ -181,6 +181,7 @@ --sidebar-background-hover: var(--rc-color-primary-dark); --sidebar-background-light: var(--rc-color-primary-lightest); --sidebar-background-light-hover: var(--rc-color-primary-light); + --sidebar-background-light-active: var(--rc-color-primary-light-medium); --sidebar-default-padding: 24px; --sidebar-small-default-padding: 16px; --sidebar-extra-small-default-padding: 12px; From c21bfd442ddfd47b3c11583894afb8a06384f12c Mon Sep 17 00:00:00 2001 From: Renato Becker Date: Mon, 20 Apr 2020 21:52:52 -0300 Subject: [PATCH 15/29] [NEW] Add ability to set tags in the Omnichannel room closing dialog (#17254) --- app/livechat/client/index.js | 1 + .../client/views/app/dialog/closeRoom.html | 72 +++++++ .../client/views/app/dialog/closeRoom.js | 190 ++++++++++++++++++ .../client/views/app/tabbar/visitorInfo.js | 67 +++--- app/livechat/server/hooks/beforeCloseRoom.js | 23 ++- app/livechat/server/lib/Livechat.js | 3 +- packages/rocketchat-i18n/i18n/en.i18n.json | 1 + packages/rocketchat-i18n/i18n/pt-BR.i18n.json | 1 + 8 files changed, 312 insertions(+), 46 deletions(-) create mode 100644 app/livechat/client/views/app/dialog/closeRoom.html create mode 100644 app/livechat/client/views/app/dialog/closeRoom.js diff --git a/app/livechat/client/index.js b/app/livechat/client/index.js index 4df9fcd92eda..26013edf70a5 100644 --- a/app/livechat/client/index.js +++ b/app/livechat/client/index.js @@ -4,6 +4,7 @@ import './route'; import './ui'; import './hooks/onCreateRoomTabBar'; import './startup/notifyUnreadRooms'; +import './views/app/dialog/closeRoom'; import './stylesheets/livechat.css'; import './views/sideNav/livechat'; import './views/sideNav/livechatFlex'; diff --git a/app/livechat/client/views/app/dialog/closeRoom.html b/app/livechat/client/views/app/dialog/closeRoom.html new file mode 100644 index 000000000000..0cb20ad48d85 --- /dev/null +++ b/app/livechat/client/views/app/dialog/closeRoom.html @@ -0,0 +1,72 @@ + diff --git a/app/livechat/client/views/app/dialog/closeRoom.js b/app/livechat/client/views/app/dialog/closeRoom.js new file mode 100644 index 000000000000..2dac3bc7956a --- /dev/null +++ b/app/livechat/client/views/app/dialog/closeRoom.js @@ -0,0 +1,190 @@ +import { Meteor } from 'meteor/meteor'; +import { Template } from 'meteor/templating'; +import { ReactiveVar } from 'meteor/reactive-var'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; + +import { settings } from '../../../../../settings'; +import { modal } from '../../../../../ui-utils/client'; +import { APIClient, handleError, t } from '../../../../../utils'; +import { hasRole } from '../../../../../authorization'; +import './closeRoom.html'; + +const validateRoomComment = (comment) => { + if (!settings.get('Livechat_request_comment_when_closing_conversation')) { + return true; + } + + return comment?.length > 0; +}; + +const validateRoomTags = (tagsRequired, tags) => { + if (!tagsRequired) { + return true; + } + + return tags?.length > 0; +}; + +const checkUserTagPermission = (availableUserTags = [], tag) => { + if (hasRole(Meteor.userId(), ['admin', 'livechat-manager'])) { + return true; + } + + return availableUserTags.includes(tag); +}; + +Template.closeRoom.helpers({ + invalidComment() { + return Template.instance().invalidComment.get(); + }, + tags() { + return Template.instance().tags.get(); + }, + invalidTags() { + return Template.instance().invalidTags.get(); + }, + availableUserTags() { + return Template.instance().availableUserTags.get(); + }, + tagsPlaceHolder() { + let placeholder = TAPi18n.__('Enter_a_tag'); + + if (!Template.instance().tagsRequired.get()) { + placeholder = placeholder.concat(`(${ TAPi18n.__('Optional') })`); + } + + return placeholder; + }, + hasAvailableTags() { + const tags = Template.instance().availableTags.get(); + return tags?.length > 0; + }, + canRemoveTag(availableUserTags, tag) { + return checkUserTagPermission(availableUserTags, tag); + }, +}); + +Template.closeRoom.events({ + async 'submit .close-room__content'(e, instance) { + e.preventDefault(); + e.stopPropagation(); + + const comment = instance.$('#comment').val(); + instance.invalidComment.set(!validateRoomComment(comment)); + if (instance.invalidComment.get()) { + return; + } + + const tagsRequired = instance.tagsRequired.get(); + const tags = instance.tags.get(); + + instance.invalidTags.set(!validateRoomTags(tagsRequired, tags)); + if (instance.invalidTags.get()) { + return; + } + + Meteor.call('livechat:closeRoom', this.rid, comment, { clientAction: true, tags }, function(error/* , result*/) { + if (error) { + console.log(error); + return handleError(error); + } + + modal.open({ + title: t('Chat_closed'), + text: t('Chat_closed_successfully'), + type: 'success', + timer: 1000, + showConfirmButton: false, + }); + }); + }, + 'click .remove-tag'(e, instance) { + e.stopPropagation(); + e.preventDefault(); + + const tag = this.valueOf(); + const availableTags = instance.availableTags.get(); + const hasAvailableTags = availableTags?.length > 0; + const availableUserTags = instance.availableUserTags.get(); + if (hasAvailableTags && !checkUserTagPermission(availableUserTags, tag)) { + return; + } + + let tags = instance.tags.get(); + tags = tags.filter((el) => el !== tag); + instance.tags.set(tags); + }, + 'click #addTag'(e, instance) { + e.stopPropagation(); + e.preventDefault(); + + if ($('#tagSelect').find(':selected').is(':disabled')) { + return; + } + + const tags = [...instance.tags.get()]; + const tagVal = $('#tagSelect').val(); + if (tagVal === '' || tags.includes(tagVal)) { + return; + } + + tags.push(tagVal); + instance.tags.set(tags); + $('#tagSelect').val('placeholder'); + }, + 'keydown #tagInput'(e, instance) { + if (e.which === 13) { + e.stopPropagation(); + e.preventDefault(); + + const tags = [...instance.tags.get()]; + const tagVal = $('#tagInput').val(); + if (tagVal === '' || tags.includes(tagVal)) { + return; + } + + tags.push(tagVal); + instance.tags.set(tags); + $('#tagInput').val(''); + } + }, +}); + +Template.closeRoom.onRendered(function() { + this.find('#comment').focus(); +}); + +Template.closeRoom.onCreated(async function() { + this.tags = new ReactiveVar([]); + this.invalidComment = new ReactiveVar(false); + this.invalidTags = new ReactiveVar(false); + this.tagsRequired = new ReactiveVar(false); + this.availableTags = new ReactiveVar([]); + this.availableUserTags = new ReactiveVar([]); + this.agentDepartments = new ReactiveVar([]); + + this.onEnterTag = () => this.invalidTags.set(!validateRoomTags(this.tagsRequired.get(), this.tags.get())); + + const { rid } = Template.currentData(); + const { room } = await APIClient.v1.get(`rooms.info?roomId=${ rid }`); + this.tags.set(room?.tags || []); + + if (room?.departmentId) { + const { department } = await APIClient.v1.get(`livechat/department/${ room.departmentId }?includeAgents=false`); + this.tagsRequired.set(department?.requestTagBeforeClosingChat); + } + + const uid = Meteor.userId(); + const { departments } = await APIClient.v1.get(`livechat/agents/${ uid }/departments`); + const agentDepartments = departments.map((dept) => dept.departmentId); + this.agentDepartments.set(agentDepartments); + + Meteor.call('livechat:getTagsList', (err, tagsList) => { + this.availableTags.set(tagsList); + const isAdmin = hasRole(uid, ['admin', 'livechat-manager']); + const availableTags = tagsList + .filter(({ departments }) => isAdmin || (departments.length === 0 || departments.some((i) => agentDepartments.includes(i)))) + .map(({ name }) => name); + this.availableUserTags.set(availableTags); + }); +}); diff --git a/app/livechat/client/views/app/tabbar/visitorInfo.js b/app/livechat/client/views/app/tabbar/visitorInfo.js index 24de8917bcee..d33cbfc0c051 100644 --- a/app/livechat/client/views/app/tabbar/visitorInfo.js +++ b/app/livechat/client/views/app/tabbar/visitorInfo.js @@ -1,11 +1,10 @@ +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { Meteor } from 'meteor/meteor'; import { ReactiveVar } from 'meteor/reactive-var'; import { FlowRouter } from 'meteor/kadira:flow-router'; import { Session } from 'meteor/session'; import { Template } from 'meteor/templating'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import _ from 'underscore'; -import s from 'underscore.string'; import moment from 'moment'; import UAParser from 'ua-parser-js'; @@ -29,6 +28,14 @@ const isSubscribedToRoom = () => { return subscription !== undefined; }; +const closingDialogRequired = (department) => { + if (settings.get('Livechat_request_comment_when_closing_conversation')) { + return true; + } + + return department && department.requestTagBeforeClosingChat; +}; + Template.visitorInfo.helpers({ user() { const user = Template.instance().user.get(); @@ -228,46 +235,36 @@ Template.visitorInfo.events({ instance.action.set('edit'); }, - 'click .close-livechat'(event) { + 'click .close-livechat'(event, instance) { event.preventDefault(); - const closeRoom = (comment) => Meteor.call('livechat:closeRoom', this.rid, comment, { clientAction: true }, function(error/* , result*/) { - if (error) { - return handleError(error); - } - modal.open({ - title: t('Chat_closed'), - text: t('Chat_closed_successfully'), - type: 'success', - timer: 1000, - showConfirmButton: false, - }); - }); - - if (!settings.get('Livechat_request_comment_when_closing_conversation')) { + if (!closingDialogRequired(instance.department.get())) { const comment = TAPi18n.__('Chat_closed_by_agent'); - return closeRoom(comment); + return Meteor.call('livechat:closeRoom', this.rid, comment, { clientAction: true }, function(error/* , result*/) { + if (error) { + return handleError(error); + } + + modal.open({ + title: t('Chat_closed'), + text: t('Chat_closed_successfully'), + type: 'success', + timer: 1000, + showConfirmButton: false, + }); + }); } - // Setting for Ask_for_conversation_finished_message is set to true modal.open({ title: t('Closing_chat'), - type: 'input', - inputPlaceholder: t('Please_add_a_comment'), - showCancelButton: true, - closeOnConfirm: false, - }, (inputValue) => { - if (!inputValue) { - modal.showInputError(t('Please_add_a_comment_to_close_the_room')); - return false; - } - - if (s.trim(inputValue) === '') { - modal.showInputError(t('Please_add_a_comment_to_close_the_room')); - return false; - } - - return closeRoom(inputValue); + modifier: 'modal', + content: 'closeRoom', + data: { + rid: this.rid, + }, + confirmOnEnter: false, + showConfirmButton: false, + showCancelButton: false, }); }, diff --git a/app/livechat/server/hooks/beforeCloseRoom.js b/app/livechat/server/hooks/beforeCloseRoom.js index de4f13a7f8c5..6c819c23e2bf 100644 --- a/app/livechat/server/hooks/beforeCloseRoom.js +++ b/app/livechat/server/hooks/beforeCloseRoom.js @@ -5,32 +5,35 @@ import { LivechatDepartment } from '../../../models'; const concatUnique = (...arrays) => [...new Set([].concat(...arrays.filter(Array.isArray)))]; -callbacks.add('livechat.beforeCloseRoom', ({ room, options }) => { - const { departmentId, tags: roomTags } = room; +const normalizeParams = (params, tags = []) => Object.assign(params, { extraData: { tags } }); + +callbacks.add('livechat.beforeCloseRoom', (originalParams = {}) => { + const { room, options } = originalParams; + const { departmentId, tags: optionsTags } = room; + const { clientAction, tags: oldRoomTags } = options; + const roomTags = concatUnique(oldRoomTags, optionsTags); + if (!departmentId) { - return; + return normalizeParams({ ...originalParams }, roomTags); } const department = LivechatDepartment.findOneById(departmentId); if (!department) { - return; + return normalizeParams({ ...originalParams }, roomTags); } const { requestTagBeforeClosingChat, chatClosingTags } = department; - const extraData = { - tags: concatUnique(roomTags, chatClosingTags), - }; + const extraRoomTags = concatUnique(roomTags, chatClosingTags); if (!requestTagBeforeClosingChat) { - return extraData; + return normalizeParams({ ...originalParams }, extraRoomTags); } - const { clientAction } = options; const checkRoomTags = !clientAction || (roomTags && roomTags.length > 0); const checkDepartmentTags = chatClosingTags && chatClosingTags.length > 0; if (!checkRoomTags || !checkDepartmentTags) { throw new Meteor.Error('error-tags-must-be-assigned-before-closing-chat', 'Tag(s) must be assigned before closing the chat', { method: 'livechat.beforeCloseRoom' }); } - return extraData; + return normalizeParams({ ...originalParams }, extraRoomTags); }, callbacks.priority.HIGH, 'livechat-before-close-Room'); diff --git a/app/livechat/server/lib/Livechat.js b/app/livechat/server/lib/Livechat.js index 323dd80b8a15..1438e24b9404 100644 --- a/app/livechat/server/lib/Livechat.js +++ b/app/livechat/server/lib/Livechat.js @@ -320,7 +320,8 @@ export const Livechat = { return false; } - const extraData = callbacks.run('livechat.beforeCloseRoom', { room, options }); + const params = callbacks.run('livechat.beforeCloseRoom', { room, options }); + const { extraData } = params; const now = new Date(); const { _id: rid, servedBy } = room; diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 4f48e53d8e26..ef3de4826644 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -692,6 +692,7 @@ "close-others-livechat-room": "Close other Omnichannel Room", "Cloud_workspace_connected_without_account": "Your workspace is now connected to the Rocket.Chat Cloud. If you would like, you can login to the Rocket.Chat Cloud and associate your workspace with your Cloud account.", "close-others-livechat-room_description": "Permission to close other Omnichannel rooms", + "Close_room_description" : "You are about to close this chat. Are you sure you want to continue?", "Closed": "Closed", "Closed_At": "Closed at", "Closed_by_visitor": "Closed by visitor", diff --git a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index 9792f3c1f07b..22ed0b99fea2 100644 --- a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -670,6 +670,7 @@ "close-others-livechat-room": "Sala de Omnichannel fechada", "Cloud_workspace_connected_without_account": "Seu workspace está agora conectado ao Rocket.Chat Cloud. Se desejar, você pode fazer o login no Rocket.Chat Cloud e associar seu workspace à sua conta do Cloud.", "close-others-livechat-room_description": "Permissão para fechar outras salas de Omnichannel", + "Close_room_description" : "Você está prestes a fechar este bate-papo. Você tem certeza que quer continuar?", "Closed": "Fechado", "Closed_At": "Encerrado em", "Closed_by_visitor": "Encerrado pelo visitante", From 9fd381e824779cd6b4d506f40a54f21dd09aff09 Mon Sep 17 00:00:00 2001 From: Subham Sahoo <43502196+subham103@users.noreply.github.com> Date: Tue, 21 Apr 2020 06:24:10 +0530 Subject: [PATCH 16/29] [IMPROVE] Add `file-title` and `file-desc` as new filter tag options on message search (#16858) --- server/methods/messageSearch.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/server/methods/messageSearch.js b/server/methods/messageSearch.js index f58674c598ef..e0fb9f394541 100644 --- a/server/methods/messageSearch.js +++ b/server/methods/messageSearch.js @@ -117,6 +117,16 @@ Meteor.methods({ return ''; } + function filterTitle(_, tag) { + query['attachments.title'] = new RegExp(s.escapeRegExp(tag), 'i'); + return ''; + } + + function filterDescription(_, tag) { + query['attachments.description'] = new RegExp(s.escapeRegExp(tag), 'i'); + return ''; + } + function sortByTimestamp(_, direction) { if (direction.startsWith('asc')) { options.sort.ts = 1; @@ -171,6 +181,10 @@ Meteor.methods({ text = text.replace(/has:location|has:map/g, filterLocation); // Filter image tags text = text.replace(/label:(\w+)/g, filterLabel); + // Filter on description of messages. + text = text.replace(/file-desc:(\w+)/g, filterDescription); + // Filter on title of messages. + text = text.replace(/file-title:(\w+)/g, filterTitle); // Filtering before/after/on a date // matches dd-MM-yyyy, dd/MM/yyyy, dd-MM-yyyy, prefixed by before:, after: and on: respectively. // Example: before:15/09/2016 after: 10-08-2016 From e7362645ff0d76d869bd88ae5bdce65141be2b2f Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Mon, 20 Apr 2020 21:54:33 -0300 Subject: [PATCH 17/29] Remove set as alias setting (#16343) --- app/lib/server/lib/processDirectEmail.js | 4 ---- app/lib/server/methods/sendMessage.js | 4 ---- app/lib/server/startup/settings.js | 5 ----- server/startup/migrations/index.js | 1 + server/startup/migrations/v185.js | 21 +++++++++++++++++++++ 5 files changed, 22 insertions(+), 13 deletions(-) create mode 100644 server/startup/migrations/v185.js diff --git a/app/lib/server/lib/processDirectEmail.js b/app/lib/server/lib/processDirectEmail.js index 4489551698e6..deab36914c60 100644 --- a/app/lib/server/lib/processDirectEmail.js +++ b/app/lib/server/lib/processDirectEmail.js @@ -102,10 +102,6 @@ export const processDirectEmail = function(email) { } } - if (message.alias == null && settings.get('Message_SetNameToAliasEnabled')) { - message.alias = user.name; - } - metrics.messagesSent.inc(); // TODO This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736 return _sendMessage(user, message, room); diff --git a/app/lib/server/methods/sendMessage.js b/app/lib/server/methods/sendMessage.js index d25284a3639f..be4ef0670d9c 100644 --- a/app/lib/server/methods/sendMessage.js +++ b/app/lib/server/methods/sendMessage.js @@ -51,7 +51,6 @@ export function executeSendMessage(uid, message) { fields: { username: 1, type: 1, - ...!!settings.get('Message_SetNameToAliasEnabled') && { name: 1 }, }, }); let { rid } = message; @@ -69,9 +68,6 @@ export function executeSendMessage(uid, message) { try { const room = canSendMessage(rid, { uid, username: user.username, type: user.type }); - if (message.alias == null && settings.get('Message_SetNameToAliasEnabled')) { - message.alias = user.name; - } metrics.messagesSent.inc(); // TODO This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736 return sendMessage(user, message, room, false); diff --git a/app/lib/server/startup/settings.js b/app/lib/server/startup/settings.js index 33bf91e477af..56020606dc8c 100644 --- a/app/lib/server/startup/settings.js +++ b/app/lib/server/startup/settings.js @@ -1062,11 +1062,6 @@ settings.addGroup('Message', function() { type: 'boolean', public: true, }); - this.add('Message_SetNameToAliasEnabled', false, { - type: 'boolean', - public: false, - i18nDescription: 'Message_SetNameToAliasEnabled_Description', - }); this.add('Message_GroupingPeriod', 300, { type: 'int', public: true, diff --git a/server/startup/migrations/index.js b/server/startup/migrations/index.js index 1085c8ea77df..dc1262908b52 100644 --- a/server/startup/migrations/index.js +++ b/server/startup/migrations/index.js @@ -182,4 +182,5 @@ import './v181'; import './v182'; import './v183'; import './v184'; +import './v185'; import './xrun'; diff --git a/server/startup/migrations/v185.js b/server/startup/migrations/v185.js new file mode 100644 index 000000000000..c6a20c9aa398 --- /dev/null +++ b/server/startup/migrations/v185.js @@ -0,0 +1,21 @@ +import { + Migrations, +} from '../../../app/migrations/server'; +import { + Settings, +} from '../../../app/models/server'; + +Migrations.add({ + version: 185, + up() { + const setting = Settings.findOne({ _id: 'Message_SetNameToAliasEnabled' }); + if (setting.value) { + Settings.update({ _id: 'UI_Use_Real_Name' }, { + $set: { + value: true, + }, + }); + } + Settings.remove({ _id: 'Message_SetNameToAliasEnabled' }); + }, +}); From 2e0e457355f1d1bf50d9572df6140e045c216d2d Mon Sep 17 00:00:00 2001 From: Ashwani Yadav Date: Tue, 21 Apr 2020 06:25:18 +0530 Subject: [PATCH 18/29] [IMPROVE] User gets UI feedback when message is pinned or unpinned (#16056) --- app/message-pin/client/pinMessage.js | 10 ++++++++++ packages/rocketchat-i18n/i18n/en.i18n.json | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/app/message-pin/client/pinMessage.js b/app/message-pin/client/pinMessage.js index 7822c2875a2d..9fbc2f778edc 100644 --- a/app/message-pin/client/pinMessage.js +++ b/app/message-pin/client/pinMessage.js @@ -1,4 +1,6 @@ import { Meteor } from 'meteor/meteor'; +import toastr from 'toastr'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { settings } from '../../settings'; import { ChatMessage, Subscriptions } from '../../models'; @@ -6,14 +8,18 @@ import { ChatMessage, Subscriptions } from '../../models'; Meteor.methods({ pinMessage(message) { if (!Meteor.userId()) { + toastr.error(TAPi18n.__('error-not-authorized')); return false; } if (!settings.get('Message_AllowPinning')) { + toastr.error(TAPi18n.__('pinning-not-allowed')); return false; } if (Subscriptions.findOne({ rid: message.rid }) == null) { + toastr.error(TAPi18n.__('error-pinning-message')); return false; } + toastr.success(TAPi18n.__('Message_has_been_pinned')); return ChatMessage.update({ _id: message._id, }, { @@ -24,14 +30,18 @@ Meteor.methods({ }, unpinMessage(message) { if (!Meteor.userId()) { + toastr.error(TAPi18n.__('error-not-authorized')); return false; } if (!settings.get('Message_AllowPinning')) { + toastr.error(TAPi18n.__('unpinning-not-allowed')); return false; } if (Subscriptions.findOne({ rid: message.rid }) == null) { + toastr.error(TAPi18n.__('error-unpinning-message')); return false; } + toastr.success(TAPi18n.__('Message_has_been_unpinned')); return ChatMessage.update({ _id: message._id, }, { diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index ef3de4826644..af5e9215cb6e 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1411,6 +1411,8 @@ "error-password-policy-not-met-oneSpecial": "Password does not meet the server's policy of at least one special character", "error-password-policy-not-met-oneUppercase": "Password does not meet the server's policy of at least one uppercase character", "error-password-policy-not-met-repeatingCharacters": "Password not not meet the server's policy of forbidden repeating characters (you have too many of the same characters next to each other)", + "error-pinning-message": "Message could not be pinned", + "error-unpinning-message": "Message could not be unpinned", "error-push-disabled": "Push is disabled", "error-remove-last-owner": "This is the last owner. Please set a new owner before removing this one.", "error-tags-must-be-assigned-before-closing-chat": "Tag(s) must be assigned before closing the chat", @@ -2329,6 +2331,8 @@ "Message_GlobalSearch": "Global Search", "Message_GroupingPeriod": "Grouping Period (in seconds)", "Message_GroupingPeriodDescription": "Messages will be grouped with previous message if both are from the same user and the elapsed time was less than the informed time in seconds.", + "Message_has_been_pinned": "Message has been pinned", + "Message_has_been_unpinned": "Message has been unpinned", "Message_has_been_starred": "Message has been starred", "Message_has_been_unstarred": "Message has been unstarred", "Message_HideType_au": "Hide \"User Added\" messages", @@ -2628,6 +2632,7 @@ "Pin_Message": "Pin Message", "Pinned_a_message": "Pinned a message:", "Pinned_Messages": "Pinned Messages", + "pinning-not-allowed": "Pinning is not allowed", "PiwikAdditionalTrackers": "Additional Piwik Sites", "PiwikAdditionalTrackers_Description": "Enter addtitional Piwik website URLs and SiteIDs in the following format, if you wnat to track the same data into different websites: [ { \"trackerURL\" : \"https://my.piwik.domain2/\", \"siteId\" : 42 }, { \"trackerURL\" : \"https://my.piwik.domain3/\", \"siteId\" : 15 } ]", "PiwikAnalytics_cookieDomain": "All Subdomains", @@ -3451,6 +3456,7 @@ "Unnamed": "Unnamed", "Unpin": "Unpin", "Unpin_Message": "Unpin Message", + "unpinning-not-allowed": "Unpinning is not allowed", "Unread": "Unread", "Unread_Count": "Unread Count", "Unread_Count_DM": "Unread Count for Direct Messages", From ead78d7061dff1887068d1830d5348278c7f156d Mon Sep 17 00:00:00 2001 From: Nikhil Kaushik Date: Tue, 21 Apr 2020 06:47:34 +0530 Subject: [PATCH 19/29] [IMPROVE] Filter markdown in notifications (#9995) --- .../server/functions/notifications/index.js | 4 +- .../server/lib/sendNotificationsOnMessage.js | 2 +- app/markdown/lib/markdown.js | 9 ++ app/markdown/lib/parser/filtered/filtered.js | 46 ++++++ app/markdown/tests/client.tests.js | 131 +++++++++++++++++- 5 files changed, 186 insertions(+), 6 deletions(-) create mode 100644 app/markdown/lib/parser/filtered/filtered.js diff --git a/app/lib/server/functions/notifications/index.js b/app/lib/server/functions/notifications/index.js index 6dbe07fb0402..8b247c8810a3 100644 --- a/app/lib/server/functions/notifications/index.js +++ b/app/lib/server/functions/notifications/index.js @@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import s from 'underscore.string'; +import { callbacks } from '../../../../callbacks'; import { settings } from '../../../../settings'; /** @@ -22,7 +23,8 @@ export function parseMessageTextPerUser(messageText, message, receiver) { return TAPi18n.__('Encrypted_message', { lng }); } - return messageText; + // perform processing required before sending message as notification such as markdown filtering + return callbacks.run('renderNotification', messageText); } /** diff --git a/app/lib/server/lib/sendNotificationsOnMessage.js b/app/lib/server/lib/sendNotificationsOnMessage.js index 1d2e831c4441..0394dc99b191 100644 --- a/app/lib/server/lib/sendNotificationsOnMessage.js +++ b/app/lib/server/lib/sendNotificationsOnMessage.js @@ -263,7 +263,7 @@ export async function sendMessageNotifications(message, room, usersInThread = [] } }); - // the find bellow is crucial. all subscription records returned will receive at least one kind of notification. + // the find below is crucial. All subscription records returned will receive at least one kind of notification. // the query is defined by the server's default values and Notifications_Max_Room_Members setting. const subscriptions = await Subscriptions.model.rawCollection().aggregate([ diff --git a/app/markdown/lib/markdown.js b/app/markdown/lib/markdown.js index eb97f4644272..c7163d0ee24d 100644 --- a/app/markdown/lib/markdown.js +++ b/app/markdown/lib/markdown.js @@ -8,6 +8,7 @@ import { Blaze } from 'meteor/blaze'; import { marked } from './parser/marked/marked.js'; import { original } from './parser/original/original.js'; +import { filtered } from './parser/filtered/filtered.js'; import { code } from './parser/original/code.js'; import { callbacks } from '../../callbacks'; import { settings } from '../../settings'; @@ -15,6 +16,7 @@ import { settings } from '../../settings'; const parsers = { original, marked, + filtered, }; class MarkdownClass { @@ -76,6 +78,10 @@ class MarkdownClass { code(...args) { return code(...args); } + + filterMarkdownFromMessage(message) { + return parsers.filtered(message); + } } export const Markdown = new MarkdownClass(); @@ -89,7 +95,10 @@ const MarkdownMessage = (message) => { return message; }; +const filterMarkdown = (message) => Markdown.filterMarkdownFromMessage(message); + callbacks.add('renderMessage', MarkdownMessage, callbacks.priority.HIGH, 'markdown'); +callbacks.add('renderNotification', filterMarkdown, callbacks.priority.HIGH, 'filter-markdown'); if (Meteor.isClient) { Blaze.registerHelper('RocketChatMarkdown', (text) => Markdown.parse(text)); diff --git a/app/markdown/lib/parser/filtered/filtered.js b/app/markdown/lib/parser/filtered/filtered.js new file mode 100644 index 000000000000..52ba496d6108 --- /dev/null +++ b/app/markdown/lib/parser/filtered/filtered.js @@ -0,0 +1,46 @@ +/* +* Filter markdown tags in message +* Use case: notifications +*/ +import { settings } from '../../../../settings'; + +const filterMarkdownTags = function(message) { + const schemes = settings.get('Markdown_SupportSchemesForLink').split(',').join('|'); + + // Remove block code backticks + message = message.replace(/```/g, ''); + + // Remove inline code backticks + message = message.replace(new RegExp(/`([^`\r\n]+)\`/gm), (match) => match.substr(1, match.length - 2)); + + // Filter [text](url), ![alt_text](image_url) + message = message.replace(new RegExp(`!?\\[([^\\]]+)\\]\\((?:${ schemes }):\\/\\/[^\\)]+\\)`, 'gm'), (match, title) => title); + + // Filter + message = message.replace(new RegExp(`(?:<|<)(?:${ schemes }):\\/\\/[^\\|]+\\|(.+?)(?=>|>)(?:>|>)`, 'gm'), (match, title) => title); + + // Filter headings + message = message.replace(/(^#{1,4}) (([\S\w\d-_\/\*\.,\\][ \u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]?)+)/gm, '$2'); + + // Filter bold + message = message.replace(/(^|>|[ >_~`])\*{1,2}([^\*\r\n]+)\*{1,2}([<_~`]|\B|\b|$)/gm, '$1$2$3'); + + // Filter italics + message = message.replace(/(^|>|[ >*~`])\_{1,2}([^\_\r\n]+)\_{1,2}([<*~`]|\B|\b|$)/gm, '$1$2$3'); + + // Filter strike-through text + message = message.replace(/(^|>|[ >_*`])\~{1,2}([^~\r\n]+)\~{1,2}([<_*`]|\B|\b|$)/gm, '$1$2$3'); + + // Filter block quotes + message = message.replace(/(?:>){3}\n+([\s\S]*?)\n+(?:<){3}/g, '$1'); + + // Filter > quote + message = message.replace(/^>(.*)$/gm, '$1'); + + return message; +}; + +export const filtered = function(message) { + message = filterMarkdownTags(message); + return message; +}; diff --git a/app/markdown/tests/client.tests.js b/app/markdown/tests/client.tests.js index 808eaddf918d..1738c61e5651 100644 --- a/app/markdown/tests/client.tests.js +++ b/app/markdown/tests/client.tests.js @@ -6,6 +6,7 @@ import s from 'underscore.string'; import './client.mocks.js'; import { original } from '../lib/parser/original/original'; +import { filtered } from '../lib/parser/filtered/filtered'; import { Markdown } from '../lib/markdown'; const wrapper = (text, tag) => `${ tag }${ text }${ tag }`; @@ -230,15 +231,119 @@ const nested = { '> some quote\n`window.location.reload();`': `${ quoteWrapper(' some quote') }${ inlinecodeWrapper('window.location.reload();') }`, }; +/* +* Markdown Filters +*/ +const boldFiltered = { + '*Hello*': 'Hello', + '**Hello**': 'Hello', + '*Hello**': 'Hello', + 'He*llo': 'He*llo', + '*Hello': '*Hello', + 'Hello*': 'Hello*', + '***Hello***': '***Hello***', + '***Hello**': '***Hello**', + '*Hello* there': 'Hello there', + '**Hello** there': 'Hello there', + 'Hi, *Hello*': 'Hi, Hello', + 'Hi, **Hello**': 'Hi, Hello', + 'Hi, *Hello* how are you?': 'Hi, Hello how are you?', + 'Hi, **Hello** how are you?': 'Hi, Hello how are you?', +}; + +const italicFiltered = { + _Hello_: 'Hello', + __Hello__: 'Hello', + _Hello__: 'Hello', + He_llo: 'He_llo', + _Hello: '_Hello', + __Hello: '__Hello', + Hello_: 'Hello_', + ___Hello___: '___Hello___', + ___Hello__: '___Hello__', + '_Hello_ there': 'Hello there', + '__Hello__ there': 'Hello there', + 'Hi, _Hello_': 'Hi, Hello', + 'Hi, __Hello__': 'Hi, Hello', + 'Hi, _Hello_ how are you?': 'Hi, Hello how are you?', + 'Hi, __Hello__ how are you?': 'Hi, Hello how are you?', +}; + +const strikeFiltered = { + '~Hello~': 'Hello', + '~~Hello~~': 'Hello', + '~~Hello': '~~Hello', + '~Hello~~': 'Hello', + 'He~llo': 'He~llo', + '~Hello': '~Hello', + 'Hello~': 'Hello~', + '~~~Hello~~~': '~~~Hello~~~', + '~~~Hello~~': '~~~Hello~~', + '~Hello~ there': 'Hello there', + '~~Hello~~ there': 'Hello there', + 'Hi, ~Hello~': 'Hi, Hello', + 'Hi, ~~Hello~~': 'Hi, Hello', + 'Hi, ~Hello~ how are you?': 'Hi, Hello how are you?', + 'Hi, ~~Hello~~ how are you?': 'Hi, Hello how are you?', +}; + +const headingFiltered = { + '# Hello': 'Hello', + '## Hello': 'Hello', + '### Hello': 'Hello', + '#### Hello': 'Hello', + '#Hello': '#Hello', + '##Hello': '##Hello', + '###Hello': '###Hello', + '####Hello': '####Hello', + 'He#llo': 'He#llo', + '# Hello there': 'Hello there', + 'Hi, # Hello': 'Hi, # Hello', + 'Hi, # Hello there': 'Hi, # Hello there', +}; + +const quoteFiltered = { + '>Hello': 'Hello', + '> Hello': ' Hello', + '>>>\nHello\n<<<': 'Hello', + '>>>\nHello there!\n<<<': 'Hello there!', + '>>>\n Hello there! \n<<<': ' Hello there! ', +}; + +const linkFiltered = { + '[Text](http://link)': 'Text', + '[Open Site For Rocket.Chat](https://open.rocket.chat/)': 'Open Site For Rocket.Chat', + '[ Open Site For Rocket.Chat](https://open.rocket.chat/ )': ' Open Site For Rocket.Chat', + '[Rocket.Chat Site](https://rocket.chat/)': 'Rocket.Chat Site', + '': 'Text', + '': 'Text for test', +}; + +const inlinecodeFiltered = { + '`code`': 'code', + '`code` begin': 'code begin', + 'End `code`': 'End code', + 'Middle `code` middle': 'Middle code middle', + '`code`begin': 'codebegin', + 'End`code`': 'Endcode', + 'Middle`code`middle': 'Middlecodemiddle', +}; + +const blockcodeFiltered = { + '```code```': 'code', + '```code': 'code', + 'code```': 'code', + 'Here ```code``` lies': 'Here code lies', + 'Here```code```lies': 'Herecodelies', +}; + const defaultObjectTest = (result, object, objectKey) => assert.equal(result.html, object[objectKey]); const testObject = (object, parser = original, test = defaultObjectTest) => { Object.keys(object).forEach((objectKey) => { describe(objectKey, () => { - const message = { - html: s.escapeHTML(objectKey), - }; - const result = Markdown.mountTokensBack(parser(message)); + const message = parser === original ? { html: s.escapeHTML(objectKey) } : objectKey; + const result = parser === original ? Markdown.mountTokensBack(parser(message)) : { html: parser(message) }; it(`should be equal to ${ object[objectKey] }`, () => { test(result, object, objectKey); }); @@ -274,6 +379,24 @@ describe('Original', function() { describe('Nested', () => testObject(nested)); }); +describe('Filtered', function() { + describe('BoldFilter', () => testObject(boldFiltered, filtered)); + + describe('Italic', () => testObject(italicFiltered, filtered)); + + describe('StrikeFilter', () => testObject(strikeFiltered, filtered)); + + describe('HeadingFilter', () => testObject(headingFiltered, filtered)); + + describe('QuoteFilter', () => testObject(quoteFiltered, filtered)); + + describe('LinkFilter', () => testObject(linkFiltered, filtered)); + + describe('inlinecodeFilter', () => testObject(inlinecodeFiltered, filtered)); + + describe('blockcodeFilter', () => testObject(blockcodeFiltered, filtered)); +}); + // describe.only('Marked', function() { // describe('Bold', () => testObject(bold, marked)); From 9506e9845cbea6a76f0e1af063541deabada6c61 Mon Sep 17 00:00:00 2001 From: Fernando Crespo Date: Mon, 20 Apr 2020 22:39:43 -0300 Subject: [PATCH 20/29] [IMPROVE] Change the SAML metadata order to conform to XSD specification (#15488) --- app/meteor-accounts-saml/server/saml_utils.js | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/app/meteor-accounts-saml/server/saml_utils.js b/app/meteor-accounts-saml/server/saml_utils.js index 9f7e4355d947..4f700769596f 100644 --- a/app/meteor-accounts-saml/server/saml_utils.js +++ b/app/meteor-accounts-saml/server/saml_utils.js @@ -772,23 +772,13 @@ SAML.prototype.generateServiceProviderMetadata = function(callbackUrl) { const metadata = { EntityDescriptor: { + '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + '@xsi:schemaLocation': 'urn:oasis:names:tc:SAML:2.0:metadata https://docs.oasis-open.org/security/saml/v2.0/saml-schema-metadata-2.0.xsd', '@xmlns': 'urn:oasis:names:tc:SAML:2.0:metadata', '@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#', '@entityID': this.options.issuer, SPSSODescriptor: { '@protocolSupportEnumeration': 'urn:oasis:names:tc:SAML:2.0:protocol', - SingleLogoutService: { - '@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', - '@Location': `${ Meteor.absoluteUrl() }_saml/logout/${ this.options.provider }/`, - '@ResponseLocation': `${ Meteor.absoluteUrl() }_saml/logout/${ this.options.provider }/`, - }, - NameIDFormat: this.options.identifierFormat, - AssertionConsumerService: { - '@index': '1', - '@isDefault': 'true', - '@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', - '@Location': callbackUrl, - }, }, }, }; @@ -826,6 +816,19 @@ SAML.prototype.generateServiceProviderMetadata = function(callbackUrl) { }; } + metadata.EntityDescriptor.SPSSODescriptor.SingleLogoutService = { + '@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + '@Location': `${ Meteor.absoluteUrl() }_saml/logout/${ this.options.provider }/`, + '@ResponseLocation': `${ Meteor.absoluteUrl() }_saml/logout/${ this.options.provider }/`, + }; + metadata.EntityDescriptor.SPSSODescriptor.NameIDFormat = this.options.identifierFormat; + metadata.EntityDescriptor.SPSSODescriptor.AssertionConsumerService = { + '@index': '1', + '@isDefault': 'true', + '@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + '@Location': callbackUrl, + }; + return xmlbuilder.create(metadata).end({ pretty: true, indent: ' ', From 65638b53d53a1731c1a8ff106bc6df5f174e4cfe Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Mon, 20 Apr 2020 23:21:22 -0300 Subject: [PATCH 21/29] [NEW] [ENTERPRISE] Omnichannel queue priorities (#17141) --- .../views/app/livechatDepartmentForm.js | 2 +- .../client/views/app/tabbar/visitorEdit.html | 3 + .../client/views/app/tabbar/visitorEdit.js | 10 ++ .../client/views/app/tabbar/visitorInfo.html | 1 + .../client/views/app/tabbar/visitorInfo.js | 11 +++ app/livechat/client/views/sideNav/livechat.js | 4 +- app/livechat/server/api/lib/livechat.js | 8 +- app/livechat/server/api/v1/room.js | 24 +++-- app/livechat/server/lib/Helper.js | 20 ++-- app/livechat/server/lib/Livechat.js | 4 +- app/livechat/server/lib/QueueManager.js | 6 +- app/livechat/server/methods/saveInfo.js | 10 +- app/models/server/models/LivechatInquiry.js | 5 +- app/models/server/models/LivechatRooms.js | 4 +- app/models/server/raw/LivechatInquiry.js | 4 +- ee/app/livechat-enterprise/client/index.js | 3 + ee/app/livechat-enterprise/client/route.js | 24 +++++ .../visitorEditCustomFieldsForm.html | 18 ++++ .../visitorEditCustomFieldsForm.js | 29 ++++++ .../visitorInfoCustomForm.html | 3 + .../customTemplates/visitorInfoCustomForm.js | 28 ++++++ .../views/app/registerCustomTemplates.js | 4 + .../client/views/livechatPriorities.html | 30 ++++++ .../client/views/livechatPriorities.js | 97 +++++++++++++++++++ .../client/views/livechatPriorityForm.html | 35 +++++++ .../client/views/livechatPriorityForm.js | 77 +++++++++++++++ .../client/views/livechatSideNavItems.js | 1 + .../livechat-enterprise/lib/messageTypes.js | 19 ++++ .../livechat-enterprise/server/api/index.js | 2 + .../server/api/inquiries.js | 17 ++++ .../server/api/lib/inquiries.js | 21 ++++ .../server/api/lib/priorities.js | 31 ++++++ .../server/api/priorities.js | 29 ++++++ .../server/hooks/beforeNewInquiry.js | 24 +++++ .../server/hooks/beforeNewRoom.js | 23 +++++ .../server/hooks/onCheckRoomParamsApi.js | 5 + .../server/hooks/onSaveVisitorInfo.js | 20 ++++ ee/app/livechat-enterprise/server/index.js | 7 ++ .../livechat-enterprise/server/lib/Helper.js | 52 +++++++++- .../server/lib/LivechatEnterprise.js | 47 +++++++++ .../server/methods/removePriority.js | 14 +++ .../server/methods/savePriority.js | 14 +++ .../livechat-enterprise/server/permissions.js | 1 + ee/app/models/server/index.js | 6 ++ .../models/server/models/LivechatInquiry.js | 18 ++++ .../models/server/models/LivechatPriority.js | 58 +++++++++++ ee/app/models/server/models/LivechatRooms.js | 22 +++++ ee/app/models/server/models/Messages.js | 27 ++++++ ee/app/models/server/raw/LivechatPriority.js | 18 ++++ ee/i18n/en.i18n.json | 11 ++- ee/i18n/pt-BR.i18n.json | 11 ++- server/publications/room/index.js | 1 + server/startup/migrations/index.js | 1 + server/startup/migrations/v153.js | 4 +- server/startup/migrations/v186.js | 19 ++++ 55 files changed, 949 insertions(+), 38 deletions(-) create mode 100644 ee/app/livechat-enterprise/client/views/app/customTemplates/visitorEditCustomFieldsForm.html create mode 100644 ee/app/livechat-enterprise/client/views/app/customTemplates/visitorEditCustomFieldsForm.js create mode 100644 ee/app/livechat-enterprise/client/views/app/customTemplates/visitorInfoCustomForm.html create mode 100644 ee/app/livechat-enterprise/client/views/app/customTemplates/visitorInfoCustomForm.js create mode 100644 ee/app/livechat-enterprise/client/views/livechatPriorities.html create mode 100644 ee/app/livechat-enterprise/client/views/livechatPriorities.js create mode 100644 ee/app/livechat-enterprise/client/views/livechatPriorityForm.html create mode 100644 ee/app/livechat-enterprise/client/views/livechatPriorityForm.js create mode 100644 ee/app/livechat-enterprise/lib/messageTypes.js create mode 100644 ee/app/livechat-enterprise/server/api/inquiries.js create mode 100644 ee/app/livechat-enterprise/server/api/lib/inquiries.js create mode 100644 ee/app/livechat-enterprise/server/api/lib/priorities.js create mode 100644 ee/app/livechat-enterprise/server/api/priorities.js create mode 100644 ee/app/livechat-enterprise/server/hooks/beforeNewInquiry.js create mode 100644 ee/app/livechat-enterprise/server/hooks/beforeNewRoom.js create mode 100644 ee/app/livechat-enterprise/server/hooks/onCheckRoomParamsApi.js create mode 100644 ee/app/livechat-enterprise/server/hooks/onSaveVisitorInfo.js create mode 100644 ee/app/livechat-enterprise/server/methods/removePriority.js create mode 100644 ee/app/livechat-enterprise/server/methods/savePriority.js create mode 100644 ee/app/models/server/models/LivechatInquiry.js create mode 100644 ee/app/models/server/models/LivechatPriority.js create mode 100644 ee/app/models/server/models/Messages.js create mode 100644 ee/app/models/server/raw/LivechatPriority.js create mode 100644 server/startup/migrations/v186.js diff --git a/app/livechat/client/views/app/livechatDepartmentForm.js b/app/livechat/client/views/app/livechatDepartmentForm.js index 4af23b45ddcf..59bbcec12286 100644 --- a/app/livechat/client/views/app/livechatDepartmentForm.js +++ b/app/livechat/client/views/app/livechatDepartmentForm.js @@ -265,7 +265,7 @@ Template.livechatDepartmentForm.onCreated(async function() { this.autorun(async () => { const id = FlowRouter.getParam('_id'); if (id) { - const { department, agents } = await APIClient.v1.get(`livechat/department/${ FlowRouter.getParam('_id') }`); + const { department, agents = [] } = await APIClient.v1.get(`livechat/department/${ FlowRouter.getParam('_id') }`); this.department.set(department); this.departmentAgents.set(agents); this.chatClosingTags.set((department && department.chatClosingTags) || []); diff --git a/app/livechat/client/views/app/tabbar/visitorEdit.html b/app/livechat/client/views/app/tabbar/visitorEdit.html index 0d3afb3118be..306d1c310246 100644 --- a/app/livechat/client/views/app/tabbar/visitorEdit.html +++ b/app/livechat/client/views/app/tabbar/visitorEdit.html @@ -45,6 +45,9 @@

    {{username}}

    {{#with room}}

    {{_ "Conversation" }}

    + {{#if customFieldsTemplate}} + {{> Template.dynamic template=customFieldsTemplate data=room }} + {{/if}}