From 14f819cf35f1c05242eb824c22996fda6da0f6d1 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Fri, 28 Jul 2017 14:50:48 -0300 Subject: [PATCH] Allow spaces on room names Closes #892 Closes #7488 --- .../client/startup/trackSettingsChange.js | 8 ++-- .../client/views/channelSettings.js | 33 ++++++++----- .../server/functions/saveRoomName.js | 34 ++++---------- packages/rocketchat-i18n/i18n/en.i18n.json | 5 +- .../rocketchat-lib/lib/getValidRoomName.js | 46 +++++++++++++++++++ packages/rocketchat-lib/package.js | 1 + .../server/functions/createRoom.js | 29 +++--------- .../rocketchat-lib/server/models/Rooms.js | 23 ++++++++-- .../server/models/Subscriptions.js | 4 +- .../rocketchat-lib/server/startup/settings.js | 4 ++ .../startup/defaultRoomTypes.js | 6 +++ .../client/chatRoomItem.js | 4 +- .../client/createCombinedFlex.js | 6 +-- .../rocketchat-ui/client/lib/RoomManager.js | 6 ++- server/publications/room.js | 1 + 15 files changed, 137 insertions(+), 73 deletions(-) create mode 100644 packages/rocketchat-lib/lib/getValidRoomName.js diff --git a/packages/rocketchat-channel-settings/client/startup/trackSettingsChange.js b/packages/rocketchat-channel-settings/client/startup/trackSettingsChange.js index d3c7a0bf084e..8df8addd789f 100644 --- a/packages/rocketchat-channel-settings/client/startup/trackSettingsChange.js +++ b/packages/rocketchat-channel-settings/client/startup/trackSettingsChange.js @@ -22,9 +22,11 @@ Meteor.startup(function() { Tracker.nonreactive(() => { if (msg.t === 'r') { if (Session.get('openedRoom') === msg.rid) { - const type = FlowRouter.current().route.name === 'channel' ? 'c' : 'p'; - RoomManager.close(type + FlowRouter.getParam('name')); - FlowRouter.go(FlowRouter.current().route.name, { name: msg.msg }, FlowRouter.current().queryParams); + const room = ChatRoom.findOne(msg.rid); + if (room.name !== FlowRouter.getParam('name')) { + RoomManager.close(room.t + FlowRouter.getParam('name')); + RocketChat.roomTypes.openRouteLink(room.t, room, FlowRouter.current().queryParams); + } } } }); diff --git a/packages/rocketchat-channel-settings/client/views/channelSettings.js b/packages/rocketchat-channel-settings/client/views/channelSettings.js index d24806ba20b2..d2ac089a722c 100644 --- a/packages/rocketchat-channel-settings/client/views/channelSettings.js +++ b/packages/rocketchat-channel-settings/client/views/channelSettings.js @@ -16,6 +16,9 @@ Template.channelSettings.helpers({ } return true; } + if (this.$value.getValue) { + return this.$value.getValue(obj, key); + } return obj && obj[key]; }, showSetting(setting, room) { @@ -152,22 +155,30 @@ Template.channelSettings.onCreated(function() { canEdit(room) { return RocketChat.authz.hasAllPermission('edit-room', room._id); }, + getValue(room) { + if (RocketChat.settings.get('UI_Allow_room_names_with_special_chars')) { + return room.fname || room.name; + } + return room.name; + }, save(value, room) { let nameValidation; if (!RocketChat.authz.hasAllPermission('edit-room', room._id) || (room.t !== 'c' && room.t !== 'p')) { return toastr.error(t('error-not-allowed')); } - try { - nameValidation = new RegExp(`^${ RocketChat.settings.get('UTF8_Names_Validation') }$`); - } catch (error1) { - nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$'); - } - if (!nameValidation.test(value)) { - return toastr.error(t('error-invalid-room-name', { - room_name: { - name: value - } - })); + if (!RocketChat.settings.get('UI_Allow_room_names_with_special_chars')) { + try { + nameValidation = new RegExp(`^${ RocketChat.settings.get('UTF8_Names_Validation') }$`); + } catch (error1) { + nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$'); + } + if (!nameValidation.test(value)) { + return toastr.error(t('error-invalid-room-name', { + room_name: { + name: value + } + })); + } } Meteor.call('saveRoomSettings', room._id, 'roomName', value, function(err) { if (err) { diff --git a/packages/rocketchat-channel-settings/server/functions/saveRoomName.js b/packages/rocketchat-channel-settings/server/functions/saveRoomName.js index 6eea873affd1..2696e1d5f079 100644 --- a/packages/rocketchat-channel-settings/server/functions/saveRoomName.js +++ b/packages/rocketchat-channel-settings/server/functions/saveRoomName.js @@ -1,35 +1,21 @@ -RocketChat.saveRoomName = function(rid, name, user, sendMessage = true) { +RocketChat.saveRoomName = function(rid, displayName, user, sendMessage = true) { const room = RocketChat.models.Rooms.findOneById(rid); if (room.t !== 'c' && room.t !== 'p') { throw new Meteor.Error('error-not-allowed', 'Not allowed', { - 'function': 'RocketChat.saveRoomName' + 'function': 'RocketChat.saveRoomdisplayName' }); } - let nameValidation; - try { - nameValidation = new RegExp(`^${ RocketChat.settings.get('UTF8_Names_Validation') }$`); - } catch (error) { - nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$'); - } - if (!nameValidation.test(name)) { - throw new Meteor.Error('error-invalid-room-name', `${ name } is not a valid room name. Use only letters, numbers, hyphens and underscores`, { - 'function': 'RocketChat.saveRoomName', - room_name: name - }); - } - if (name === room.name) { + if (displayName === room.name) { return; } - if (RocketChat.models.Rooms.findOneByName(name)) { - throw new Meteor.Error('error-duplicate-channel-name', `A channel with name '${ name }' exists`, { - 'function': 'RocketChat.saveRoomName', - channel_name: name - }); - } - const update = RocketChat.models.Rooms.setNameById(rid, name) && RocketChat.models.Subscriptions.updateNameAndAlertByRoomId(rid, name); + + const slugifiedRoomName = RocketChat.getValidRoomName(displayName, rid); + + const update = RocketChat.models.Rooms.setNameById(rid, slugifiedRoomName, displayName) && RocketChat.models.Subscriptions.updateNameAndAlertByRoomId(rid, slugifiedRoomName, displayName); + if (update && sendMessage) { - RocketChat.models.Messages.createRoomRenamedWithRoomIdRoomNameAndUser(rid, name, user); + RocketChat.models.Messages.createRoomRenamedWithRoomIdRoomNameAndUser(rid, displayName, user); } - return name; + return displayName; }; diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 5d66bc4721a7..99c8fd3ba016 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -531,7 +531,7 @@ "error-invalid-redirectUri": "Invalid redirectUri", "error-invalid-role": "Invalid role", "error-invalid-room": "Invalid room", - "error-invalid-room-name": "__room_name__ is not a valid room name,
use only letters, numbers, hyphens and underscores", + "error-invalid-room-name": "__room_name__ is not a valid room name", "error-invalid-room-type": "__type__ is not a valid room type.", "error-invalid-settings": "Invalid settings provided", "error-invalid-subscription": "Invalid subscription", @@ -772,7 +772,7 @@ "Invalid_name": "The name must not be empty", "Invalid_notification_setting_s": "Invalid notification setting: %s", "Invalid_pass": "The password must not be empty", - "Invalid_room_name": "%s is not a valid room name,
use only letters, numbers, hyphens and underscores", + "Invalid_room_name": "%s is not a valid room name", "Invalid_secret_URL_message": "The URL provided is invalid.", "Invalid_setting_s": "Invalid setting: %s", "Invalid_two_factor_code": "Invalid two factor code", @@ -1725,6 +1725,7 @@ "Type_your_message": "Type your message", "Type_your_name": "Type your name", "Type_your_new_password": "Type your new password", + "UI_Allow_room_names_with_special_chars": "Allow special chars on room names", "UI_DisplayRoles": "Display Roles", "UI_Merge_Channels_Groups": "Merge private groups with channels", "UI_Unread_Counter_Style": "Unread counter style", diff --git a/packages/rocketchat-lib/lib/getValidRoomName.js b/packages/rocketchat-lib/lib/getValidRoomName.js new file mode 100644 index 000000000000..149c01140891 --- /dev/null +++ b/packages/rocketchat-lib/lib/getValidRoomName.js @@ -0,0 +1,46 @@ +RocketChat.getValidRoomName = function getValidRoomName(displayName, rid = '') { + let slugifiedName = displayName; + + if (RocketChat.settings.get('UI_Allow_room_names_with_special_chars')) { + const room = RocketChat.models.Rooms.findOneByDisplayName(displayName); + if (room && room._id !== rid) { + if (room.archived) { + throw new Meteor.Error('error-archived-duplicate-name', `There's an archived channel with name ${ displayName }`, { function: 'RocketChat.getValidRoomName', channel_name: displayName }); + } else { + throw new Meteor.Error('error-duplicate-channel-name', `A channel with name '${ displayName }' exists`, { function: 'RocketChat.getValidRoomName', channel_name: displayName }); + } + } + slugifiedName = s.slugify(displayName); + } + + let nameValidation; + try { + nameValidation = new RegExp(`^${ RocketChat.settings.get('UTF8_Names_Validation') }$`); + } catch (error) { + nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$'); + } + if (!nameValidation.test(slugifiedName)) { + throw new Meteor.Error('error-invalid-room-name', `${ slugifiedName } is not a valid room name.`, { + 'function': 'RocketChat.getValidRoomName', + channel_name: slugifiedName + }); + } + + const room = RocketChat.models.Rooms.findOneByName(slugifiedName); + if (room && room._id !== rid) { + if (RocketChat.settings.get('UI_Allow_room_names_with_special_chars')) { + let tmpName = slugifiedName; + let next = 0; + while (RocketChat.models.Rooms.findOneByNameAndNotId(tmpName, rid)) { + tmpName = `${ slugifiedName }-${ ++next }`; + } + slugifiedName = tmpName; + } else if (room.archived) { + throw new Meteor.Error('error-archived-duplicate-name', `There's an archived channel with name ${ slugifiedName }`, { function: 'RocketChat.getValidRoomName', channel_name: slugifiedName }); + } else { + throw new Meteor.Error('error-duplicate-channel-name', `A channel with name '${ slugifiedName }' exists`, { function: 'RocketChat.getValidRoomName', channel_name: slugifiedName }); + } + } + + return slugifiedName; +}; diff --git a/packages/rocketchat-lib/package.js b/packages/rocketchat-lib/package.js index a9c823b9bf06..6b18ba7928e7 100644 --- a/packages/rocketchat-lib/package.js +++ b/packages/rocketchat-lib/package.js @@ -55,6 +55,7 @@ Package.onUse(function(api) { api.addFiles('lib/settings.js'); api.addFiles('lib/callbacks.js'); api.addFiles('lib/fileUploadRestrictions.js'); + api.addFiles('lib/getValidRoomName.js'); api.addFiles('lib/placeholders.js'); api.addFiles('lib/promises.js'); api.addFiles('lib/roomTypesCommon.js'); diff --git a/packages/rocketchat-lib/server/functions/createRoom.js b/packages/rocketchat-lib/server/functions/createRoom.js index 916c9871ba8b..5c9a176c66a8 100644 --- a/packages/rocketchat-lib/server/functions/createRoom.js +++ b/packages/rocketchat-lib/server/functions/createRoom.js @@ -13,36 +13,18 @@ RocketChat.createRoom = function(type, name, owner, members, readOnly, extraData throw new Meteor.Error('error-invalid-user', 'Invalid user', { function: 'RocketChat.createRoom' }); } - let nameValidation; - try { - nameValidation = new RegExp(`^${ RocketChat.settings.get('UTF8_Names_Validation') }$`); - } catch (error) { - nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$'); - } - - if (!nameValidation.test(name)) { - throw new Meteor.Error('error-invalid-name', 'Invalid name', { function: 'RocketChat.createRoom' }); - } + const slugifiedRoomName = RocketChat.getValidRoomName(name); const now = new Date(); if (!_.contains(members, owner.username)) { members.push(owner.username); } - // avoid duplicate names - let room = RocketChat.models.Rooms.findOneByName(name); - if (room) { - if (room.archived) { - throw new Meteor.Error('error-archived-duplicate-name', `There's an archived channel with name ${ name }`, { function: 'RocketChat.createRoom', room_name: name }); - } else { - throw new Meteor.Error('error-duplicate-channel-name', `A channel with name '${ name }' exists`, { function: 'RocketChat.createRoom', room_name: name }); - } - } - if (type === 'c') { RocketChat.callbacks.run('beforeCreateChannel', owner, { t: 'c', - name, + name: slugifiedRoomName, + fname: name, ts: now, ro: readOnly === true, sysMes: readOnly !== true, @@ -60,7 +42,7 @@ RocketChat.createRoom = function(type, name, owner, members, readOnly, extraData sysMes: readOnly !== true }); - room = RocketChat.models.Rooms.createWithTypeNameUserAndUsernames(type, name, owner, members, extraData); + const room = RocketChat.models.Rooms.createWithTypeNameUserAndUsernames(type, slugifiedRoomName, name, owner, members, extraData); for (const username of members) { const member = RocketChat.models.Users.findOneByUsername(username, { fields: { username: 1 }}); @@ -95,6 +77,7 @@ RocketChat.createRoom = function(type, name, owner, members, readOnly, extraData } return { - rid: room._id + rid: room._id, + name: slugifiedRoomName }; }; diff --git a/packages/rocketchat-lib/server/models/Rooms.js b/packages/rocketchat-lib/server/models/Rooms.js index 7cc89041c72b..cdcf1e14bbcf 100644 --- a/packages/rocketchat-lib/server/models/Rooms.js +++ b/packages/rocketchat-lib/server/models/Rooms.js @@ -37,6 +37,21 @@ class ModelRooms extends RocketChat.models._Base { return this.findOne(query, options); } + findOneByNameAndNotId(name, rid) { + const query = { + _id: { $ne: rid }, + name + }; + + return this.findOne(query); + } + + findOneByDisplayName(fname, options) { + const query = {fname}; + + return this.findOne(query, options); + } + findOneByNameAndType(name, type, options) { const query = { name, @@ -490,12 +505,13 @@ class ModelRooms extends RocketChat.models._Base { return this.update(query, update); } - setNameById(_id, name) { + setNameById(_id, name, fname) { const query = {_id}; const update = { $set: { - name + name, + fname } }; @@ -719,9 +735,10 @@ class ModelRooms extends RocketChat.models._Base { } // INSERT - createWithTypeNameUserAndUsernames(type, name, user, usernames, extraData) { + createWithTypeNameUserAndUsernames(type, name, fname, user, usernames, extraData) { const room = { name, + fname, t: type, usernames, msgs: 0, diff --git a/packages/rocketchat-lib/server/models/Subscriptions.js b/packages/rocketchat-lib/server/models/Subscriptions.js index 51cbc2524728..0643bddb734b 100644 --- a/packages/rocketchat-lib/server/models/Subscriptions.js +++ b/packages/rocketchat-lib/server/models/Subscriptions.js @@ -283,13 +283,14 @@ class ModelSubscriptions extends RocketChat.models._Base { return this.update(query, update); } - updateNameAndAlertByRoomId(roomId, name) { + updateNameAndAlertByRoomId(roomId, name, fname) { const query = {rid: roomId}; const update = { $set: { name, + fname, alert: true } }; @@ -543,6 +544,7 @@ class ModelSubscriptions extends RocketChat.models._Base { ts: room.ts, rid: room._id, name: room.name, + fname: room.fname, t: room.t, u: { _id: user._id, diff --git a/packages/rocketchat-lib/server/startup/settings.js b/packages/rocketchat-lib/server/startup/settings.js index 54ded7361d97..dbf12f6ee996 100644 --- a/packages/rocketchat-lib/server/startup/settings.js +++ b/packages/rocketchat-lib/server/startup/settings.js @@ -1057,6 +1057,10 @@ RocketChat.settings.addGroup('Layout', function() { ], 'public': true }); + this.add('UI_Allow_room_names_with_special_chars', false, { + type: 'boolean', + public: true + }); }); }); diff --git a/packages/rocketchat-lib/startup/defaultRoomTypes.js b/packages/rocketchat-lib/startup/defaultRoomTypes.js index 005d7739cd8c..2c1733bf472d 100644 --- a/packages/rocketchat-lib/startup/defaultRoomTypes.js +++ b/packages/rocketchat-lib/startup/defaultRoomTypes.js @@ -25,6 +25,9 @@ RocketChat.roomTypes.add('c', 10, { }, roomName(roomData) { + if (RocketChat.settings.get('UI_Allow_room_names_with_special_chars')) { + return roomData.fname || roomData.name; + } return roomData.name; }, @@ -114,6 +117,9 @@ RocketChat.roomTypes.add('p', 30, { }, roomName(roomData) { + if (RocketChat.settings.get('UI_Allow_room_names_with_special_chars')) { + return roomData.fname || roomData.name; + } return roomData.name; }, diff --git a/packages/rocketchat-ui-sidenav/client/chatRoomItem.js b/packages/rocketchat-ui-sidenav/client/chatRoomItem.js index 513ba1c970c6..453913f580d1 100644 --- a/packages/rocketchat-ui-sidenav/client/chatRoomItem.js +++ b/packages/rocketchat-ui-sidenav/client/chatRoomItem.js @@ -32,7 +32,9 @@ Template.chatRoomItem.helpers({ }, name() { - if (RocketChat.settings.get('UI_Use_Real_Name') && this.fname) { + const realNameForDirectMessages = RocketChat.settings.get('UI_Use_Real_Name') && this.t === 'd'; + const realNameForChannel = RocketChat.settings.get('UI_Allow_room_names_with_special_chars') && this.t !== 'd'; + if ((realNameForDirectMessages || realNameForChannel) && this.fname) { return this.fname; } diff --git a/packages/rocketchat-ui-sidenav/client/createCombinedFlex.js b/packages/rocketchat-ui-sidenav/client/createCombinedFlex.js index 4d4c16a39ab6..386af43b7801 100644 --- a/packages/rocketchat-ui-sidenav/client/createCombinedFlex.js +++ b/packages/rocketchat-ui-sidenav/client/createCombinedFlex.js @@ -111,7 +111,7 @@ Template.createCombinedFlex.events({ if (!err) { return Meteor.call(createRoute, name, instance.selectedUsers.get(), readOnly, function(err, result) { if (err) { - if (err.error === 'error-invalid-name') { + if (err.error === 'error-invalid-room-name') { instance.error.set({ invalid: true }); return; } @@ -130,10 +130,10 @@ Template.createCombinedFlex.events({ SideNav.closeFlex(() => instance.clearForm()); if (!privateGroup) { - RocketChat.callbacks.run('aftercreateCombined', { _id: result.rid, name }); + RocketChat.callbacks.run('aftercreateCombined', { _id: result.rid, name: result.name }); } - return FlowRouter.go(successRoute, { name }, FlowRouter.current().queryParams); + return FlowRouter.go(successRoute, { name: result.name }, FlowRouter.current().queryParams); }); } else { return instance.error.set({ fields: err }); diff --git a/packages/rocketchat-ui/client/lib/RoomManager.js b/packages/rocketchat-ui/client/lib/RoomManager.js index c7d5cac27516..32c998e0132f 100644 --- a/packages/rocketchat-ui/client/lib/RoomManager.js +++ b/packages/rocketchat-ui/client/lib/RoomManager.js @@ -49,8 +49,10 @@ const RoomManager = new function() { ].map(e => e.roles); msg.roles = _.union.apply(_.union, roles); ChatMessage.upsert({ _id: msg._id }, msg); - msg.t = typeName[0]; - msg.recipient = typeName.substr(1, typeName.length); + msg.room = { + type, + name + }; } msg.name = room.name; Meteor.defer(() => RoomManager.updateMentionsMarksOfRoom(typeName)); diff --git a/server/publications/room.js b/server/publications/room.js index 718b0dd64f88..aafa57e8e517 100644 --- a/server/publications/room.js +++ b/server/publications/room.js @@ -2,6 +2,7 @@ const options = { fields: { _id: 1, name: 1, + fname: 1, t: 1, cl: 1, u: 1,