From 3e31d9ec9b8b278f44b8eefab44a5a57d7616ae7 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 27 May 2020 23:09:12 -0300 Subject: [PATCH 001/154] Bump version to 3.4.0-develop --- .docker/Dockerfile.rhel | 2 +- .snapcraft/resources/prepareRocketChat | 2 +- .snapcraft/snap/snapcraft.yaml | 2 +- app/utils/rocketchat.info | 2 +- package-lock.json | 2 +- package.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.docker/Dockerfile.rhel b/.docker/Dockerfile.rhel index f9056ace94a6a..7cf6423267efa 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.3.0 +ENV RC_VERSION 3.4.0-develop MAINTAINER buildmaster@rocket.chat diff --git a/.snapcraft/resources/prepareRocketChat b/.snapcraft/resources/prepareRocketChat index 3e66b3f64ce10..bb722af09279b 100755 --- a/.snapcraft/resources/prepareRocketChat +++ b/.snapcraft/resources/prepareRocketChat @@ -1,6 +1,6 @@ #!/bin/bash -curl -SLf "https://releases.rocket.chat/3.3.0/download/" -o rocket.chat.tgz +curl -SLf "https://releases.rocket.chat/3.4.0-develop/download/" -o rocket.chat.tgz tar xf rocket.chat.tgz --strip 1 diff --git a/.snapcraft/snap/snapcraft.yaml b/.snapcraft/snap/snapcraft.yaml index 56a25a0364531..3381a097a08ad 100644 --- a/.snapcraft/snap/snapcraft.yaml +++ b/.snapcraft/snap/snapcraft.yaml @@ -7,7 +7,7 @@ # 5. `snapcraft snap` name: rocketchat-server -version: 3.3.0 +version: 3.4.0-develop summary: Rocket.Chat server description: Have your own Slack like online chat, built with Meteor. https://rocket.chat/ confinement: strict diff --git a/app/utils/rocketchat.info b/app/utils/rocketchat.info index 61d70c69f2e7c..d617200a664ab 100644 --- a/app/utils/rocketchat.info +++ b/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "3.3.0" + "version": "3.4.0-develop" } diff --git a/package-lock.json b/package-lock.json index 22bdfd1cca4d4..73e3d7aa28ed0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "Rocket.Chat", - "version": "3.3.0", + "version": "3.4.0-develop", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 42c5580c27393..5d35512f0fd7e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "Rocket.Chat", "description": "The Ultimate Open Source WebChat Platform", - "version": "3.3.0", + "version": "3.4.0-develop", "author": { "name": "Rocket.Chat", "url": "https://rocket.chat/" From cae6aeba9982aa05d48161430966c2ef14621679 Mon Sep 17 00:00:00 2001 From: Bradley Hilton Date: Thu, 28 May 2020 17:29:46 -0500 Subject: [PATCH 002/154] Submit a payload to the release service when a release happens (#17775) --- .circleci/update-releases.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/update-releases.sh b/.circleci/update-releases.sh index 3d831115b4410..d3500a3ce0e6c 100644 --- a/.circleci/update-releases.sh +++ b/.circleci/update-releases.sh @@ -2,9 +2,9 @@ set -euvo pipefail IFS=$'\n\t' -curl -X POST \ --H "X-Update-Token: ${UPDATE_TOKEN}" \ -https://releases.rocket.chat/update +curl -H "Content-Type: application/json" -H "X-Update-Token: $UPDATE_TOKEN" -d \ + "{\"commit\": \"$CIRCLE_SHA1\", \"tag\": \"$CIRCLE_TAG\", \"branch\": \"$CIRCLE_BRANCH\", \"artifactName\": \"$ARTIFACT_NAME\", \"releaseType\": \"$RC_RELEASE\" }" \ + https://releases.rocket.chat/update # Makes build fail if the release isn't there curl --fail https://releases.rocket.chat/$RC_VERSION/info From d8b4e02ec09b6994cdd88c22b52cc91665f3a610 Mon Sep 17 00:00:00 2001 From: Arcuri Davide Date: Sat, 30 May 2020 18:43:09 +0200 Subject: [PATCH 003/154] Fixes some italian word (#14008) --- packages/rocketchat-i18n/i18n/it.i18n.json | 28 +++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/rocketchat-i18n/i18n/it.i18n.json b/packages/rocketchat-i18n/i18n/it.i18n.json index 7c96af8a26997..b09828d028d4a 100644 --- a/packages/rocketchat-i18n/i18n/it.i18n.json +++ b/packages/rocketchat-i18n/i18n/it.i18n.json @@ -24,9 +24,9 @@ "Accessing_permissions": "Permessi di Accesso", "Account_SID": "Account SID", "Accounts": "Account", - "Accounts_Admin_Email_Approval_Needed_Default": "

L'utente [nome] ([email])è stato registrato.

Andare in \"Amministrazione -> Utenti\" per attivarlo o eliminarlo.

", + "Accounts_Admin_Email_Approval_Needed_Default": "

L'utente [nome] ([email])è stato registrato.

Andare in \"Amministrazione -> Utenti\" per attivarlo o cancellarlo.

", "Accounts_Admin_Email_Approval_Needed_Subject_Default": "Un nuovo utente si è registrato ed è necessario approvarlo", - "Accounts_Admin_Email_Approval_Needed_With_Reason_Default": "

L'utente [name] ([email])è stato registrato.

Motivo: [reason]

Andare a \"Amministrazione -> Utenti\" per attivarlo o eliminarlo.

", + "Accounts_Admin_Email_Approval_Needed_With_Reason_Default": "

L'utente [name] ([email])è stato registrato.

Motivo: [reason]

Andare a \"Amministrazione -> Utenti\" per attivarlo o cancellarlo.

", "Accounts_AllowAnonymousRead": "Consenti lettura anonima", "Accounts_AllowAnonymousWrite": "Consenti scrittura anonima", "Accounts_AllowDeleteOwnAccount": "Consenti agli utenti di cancellare il proprio account", @@ -890,8 +890,8 @@ "Delete_message": "Cancella messaggio", "Delete_my_account": "Cancella il mio account", "Delete_Room_Warning": "Cancellando un canale rimuoverà tutti i messaggi postati all'interno del canale. Questa azione non può essere annullata.", - "Delete_User_Warning": "Cancellando un utente verranno eliminati anche tutti i suoi messaggi. Questa operazione non può essere annullata.", - "Delete_User_Warning_Delete": "Cancellando un utente verranno eliminati anche tutti i suoi messaggi. Questa operazione non può essere annullata.", + "Delete_User_Warning": "Cancellando un utente verranno cancellati anche tutti i suoi messaggi. Questa operazione non può essere annullata.", + "Delete_User_Warning_Delete": "Cancellando un utente verranno cancellati anche tutti i suoi messaggi. Questa operazione non può essere annullata.", "Delete_User_Warning_Keep": "L'utente verrà eliminato, ma i suoi messaggi rimarranno visibili. Questo non può essere annullato.", "Delete_User_Warning_Unlink": "L'eliminazione di un utente rimuoverà il nome utente da tutti i loro messaggi. Questo non può essere annullato.", "Deleted": "Cancellato!", @@ -1954,7 +1954,7 @@ "Online": "Online", "online": "in linea", "Only_authorized_users_can_write_new_messages": "Solo gli utenti autorizzati possono scrivere nuovi messaggi", - "Only_from_users": "Elimina solo i contenuti da questi utenti (lascia vuoto per potare il contenuto di tutti)", + "Only_from_users": "Elimina solo i contenuti da questi utenti (lascia vuoto per cancellare il contenuto di tutti)", "Only_On_Desktop": "Modalità Desktop (invia solo con invio su desktop)", "Only_you_can_see_this_message": "Solo tu puoi vedere questo messaggio", "Oops!": "Spiacenti", @@ -2063,18 +2063,18 @@ "Profile_details": "Dettagli del profilo", "Profile_picture": "Immagine del profilo", "Profile_saved_successfully": "Profilo salvato con successo", - "Prune": "Fesso", - "Prune_finished": "Prugna finita", - "Prune_Messages": "Prune Messaggi", - "Prune_Modal": "Sei sicuro di voler potare questi messaggi? I messaggi potati non possono essere recuperati.", + "Prune": "Cancella", + "Prune_finished": "Cancellazione finita", + "Prune_Messages": "Cancella Messaggi", + "Prune_Modal": "Sei sicuro di voler eliminare questi messaggi? I messaggi eliminati non possono essere recuperati.", "Prune_Warning_all": "Questo cancellerà tutto %s in %s!", "Prune_Warning_before": "Questo cancellerà tutto %s in %s prima di %s.", "Prune_Warning_after": "Questo cancellerà tutto %s in %s dopo %s.", "Prune_Warning_between": "Questo cancellerà tutto %s in %s tra %se %s.", - "Pruning_messages": "Messaggi di potatura ...", + "Pruning_messages": "Messaggi di eliminazione ...", "Pruning_files": "File di eliminazione ...", - "messages_pruned": "messaggi potati", - "files_pruned": "file potati", + "messages_pruned": "messaggi cancellati", + "files_pruned": "file cancellati", "Public": "Pubblico", "Public_Channel": "Canale pubblico", "Public_Community": "Comunità pubblica", @@ -2848,7 +2848,7 @@ "Yes_hide_it": "Sì, nascondilo!", "Yes_leave_it": "Sì, lascia!", "Yes_mute_user": "Sì, silenzia l'utente!", - "Yes_prune_them": "Sì, li pota!", + "Yes_prune_them": "Sì, cancella!", "Yes_remove_user": "Sì, rimuovi l'utente!", "Yes_unarchive_it": "Si, togli dall'archivio!", "yesterday": "ieri", @@ -2881,7 +2881,7 @@ "You_wont_receive_email_notifications_because_you_have_not_verified_your_email": "Non riceverai notifiche via email, perché non hai verificato l'indirizzo.", "Your_email_has_been_queued_for_sending": "La tua email è stata messa in coda per l'invio", "Your_entry_has_been_deleted": "La tua immissione è stata cancellata.", - "Your_file_has_been_deleted": "Il file è stato eliminato.", + "Your_file_has_been_deleted": "Il file è stato cancellato.", "Your_mail_was_sent_to_s": "La tua mail è stata inviata a %s", "your_message": "il tuo messaggio", "your_message_optional": "il tuo messaggio (opzionale)", From 673795673d09fdf18753281ec6514c1791872c5c Mon Sep 17 00:00:00 2001 From: Anton Kazarinov Date: Sun, 31 May 2020 01:34:17 +0500 Subject: [PATCH 004/154] [FIX] Disabling `Json Web Tokens protection to file uploads` disables the File Upload protection entirely (#16262) --- app/file-upload/server/lib/FileUpload.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/file-upload/server/lib/FileUpload.js b/app/file-upload/server/lib/FileUpload.js index e17ed2dbc9392..adc94851aee42 100644 --- a/app/file-upload/server/lib/FileUpload.js +++ b/app/file-upload/server/lib/FileUpload.js @@ -307,7 +307,7 @@ export const FileUpload = { const isAuthorizedByCookies = rc_uid && rc_token && Users.findOneByIdAndLoginToken(rc_uid, rc_token); const isAuthorizedByHeaders = headers['x-user-id'] && headers['x-auth-token'] && Users.findOneByIdAndLoginToken(headers['x-user-id'], headers['x-auth-token']); const isAuthorizedByRoom = rc_room_type && roomTypes.getConfig(rc_room_type).canAccessUploadedFile({ rc_uid, rc_rid, rc_token }); - const isAuthorizedByJWT = !settings.get('FileUpload_Enable_json_web_token_for_files') || (token && isValidJWT(token, settings.get('FileUpload_json_web_token_secret_for_files'))); + const isAuthorizedByJWT = settings.get('FileUpload_Enable_json_web_token_for_files') && token && isValidJWT(token, settings.get('FileUpload_json_web_token_secret_for_files')); return isAuthorizedByCookies || isAuthorizedByHeaders || isAuthorizedByRoom || isAuthorizedByJWT; }, addExtensionTo(file) { From b520907ec1c3e2065b635a9d0859dced86aedf1a Mon Sep 17 00:00:00 2001 From: Aviral Gangwar Date: Sun, 31 May 2020 02:04:50 +0530 Subject: [PATCH 005/154] [FIX] Set `x-content-type-options: nosniff` header (#16232) --- app/cors/server/cors.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/cors/server/cors.js b/app/cors/server/cors.js index 9c6d8eb318035..1022c1edcb878 100644 --- a/app/cors/server/cors.js +++ b/app/cors/server/cors.js @@ -54,6 +54,9 @@ WebApp.rawConnectHandlers.use(function(req, res, next) { // XSS Protection for old browsers (IE) res.setHeader('X-XSS-Protection', '1'); + // X-Content-Type-Options header to prevent MIME Sniffing + res.setHeader('X-Content-Type-Options', 'nosniff'); + if (settings.get('Iframe_Restrict_Access')) { res.setHeader('X-Frame-Options', settings.get('Iframe_X_Frame_Options')); } From 05290be00b24dc831912ddc7dff3421894e873d1 Mon Sep 17 00:00:00 2001 From: Ashwani Yadav Date: Sun, 31 May 2020 02:05:15 +0530 Subject: [PATCH 006/154] [NEW] Highlight matching words in message search results (#16166) --- app/search/client/provider/result.js | 2 ++ app/ui-message/client/message.js | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/app/search/client/provider/result.js b/app/search/client/provider/result.js index 3ab80e13943b0..6809f2915874c 100644 --- a/app/search/client/provider/result.js +++ b/app/search/client/provider/result.js @@ -100,6 +100,8 @@ Template.DefaultSearchResultTemplate.helpers({ return Template.instance().hasMore.get(); }, messageParse(msg) { + const text = Template.instance().data.text.get(); + msg.searchedText = text; return { customClass: 'search', actionContext: 'search', ...msg, groupable: false }; }, messageContext, diff --git a/app/ui-message/client/message.js b/app/ui-message/client/message.js index a237f26b2ba24..3f15f7b4c3973 100644 --- a/app/ui-message/client/message.js +++ b/app/ui-message/client/message.js @@ -19,6 +19,7 @@ import './messageThread.html'; import { AutoTranslate } from '../../autotranslate/client'; const renderBody = (msg, settings) => { + const searchedText = msg.searchedText ? msg.searchedText : ''; const isSystemMessage = MessageTypes.isSystemMessage(msg); const messageType = MessageTypes.getType(msg) || {}; @@ -40,6 +41,11 @@ const renderBody = (msg, settings) => { if (isSystemMessage) { msg.html = Markdown.parse(msg.html); } + + if (searchedText) { + msg = msg.replace(new RegExp(searchedText, 'gi'), (str) => `${ str }`); + } + return msg; }; From ae90799288bfeee966a25605429ed90656ba0029 Mon Sep 17 00:00:00 2001 From: pierre-lehnen-rc <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Sat, 30 May 2020 17:36:48 -0300 Subject: [PATCH 007/154] [NEW] Skip Export Operations that haven't been updated in over a day (#16135) --- app/models/server/models/ExportOperations.js | 8 ++++---- app/user-data-download/server/cronProcessDownloads.js | 10 ++++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/models/server/models/ExportOperations.js b/app/models/server/models/ExportOperations.js index bb0f968b17c19..fb70d38925cab 100644 --- a/app/models/server/models/ExportOperations.js +++ b/app/models/server/models/ExportOperations.js @@ -31,7 +31,7 @@ export class ExportOperations extends Base { const query = { userId, status: { - $nin: ['completed'], + $nin: ['completed', 'skipped'], }, }; @@ -40,7 +40,7 @@ export class ExportOperations extends Base { findAllPending(options) { const query = { - status: { $nin: ['completed'] }, + status: { $nin: ['completed', 'skipped'] }, }; return this.find(query, options); @@ -48,7 +48,7 @@ export class ExportOperations extends Base { findOnePending(options) { const query = { - status: { $nin: ['completed'] }, + status: { $nin: ['completed', 'skipped'] }, }; return this.findOne(query, options); @@ -56,7 +56,7 @@ export class ExportOperations extends Base { findAllPendingBeforeMyRequest(requestDay, options) { const query = { - status: { $nin: ['completed'] }, + status: { $nin: ['completed', 'skipped'] }, createdAt: { $lt: requestDay }, }; diff --git a/app/user-data-download/server/cronProcessDownloads.js b/app/user-data-download/server/cronProcessDownloads.js index 02bdcc39707fb..3b1d8c785c7bc 100644 --- a/app/user-data-download/server/cronProcessDownloads.js +++ b/app/user-data-download/server/cronProcessDownloads.js @@ -6,6 +6,7 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { SyncedCron } from 'meteor/littledata:synced-cron'; import archiver from 'archiver'; +import moment from 'moment'; import { settings } from '../../settings'; import { Subscriptions, Rooms, Users, Uploads, Messages, UserDataFiles, ExportOperations, Avatars } from '../../models'; @@ -564,6 +565,15 @@ async function processDataDownloads() { return; } + if (operation.status !== 'pending') { + // If the operation has started but was not updated in over a day, then skip it + if (operation._updatedAt && moment().diff(moment(operation._updatedAt), 'days') > 1) { + operation.status = 'skipped'; + await ExportOperations.updateOperation(operation); + return processDataDownloads(); + } + } + await continueExportOperation(operation); await ExportOperations.updateOperation(operation); From 53c4ecf610ca3ce543b5eeab3cbce488f4e9f1fe Mon Sep 17 00:00:00 2001 From: justinr1234 Date: Sat, 30 May 2020 15:38:08 -0500 Subject: [PATCH 008/154] [IMPROVE] Make the implementation of custom code easier by having placeholders for a custom folder (#15106) --- app/callbacks/lib/callbacks.js | 5 ++-- app/custom/client/index.js | 0 app/custom/server/index.js | 0 .../hooks/propagateDiscussionMetadata.js | 28 +++++++++++++------ app/ui-utils/client/lib/RoomManager.js | 4 +-- app/ui-utils/client/lib/openRoom.js | 2 +- client/importPackages.js | 1 + server/importPackages.js | 1 + 8 files changed, 27 insertions(+), 14 deletions(-) create mode 100644 app/custom/client/index.js create mode 100644 app/custom/server/index.js diff --git a/app/callbacks/lib/callbacks.js b/app/callbacks/lib/callbacks.js index d602e03842037..93a7e8d04d58d 100644 --- a/app/callbacks/lib/callbacks.js +++ b/app/callbacks/lib/callbacks.js @@ -59,8 +59,8 @@ const combinedCallbacks = new Map(); /* * Callback priorities +* @enum {CallbackPriority} */ - callbacks.priority = { HIGH: -1000, MEDIUM: 0, @@ -73,8 +73,9 @@ const getHooks = (hookName) => callbacks[hookName] || []; * Add a callback function to a hook * @param {String} hook - The name of the hook * @param {Function} callback - The callback function +* @param {CallbackPriority} priority - The callback run priority (order) +* @param {String} id - Human friendly name for this callback */ - callbacks.add = function( hook, callback, diff --git a/app/custom/client/index.js b/app/custom/client/index.js new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/app/custom/server/index.js b/app/custom/server/index.js new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/app/discussion/server/hooks/propagateDiscussionMetadata.js b/app/discussion/server/hooks/propagateDiscussionMetadata.js index 902965ca23e45..271b192211601 100644 --- a/app/discussion/server/hooks/propagateDiscussionMetadata.js +++ b/app/discussion/server/hooks/propagateDiscussionMetadata.js @@ -23,15 +23,25 @@ callbacks.add('afterDeleteMessage', function(message, { _id, prid } = {}) { return message; }, callbacks.priority.LOW, 'PropagateDiscussionMetadata'); -callbacks.add('afterDeleteRoom', (rid) => Rooms.find({ prid: rid }, { fields: { _id: 1 } }).forEach(({ _id }) => deleteRoom(_id)), 'DeleteDiscussionChain'); +callbacks.add('afterDeleteRoom', (rid) => { + Rooms.find({ prid: rid }, { fields: { _id: 1 } }).forEach(({ _id }) => deleteRoom(_id)); + return rid; +}, callbacks.priority.LOW, 'DeleteDiscussionChain'); // TODO discussions define new fields -callbacks.add('afterRoomNameChange', ({ rid, name, oldName }) => Rooms.update({ prid: rid, ...oldName && { topic: oldName } }, { $set: { topic: name } }, { multi: true }), 'updateTopicDiscussion'); +callbacks.add('afterRoomNameChange', (roomConfig) => { + const { rid, name, oldName } = roomConfig; + Rooms.update({ prid: rid, ...oldName && { topic: oldName } }, { $set: { topic: name } }, { multi: true }); + return roomConfig; +}, callbacks.priority.LOW, 'updateTopicDiscussion'); -callbacks.add('afterDeleteRoom', (drid) => Messages.update({ drid }, { - $unset: { - dcount: 1, - dlm: 1, - drid: 1, - }, -}), 'CleanDiscussionMessage'); +callbacks.add('afterDeleteRoom', (drid) => { + Messages.update({ drid }, { + $unset: { + dcount: 1, + dlm: 1, + drid: 1, + }, + }); + return drid; +}, callbacks.priority.LOW, 'CleanDiscussionMessage'); diff --git a/app/ui-utils/client/lib/RoomManager.js b/app/ui-utils/client/lib/RoomManager.js index 84eff4b04a383..43a478adee192 100644 --- a/app/ui-utils/client/lib/RoomManager.js +++ b/app/ui-utils/client/lib/RoomManager.js @@ -114,7 +114,7 @@ export const RoomManager = new function() { return Object.keys(openedRooms).map((typeName) => openedRooms[typeName]).find((openedRoom) => openedRoom.rid === rid); } - getDomOfRoom(typeName, rid) { + getDomOfRoom(typeName, rid, templateName) { const room = openedRooms[typeName]; if (room == null) { return; @@ -125,7 +125,7 @@ export const RoomManager = new function() { room.dom.classList.add('room-container'); const contentAsFunc = (content) => () => content; - room.template = Blaze._TemplateWith({ _id: rid }, contentAsFunc(Template.room)); + room.template = Blaze._TemplateWith({ _id: rid }, contentAsFunc(Template[templateName || 'room'])); Blaze.render(room.template, room.dom); // , nextNode, parentView } diff --git a/app/ui-utils/client/lib/openRoom.js b/app/ui-utils/client/lib/openRoom.js index 54252661781d5..71b35a397d0c0 100644 --- a/app/ui-utils/client/lib/openRoom.js +++ b/app/ui-utils/client/lib/openRoom.js @@ -84,7 +84,7 @@ export const openRoom = async function(type, name) { return FlowRouter.go('direct', { rid: room._id }, FlowRouter.current().queryParams); } - const roomDom = RoomManager.getDomOfRoom(type + name, room._id); + const roomDom = RoomManager.getDomOfRoom(type + name, room._id, roomTypes.getConfig(type).mainTemplate); const mainNode = replaceCenterDomBy(roomDom); if (mainNode) { diff --git a/client/importPackages.js b/client/importPackages.js index 7d95bc6f83e70..115a1d35665f3 100644 --- a/client/importPackages.js +++ b/client/importPackages.js @@ -107,3 +107,4 @@ import '../app/reactions/client'; import '../app/livechat/client'; import '../app/meteor-autocomplete/client'; import '../app/theme/client'; +import '../app/custom/client'; diff --git a/server/importPackages.js b/server/importPackages.js index aa56233f2bf50..bf5a117a6dd3d 100644 --- a/server/importPackages.js +++ b/server/importPackages.js @@ -112,3 +112,4 @@ import '../app/ui-utils'; import '../app/action-links/server'; import '../app/reactions/server'; import '../app/livechat/server'; +import '../app/custom/server'; From 16d5da214f22d2a0d3a4735dfc756a10534b8c34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Rauh=C3=B6ft?= Date: Sat, 30 May 2020 22:38:35 +0200 Subject: [PATCH 009/154] [NEW][API] Endpoint `settings.addCustomOAuth` to create Custom OAuth services (#14912) --- app/api/server/v1/settings.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/api/server/v1/settings.js b/app/api/server/v1/settings.js index 42db479903feb..284fea491be7e 100644 --- a/app/api/server/v1/settings.js +++ b/app/api/server/v1/settings.js @@ -64,6 +64,21 @@ API.v1.addRoute('settings.oauth', { authRequired: false }, { }, }); +API.v1.addRoute('settings.addCustomOAuth', { authRequired: true }, { + post() { + if (!this.requestParams().name || !this.requestParams().name.trim()) { + throw new Meteor.Error('error-name-param-not-provided', 'The parameter "name" is required'); + } + + Meteor.runAsUser(this.userId, () => { + Meteor.call('addOAuthService', this.requestParams().name, this.userId); + }); + + + return API.v1.success(); + }, +}); + API.v1.addRoute('settings', { authRequired: true }, { get() { const { offset, count } = this.getPaginationItems(); From e79b3b492fd51f1043774907bd36a2e4b4b8e45f Mon Sep 17 00:00:00 2001 From: Kautilya Tripathi Date: Sat, 30 May 2020 20:40:05 +0000 Subject: [PATCH 010/154] [FIX] Markdown links not accepting URLs with parentheses (#13605) --- app/markdown/lib/parser/original/markdown.js | 4 ++-- app/markdown/tests/client.tests.js | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/markdown/lib/parser/original/markdown.js b/app/markdown/lib/parser/original/markdown.js index 7f047bd1204a0..9b17a9e098995 100644 --- a/app/markdown/lib/parser/original/markdown.js +++ b/app/markdown/lib/parser/original/markdown.js @@ -76,7 +76,7 @@ const parseNotEscaped = function(msg, message) { msg = msg.replace(/<\/blockquote>\n
{ + msg = msg.replace(new RegExp(`!\\[([^\\]]+)\\]\\(((?:${ schemes }):\\/\\/[^\\s]+)\\)`, 'gm'), (match, title, url) => { if (!validateUrl(url)) { return match; } @@ -85,7 +85,7 @@ const parseNotEscaped = function(msg, message) { }); // Support [Text](http://link) - msg = msg.replace(new RegExp(`\\[([^\\]]+)\\]\\(((?:${ schemes }):\\/\\/[^\\)]+)\\)`, 'gm'), (match, title, url) => { + msg = msg.replace(new RegExp(`\\[([^\\]]+)\\]\\(((?:${ schemes }):\\/\\/[^\\s]+)\\)`, 'gm'), (match, title, url) => { if (!validateUrl(url)) { return match; } diff --git a/app/markdown/tests/client.tests.js b/app/markdown/tests/client.tests.js index 1738c61e56512..c665ad7cc30af 100644 --- a/app/markdown/tests/client.tests.js +++ b/app/markdown/tests/client.tests.js @@ -188,7 +188,7 @@ const link = { '[Text](http://invalid link)': '[Text](http://invalid link)', '[Text](http://link)': linkWrapped('http://link', 'Text'), '[Open Site For Rocket.Chat](https://open.rocket.chat/)': linkWrapped('https://open.rocket.chat/', 'Open Site For Rocket.Chat'), - '[ Open Site For Rocket.Chat](https://open.rocket.chat/ )': linkWrapped('https://open.rocket.chat/ ', ' Open Site For Rocket.Chat'), + '[ Open Site For Rocket.Chat ](https://open.rocket.chat/)': linkWrapped('https://open.rocket.chat/', ' Open Site For Rocket.Chat '), '[Rocket.Chat Site](https://rocket.chat/)': linkWrapped('https://rocket.chat/', 'Rocket.Chat Site'), '[Testing Entry on Rocket.Chat Docs Site](https://rocket.chat/docs/developer-guides/testing/#testing)': linkWrapped('https://rocket.chat/docs/developer-guides/testing/#testing', 'Testing Entry on Rocket.Chat Docs Site'), '[](http://linkText)': '[](http://linkText)', @@ -201,8 +201,14 @@ const link = { '[Open Site For Rocket.Chat](open.rocket.chat/)': '[Open Site For Rocket.Chat](open.rocket.chat/)', '[Testing Entry on Rocket.Chat Docs Site](htts://rocket.chat/docs/developer-guides/testing/#testing)': '[Testing Entry on Rocket.Chat Docs Site](htts://rocket.chat/docs/developer-guides/testing/#testing)', '[Text](http://link?param1=1¶m2=2)': linkWrapped('http://link?param1=1¶m2=2', 'Text'), + '[Testing Double parentheses](https://en.wikipedia.org/wiki/Disambiguation_(disambiguation))': linkWrapped('https://en.wikipedia.org/wiki/Disambiguation_(disambiguation)', 'Testing Double parentheses'), + '[Testing data after Double parentheses](https://en.wikipedia.org/wiki/Disambiguation_(disambiguation)/blabla/bla)': linkWrapped('https://en.wikipedia.org/wiki/Disambiguation_(disambiguation)/blabla/bla', 'Testing data after Double parentheses'), }; +Object.entries(link).forEach(([key, value]) => { + link[`before (test) ${ key } after (test)`] = `before (test) ${ value } after (test)`; +}); + const inlinecode = { '`code`': inlinecodeWrapper('code'), '`code` begin': `${ inlinecodeWrapper('code') } begin`, From ae95908a2bdcd30a8131a915b20a11e6c3edd3b9 Mon Sep 17 00:00:00 2001 From: Aditya Bhardwaj Date: Sun, 31 May 2020 02:11:19 +0530 Subject: [PATCH 011/154] [FIX] Spotify embed link opens in same tab (#13637) --- app/spotify/lib/client/oembedSpotifyWidget.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/spotify/lib/client/oembedSpotifyWidget.html b/app/spotify/lib/client/oembedSpotifyWidget.html index 46fa62d75f528..9da698d778f2c 100644 --- a/app/spotify/lib/client/oembedSpotifyWidget.html +++ b/app/spotify/lib/client/oembedSpotifyWidget.html @@ -3,9 +3,9 @@
Spotify
{{#if match meta.ogAudio "spotify:artist:\\S+"}} - {{{meta.ogTitle}}} + {{{meta.ogTitle}}} {{else}} - {{{replace meta.ogDescription ", an? (?:song|album) by (.+?) on Spotify" " - $1" regex=true}}} + {{{replace meta.ogDescription ", an? (?:song|album) by (.+?) on Spotify" " - $1" regex=true}}} {{/if}} {{> collapseArrow collapsedMedia=collapsedMediaVar}}
{{#unless collapsed}} From e4aebd21df841bab9ba1a86fad732a9b03a958f7 Mon Sep 17 00:00:00 2001 From: ChrissW-R1 Date: Sat, 30 May 2020 22:41:37 +0200 Subject: [PATCH 012/154] [NEW] Accept variable `#{userdn}` on LDAP group filter (#16273) --- app/ldap/server/sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/ldap/server/sync.js b/app/ldap/server/sync.js index 9c3249b9172c4..5e290d9bb8e4a 100644 --- a/app/ldap/server/sync.js +++ b/app/ldap/server/sync.js @@ -28,7 +28,7 @@ export function isUserInLDAPGroup(ldap, ldapUser, user, ldapGroup) { return false; } const searchOptions = { - filter: syncUserRolesFilter.replace(/#{username}/g, user.username).replace(/#{groupName}/g, ldapGroup), + filter: syncUserRolesFilter.replace(/#{username}/g, user.username).replace(/#{groupName}/g, ldapGroup).replace(/#{userdn}/g, ldapUser.dn), scope: 'sub', }; From 81393228862d99344b95cca787c8afc42ea93c26 Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven Date: Sat, 30 May 2020 22:47:05 +0200 Subject: [PATCH 013/154] Readme: Update Raspberry Pi 2 to Pi 4 (#17031) --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b2bda6396d049..bba684655ecb5 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ * [Sloppy.io](#sloppyio) * [Docker](#docker) * [Ansible](#ansible) - * [Raspberry Pi 2](#raspberry-pi-2) + * [Raspberry Pi 4](#raspberry-pi-4) * [Koozali SME](#koozali-sme) * [Ubuntu VPS](#ubuntu-vps) * [WeDeploy](#wedeploy) @@ -181,10 +181,10 @@ Automated production-grade deployment in minutes, for RHEL / CentOS 7 or Ubuntu [![Ansible deployment](https://raw.githubusercontent.com/Sing-Li/bbug/master/images/ansible.png)](https://rocket.chat/docs/installation/automation-tools/ansible/) -## Raspberry Pi 2 -Run Rocket.Chat on this world famous $30 quad-core server. +## Raspberry Pi 4 +Run Rocket.Chat on this world famous $35 quad-core server. -[![Raspberry Pi 2](https://raw.githubusercontent.com/Sing-Li/bbug/master/images/pitiny.png)](https://github.com/RocketChat/Rocket.Chat.RaspberryPi) +[![Raspberry Pi 4](https://raw.githubusercontent.com/Sing-Li/bbug/master/images/pitiny.png)](https://github.com/RocketChat/Rocket.Chat.RaspberryPi) ## Koozali SME From 6f041baa25e14f45d43d77698db216c14815d294 Mon Sep 17 00:00:00 2001 From: Bradley Hilton Date: Mon, 1 Jun 2020 16:04:03 -0500 Subject: [PATCH 014/154] Fix invalid develop payload to release service (#17799) The payload sent to the release service contains an invalid reference to the tag/version and wasn't working when building develop branch. --- .circleci/update-releases.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/update-releases.sh b/.circleci/update-releases.sh index d3500a3ce0e6c..1f5f527c4d086 100644 --- a/.circleci/update-releases.sh +++ b/.circleci/update-releases.sh @@ -3,7 +3,7 @@ set -euvo pipefail IFS=$'\n\t' curl -H "Content-Type: application/json" -H "X-Update-Token: $UPDATE_TOKEN" -d \ - "{\"commit\": \"$CIRCLE_SHA1\", \"tag\": \"$CIRCLE_TAG\", \"branch\": \"$CIRCLE_BRANCH\", \"artifactName\": \"$ARTIFACT_NAME\", \"releaseType\": \"$RC_RELEASE\" }" \ + "{\"commit\": \"$CIRCLE_SHA1\", \"tag\": \"$RC_VERSION\", \"branch\": \"$CIRCLE_BRANCH\", \"artifactName\": \"$ARTIFACT_NAME\", \"releaseType\": \"$RC_RELEASE\" }" \ https://releases.rocket.chat/update # Makes build fail if the release isn't there From ed620717c70b07f2dc61c3b8af05265868eb8222 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Tue, 2 Jun 2020 13:17:19 -0300 Subject: [PATCH 015/154] Update Apps-Engine version (#17804) --- package-lock.json | 11 +++-------- package.json | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 73e3d7aa28ed0..8bfc9808c347a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2797,9 +2797,9 @@ } }, "@rocket.chat/apps-engine": { - "version": "1.15.0-beta.3411", - "resolved": "https://registry.npmjs.org/@rocket.chat/apps-engine/-/apps-engine-1.15.0-beta.3411.tgz", - "integrity": "sha512-e1ddaAfjWXWGyb2tlW8eZHgg6sBHN73n52i8b62GfqSJtf1cIM9VhLA4igq8Anaai5UtcxmmhdAQgmt0xhnNuw==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@rocket.chat/apps-engine/-/apps-engine-1.15.0.tgz", + "integrity": "sha512-gwsHa/zTYMmoSG3PP3sZfmVRDRBmIDacOAdCv1FsgJog89ZBCICeoab3VyYAdOMliV5XoygygduYFtc6rinFHQ==", "requires": { "adm-zip": "^0.4.9", "cryptiles": "^4.1.3", @@ -2810,11 +2810,6 @@ "uuid": "^3.2.1" }, "dependencies": { - "adm-zip": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.14.tgz", - "integrity": "sha512-/9aQCnQHF+0IiCl0qhXoK7qs//SwYE7zX8lsr/DNk1BRAHYxeLZPL4pguwK29gUEqasYQjqPtEpDRSWEkdHn9g==" - }, "typescript": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", diff --git a/package.json b/package.json index 5d35512f0fd7e..dd95758fc3762 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,7 @@ "@nivo/heatmap": "^0.61.0", "@nivo/line": "^0.61.1", "@nivo/pie": "^0.61.1", - "@rocket.chat/apps-engine": "1.15.0-beta.3411", + "@rocket.chat/apps-engine": "1.15.0", "@rocket.chat/fuselage": "^0.9.0", "@rocket.chat/fuselage-hooks": "^0.9.0", "@rocket.chat/fuselage-polyfills": "^0.9.0", From 5c530d0be6f2e04bcacbf235efd1f67f66d01e38 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Tue, 2 Jun 2020 14:42:45 -0300 Subject: [PATCH 016/154] [FIX] Error when re-installing an App (#17789) * Fix wrong method call on app installation * Update subscription check to take cancelled state into account --- app/apps/client/admin/appManage.js | 3 ++- app/apps/client/admin/helpers.js | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/apps/client/admin/appManage.js b/app/apps/client/admin/appManage.js index daf10f0dd176f..34550cf9b1b10 100644 --- a/app/apps/client/admin/appManage.js +++ b/app/apps/client/admin/appManage.js @@ -405,7 +405,8 @@ Template.appManage.events({ _app.set('working', true); try { - const { status } = await Apps.updateApp(appId, _app.get('marketplaceVersion')); + const method = _app.get('installed') ? 'updateApp' : 'installApp'; + const { status } = await Apps[method](appId, _app.get('marketplaceVersion')); warnStatusChange(_app.get('name'), status); } catch (error) { handleAPIError(error); diff --git a/app/apps/client/admin/helpers.js b/app/apps/client/admin/helpers.js index a1b0b2cdf9c55..3ae7f805a4837 100644 --- a/app/apps/client/admin/helpers.js +++ b/app/apps/client/admin/helpers.js @@ -19,6 +19,11 @@ const appErroredStatuses = [ AppStatus.INVALID_LICENSE_DISABLED, ]; +const subscriptionActiveStatuses = [ + 'trialing', + 'active', +]; + export const handleAPIError = (error) => { console.error(error); const message = (error.xhr && error.xhr.responseJSON && error.xhr.responseJSON.error) || error.message; @@ -298,7 +303,7 @@ export const appButtonProps = ({ }; } - const canTrial = purchaseType === 'subscription' && !subscriptionInfo.status; + const canTrial = purchaseType === 'subscription' && !subscriptionActiveStatuses.includes(subscriptionInfo.status); if (canTrial) { return { action: 'purchase', From 2503fa44280a1fb0455353ce7581f2e64afa3a6b Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 2 Jun 2020 14:47:57 -0300 Subject: [PATCH 017/154] [FIX] Email link "go to message" being incorrectly escaped (#17803) --- app/mailer/server/api.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/app/mailer/server/api.js b/app/mailer/server/api.js index 38bb11418273f..da22acc8208a6 100644 --- a/app/mailer/server/api.js +++ b/app/mailer/server/api.js @@ -6,7 +6,7 @@ import s from 'underscore.string'; import juice from 'juice'; import stripHtml from 'string-strip-html'; -import { settings } from '../../settings'; +import { settings } from '../../settings/server'; let contentHeader; let contentFooter; @@ -42,11 +42,13 @@ export const replace = function replace(str, data = {}) { return Object.entries(options).reduce((ret, [key, value]) => replacekey(ret, key, value), translate(str)); }; +const nonEscapeKeys = ['room_path']; + export const replaceEscaped = (str, data = {}) => replace(str, { Site_Name: s.escapeHTML(settings.get('Site_Name')), Site_Url: s.escapeHTML(settings.get('Site_Url')), ...Object.entries(data).reduce((ret, [key, value]) => { - ret[key] = s.escapeHTML(value); + ret[key] = nonEscapeKeys.includes(key) ? value : s.escapeHTML(value); return ret; }, {}), }); @@ -116,7 +118,18 @@ export const sendNoWrap = ({ to, from, replyTo, subject, html, text, headers }) Meteor.defer(() => Email.send({ to, from, replyTo, subject, html, text, headers })); }; -export const send = ({ to, from, replyTo, subject, html, text, data, headers }) => sendNoWrap({ to, from, replyTo, subject: replace(subject, data), text: text ? replace(text, data) : stripHtml(replace(html, data)), html: wrap(html, data), headers }); +export const send = ({ to, from, replyTo, subject, html, text, data, headers }) => + sendNoWrap({ + to, + from, + replyTo, + subject: replace(subject, data), + text: text + ? replace(text, data) + : stripHtml(replace(html, data)), + html: wrap(html, data), + headers, + }); export const checkAddressFormatAndThrow = (from, func) => { if (checkAddressFormat(from)) { From 68d25770cd3ad9c16611c33bf0ce292ba34772ae Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Tue, 2 Jun 2020 19:05:32 -0300 Subject: [PATCH 018/154] [FIX] Link preview containing HTML encoded chars (#16512) --- app/oembed/client/oembedUrlWidget.html | 2 +- app/oembed/client/oembedUrlWidget.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/oembed/client/oembedUrlWidget.html b/app/oembed/client/oembedUrlWidget.html index 3d411cdebb372..4f09558bcecc7 100644 --- a/app/oembed/client/oembedUrlWidget.html +++ b/app/oembed/client/oembedUrlWidget.html @@ -25,7 +25,7 @@ {{{title}}} {{/if}} -
{{{description}}}
+
{{description}}
{{/if}} diff --git a/app/oembed/client/oembedUrlWidget.js b/app/oembed/client/oembedUrlWidget.js index 4304c3db216be..304f7a862867e 100644 --- a/app/oembed/client/oembedUrlWidget.js +++ b/app/oembed/client/oembedUrlWidget.js @@ -1,6 +1,7 @@ import { Blaze } from 'meteor/blaze'; import { Template } from 'meteor/templating'; import _ from 'underscore'; +import s from 'underscore.string'; const getTitle = function(self) { if (self.meta == null) { @@ -17,14 +18,14 @@ const getDescription = function(self) { if (description == null) { return; } - return _.unescape(description.replace(/(^[“\s]*)|([”\s]*$)/g, '')); + return s.unescapeHTML(description.replace(/(^[“\s]*)|([”\s]*$)/g, '')); }; Template.oembedUrlWidget.helpers({ description() { const description = getDescription(this); if (_.isString(description)) { - return Blaze._escape(description); + return description; } }, title() { From 62a7ffc24210b1d5b406e7e099077b916a67c87f Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Wed, 3 Jun 2020 17:01:44 -0300 Subject: [PATCH 019/154] Add Apps-Engine to Engine Versions on History (#17810) --- .houston/metadata.js | 18 ++++++++++++++++++ .houston/templates/versions.hbs | 3 +++ 2 files changed, 21 insertions(+) diff --git a/.houston/metadata.js b/.houston/metadata.js index 34de398bc857e..df78a54991e52 100644 --- a/.houston/metadata.js +++ b/.houston/metadata.js @@ -36,8 +36,24 @@ const getNodeNpmVersions = async function({ version, git, request }) { return {}; }; +const getAppsEngineVersion = async function({ version, git }) { + try { + const packageJson = await git.show([`${ version }:package-lock.json`]); + const { dependencies } = JSON.parse(packageJson); + const { version: appsEngineVersion } = dependencies['@rocket.chat/apps-engine']; + + return appsEngineVersion; + } catch (e) { + console.error(e); + } + + return undefined; +}; + module.exports = async function({ version, git, request }) { const mongo_versions = await getMongoVersion({ version, git }); + const apps_engine_version = await getAppsEngineVersion({ version, git }); + const { node_version, npm_version, @@ -46,6 +62,8 @@ module.exports = async function({ version, git, request }) { return { node_version, npm_version, + apps_engine_version, mongo_versions, }; }; + diff --git a/.houston/templates/versions.hbs b/.houston/templates/versions.hbs index 8ee2a4f985511..46da32d49ed4e 100644 --- a/.houston/templates/versions.hbs +++ b/.houston/templates/versions.hbs @@ -10,4 +10,7 @@ {{#if release.mongo_versions}} - MongoDB: `{{ join release.mongo_versions ', ' }}` {{/if}} +{{#if release.apps_engine_version}} +- Apps-Engine: `{{ release.apps_engine_version }}` +{{/if}} {{/if}} From bcf26aaf893970796e665ed20c28eb6e5f4a0d02 Mon Sep 17 00:00:00 2001 From: Bradley Hilton Date: Wed, 3 Jun 2020 15:02:31 -0500 Subject: [PATCH 020/154] Fix the update check not working (#17809) Co-authored-by: Aaron Ogle --- .../server/functions/checkVersionUpdate.js | 4 ++-- app/version-check/server/functions/getNewUpdates.js | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/version-check/server/functions/checkVersionUpdate.js b/app/version-check/server/functions/checkVersionUpdate.js index 0c6ab86de956c..1ea0e8b9c924f 100644 --- a/app/version-check/server/functions/checkVersionUpdate.js +++ b/app/version-check/server/functions/checkVersionUpdate.js @@ -44,7 +44,7 @@ export default () => { sendMessagesToAdmins({ msgs: ({ adminUser }) => [{ msg: `*${ TAPi18n.__('Update_your_RocketChat', adminUser.language) }*\n${ TAPi18n.__('New_version_available_(s)', update.lastestVersion.version, adminUser.language) }\n${ update.lastestVersion.infoUrl }` }], banners: [{ - id: 'versionUpdate', + id: `versionUpdate-${ update.lastestVersion.version }`.replace(/\./g, '_'), priority: 10, title: 'Update_your_RocketChat', text: 'New_version_available_(s)', @@ -62,7 +62,7 @@ export default () => { msg: `*${ TAPi18n.__('Rocket_Chat_Alert', adminUser.language) }:*\n\n*${ TAPi18n.__(alert.title, adminUser.language) }*\n${ TAPi18n.__(alert.text, ...alert.textArguments || [], adminUser.language) }\n${ alert.infoUrl }`, })), banners: alerts.map((alert) => ({ - id: `alert-${ alert.id }`, + id: `alert-${ alert.id }`.replace(/\./g, '_'), priority: 10, title: alert.title, text: alert.text, diff --git a/app/version-check/server/functions/getNewUpdates.js b/app/version-check/server/functions/getNewUpdates.js index f2f35cb622e62..45baffb0b33d3 100644 --- a/app/version-check/server/functions/getNewUpdates.js +++ b/app/version-check/server/functions/getNewUpdates.js @@ -2,7 +2,6 @@ import os from 'os'; import { HTTP } from 'meteor/http'; import { check, Match } from 'meteor/check'; -import { MongoInternals } from 'meteor/mongo'; import { Settings } from '../../../models'; import { Info } from '../../../utils'; @@ -11,14 +10,11 @@ import { getWorkspaceAccessToken } from '../../../cloud/server'; export default () => { try { const uniqueID = Settings.findOne('uniqueID'); - const { _oplogHandle } = MongoInternals.defaultRemoteCollectionDriver().mongo; - const oplogEnabled = _oplogHandle && _oplogHandle.onOplogEntry; const params = { uniqueId: uniqueID.value, - installedAt: uniqueID.createdAt, + installedAt: uniqueID.createdAt.toJSON(), version: Info.version, - oplogEnabled, osType: os.type(), osPlatform: os.platform(), osArch: os.arch(), @@ -40,7 +36,11 @@ export default () => { }); check(data, Match.ObjectIncluding({ - versions: [String], + versions: [Match.ObjectIncluding({ + version: Match.Optional(String), + security: Match.Optional(Boolean), + infoUrl: Match.Optional(String), + })], alerts: Match.Optional([Match.ObjectIncluding({ id: Match.Optional(String), title: Match.Optional(String), From 02645e4f61535ec9aca68e8432c9591ad4da1d3e Mon Sep 17 00:00:00 2001 From: Alan Sikora Date: Wed, 3 Jun 2020 17:40:07 -0300 Subject: [PATCH 021/154] [IMPROVE][Federation] Add support for _tcp and protocol DNS entries (#17818) --- app/federation/server/lib/dns.js | 49 +++++++++++++++++++++++++++---- app/federation/server/lib/http.js | 2 ++ 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/app/federation/server/lib/dns.js b/app/federation/server/lib/dns.js index b6a58ba21ff67..8163ca8d69dc5 100644 --- a/app/federation/server/lib/dns.js +++ b/app/federation/server/lib/dns.js @@ -29,15 +29,20 @@ export function registerWithHub(peerDomain, url, publicKey) { export function searchHub(peerDomain) { try { + logger.dns.debug(`searchHub: peerDomain=${ peerDomain }`); + // If there is no DNS entry for that, get from the Hub const { data: { peer } } = federationRequest('GET', `${ hubUrl }/api/v1/peers?search=${ peerDomain }`); if (!peer) { + logger.dns.debug(`searchHub: could not find peerDomain=${ peerDomain }`); throw federationErrors.peerCouldNotBeRegisteredWithHub('dns.registerWithHub'); } const { url, public_key: publicKey } = peer; + logger.dns.debug(`searchHub: found peerDomain=${ peerDomain } url=${ url }`); + return { url, peerDomain, @@ -55,13 +60,14 @@ export function search(peerDomain) { throw federationErrors.disabled('dns.search'); } - logger.dns.debug(`search: ${ peerDomain }`); + logger.dns.debug(`search: peerDomain=${ peerDomain }`); let srvEntries = []; let protocol = ''; // Search by HTTPS first try { + logger.dns.debug(`search: peerDomain=${ peerDomain } srv=_rocketchat._https.${ peerDomain }`); srvEntries = dnsResolveSRV(`_rocketchat._https.${ peerDomain }`); protocol = 'https'; } catch (err) { @@ -71,6 +77,7 @@ export function search(peerDomain) { // If there is not entry, try with http if (!srvEntries.length) { try { + logger.dns.debug(`search: peerDomain=${ peerDomain } srv=_rocketchat._http.${ peerDomain }`); srvEntries = dnsResolveSRV(`_rocketchat._http.${ peerDomain }`); protocol = 'http'; } catch (err) { @@ -78,24 +85,56 @@ export function search(peerDomain) { } } + // If there is not entry, try with tcp + if (!srvEntries.length) { + try { + logger.dns.debug(`search: peerDomain=${ peerDomain } srv=_rocketchat._tcp.${ peerDomain }`); + srvEntries = dnsResolveSRV(`_rocketchat._tcp.${ peerDomain }`); + protocol = 'https'; // https is the default + + // Then, also try to get the protocol + logger.dns.debug(`search: peerDomain=${ peerDomain } txt=rocketchat-tcp-protocol.${ peerDomain }`); + protocol = dnsResolveTXT(`rocketchat-tcp-protocol.${ peerDomain }`); + protocol = protocol[0].join(''); + + if (protocol !== 'http' && protocol !== 'https') { + protocol = null; + } + } catch (err) { + // if there is an error while getting the _tcp entry, it means the config is not there + // but if there is an error looking for the `_rocketchat_tcp_protocol` entry, it means we should use https + } + } + const [srvEntry] = srvEntries; // If there is no entry, throw error - if (!srvEntry) { + if (!srvEntry || !protocol) { + logger.dns.debug(`search: could not find valid SRV entry peerDomain=${ peerDomain } srvEntry=${ JSON.stringify(srvEntry) } protocol=${ protocol }`); return searchHub(peerDomain); } + let publicKey = null; + // Get the public key from the TXT record - const publicKeyTxtRecords = dnsResolveTXT(`rocketchat-public-key.${ peerDomain }`); + try { + logger.dns.debug(`search: peerDomain=${ peerDomain } txt=rocketchat-public-key.${ peerDomain }`); + const publicKeyTxtRecords = dnsResolveTXT(`rocketchat-public-key.${ peerDomain }`); - // Join the TXT record, that might be split - const publicKey = publicKeyTxtRecords[0].join(''); + // Join the TXT record, that might be split + publicKey = publicKeyTxtRecords[0].join(''); + } catch (err) { + // Ignore errors when looking for DNS entries + } // If there is no entry, throw error if (!publicKey) { + logger.dns.debug(`search: could not find TXT entry for peerDomain=${ peerDomain } - SRV entry found`); return searchHub(peerDomain); } + logger.dns.debug(`search: found peerDomain=${ peerDomain } srvEntry=${ srvEntry.name }:${ srvEntry.port } protocol=${ protocol }`); + return { url: `${ protocol }://${ srvEntry.name }:${ srvEntry.port }`, peerDomain, diff --git a/app/federation/server/lib/http.js b/app/federation/server/lib/http.js index 191e7a473ea3d..52cb314801636 100644 --- a/app/federation/server/lib/http.js +++ b/app/federation/server/lib/http.js @@ -37,6 +37,8 @@ export function federationRequestToPeer(method, peerDomain, uri, body, options = let result; try { + logger.http.debug(() => `federationRequestToPeer => url=${ baseUrl }${ uri }`); + result = federationRequest(method, `${ baseUrl }${ uri }`, body, options.headers || {}, peerKey); } catch (err) { logger.http.error(`${ ignoreErrors ? '[IGNORED] ' : '' }Error ${ err }`); From 46856e4878875e27f125e2738ea92324e4cef450 Mon Sep 17 00:00:00 2001 From: ishriom53tyagi <36675100+ishriom53tyagi@users.noreply.github.com> Date: Thu, 4 Jun 2020 06:18:37 +0530 Subject: [PATCH 022/154] [FIX] When the message is too long declining to send as an attachment does not restore the content into the composer (#16332) Co-authored-by: Danish Co-authored-by: Rodrigo Nascimento Co-authored-by: Guilherme Gazzo --- app/ui-utils/client/lib/modal.js | 6 +++--- app/ui/client/lib/chatMessages.js | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/ui-utils/client/lib/modal.js b/app/ui-utils/client/lib/modal.js index 759fef8c1367a..d46b8f8c4e7ff 100644 --- a/app/ui-utils/client/lib/modal.js +++ b/app/ui-utils/client/lib/modal.js @@ -70,6 +70,9 @@ const createModal = (config = {}, fn, onCancel) => { }, close: () => { + if (onCancel) { + onCancel.call(instance); + } instance.destroy(); modalStack = modalStack.filter((modal) => modal !== instance); if (modalStack.length) { @@ -89,9 +92,6 @@ const createModal = (config = {}, fn, onCancel) => { }, cancel: () => { - if (onCancel) { - onCancel.call(instance); - } instance.close(); }, diff --git a/app/ui/client/lib/chatMessages.js b/app/ui/client/lib/chatMessages.js index 59fcdc437b025..95e8c38d9d110 100644 --- a/app/ui/client/lib/chatMessages.js +++ b/app/ui/client/lib/chatMessages.js @@ -374,6 +374,7 @@ export class ChatMessages { const file = new File([messageBlob], fileName, { type: contentType, lastModified: Date.now() }); fileUpload([{ file, name: fileName }], this.input, { rid, tmid }); } catch (e) { + messageBoxState.set(this.input, msg); return true; } return true; From 50334ad51f37b363d8c315ac247ca8245c188be4 Mon Sep 17 00:00:00 2001 From: pierre-lehnen-rc <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Thu, 4 Jun 2020 19:00:30 -0300 Subject: [PATCH 023/154] [NEW] Assign oldest active user as owner when deleting last room owner (#16088) Co-authored-by: Diego Sampaio --- app/api/server/v1/users.js | 6 +- app/authorization/server/functions/hasRole.js | 2 + app/authorization/server/index.js | 3 +- app/lib/server/functions/deleteUser.js | 77 ++---------- .../functions/getRoomsWithSingleOwner.js | 60 +++++++++ .../functions/getUserSingleOwnedRooms.js | 25 ++++ app/lib/server/functions/index.js | 2 + .../functions/relinquishRoomOwnerships.js | 30 +++++ .../server/functions/setUserActiveStatus.js | 18 ++- .../server/methods/deleteUserOwnAccount.js | 6 +- app/models/server/models/Subscriptions.js | 9 ++ app/models/server/models/Users.js | 9 ++ app/ui-account/client/accountProfile.js | 55 ++++---- app/ui-flextab/client/tabs/userActions.js | 10 +- app/ui-utils/client/index.js | 1 + .../lib/warnUserDeletionMayRemoveRooms.js | 63 ++++++++++ client/admin/users/UserInfoActions.js | 119 ++++++++++++++++-- packages/rocketchat-i18n/i18n/en.i18n.json | 7 ++ server/methods/deleteUser.js | 6 +- server/methods/setUserActiveStatus.js | 4 +- 20 files changed, 396 insertions(+), 116 deletions(-) create mode 100644 app/lib/server/functions/getRoomsWithSingleOwner.js create mode 100644 app/lib/server/functions/getUserSingleOwnedRooms.js create mode 100644 app/lib/server/functions/relinquishRoomOwnerships.js create mode 100644 app/ui-utils/client/lib/warnUserDeletionMayRemoveRooms.js diff --git a/app/api/server/v1/users.js b/app/api/server/v1/users.js index 65aee384de9de..c252b743e8491 100644 --- a/app/api/server/v1/users.js +++ b/app/api/server/v1/users.js @@ -76,9 +76,10 @@ API.v1.addRoute('users.delete', { authRequired: true }, { } const user = this.getUserFromParams(); + const { confirmRelinquish = false } = this.requestParams(); Meteor.runAsUser(this.userId, () => { - Meteor.call('deleteUser', user._id); + Meteor.call('deleteUser', user._id, confirmRelinquish); }); return API.v1.success(); @@ -122,6 +123,7 @@ API.v1.addRoute('users.setActiveStatus', { authRequired: true }, { check(this.bodyParams, { userId: String, activeStatus: Boolean, + confirmRelinquish: Match.Maybe(Boolean), }); if (!hasPermission(this.userId, 'edit-other-user-active-status')) { @@ -129,7 +131,7 @@ API.v1.addRoute('users.setActiveStatus', { authRequired: true }, { } Meteor.runAsUser(this.userId, () => { - Meteor.call('setUserActiveStatus', this.bodyParams.userId, this.bodyParams.activeStatus); + Meteor.call('setUserActiveStatus', this.bodyParams.userId, this.bodyParams.activeStatus, this.bodyParams.confirmRelinquish); }); return API.v1.success({ user: Users.findOneById(this.bodyParams.userId, { fields: { active: 1 } }) }); }, diff --git a/app/authorization/server/functions/hasRole.js b/app/authorization/server/functions/hasRole.js index 0159026d46fed..e5d8927cbb747 100644 --- a/app/authorization/server/functions/hasRole.js +++ b/app/authorization/server/functions/hasRole.js @@ -6,3 +6,5 @@ export const hasRoleAsync = async (userId, roleNames, scope) => { }; export const hasRole = (userId, roleNames, scope) => Promise.await(hasRoleAsync(userId, roleNames, scope)); + +export const subscriptionHasRole = (sub, role) => sub.roles && sub.roles.includes(role); diff --git a/app/authorization/server/index.js b/app/authorization/server/index.js index 0ebc74f0ce772..485038ae3e233 100644 --- a/app/authorization/server/index.js +++ b/app/authorization/server/index.js @@ -12,7 +12,7 @@ import { hasAtLeastOnePermission, hasPermission, } from './functions/hasPermission'; -import { hasRole } from './functions/hasRole'; +import { hasRole, subscriptionHasRole } from './functions/hasRole'; import { removeUserFromRoles } from './functions/removeUserFromRoles'; import { AuthorizationUtils } from '../lib/AuthorizationUtils'; import './methods/addPermissionToRole'; @@ -28,6 +28,7 @@ export { getRoles, getUsersInRole, hasRole, + subscriptionHasRole, removeUserFromRoles, canSendMessage, addRoomAccessValidator, diff --git a/app/lib/server/functions/deleteUser.js b/app/lib/server/functions/deleteUser.js index ef39206c74bd4..6f4f4b16f5e0c 100644 --- a/app/lib/server/functions/deleteUser.js +++ b/app/lib/server/functions/deleteUser.js @@ -3,23 +3,14 @@ import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { FileUpload } from '../../../file-upload/server'; import { Users, Subscriptions, Messages, Rooms, Integrations, FederationServers } from '../../../models/server'; -import { hasRole, getUsersInRole } from '../../../authorization/server'; import { settings } from '../../../settings/server'; import { Notifications } from '../../../notifications/server'; import { updateGroupDMsName } from './updateGroupDMsName'; +import { relinquishRoomOwnerships } from './relinquishRoomOwnerships'; +import { getSubscribedRoomsForUserWithDetails, shouldRemoveOrChangeOwner } from './getRoomsWithSingleOwner'; +import { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms'; -const bulkRoomCleanUp = (rids) => { - // no bulk deletion for files - rids.forEach((rid) => FileUpload.removeFilesByRoomId(rid)); - - return Promise.await(Promise.all([ - Subscriptions.removeByRoomIds(rids), - Messages.removeByRoomIds(rids), - Rooms.removeByIds(rids), - ])); -}; - -export const deleteUser = function(userId) { +export const deleteUser = function(userId, confirmRelinquish = false) { const user = Users.findOneById(userId, { fields: { username: 1, avatarOrigin: 1, federation: 1 }, }); @@ -36,47 +27,16 @@ export const deleteUser = function(userId) { } } + const subscribedRooms = getSubscribedRoomsForUserWithDetails(userId); + + if (shouldRemoveOrChangeOwner(subscribedRooms) && !confirmRelinquish) { + const rooms = getUserSingleOwnedRooms(subscribedRooms); + throw new Meteor.Error('user-last-owner', '', rooms); + } + // Users without username can't do anything, so there is nothing to remove if (user.username != null) { - const roomCache = []; - - // Iterate through all the rooms the user is subscribed to, to check if they are the last owner of any of them. - Subscriptions.db.findByUserId(userId).forEach((subscription) => { - const roomData = { - rid: subscription.rid, - t: subscription.t, - subscribers: null, - }; - - // DMs can always be deleted, so let's ignore it on this check - if (roomData.t !== 'd') { - // If the user is an owner on this room - if (hasRole(user._id, 'owner', subscription.rid)) { - // Fetch the number of owners - const numOwners = getUsersInRole('owner', subscription.rid).fetch().length; - // If it's only one, then this user is the only owner. - if (numOwners === 1) { - // If the user is the last owner of a public channel, then we need to abort the deletion - if (roomData.t === 'c') { - throw new Meteor.Error('error-user-is-last-owner', `To delete this user you'll need to set a new owner to the following room: ${ subscription.name }.`, { - method: 'deleteUser', - }); - } - - // For private groups, let's check how many subscribers it has. If the user is the only subscriber, then it will be eliminated and doesn't need to abort the deletion - roomData.subscribers = Subscriptions.findByRoomId(subscription.rid).count(); - - if (roomData.subscribers > 1) { - throw new Meteor.Error('error-user-is-last-owner', `To delete this user you'll need to set a new owner to the following room: ${ subscription.name }.`, { - method: 'deleteUser', - }); - } - } - } - } - - roomCache.push(roomData); - }); + relinquishRoomOwnerships(userId, subscribedRooms); const messageErasureType = settings.get('Message_ErasureType'); switch (messageErasureType) { @@ -94,19 +54,6 @@ export const deleteUser = function(userId) { break; } - const roomIds = roomCache.filter((roomData) => { - if (roomData.subscribers === null && roomData.t !== 'd' && roomData.t !== 'c') { - roomData.subscribers = Subscriptions.findByRoomId(roomData.rid).count(); - } - - // Remove non-channel rooms with only 1 user (the one being deleted) - return roomData.t !== 'c' && roomData.subscribers === 1; - }).map(({ rid }) => rid); - - Rooms.find1On1ByUserId(user._id, { fields: { _id: 1 } }).forEach(({ _id }) => roomIds.push(_id)); - - bulkRoomCleanUp(roomIds); - Rooms.updateGroupDMsRemovingUsernamesByUsername(user.username); // Remove direct rooms with the user Rooms.removeDirectRoomContainingUsername(user.username); // Remove direct rooms with the user diff --git a/app/lib/server/functions/getRoomsWithSingleOwner.js b/app/lib/server/functions/getRoomsWithSingleOwner.js new file mode 100644 index 0000000000000..b966de6f95861 --- /dev/null +++ b/app/lib/server/functions/getRoomsWithSingleOwner.js @@ -0,0 +1,60 @@ +import { subscriptionHasRole } from '../../../authorization/server'; +import { Users, Subscriptions } from '../../../models/server'; + +export function shouldRemoveOrChangeOwner(subscribedRooms) { + return subscribedRooms + .some(({ shouldBeRemoved, shouldChangeOwner }) => shouldBeRemoved || shouldChangeOwner); +} + +export function getSubscribedRoomsForUserWithDetails(userId) { + const subscribedRooms = []; + + // Iterate through all the rooms the user is subscribed to, to check if he is the last owner of any of them. + Subscriptions.findByUserIdExceptType(userId, 'd').forEach((subscription) => { + const roomData = { + rid: subscription.rid, + t: subscription.t, + shouldBeRemoved: false, + shouldChangeOwner: false, + newOwner: null, + }; + + if (subscriptionHasRole(subscription, 'owner')) { + // Fetch the number of owners + const numOwners = Subscriptions.findByRoomIdAndRoles(subscription.rid, ['owner']).count(); + + // If it's only one, then this user is the only owner. + if (numOwners === 1) { + // Let's check how many subscribers the room has. + const options = { fields: { 'u._id': 1 }, sort: { ts: 1 } }; + const subscribersCursor = Subscriptions.findByRoomId(subscription.rid, options); + + subscribersCursor.forEach(({ u: { _id: uid } }) => { + // If we already changed the owner or this subscription is for the user we are removing, then don't try to give it ownership + if (roomData.shouldChangeOwner || uid === userId) { + return; + } + const newOwner = Users.findOneActiveById(uid, { fields: { _id: 1 } }); + if (!newOwner) { + return; + } + + roomData.newOwner = uid; + roomData.shouldChangeOwner = true; + }); + + // If there's no subscriber available to be the new owner and it's not a public room, we can remove it. + if (!roomData.shouldChangeOwner && roomData.t !== 'c') { + roomData.shouldBeRemoved = true; + } + } + } else if (roomData.t !== 'c') { + // If the user is not an owner, remove the room if the user is the only subscriber + roomData.shouldBeRemoved = Subscriptions.findByRoomId(roomData.rid).count() === 1; + } + + subscribedRooms.push(roomData); + }); + + return subscribedRooms; +} diff --git a/app/lib/server/functions/getUserSingleOwnedRooms.js b/app/lib/server/functions/getUserSingleOwnedRooms.js new file mode 100644 index 0000000000000..45d7b4824f852 --- /dev/null +++ b/app/lib/server/functions/getUserSingleOwnedRooms.js @@ -0,0 +1,25 @@ +import { Rooms } from '../../../models/server'; + +export const getUserSingleOwnedRooms = function(subscribedRooms) { + const roomsThatWillChangeOwner = subscribedRooms.filter(({ shouldChangeOwner }) => shouldChangeOwner).map(({ rid }) => rid); + const roomsThatWillBeRemoved = subscribedRooms.filter(({ shouldBeRemoved }) => shouldBeRemoved).map(({ rid }) => rid); + + const roomIds = roomsThatWillBeRemoved.concat(roomsThatWillChangeOwner); + const rooms = Rooms.findByIds(roomIds, { fields: { _id: 1, name: 1, fname: 1 } }); + + const result = { + shouldBeRemoved: [], + shouldChangeOwner: [], + }; + + rooms.forEach((room) => { + const name = room.fname || room.name; + if (roomsThatWillBeRemoved.includes(room._id)) { + result.shouldBeRemoved.push(name); + } else { + result.shouldChangeOwner.push(name); + } + }); + + return result; +}; diff --git a/app/lib/server/functions/index.js b/app/lib/server/functions/index.js index 7685736716489..ecb2881083d2e 100644 --- a/app/lib/server/functions/index.js +++ b/app/lib/server/functions/index.js @@ -12,12 +12,14 @@ export { deleteRoom } from './deleteRoom'; export { deleteUser } from './deleteUser'; export { getFullUserData } from './getFullUserData'; export { getRoomByNameOrIdWithOptionToJoin } from './getRoomByNameOrIdWithOptionToJoin'; +export { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms'; export { generateUsernameSuggestion } from './getUsernameSuggestion'; export { insertMessage } from './insertMessage'; export { isTheLastMessage } from './isTheLastMessage'; export { loadMessageHistory } from './loadMessageHistory'; export { processWebhookMessage } from './processWebhookMessage'; export { removeUserFromRoom } from './removeUserFromRoom'; +export { relinquishRoomOwnerships } from './relinquishRoomOwnerships'; export { saveCustomFields } from './saveCustomFields'; export { saveCustomFieldsWithoutValidation } from './saveCustomFieldsWithoutValidation'; export { saveUser } from './saveUser'; diff --git a/app/lib/server/functions/relinquishRoomOwnerships.js b/app/lib/server/functions/relinquishRoomOwnerships.js new file mode 100644 index 0000000000000..7c56e3bc05a52 --- /dev/null +++ b/app/lib/server/functions/relinquishRoomOwnerships.js @@ -0,0 +1,30 @@ +import { FileUpload } from '../../../file-upload/server'; +import { Subscriptions, Messages, Rooms, Roles } from '../../../models/server'; + +const bulkRoomCleanUp = (rids) => { + // no bulk deletion for files + rids.forEach((rid) => FileUpload.removeFilesByRoomId(rid)); + + return Promise.await(Promise.all([ + Subscriptions.removeByRoomIds(rids), + Messages.removeByRoomIds(rids), + Rooms.removeByIds(rids), + ])); +}; + +export const relinquishRoomOwnerships = function(userId, subscribedRooms, removeDirectMessages = true) { + // change owners + subscribedRooms + .filter(({ shouldChangeOwner }) => shouldChangeOwner) + .forEach(({ newOwner, rid }) => Roles.addUserRoles(newOwner, ['owner'], rid)); + + const roomIdsToRemove = subscribedRooms.filter(({ shouldBeRemoved }) => shouldBeRemoved).map(({ rid }) => rid); + + if (removeDirectMessages) { + Rooms.find1On1ByUserId(userId, { fields: { _id: 1 } }).forEach(({ _id }) => roomIdsToRemove.push(_id)); + } + + bulkRoomCleanUp(roomIdsToRemove); + + return subscribedRooms; +}; diff --git a/app/lib/server/functions/setUserActiveStatus.js b/app/lib/server/functions/setUserActiveStatus.js index 5a432d8cb769d..c85651250715f 100644 --- a/app/lib/server/functions/setUserActiveStatus.js +++ b/app/lib/server/functions/setUserActiveStatus.js @@ -1,11 +1,15 @@ +import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import { Accounts } from 'meteor/accounts-base'; import * as Mailer from '../../../mailer'; import { Users, Subscriptions } from '../../../models'; import { settings } from '../../../settings'; +import { relinquishRoomOwnerships } from './relinquishRoomOwnerships'; +import { shouldRemoveOrChangeOwner, getSubscribedRoomsForUserWithDetails } from './getRoomsWithSingleOwner'; +import { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms'; -export function setUserActiveStatus(userId, active) { +export function setUserActiveStatus(userId, active, confirmRelinquish = false) { check(userId, String); check(active, Boolean); @@ -15,6 +19,18 @@ export function setUserActiveStatus(userId, active) { return false; } + // Users without username can't do anything, so there is no need to check for owned rooms + if (user.username != null && !active) { + const subscribedRooms = getSubscribedRoomsForUserWithDetails(userId); + + if (shouldRemoveOrChangeOwner(subscribedRooms) && !confirmRelinquish) { + const rooms = getUserSingleOwnedRooms(subscribedRooms); + throw new Meteor.Error('user-last-owner', '', rooms); + } + + relinquishRoomOwnerships(user._id, subscribedRooms, false); + } + Users.setUserActive(userId, active); if (user.username) { diff --git a/app/lib/server/methods/deleteUserOwnAccount.js b/app/lib/server/methods/deleteUserOwnAccount.js index 0b5670abd25cd..4a655856ec571 100644 --- a/app/lib/server/methods/deleteUserOwnAccount.js +++ b/app/lib/server/methods/deleteUserOwnAccount.js @@ -8,7 +8,7 @@ import { Users } from '../../../models'; import { deleteUser } from '../functions'; Meteor.methods({ - deleteUserOwnAccount(password) { + deleteUserOwnAccount(password, confirmRelinquish) { check(password, String); if (!Meteor.userId()) { @@ -38,9 +38,7 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-username', 'Invalid username', { method: 'deleteUserOwnAccount' }); } - Meteor.defer(function() { - deleteUser(userId); - }); + deleteUser(userId, confirmRelinquish); return true; }, diff --git a/app/models/server/models/Subscriptions.js b/app/models/server/models/Subscriptions.js index e991c1d925a1c..1183865ab2881 100644 --- a/app/models/server/models/Subscriptions.js +++ b/app/models/server/models/Subscriptions.js @@ -472,6 +472,15 @@ export class Subscriptions extends Base { return this.find(query, options); } + findByUserIdExceptType(userId, typeException, options) { + const query = { + 'u._id': userId, + t: { $ne: typeException }, + }; + + return this.find(query, options); + } + findByUserIdAndType(userId, type, options) { const query = { 'u._id': userId, diff --git a/app/models/server/models/Users.js b/app/models/server/models/Users.js index 5f9374fac4a66..d5ef36e75ef1f 100644 --- a/app/models/server/models/Users.js +++ b/app/models/server/models/Users.js @@ -549,6 +549,15 @@ export class Users extends Base { return this.findOne(query, options); } + findOneActiveById(userId, options) { + const query = { + _id: userId, + active: true, + }; + + return this.findOne(query, options); + } + findOneByIdOrUsername(idOrUsername, options) { const query = { $or: [{ diff --git a/app/ui-account/client/accountProfile.js b/app/ui-account/client/accountProfile.js index 12f01b5b54b04..4fabb780af709 100644 --- a/app/ui-account/client/accountProfile.js +++ b/app/ui-account/client/accountProfile.js @@ -9,11 +9,11 @@ import _ from 'underscore'; import s from 'underscore.string'; import toastr from 'toastr'; -import { modal, SideNav, popover } from '../../ui-utils'; -import { t, handleError } from '../../utils'; -import { settings } from '../../settings'; -import { Notifications } from '../../notifications'; -import { callbacks } from '../../callbacks'; +import { modal, SideNav, warnUserDeletionMayRemoveRooms, popover } from '../../ui-utils/client'; +import { t, handleError } from '../../utils/client'; +import { settings } from '../../settings/client'; +import { Notifications } from '../../notifications/client'; +import { callbacks } from '../../callbacks/client'; import { getPopoverStatusConfig } from '../../ui/client'; const validateEmail = (email) => /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(email); @@ -492,6 +492,31 @@ Template.accountProfile.events({ 'click .js-delete-account'(e) { e.preventDefault(); const user = Meteor.user(); + + const deleteOwnAccount = (deleteConfirmation, confirmRelinquish = false) => { + Meteor.call('deleteUserOwnAccount', deleteConfirmation, confirmRelinquish, function(error) { + if (error) { + toastr.remove(); + + if (error.error === 'user-last-owner') { + const { shouldChangeOwner, shouldBeRemoved } = error.details; + warnUserDeletionMayRemoveRooms(user._id, () => deleteOwnAccount(deleteConfirmation, true), { + confirmButtonKey: 'Continue', + closeOnConfirm: true, + skipModalIfEmpty: true, + shouldChangeOwner, + shouldBeRemoved, + }); + return; + } + + modal.showInputError(t('Your_password_is_wrong')); + } else { + modal.close(); + } + }); + }; + if (s.trim(user && user.services && user.services.password && user.services.password.bcrypt)) { modal.open({ title: t('Are_you_sure_you_want_to_delete_your_account'), @@ -506,14 +531,8 @@ Template.accountProfile.events({ if (typedPassword) { toastr.remove(); toastr.warning(t('Please_wait_while_your_account_is_being_deleted')); - Meteor.call('deleteUserOwnAccount', SHA256(typedPassword), function(error) { - if (error) { - toastr.remove(); - modal.showInputError(t('Your_password_is_wrong')); - } else { - modal.close(); - } - }); + + deleteOwnAccount(SHA256(typedPassword)); } else { modal.showInputError(t('You_need_to_type_in_your_password_in_order_to_do_this')); return false; @@ -533,14 +552,8 @@ Template.accountProfile.events({ if (deleteConfirmation === (user && user.username)) { toastr.remove(); toastr.warning(t('Please_wait_while_your_account_is_being_deleted')); - Meteor.call('deleteUserOwnAccount', deleteConfirmation, function(error) { - if (error) { - toastr.remove(); - modal.showInputError(t('Your_password_is_wrong')); - } else { - modal.close(); - } - }); + + deleteOwnAccount(deleteConfirmation); } else { modal.showInputError(t('You_need_to_type_in_your_username_in_order_to_do_this')); return false; diff --git a/app/ui-flextab/client/tabs/userActions.js b/app/ui-flextab/client/tabs/userActions.js index fa2f4bc439bc9..deda9cbc7715b 100644 --- a/app/ui-flextab/client/tabs/userActions.js +++ b/app/ui-flextab/client/tabs/userActions.js @@ -6,11 +6,11 @@ import toastr from 'toastr'; import _ from 'underscore'; import { WebRTC } from '../../../webrtc/client'; -import { ChatRoom, ChatSubscription, RoomRoles, Subscriptions } from '../../../models'; -import { modal } from '../../../ui-utils'; +import { ChatRoom, ChatSubscription, RoomRoles, Subscriptions } from '../../../models/client'; +import { modal } from '../../../ui-utils/client'; import { t, handleError, roomTypes } from '../../../utils'; -import { settings } from '../../../settings'; -import { hasPermission, hasAllPermission, userHasAllPermission } from '../../../authorization'; +import { settings } from '../../../settings/client'; +import { hasPermission, hasAllPermission, userHasAllPermission } from '../../../authorization/client'; import { RoomMemberActions } from '../../../utils/client'; const canSetLeader = () => hasAllPermission('set-leader', Session.get('openedRoom')); @@ -442,6 +442,7 @@ export const getActions = ({ user, directActions, hideAdminControls }) => { this.editingUser.set(user._id); }), }, { + // deprecated, this action should not be called as this component is not used on admin pages anymore icon: 'trash', name: 'Delete', action: prevent(getUser, ({ _id }) => { @@ -502,6 +503,7 @@ export const getActions = ({ user, directActions, hideAdminControls }) => { ), }; }, () => { + // deprecated, this action should not be called as this component is not used on admin pages anymore if (hideAdminControls || !hasPermission('edit-other-user-active-status')) { return; } diff --git a/app/ui-utils/client/index.js b/app/ui-utils/client/index.js index 5dbb840cc4f90..612e01195e221 100644 --- a/app/ui-utils/client/index.js +++ b/app/ui-utils/client/index.js @@ -24,6 +24,7 @@ export { MessageTypes } from '../lib/MessageTypes'; export { alerts } from './lib/alerts'; export { Message } from '../lib/Message'; export { openRoom } from './lib/openRoom'; +export { warnUserDeletionMayRemoveRooms } from './lib/warnUserDeletionMayRemoveRooms'; export * from './lib/rtl'; export * from './lib/keyCodes'; export * from './lib/prependReplies'; diff --git a/app/ui-utils/client/lib/warnUserDeletionMayRemoveRooms.js b/app/ui-utils/client/lib/warnUserDeletionMayRemoveRooms.js new file mode 100644 index 0000000000000..31c36278c9969 --- /dev/null +++ b/app/ui-utils/client/lib/warnUserDeletionMayRemoveRooms.js @@ -0,0 +1,63 @@ +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; + +import { t } from '../../../utils/client'; +import { modal } from './modal'; + +export const warnUserDeletionMayRemoveRooms = async function(userId, callbackFn, { warningKey, confirmButtonKey, closeOnConfirm = false, skipModalIfEmpty = false, shouldChangeOwner, shouldBeRemoved }) { + let warningText = warningKey ? t(warningKey) : false; + + if (shouldBeRemoved.length + shouldChangeOwner.length === 0 && skipModalIfEmpty) { + callbackFn(); + return; + } + + if (shouldChangeOwner.length > 0) { + let newText; + + if (shouldChangeOwner.length === 1) { + newText = TAPi18n.__('A_new_owner_will_be_assigned_automatically_to_the__roomName__room', { roomName: shouldChangeOwner.pop() }); + } else if (shouldChangeOwner.length <= 5) { + newText = TAPi18n.__('A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__', { count: shouldChangeOwner.length, rooms: shouldChangeOwner.join(', ') }); + } else { + newText = TAPi18n.__('A_new_owner_will_be_assigned_automatically_to__count__rooms', { count: shouldChangeOwner.length }); + } + + if (warningText) { + warningText = `${ warningText }

 

${ newText }

`; + } else { + warningText = newText; + } + } + + if (shouldBeRemoved.length > 0) { + let newText; + + if (shouldBeRemoved.length === 1) { + newText = TAPi18n.__('The_empty_room__roomName__will_be_removed_automatically', { roomName: shouldBeRemoved.pop() }); + } else if (shouldBeRemoved.length <= 5) { + newText = TAPi18n.__('__count__empty_rooms_will_be_removed_automatically__rooms__', { count: shouldBeRemoved.length, rooms: shouldBeRemoved.join(', ') }); + } else { + newText = TAPi18n.__('__count__empty_rooms_will_be_removed_automatically', { count: shouldBeRemoved.length }); + } + + if (warningText) { + warningText = `${ warningText }

 

${ newText }

`; + } else { + warningText = newText; + } + } + + modal.open({ + title: t('Are_you_sure'), + text: warningText, + type: 'warning', + showCancelButton: true, + confirmButtonColor: '#DD6B55', + confirmButtonText: t(confirmButtonKey || 'Yes_delete_it'), + cancelButtonText: t('Cancel'), + closeOnConfirm, + html: true, + }, () => { + callbackFn(); + }); +}; diff --git a/client/admin/users/UserInfoActions.js b/client/admin/users/UserInfoActions.js index 7dd3551035f5e..31604c6be6ec9 100644 --- a/client/admin/users/UserInfoActions.js +++ b/client/admin/users/UserInfoActions.js @@ -6,9 +6,9 @@ import { useTranslation } from '../../contexts/TranslationContext'; import { useRoute } from '../../contexts/RouterContext'; import { usePermission } from '../../contexts/AuthorizationContext'; import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; -import { useMethod } from '../../contexts/ServerContext'; +import { useMethod, useEndpoint } from '../../contexts/ServerContext'; import { useSetting } from '../../contexts/SettingsContext'; -import { useEndpointAction } from '../../hooks/useEndpointAction'; +import RawText from '../../components/basic/RawText'; const DeleteWarningModal = ({ onDelete, onCancel, ...props }) => { @@ -33,6 +33,52 @@ const DeleteWarningModal = ({ onDelete, onCancel, ...props }) => { ; }; +const ConfirmOwnerChangeWarningModal = ({ onConfirm, onCancel, contentTitle = '', confirmLabel = '', shouldChangeOwner, shouldBeRemoved, ...props }) => { + const t = useTranslation(); + + let changeOwnerRooms = ''; + if (shouldChangeOwner.length > 0) { + if (shouldChangeOwner.length === 1) { + changeOwnerRooms = t('A_new_owner_will_be_assigned_automatically_to_the__roomName__room', { roomName: shouldChangeOwner.pop() }); + } else if (shouldChangeOwner.length <= 5) { + changeOwnerRooms = t('A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__', { count: shouldChangeOwner.length, rooms: shouldChangeOwner.join(', ') }); + } else { + changeOwnerRooms = t('A_new_owner_will_be_assigned_automatically_to__count__rooms', { count: shouldChangeOwner.length }); + } + } + + let removedRooms = ''; + if (shouldBeRemoved.length > 0) { + if (shouldBeRemoved.length === 1) { + removedRooms = t('The_empty_room__roomName__will_be_removed_automatically', { roomName: shouldBeRemoved.pop() }); + } else if (shouldBeRemoved.length <= 5) { + removedRooms = t('__count__empty_rooms_will_be_removed_automatically__rooms__', { count: shouldBeRemoved.length, rooms: shouldBeRemoved.join(', ') }); + } else { + removedRooms = t('__count__empty_rooms_will_be_removed_automatically', { count: shouldBeRemoved.length }); + } + } + + return + + + {t('Are_you_sure')} + + + + {contentTitle} + + { changeOwnerRooms && {changeOwnerRooms} } + { removedRooms && {removedRooms} } + + + + + + + + ; +}; + const SuccessModal = ({ onClose, ...props }) => { const t = useTranslation(); return @@ -67,20 +113,52 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange, .. const canEditOtherUserActiveStatus = usePermission('edit-other-user-active-status'); const canDeleteUser = usePermission('delete-user'); + const confirmOwnerChanges = (action, modalProps = {}) => async () => { + try { + return await action(); + } catch (error) { + if (error.xhr?.responseJSON?.errorType === 'user-last-owner') { + const { shouldChangeOwner, shouldBeRemoved } = error.xhr.responseJSON.details; + setModal( { + await action(true); + setModal(); + }} + onCancel={() => { setModal(); onChange(); }} + />); + return; + } + dispatchToastMessage({ type: 'error', message: error }); + } + }; + const deleteUserQuery = useMemo(() => ({ userId: _id }), [_id]); - const deleteUser = useEndpointAction('POST', 'users.delete', deleteUserQuery); + const deleteUserEndpoint = useEndpoint('POST', 'users.delete'); - const willDeleteUser = useCallback(async () => { - const result = await deleteUser(); + const erasureType = useSetting('Message_ErasureType'); + + const deleteUser = confirmOwnerChanges(async (confirm = false) => { + if (confirm) { + deleteUserQuery.confirmRelinquish = confirm; + } + + const result = await deleteUserEndpoint(deleteUserQuery); if (result.success) { setModal( { setModal(); onChange(); }}/>); } else { setModal(); } - }, [deleteUser]); + }, { + contentTitle: t(`Delete_User_Warning_${ erasureType }`), + confirmLabel: t('Delete'), + }); + const confirmDeleteUser = useCallback(() => { - setModal( setModal()}/>); - }, [deleteUser]); + setModal( setModal()}/>); + }, [deleteUserEndpoint]); const setAdminStatus = useMethod('setAdminStatus'); const changeAdminStatus = useCallback(() => { @@ -99,7 +177,25 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange, .. activeStatus: !isActive, }), [_id, isActive]); const changeActiveStatusMessage = isActive ? 'User_has_been_deactivated' : 'User_has_been_activated'; - const changeActiveStatus = useEndpointAction('POST', 'users.setActiveStatus', activeStatusQuery, t(changeActiveStatusMessage)); + const changeActiveStatusRequest = useEndpoint('POST', 'users.setActiveStatus'); + + const changeActiveStatus = confirmOwnerChanges(async (confirm = false) => { + if (confirm) { + activeStatusQuery.confirmRelinquish = confirm; + } + + try { + const result = await changeActiveStatusRequest(activeStatusQuery); + if (result.success) { + dispatchToastMessage({ type: 'success', message: t(changeActiveStatusMessage) }); + onChange(); + } + } catch (error) { + throw error; + } + }, { + confirmLabel: t('Yes_deactivate_it'), + }); const directMessageClick = () => directRoute.push({ rid: username, @@ -129,10 +225,7 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange, .. } }, ...canEditOtherUserActiveStatus && { changeActiveStatus: { label: <>{ isActive ? t('Deactivate') : t('Activate')}, - action: async () => { - const result = await changeActiveStatus(); - result.success ? onChange() : undefined; - }, + action: changeActiveStatus, } }, }), [canAssignAdminRole, canDeleteUser, canEditOtherUserActiveStatus, canEditOtherUserInfo, canDirectMessage, isActive, isAdmin]); diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 9eae064e5cf43..4fb6734518702 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -9,9 +9,14 @@ "2_Erros_Information_and_Debug": "2 - Errors, Information and Debug", "@username": "@username", "@username_message": "@username ", + "__count__empty_rooms_will_be_removed_automatically": "__count__ empty rooms will be removed automatically.", + "__count__empty_rooms_will_be_removed_automatically__rooms__": "__count__ empty rooms will be removed automatically:
__rooms__.", "__username__is_no_longer__role__defined_by__user_by_": "__username__ is no longer __role__ by __user_by__", "__username__was_set__role__by__user_by_": "__username__ was set __role__ by __user_by__", "%_of_conversations": "% of Conversations", + "A_new_owner_will_be_assigned_automatically_to__count__rooms": "A new owner will be assigned automatically to __count__ rooms.", + "A_new_owner_will_be_assigned_automatically_to_the__roomName__room": "A new owner will be assigned automatically to the __roomName__ room.", + "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "A new owner will be assigned automatically to those __count__ rooms:
__rooms__.", "Accept": "Accept", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Accept incoming omnichannel requests even if there are no online agents", "Accept_new_livechats_when_agent_is_idle": "Accept new omnichannel requests when the agent is idle", @@ -3318,6 +3323,7 @@ "The_application_name_is_required": "The application name is required", "The_channel_name_is_required": "The channel name is required", "The_emails_are_being_sent": "The emails are being sent.", + "The_empty_room__roomName__will_be_removed_automatically": "The empty room __roomName__ will be removed automatically.", "The_field_is_required": "The field %s is required.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "The image resize will not work because we can not detect ImageMagick or GraphicsMagick installed on your server.", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "The message is a discussion you will not be able to recover the messages!", @@ -3784,6 +3790,7 @@ "Yes_archive_it": "Yes, archive it!", "Yes_clear_all": "Yes, clear all!", "Yes_delete_it": "Yes, delete it!", + "Yes_deactivate_it": "Yes, deactivate it!", "Yes_hide_it": "Yes, hide it!", "Yes_leave_it": "Yes, leave it!", "Yes_mute_user": "Yes, mute user!", diff --git a/server/methods/deleteUser.js b/server/methods/deleteUser.js index f035d5b2f47ba..d8b0ce2e7bd32 100644 --- a/server/methods/deleteUser.js +++ b/server/methods/deleteUser.js @@ -3,10 +3,10 @@ import { check } from 'meteor/check'; import { Users } from '../../app/models'; import { hasPermission } from '../../app/authorization'; -import { deleteUser } from '../../app/lib'; +import { deleteUser } from '../../app/lib/server'; Meteor.methods({ - deleteUser(userId) { + deleteUser(userId, confirmRelinquish = false) { check(userId, String); if (!Meteor.userId()) { @@ -45,7 +45,7 @@ Meteor.methods({ }); } - deleteUser(userId); + deleteUser(userId, confirmRelinquish); return true; }, diff --git a/server/methods/setUserActiveStatus.js b/server/methods/setUserActiveStatus.js index d06e2aea0f3a1..adecec84054bf 100644 --- a/server/methods/setUserActiveStatus.js +++ b/server/methods/setUserActiveStatus.js @@ -5,7 +5,7 @@ import { hasPermission } from '../../app/authorization'; import { setUserActiveStatus } from '../../app/lib/server/functions/setUserActiveStatus'; Meteor.methods({ - setUserActiveStatus(userId, active) { + setUserActiveStatus(userId, active, confirmRelenquish) { check(userId, String); check(active, Boolean); @@ -21,7 +21,7 @@ Meteor.methods({ }); } - setUserActiveStatus(userId, active); + setUserActiveStatus(userId, active, confirmRelenquish); return true; }, From 2abe15d097aa842de98fbf0f2582185d0d549f38 Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Fri, 5 Jun 2020 12:05:29 -0300 Subject: [PATCH 024/154] [REGRESSION] Omnichannel visitor forward was applying wrong restrictions (#17826) * return empty object if there is no callback * Improve department forward restrictions --- .../methods/getDepartmentForwardRestrictions.js | 5 ++++- .../hooks/onLoadForwardDepartmentRestrictions.js | 11 ++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/livechat/server/methods/getDepartmentForwardRestrictions.js b/app/livechat/server/methods/getDepartmentForwardRestrictions.js index e08adc9792ba8..ffb1788bd1506 100644 --- a/app/livechat/server/methods/getDepartmentForwardRestrictions.js +++ b/app/livechat/server/methods/getDepartmentForwardRestrictions.js @@ -8,6 +8,9 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:getDepartmentForwardRestrictions' }); } - return callbacks.run('livechat.onLoadForwardDepartmentRestrictions', departmentId); + const options = callbacks.run('livechat.onLoadForwardDepartmentRestrictions', { departmentId }); + const { restrictions } = options; + + return restrictions; }, }); diff --git a/ee/app/livechat-enterprise/server/hooks/onLoadForwardDepartmentRestrictions.js b/ee/app/livechat-enterprise/server/hooks/onLoadForwardDepartmentRestrictions.js index 538ce151b30c9..baae278009b7a 100644 --- a/ee/app/livechat-enterprise/server/hooks/onLoadForwardDepartmentRestrictions.js +++ b/ee/app/livechat-enterprise/server/hooks/onLoadForwardDepartmentRestrictions.js @@ -1,17 +1,18 @@ import { callbacks } from '../../../../../app/callbacks'; import { LivechatDepartment } from '../../../../../app/models/server'; -callbacks.add('livechat.onLoadForwardDepartmentRestrictions', (departmentId) => { +callbacks.add('livechat.onLoadForwardDepartmentRestrictions', (options = {}) => { + const { departmentId } = options; if (!departmentId) { - return {}; + return options; } const department = LivechatDepartment.findOneById(departmentId, { fields: { departmentsAllowedToForward: 1 } }); if (!department) { - return {}; + return options; } const { departmentsAllowedToForward } = department; if (!departmentsAllowedToForward) { - return {}; + return options; } - return { _id: { $in: departmentsAllowedToForward } }; + return Object.assign({ restrictions: { _id: { $in: departmentsAllowedToForward } } }, options); }, callbacks.priority.MEDIUM, 'livechat-on-load-forward-department-restrictions'); From e01297c5a041a2c00e0714087ba5fd893baecad0 Mon Sep 17 00:00:00 2001 From: Maria Eduarda Cunha <42151808+mariaeduardacunha@users.noreply.github.com> Date: Fri, 5 Jun 2020 19:47:02 -0300 Subject: [PATCH 025/154] [FIX] Administration User page blank opening users without email (#17836) --- client/admin/users/UsersTable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/admin/users/UsersTable.js b/client/admin/users/UsersTable.js index 72bb1972fec8c..715fcff77ff76 100644 --- a/client/admin/users/UsersTable.js +++ b/client/admin/users/UsersTable.js @@ -96,7 +96,7 @@ export function UsersTable() { {mediaQuery && { username } } - {emails && emails[0].address} + {emails && emails.length && emails[0].address} {mediaQuery && {roles && roles.join(', ')}} {status} ; From 600dc0e87d3c332ca41afa0fabe3da25f31e863a Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 9 Jun 2020 08:39:55 -0300 Subject: [PATCH 026/154] Create codeql-analysis.yml --- .github/workflows/codeql-analysis.yml | 52 +++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000000000..a88b2a63f8137 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,52 @@ +name: "Code scanning - action" + +on: + push: + pull_request: + schedule: + - cron: '0 13 * * *' + +jobs: + CodeQL-Build: + + # CodeQL runs on ubuntu-latest and windows-latest + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + # Override language selection by uncommenting this and choosing your languages + with: + languages: javascript + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 From 682800766948ba16de004eb3378b0ff0ddc28dac Mon Sep 17 00:00:00 2001 From: lpilz Date: Tue, 9 Jun 2020 14:50:16 +0200 Subject: [PATCH 027/154] [FIX] Slack importer settings object (#17776) --- app/importer-slack/server/importer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/importer-slack/server/importer.js b/app/importer-slack/server/importer.js index a596bc6e79a8e..258af97b076aa 100644 --- a/app/importer-slack/server/importer.js +++ b/app/importer-slack/server/importer.js @@ -15,7 +15,7 @@ import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL'; import { Users, Rooms, Messages } from '../../models'; import { insertMessage, createDirectRoom } from '../../lib'; import { getValidRoomName } from '../../utils'; -import { settings } from '../../settings/lib/settings'; +import { settings } from '../../settings/server'; export class SlackImporter extends Base { constructor(info, importRecord) { From 6be2861f0cdc27788b5f0feccfdee4571f1bef24 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Tue, 9 Jun 2020 09:55:45 -0300 Subject: [PATCH 028/154] Refactor components and views to Storybook compatibility (#17800) --- .storybook/{mocks => }/decorators.js | 20 +- .storybook/hooks.js | 15 + .storybook/main.js | 1 + .storybook/preview.js | 10 +- .storybook/webpack.config.js | 4 +- app/2fa/server/code/EmailCheck.ts | 2 +- app/2fa/server/code/index.ts | 2 +- .../server/functions/hasPermission.js | 2 +- app/livechat/server/lib/stream/agentStatus.ts | 20 +- app/ui-message/client/blocks/MessageBlock.js | 3 +- app/ui-message/client/blocks/ModalBlock.js | 3 +- .../client/components/GenericTable.stories.js | 2 +- .../app/components/Directory/ChannelsTab.js | 4 +- .../cloud/ManualWorkspaceRegistrationModal.js | 6 +- client/admin/invites/InvitesPage.js | 2 +- client/admin/mailer/Mailer.js | 5 +- client/admin/settings/GroupPage.js | 2 +- client/admin/settings/GroupPage.stories.js | 6 - client/admin/settings/GroupSelector.js | 4 +- .../admin/settings/GroupSelector.stories.js | 4 - client/admin/settings/Section.js | 9 +- client/admin/settings/Section.stories.js | 4 - client/admin/settings/Setting.js | 9 +- client/admin/settings/Setting.stories.js | 11 +- client/admin/settings/SettingsState.js | 210 +-- .../admin/settings/groups/OAuthGroupPage.js | 5 +- client/admin/users/UserInfo.js | 2 +- client/admin/viewLogs/ViewLogs.js | 3 +- client/components/basic/BurgerMenuButton.css | 95 - client/components/basic/BurgerMenuButton.js | 38 - .../basic/BurgerMenuButton.stories.js | 46 - .../components/{setupWizard => basic}/Logo.js | 5 +- .../{setupWizard => basic}/Logo.stories.js | 4 +- client/components/basic/MarkdownText.js | 20 +- .../components/basic/MarkdownText.stories.js | 39 + client/components/basic/Page.js | 20 +- client/components/basic/Page.stories.js | 46 +- client/components/basic/burger/BurgerBadge.js | 15 + .../basic/burger/BurgerBadge.stories.js | 21 + client/components/basic/burger/BurgerIcon.js | 69 + .../basic/burger/BurgerIcon.stories.js | 22 + .../basic/burger/BurgerMenuButton.js | 27 + .../basic/burger/BurgerMenuButton.stories.js | 22 + .../connectionStatus/ConnectionStatusAlert.js | 4 +- .../ConnectionStatusAlert.stories.js | 70 +- .../pageNotFound/PageNotFound.stories.js | 10 - client/contexts/ModalContext.ts | 7 + client/contexts/PrivateSettingsContext.ts | 249 +++ client/fuselage-hooks.d.ts | 4 + client/hooks/useIsReducedMotionPreferred.js | 3 + client/hooks/useModal.js | 3 - client/providers/MeteorProvider.js | 25 +- client/providers/ModalProvider.js | 10 + client/routes.js | 4 +- .../notFound/NotFoundPage.js} | 8 +- client/views/notFound/NotFoundPage.stories.js | 10 + .../setupWizard/Pager.js | 0 .../setupWizard/Pager.stories.js | 2 +- .../setupWizard/SetupWizardPage.js | 2 +- .../setupWizard/SetupWizardPage.stories.js | 2 +- .../setupWizard/SetupWizardRoute.js | 0 .../setupWizard/SetupWizardState.js | 0 .../setupWizard/SideBar.css | 0 .../setupWizard/SideBar.js | 2 +- .../setupWizard/SideBar.stories.js | 2 +- .../setupWizard/Step.css | 0 .../{components => views}/setupWizard/Step.js | 0 .../setupWizard/StepHeader.js | 0 .../setupWizard/StepHeader.stories.js | 2 +- .../steps/AdminUserInformationStep.js | 0 .../steps/AdminUserInformationStep.stories.js | 2 +- .../setupWizard/steps/FinalStep.js | 0 .../setupWizard/steps/FinalStep.stories.js | 2 +- .../setupWizard/steps/RegisterServerStep.js | 0 .../steps/RegisterServerStep.stories.js | 2 +- .../setupWizard/steps/SettingsBasedStep.js | 0 .../steps/SettingsBasedStep.stories.js | 2 +- .../components/ChannelsTab/index.stories.js | 2 +- .../EngagementDashboardPage.stories.js | 2 +- .../components/MessagesTab/index.stories.js | 2 +- .../components/UsersTab/index.stories.js | 2 +- .../client/components/data/Counter.stories.js | 2 +- .../components/data/CounterSet.stories.js | 2 +- .../client/components/data/Growth.stories.js | 2 +- .../components/data/Histogram.stories.js | 2 +- .../components/data/LegendSymbol.stories.js | 2 +- .../data/NegativeGrowthSymbol.stories.js | 2 +- .../data/PositiveGrowthSymbol.stories.js | 2 +- ee/app/license/server/license.ts | 2 +- package-lock.json | 1554 +++++++++-------- package.json | 13 +- server/main.d.ts | 2 +- 92 files changed, 1578 insertions(+), 1301 deletions(-) rename .storybook/{mocks => }/decorators.js (53%) create mode 100644 .storybook/hooks.js delete mode 100644 client/components/basic/BurgerMenuButton.css delete mode 100644 client/components/basic/BurgerMenuButton.js delete mode 100644 client/components/basic/BurgerMenuButton.stories.js rename client/components/{setupWizard => basic}/Logo.js (97%) rename client/components/{setupWizard => basic}/Logo.stories.js (54%) create mode 100644 client/components/basic/MarkdownText.stories.js create mode 100644 client/components/basic/burger/BurgerBadge.js create mode 100644 client/components/basic/burger/BurgerBadge.stories.js create mode 100644 client/components/basic/burger/BurgerIcon.js create mode 100644 client/components/basic/burger/BurgerIcon.stories.js create mode 100644 client/components/basic/burger/BurgerMenuButton.js create mode 100644 client/components/basic/burger/BurgerMenuButton.stories.js delete mode 100644 client/components/pageNotFound/PageNotFound.stories.js create mode 100644 client/contexts/ModalContext.ts create mode 100644 client/contexts/PrivateSettingsContext.ts create mode 100644 client/fuselage-hooks.d.ts create mode 100644 client/hooks/useIsReducedMotionPreferred.js delete mode 100644 client/hooks/useModal.js create mode 100644 client/providers/ModalProvider.js rename client/{components/pageNotFound/PageNotFound.js => views/notFound/NotFoundPage.js} (91%) create mode 100644 client/views/notFound/NotFoundPage.stories.js rename client/{components => views}/setupWizard/Pager.js (100%) rename client/{components => views}/setupWizard/Pager.stories.js (94%) rename client/{components => views}/setupWizard/SetupWizardPage.js (95%) rename client/{components => views}/setupWizard/SetupWizardPage.stories.js (92%) rename client/{components => views}/setupWizard/SetupWizardRoute.js (100%) rename client/{components => views}/setupWizard/SetupWizardState.js (100%) rename client/{components => views}/setupWizard/SideBar.css (100%) rename client/{components => views}/setupWizard/SideBar.js (97%) rename client/{components => views}/setupWizard/SideBar.stories.js (96%) rename client/{components => views}/setupWizard/Step.css (100%) rename client/{components => views}/setupWizard/Step.js (100%) rename client/{components => views}/setupWizard/StepHeader.js (100%) rename client/{components => views}/setupWizard/StepHeader.stories.js (87%) rename client/{components => views}/setupWizard/steps/AdminUserInformationStep.js (100%) rename client/{components => views}/setupWizard/steps/AdminUserInformationStep.stories.js (87%) rename client/{components => views}/setupWizard/steps/FinalStep.js (100%) rename client/{components => views}/setupWizard/steps/FinalStep.stories.js (82%) rename client/{components => views}/setupWizard/steps/RegisterServerStep.js (100%) rename client/{components => views}/setupWizard/steps/RegisterServerStep.stories.js (87%) rename client/{components => views}/setupWizard/steps/SettingsBasedStep.js (100%) rename client/{components => views}/setupWizard/steps/SettingsBasedStep.stories.js (87%) diff --git a/.storybook/mocks/decorators.js b/.storybook/decorators.js similarity index 53% rename from .storybook/mocks/decorators.js rename to .storybook/decorators.js index 0c545d5c16111..db662ca230396 100644 --- a/.storybook/mocks/decorators.js +++ b/.storybook/decorators.js @@ -1,13 +1,13 @@ import React from 'react'; -import { MeteorProviderMock } from './providers'; +import { MeteorProviderMock } from './mocks/providers'; export const rocketChatDecorator = (fn) => { const linkElement = document.getElementById('theme-styles') || document.createElement('link'); if (linkElement.id !== 'theme-styles') { - require('../../app/theme/client/main.css'); - require('../../app/theme/client/vendor/fontello/css/fontello.css'); - require('../../app/theme/client/rocketchat.font.css'); + require('../app/theme/client/main.css'); + require('../app/theme/client/vendor/fontello/css/fontello.css'); + require('../app/theme/client/rocketchat.font.css'); linkElement.setAttribute('id', 'theme-styles'); linkElement.setAttribute('rel', 'stylesheet'); linkElement.setAttribute('href', 'https://open.rocket.chat/theme.css'); @@ -15,7 +15,7 @@ export const rocketChatDecorator = (fn) => { } // eslint-disable-next-line import/no-unresolved - const { default: icons } = require('!!raw-loader!../../private/public/icons.svg'); + const { default: icons } = require('!!raw-loader!../private/public/icons.svg'); return