diff --git a/app/livechat/client/views/app/livechatAppearance.js b/app/livechat/client/views/app/livechatAppearance.js
index 8cfdfefd6ca4..1b5477ec1fed 100644
--- a/app/livechat/client/views/app/livechatAppearance.js
+++ b/app/livechat/client/views/app/livechatAppearance.js
@@ -15,6 +15,9 @@ Template.livechatAppearance.helpers({
color() {
return Template.instance().color.get();
},
+ showAgentInfo() {
+ return Template.instance().showAgentInfo.get();
+ },
showAgentEmail() {
return Template.instance().showAgentEmail.get();
},
@@ -39,6 +42,16 @@ Template.livechatAppearance.helpers({
sampleOfflineSuccessMessage() {
return Template.instance().offlineSuccessMessage.get().replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1
$2');
},
+ showAgentInfoFormTrueChecked() {
+ if (Template.instance().showAgentInfo.get()) {
+ return 'checked';
+ }
+ },
+ showAgentInfoFormFalseChecked() {
+ if (!Template.instance().showAgentInfo.get()) {
+ return 'checked';
+ }
+ },
showAgentEmailFormTrueChecked() {
if (Template.instance().showAgentEmail.get()) {
return 'checked';
@@ -97,6 +110,7 @@ Template.livechatAppearance.onCreated(function() {
this.title = new ReactiveVar(null);
this.color = new ReactiveVar(null);
+ this.showAgentInfo = new ReactiveVar(null);
this.showAgentEmail = new ReactiveVar(null);
this.displayOfflineForm = new ReactiveVar(null);
this.offlineUnavailableMessage = new ReactiveVar(null);
@@ -119,6 +133,10 @@ Template.livechatAppearance.onCreated(function() {
const setting = LivechatAppearance.findOne('Livechat_title_color');
this.color.set(setting && setting.value);
});
+ this.autorun(() => {
+ const setting = LivechatAppearance.findOne('Livechat_show_agent_info');
+ this.showAgentInfo.set(setting && setting.value);
+ });
this.autorun(() => {
const setting = LivechatAppearance.findOne('Livechat_show_agent_email');
this.showAgentEmail.set(setting && setting.value);
@@ -193,6 +211,9 @@ Template.livechatAppearance.events({
const settingTitleColor = LivechatAppearance.findOne('Livechat_title_color');
instance.color.set(settingTitleColor && settingTitleColor.value);
+ const settingShowAgentInfo = LivechatAppearance.findOne('Livechat_show_agent_info');
+ instance.showAgentInfo.set(settingShowAgentInfo && settingShowAgentInfo.value);
+
const settingShowAgentEmail = LivechatAppearance.findOne('Livechat_show_agent_email');
instance.showAgentEmail.set(settingShowAgentEmail && settingShowAgentEmail.value);
@@ -240,6 +261,10 @@ Template.livechatAppearance.events({
_id: 'Livechat_title_color',
value: instance.color.get(),
},
+ {
+ _id: 'Livechat_show_agent_info',
+ value: instance.showAgentInfo.get(),
+ },
{
_id: 'Livechat_show_agent_email',
value: instance.showAgentEmail.get(),
diff --git a/app/livechat/imports/server/rest/departments.js b/app/livechat/imports/server/rest/departments.js
index 11b6c4b15e4b..a460f3d58b63 100644
--- a/app/livechat/imports/server/rest/departments.js
+++ b/app/livechat/imports/server/rest/departments.js
@@ -1,4 +1,4 @@
-import { check } from 'meteor/check';
+import { Match, check } from 'meteor/check';
import { API } from '../../../../api';
import { hasPermission } from '../../../../authorization';
@@ -23,7 +23,7 @@ API.v1.addRoute('livechat/department', { authRequired: true }, {
try {
check(this.bodyParams, {
department: Object,
- agents: Array,
+ agents: Match.Maybe(Array),
});
const department = Livechat.saveDepartment(null, this.bodyParams.department, this.bodyParams.agents);
@@ -76,26 +76,32 @@ API.v1.addRoute('livechat/department/:_id', { authRequired: true }, {
check(this.bodyParams, {
department: Object,
- agents: Array,
+ agents: Match.Maybe(Array),
+
});
+ const { _id } = this.urlParams;
+ const { department, agents } = this.bodyParams;
+
let success;
if (permissionToSave) {
- success = Livechat.saveDepartment(this.urlParams._id, this.bodyParams.department, this.bodyParams.agents);
- } else if (permissionToAddAgents) {
- success = Livechat.saveDepartmentAgents(this.urlParams._id, this.bodyParams.agents);
+ success = Livechat.saveDepartment(_id, department, agents);
+ }
+
+ if (success && agents && permissionToAddAgents) {
+ success = Livechat.saveDepartmentAgents(_id, agents);
}
if (success) {
return API.v1.success({
- department: LivechatDepartment.findOneById(this.urlParams._id),
- agents: LivechatDepartmentAgents.find({ departmentId: this.urlParams._id }).fetch(),
+ department: LivechatDepartment.findOneById(_id),
+ agents: LivechatDepartmentAgents.find({ departmentId: _id }).fetch(),
});
}
return API.v1.failure();
} catch (e) {
- return API.v1.failure(e.error);
+ return API.v1.failure(e);
}
},
delete() {
@@ -114,7 +120,7 @@ API.v1.addRoute('livechat/department/:_id', { authRequired: true }, {
return API.v1.failure();
} catch (e) {
- return API.v1.failure(e.error);
+ return API.v1.failure(e);
}
},
});
diff --git a/app/livechat/server/api/lib/livechat.js b/app/livechat/server/api/lib/livechat.js
index 22a42b065169..302cd5ceae13 100644
--- a/app/livechat/server/api/lib/livechat.js
+++ b/app/livechat/server/api/lib/livechat.js
@@ -2,9 +2,10 @@ import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
import _ from 'underscore';
-import { Users, LivechatRooms, LivechatVisitors, LivechatDepartment, LivechatTrigger } from '../../../../models';
+import { LivechatRooms, LivechatVisitors, LivechatDepartment, LivechatTrigger } from '../../../../models';
import { Livechat } from '../../lib/Livechat';
import { callbacks } from '../../../../callbacks/server';
+import { normalizeAgent } from '../../lib/Helper';
export function online() {
return Livechat.online();
@@ -80,7 +81,7 @@ export function getRoom({ guest, rid, roomInfo, agent }) {
}
export function findAgent(agentId) {
- return Users.getAgentInfo(agentId);
+ return normalizeAgent(agentId);
}
export function normalizeHttpHeaderData(headers = {}) {
@@ -109,6 +110,7 @@ export function settings() {
historyMonitorType: initSettings.Livechat_history_monitor_type,
forceAcceptDataProcessingConsent: initSettings.Livechat_force_accept_data_processing_consent,
showConnecting: initSettings.Livechat_Show_Connecting,
+ agentHiddenInfo: initSettings.Livechat_show_agent_info === false,
},
theme: {
title: initSettings.Livechat_title,
diff --git a/app/livechat/server/api/v1/config.js b/app/livechat/server/api/v1/config.js
index 50381262b21c..4ebdbc345e61 100644
--- a/app/livechat/server/api/v1/config.js
+++ b/app/livechat/server/api/v1/config.js
@@ -1,8 +1,7 @@
import { Match, check } from 'meteor/check';
-import { Users } from '../../../../models';
import { API } from '../../../../api';
-import { findGuest, settings, online, findOpenRoom, getExtraConfigInfo } from '../lib/livechat';
+import { findGuest, settings, online, findOpenRoom, getExtraConfigInfo, findAgent } from '../lib/livechat';
API.v1.addRoute('livechat/config', {
get() {
@@ -26,9 +25,8 @@ API.v1.addRoute('livechat/config', {
if (guest) {
room = findOpenRoom(token);
- agent = room && room.servedBy && Users.getAgentInfo(room.servedBy._id);
+ agent = room && room.servedBy && findAgent(room.servedBy._id);
}
-
const extraConfig = room && Promise.await(getExtraConfigInfo(room));
Object.assign(config, { online: status, guest, room, agent }, extraConfig);
diff --git a/app/livechat/server/api/v1/message.js b/app/livechat/server/api/v1/message.js
index d4e2ba3b8a03..9fb8ba33e02b 100644
--- a/app/livechat/server/api/v1/message.js
+++ b/app/livechat/server/api/v1/message.js
@@ -8,6 +8,7 @@ import { API } from '../../../../api';
import { loadMessageHistory } from '../../../../lib';
import { findGuest, findRoom, normalizeHttpHeaderData } from '../lib/livechat';
import { Livechat } from '../../lib/Livechat';
+import { normalizeMessageFileUpload } from '../../../../utils/server/functions/normalizeMessageFileUpload';
API.v1.addRoute('livechat/message', {
post() {
@@ -90,14 +91,18 @@ API.v1.addRoute('livechat/message/:_id', {
throw new Meteor.Error('invalid-room');
}
- const message = Messages.findOneById(_id);
+ let message = Messages.findOneById(_id);
if (!message) {
throw new Meteor.Error('invalid-message');
}
+ if (message.file) {
+ message = normalizeMessageFileUpload(message);
+ }
+
return API.v1.success({ message });
} catch (e) {
- return API.v1.failure(e.error);
+ return API.v1.failure(e);
}
},
@@ -133,13 +138,17 @@ API.v1.addRoute('livechat/message/:_id', {
const result = Livechat.updateMessage({ guest, message: { _id: msg._id, msg: this.bodyParams.msg } });
if (result) {
- const message = Messages.findOneById(_id);
+ let message = Messages.findOneById(_id);
+ if (message.file) {
+ message = normalizeMessageFileUpload(message);
+ }
+
return API.v1.success({ message });
}
return API.v1.failure();
} catch (e) {
- return API.v1.failure(e.error);
+ return API.v1.failure(e);
}
},
delete() {
@@ -183,7 +192,7 @@ API.v1.addRoute('livechat/message/:_id', {
return API.v1.failure();
} catch (e) {
- return API.v1.failure(e.error);
+ return API.v1.failure(e);
}
},
});
@@ -227,10 +236,12 @@ API.v1.addRoute('livechat/messages.history/:rid', {
limit = parseInt(this.queryParams.limit);
}
- const messages = loadMessageHistory({ userId: guest._id, rid, end, limit, ls });
- return API.v1.success(messages);
+ const messages = loadMessageHistory({ userId: guest._id, rid, end, limit, ls })
+ .messages
+ .map(normalizeMessageFileUpload);
+ return API.v1.success({ messages });
} catch (e) {
- return API.v1.failure(e.error);
+ return API.v1.failure(e);
}
},
});
diff --git a/app/livechat/server/config.js b/app/livechat/server/config.js
index 0dfb843f32f8..46c7f704ff28 100644
--- a/app/livechat/server/config.js
+++ b/app/livechat/server/config.js
@@ -79,7 +79,15 @@ Meteor.startup(function() {
});
settings.add('Livechat_allow_switching_departments', true, { type: 'boolean', group: 'Livechat', public: true, i18nLabel: 'Allow_switching_departments' });
- settings.add('Livechat_show_agent_email', true, { type: 'boolean', group: 'Livechat', public: true, i18nLabel: 'Show_agent_email' });
+ settings.add('Livechat_show_agent_info', true, {
+ type: 'boolean', group: 'Livechat', public: true, i18nLabel: 'Show_agent_info' });
+ settings.add('Livechat_show_agent_email', true, {
+ type: 'boolean',
+ group: 'Livechat',
+ public: true,
+ enableQuery: { _id: 'Livechat_show_agent_info', value: true },
+ i18nLabel: 'Show_agent_email',
+ });
settings.add('Livechat_request_comment_when_closing_conversation', true, {
type: 'boolean',
diff --git a/app/livechat/server/hooks/externalMessage.js b/app/livechat/server/hooks/externalMessage.js
index 2ef76fbb88f1..1e1f4ee242c6 100644
--- a/app/livechat/server/hooks/externalMessage.js
+++ b/app/livechat/server/hooks/externalMessage.js
@@ -5,6 +5,7 @@ import { settings } from '../../../settings';
import { callbacks } from '../../../callbacks';
import { SystemLogger } from '../../../logger';
import { LivechatExternalMessage } from '../../lib/LivechatExternalMessage';
+import { normalizeMessageFileUpload } from '../../../utils/server/functions/normalizeMessageFileUpload';
let knowledgeEnabled = false;
let apiaiKey = '';
@@ -33,6 +34,10 @@ callbacks.add('afterSaveMessage', function(message, room) {
return message;
}
+ if (message.file) {
+ message = normalizeMessageFileUpload(message);
+ }
+
// if the message hasn't a token, it was not sent by the visitor, so ignore it
if (!message.token) {
return message;
diff --git a/app/livechat/server/hooks/saveAnalyticsData.js b/app/livechat/server/hooks/saveAnalyticsData.js
index cfd5215e8162..4ca8832c153d 100644
--- a/app/livechat/server/hooks/saveAnalyticsData.js
+++ b/app/livechat/server/hooks/saveAnalyticsData.js
@@ -1,5 +1,6 @@
import { callbacks } from '../../../callbacks';
import { LivechatRooms } from '../../../models';
+import { normalizeMessageFileUpload } from '../../../utils/server/functions/normalizeMessageFileUpload';
callbacks.add('afterSaveMessage', function(message, room) {
// skips this callback if the message was edited
@@ -12,6 +13,9 @@ callbacks.add('afterSaveMessage', function(message, room) {
return message;
}
+ if (message.file) {
+ message = normalizeMessageFileUpload(message);
+ }
const now = new Date();
let analyticsData;
diff --git a/app/livechat/server/hooks/sendToCRM.js b/app/livechat/server/hooks/sendToCRM.js
index acbf777826a8..adaa1d039ea2 100644
--- a/app/livechat/server/hooks/sendToCRM.js
+++ b/app/livechat/server/hooks/sendToCRM.js
@@ -2,6 +2,7 @@ import { settings } from '../../../settings';
import { callbacks } from '../../../callbacks';
import { Messages, LivechatRooms } from '../../../models';
import { Livechat } from '../lib/Livechat';
+import { normalizeMessageFileUpload } from '../../../utils/server/functions/normalizeMessageFileUpload';
const msgNavType = 'livechat_navigation_history';
@@ -55,7 +56,12 @@ function sendToCRM(type, room, includeMessages = true) {
msg.navigation = message.navigation;
}
- postData.messages.push(msg);
+ if (message.file) {
+ msg.file = message.file;
+ msg.attachments = message.attachments;
+ }
+
+ postData.messages.push(normalizeMessageFileUpload(msg));
});
}
diff --git a/app/livechat/server/hooks/sendToFacebook.js b/app/livechat/server/hooks/sendToFacebook.js
index 30cafd9859a1..1af4767e3656 100644
--- a/app/livechat/server/hooks/sendToFacebook.js
+++ b/app/livechat/server/hooks/sendToFacebook.js
@@ -1,6 +1,7 @@
import { callbacks } from '../../../callbacks';
import { settings } from '../../../settings';
import OmniChannel from '../lib/OmniChannel';
+import { normalizeMessageFileUpload } from '../../../utils/server/functions/normalizeMessageFileUpload';
callbacks.add('afterSaveMessage', function(message, room) {
// skips this callback if the message was edited
@@ -27,6 +28,10 @@ callbacks.add('afterSaveMessage', function(message, room) {
return message;
}
+ if (message.file) {
+ message = normalizeMessageFileUpload(message);
+ }
+
OmniChannel.reply({
page: room.facebook.page.id,
token: room.v.token,
diff --git a/app/livechat/server/lib/Helper.js b/app/livechat/server/lib/Helper.js
index 7e47cb621f57..75eef3e4b40e 100644
--- a/app/livechat/server/lib/Helper.js
+++ b/app/livechat/server/lib/Helper.js
@@ -7,6 +7,7 @@ import { LivechatInquiry } from '../../lib/LivechatInquiry';
import { Livechat } from './Livechat';
import { RoutingManager } from './RoutingManager';
import { callbacks } from '../../../callbacks/server';
+import { settings } from '../../../settings';
export const createLivechatRoom = (rid, name, guest, extraData) => {
check(rid, String);
@@ -154,8 +155,17 @@ export const removeAgentFromSubscription = (rid, { _id, username }) => {
Messages.createUserLeaveWithRoomIdAndUser(rid, { _id, username });
};
+export const normalizeAgent = (agentId) => {
+ if (!agentId) {
+ return;
+ }
+
+ return settings.get('Livechat_show_agent_info') ? Users.getAgentInfo(agentId) : { hiddenInfo: true };
+};
+
export const dispatchAgentDelegated = (rid, agentId) => {
- const agent = agentId && Users.getAgentInfo(agentId);
+ const agent = normalizeAgent(agentId);
+
Livechat.stream.emit(rid, {
type: 'agentData',
data: agent,
diff --git a/app/livechat/server/lib/Livechat.js b/app/livechat/server/lib/Livechat.js
index 908b5b25ad26..6a9bca7bd8a3 100644
--- a/app/livechat/server/lib/Livechat.js
+++ b/app/livechat/server/lib/Livechat.js
@@ -29,10 +29,12 @@ import {
LivechatOfficeHour,
} from '../../../models';
import { Logger } from '../../../logger';
-import { sendMessage, deleteMessage, updateMessage } from '../../../lib';
import { addUserRoles, removeUserFromRoles } from '../../../authorization';
import * as Mailer from '../../../mailer';
import { LivechatInquiry } from '../../lib/LivechatInquiry';
+import { sendMessage } from '../../../lib/server/functions/sendMessage';
+import { updateMessage } from '../../../lib/server/functions/updateMessage';
+import { deleteMessage } from '../../../lib/server/functions/deleteMessage';
export const Livechat = {
Analytics,
@@ -367,6 +369,7 @@ export const Livechat = {
'Livechat_registration_form_message',
'Livechat_force_accept_data_processing_consent',
'Livechat_data_processing_consent_text',
+ 'Livechat_show_agent_info',
]).forEach((setting) => {
rcSettings[setting._id] = setting.value;
});
@@ -678,12 +681,12 @@ export const Livechat = {
check(departmentData, defaultValidations);
- check(departmentAgents, [
+ check(departmentAgents, Match.Maybe([
Match.ObjectIncluding({
agentId: String,
username: String,
}),
- ]);
+ ]));
if (_id) {
const department = LivechatDepartment.findOneById(_id);
@@ -836,6 +839,10 @@ export const Livechat = {
},
notifyAgentStatusChanged(userId, status) {
+ if (!settings.get('Livechat_show_agent_info')) {
+ return;
+ }
+
LivechatRooms.findOpenByAgent(userId).forEach((room) => {
Livechat.stream.emit(room._id, {
type: 'agentStatus',
diff --git a/app/livechat/server/methods/saveAppearance.js b/app/livechat/server/methods/saveAppearance.js
index 34eecd65e462..07445c9672dc 100644
--- a/app/livechat/server/methods/saveAppearance.js
+++ b/app/livechat/server/methods/saveAppearance.js
@@ -12,6 +12,7 @@ Meteor.methods({
const validSettings = [
'Livechat_title',
'Livechat_title_color',
+ 'Livechat_show_agent_info',
'Livechat_show_agent_email',
'Livechat_display_offline_form',
'Livechat_offline_form_unavailable',
diff --git a/app/livechat/server/methods/sendMessageLivechat.js b/app/livechat/server/methods/sendMessageLivechat.js
index 47c2dad856cd..b294d00f22c0 100644
--- a/app/livechat/server/methods/sendMessageLivechat.js
+++ b/app/livechat/server/methods/sendMessageLivechat.js
@@ -5,7 +5,7 @@ import { LivechatVisitors } from '../../../models';
import { Livechat } from '../lib/Livechat';
Meteor.methods({
- sendMessageLivechat({ token, _id, rid, msg, attachments }, agent) {
+ sendMessageLivechat({ token, _id, rid, msg, file, attachments }, agent) {
check(token, String);
check(_id, String);
check(rid, String);
@@ -36,6 +36,7 @@ Meteor.methods({
rid,
msg,
token,
+ file,
attachments,
},
agent,
diff --git a/app/livechat/server/publications/livechatAppearance.js b/app/livechat/server/publications/livechatAppearance.js
index 285d3cfefd0b..e9bc5ff1ef5a 100644
--- a/app/livechat/server/publications/livechatAppearance.js
+++ b/app/livechat/server/publications/livechatAppearance.js
@@ -17,6 +17,7 @@ Meteor.publish('livechat:appearance', function() {
$in: [
'Livechat_title',
'Livechat_title_color',
+ 'Livechat_show_agent_info',
'Livechat_show_agent_email',
'Livechat_display_offline_form',
'Livechat_offline_form_unavailable',
diff --git a/app/livechat/server/sendMessageBySMS.js b/app/livechat/server/sendMessageBySMS.js
index 1715cd6e1330..cbceda93baff 100644
--- a/app/livechat/server/sendMessageBySMS.js
+++ b/app/livechat/server/sendMessageBySMS.js
@@ -2,6 +2,7 @@ import { callbacks } from '../../callbacks';
import { settings } from '../../settings';
import { SMS } from '../../sms';
import { LivechatVisitors } from '../../models';
+import { normalizeMessageFileUpload } from '../../utils/server/functions/normalizeMessageFileUpload';
callbacks.add('afterSaveMessage', function(message, room) {
// skips this callback if the message was edited
@@ -28,6 +29,13 @@ callbacks.add('afterSaveMessage', function(message, room) {
return message;
}
+ let extraData;
+ if (message.file) {
+ message = normalizeMessageFileUpload(message);
+ const { fileUpload, rid, u: { _id: userId } = {} } = message;
+ extraData = Object.assign({}, { rid, userId, fileUpload });
+ }
+
const SMSService = SMS.getService(settings.get('SMS_Service'));
if (!SMSService) {
@@ -40,7 +48,7 @@ callbacks.add('afterSaveMessage', function(message, room) {
return message;
}
- SMSService.send(room.sms.from, visitor.phone[0].phoneNumber, message.msg);
+ SMSService.send(room.sms.from, visitor.phone[0].phoneNumber, message.msg, extraData);
return message;
}, callbacks.priority.LOW, 'sendMessageBySms');
diff --git a/app/meteor-accounts-saml/server/saml_rocketchat.js b/app/meteor-accounts-saml/server/saml_rocketchat.js
index 0d401b2c6d30..ba28f02a1139 100644
--- a/app/meteor-accounts-saml/server/saml_rocketchat.js
+++ b/app/meteor-accounts-saml/server/saml_rocketchat.js
@@ -88,18 +88,6 @@ Meteor.methods({
section: name,
i18nLabel: 'Accounts_OAuth_Custom_Button_Color',
});
- settings.add(`SAML_Custom_${ name }_email_field`, 'email', {
- type: 'string',
- group: 'SAML',
- section: name,
- i18nLabel: 'SAML_Custom_EMail_Field',
- });
- settings.add(`SAML_Custom_${ name }_username_field`, 'username', {
- type: 'string',
- group: 'SAML',
- section: name,
- i18nLabel: 'SAML_Custom_Username_Field',
- });
settings.add(`SAML_Custom_${ name }_generate_username`, false, {
type: 'boolean',
group: 'SAML',
@@ -160,6 +148,40 @@ Meteor.methods({
section: name,
i18nLabel: 'SAML_Custom_Authn_Context',
});
+ settings.add(`SAML_Custom_${ name }_user_data_fieldmap`, '{"username":"username", "email":"email", "cn": "name"}', {
+ type: 'string',
+ group: 'SAML',
+ section: name,
+ i18nLabel: 'SAML_Custom_user_data_fieldmap',
+ i18nDescription: 'SAML_Custom_user_data_fieldmap_description',
+ });
+ settings.add(`SAML_Custom_${ name }_authn_context_comparison`, 'exact', {
+ type: 'select',
+ values: [
+ { key: 'better', i18nLabel: 'Better' },
+ { key: 'exact', i18nLabel: 'Exact' },
+ { key: 'maximum', i18nLabel: 'Maximum' },
+ { key: 'minimum', i18nLabel: 'Minimum' },
+ ],
+ group: 'SAML',
+ section: name,
+ i18nLabel: 'SAML_Custom_Authn_Context_Comparison',
+ });
+
+ settings.add(`SAML_Custom_${ name }_default_user_role`, 'user', {
+ type: 'string',
+ group: 'SAML',
+ section: name,
+ i18nLabel: 'SAML_Default_User_Role',
+ i18nDescription: 'SAML_Default_User_Role_Description',
+ });
+ settings.add(`SAML_Custom_${ name }_role_attribute_name`, '', {
+ type: 'string',
+ group: 'SAML',
+ section: name,
+ i18nLabel: 'SAML_Role_Attribute_Name',
+ i18nDescription: 'SAML_Role_Attribute_Name_Description',
+ });
},
});
@@ -181,9 +203,7 @@ const getSamlConfigs = function(service) {
},
entryPoint: settings.get(`${ service.key }_entry_point`),
idpSLORedirectURL: settings.get(`${ service.key }_idp_slo_redirect_url`),
- usernameField: settings.get(`${ service.key }_username_field`),
usernameNormalize: settings.get(`${ service.key }_username_normalize`),
- emailField: settings.get(`${ service.key }_email_field`),
immutableProperty: settings.get(`${ service.key }_immutable_property`),
generateUsername: settings.get(`${ service.key }_generate_username`),
debug: settings.get(`${ service.key }_debug`),
@@ -192,12 +212,16 @@ const getSamlConfigs = function(service) {
issuer: settings.get(`${ service.key }_issuer`),
logoutBehaviour: settings.get(`${ service.key }_logout_behaviour`),
customAuthnContext: settings.get(`${ service.key }_custom_authn_context`),
+ authnContextComparison: settings.get(`${ service.key }_authn_context_comparison`),
+ defaultUserRole: settings.get(`${ service.key }_default_user_role`),
+ roleAttributeName: settings.get(`${ service.key }_role_attribute_name`),
secret: {
privateKey: settings.get(`${ service.key }_private_key`),
publicCert: settings.get(`${ service.key }_public_cert`),
// People often overlook the instruction to remove the header and footer of the certificate on this specific setting, so let's do it for them.
cert: normalizeCert(settings.get(`${ service.key }_cert`)),
},
+ userDataFieldMap: settings.get(`${ service.key }_user_data_fieldmap`),
};
};
@@ -227,10 +251,11 @@ const configureSamlService = function(samlConfigs) {
Accounts.saml.settings.nameOverwrite = samlConfigs.nameOverwrite;
Accounts.saml.settings.mailOverwrite = samlConfigs.mailOverwrite;
Accounts.saml.settings.immutableProperty = samlConfigs.immutableProperty;
- Accounts.saml.settings.emailField = samlConfigs.emailField;
- Accounts.saml.settings.usernameField = samlConfigs.usernameField;
+ Accounts.saml.settings.userDataFieldMap = samlConfigs.userDataFieldMap;
Accounts.saml.settings.usernameNormalize = samlConfigs.usernameNormalize;
Accounts.saml.settings.debug = samlConfigs.debug;
+ Accounts.saml.settings.defaultUserRole = samlConfigs.defaultUserRole;
+ Accounts.saml.settings.roleAttributeName = samlConfigs.roleAttributeName;
return {
provider: samlConfigs.clientConfig.provider,
@@ -241,6 +266,9 @@ const configureSamlService = function(samlConfigs) {
privateCert,
privateKey,
customAuthnContext: samlConfigs.customAuthnContext,
+ authnContextComparison: samlConfigs.authnContextComparison,
+ defaultUserRole: samlConfigs.defaultUserRole,
+ roleAttributeName: samlConfigs.roleAttributeName,
};
};
diff --git a/app/meteor-accounts-saml/server/saml_server.js b/app/meteor-accounts-saml/server/saml_server.js
index 1c13ebe75476..b1264df1b61f 100644
--- a/app/meteor-accounts-saml/server/saml_server.js
+++ b/app/meteor-accounts-saml/server/saml_server.js
@@ -105,15 +105,68 @@ Accounts.normalizeUsername = function(name) {
return name;
};
+function debugLog(content) {
+ if (Accounts.saml.settings.debug) {
+ console.log(content);
+ }
+}
+
+function getUserDataMapping() {
+ const { userDataFieldMap } = Accounts.saml.settings;
+
+ let map;
+
+ try {
+ map = JSON.parse(userDataFieldMap);
+ } catch (e) {
+ map = {};
+ }
+
+ let emailField = 'email';
+ let usernameField = 'username';
+ let nameField = 'cn';
+ const newMapping = {};
+
+ for (const field in map) {
+ if (!map.hasOwnProperty(field)) {
+ continue;
+ }
+
+ if (map[field] === 'email') {
+ emailField = field;
+ continue;
+ }
+
+ if (map[field] === 'username') {
+ usernameField = field;
+ continue;
+ }
+
+ if (map[field] === 'name') {
+ nameField = field;
+ continue;
+ }
+
+ newMapping[field] = map[field];
+ }
+
+ return { emailField, usernameField, nameField, userDataFieldMap: newMapping };
+}
+
+const guessNameFromUsername = (username) =>
+ username
+ .replace(/\W/g, ' ')
+ .replace(/\s(.)/g, (u) => u.toUpperCase())
+ .replace(/^(.)/, (u) => u.toLowerCase())
+ .replace(/^\w/, (u) => u.toUpperCase());
+
Accounts.registerLoginHandler(function(loginRequest) {
if (!loginRequest.saml || !loginRequest.credentialToken) {
return undefined;
}
const loginResult = Accounts.saml.retrieveCredential(loginRequest.credentialToken);
- if (Accounts.saml.settings.debug) {
- console.log(`RESULT :${ JSON.stringify(loginResult) }`);
- }
+ debugLog(`RESULT :${ JSON.stringify(loginResult) }`);
if (loginResult === undefined) {
return {
@@ -122,14 +175,15 @@ Accounts.registerLoginHandler(function(loginRequest) {
};
}
- const { emailField, usernameField } = Accounts.saml.settings;
+ const { emailField, usernameField, nameField, userDataFieldMap } = getUserDataMapping();
+ const { defaultUserRole = 'user', roleAttributeName } = Accounts.saml.settings;
- if (loginResult && loginResult.profile && loginResult.profile.email) {
+ if (loginResult && loginResult.profile && loginResult.profile[emailField]) {
const emailList = Array.isArray(loginResult.profile[emailField]) ? loginResult.profile[emailField] : [loginResult.profile[emailField]];
const emailRegex = new RegExp(emailList.map((email) => `^${ RegExp.escape(email) }$`).join('|'), 'i');
const eduPersonPrincipalName = loginResult.profile.eppn;
- const fullName = loginResult.profile.cn || loginResult.profile.displayName || loginResult.profile.username;
+ const fullName = loginResult.profile[nameField] || loginResult.profile.displayName || loginResult.profile.username;
let eppnMatch = false;
let user = null;
@@ -170,12 +224,19 @@ Accounts.registerLoginHandler(function(loginRequest) {
verified: true,
}));
+ let globalRoles;
+ if (roleAttributeName && loginResult.profile[roleAttributeName]) {
+ globalRoles = [].concat(loginResult.profile[roleAttributeName]);
+ } else {
+ globalRoles = [].concat(defaultUserRole.split(','));
+ }
+
if (!user) {
const newUser = {
name: fullName,
active: true,
eppn: eduPersonPrincipalName,
- globalRoles: ['user'],
+ globalRoles,
emails,
};
@@ -185,6 +246,7 @@ Accounts.registerLoginHandler(function(loginRequest) {
if (username) {
newUser.username = username;
+ newUser.name = newUser.name || guessNameFromUsername(username);
}
const userId = Accounts.insertUserDoc({}, newUser);
@@ -222,6 +284,17 @@ Accounts.registerLoginHandler(function(loginRequest) {
'services.saml': samlLogin,
};
+ for (const field in userDataFieldMap) {
+ if (!userDataFieldMap.hasOwnProperty(field)) {
+ continue;
+ }
+
+ if (loginResult.profile[field]) {
+ const rcField = userDataFieldMap[field];
+ updateData[`customFields.${ rcField }`] = loginResult.profile[field];
+ }
+ }
+
if (Accounts.saml.settings.immutableProperty !== 'EMail') {
updateData.emails = emails;
}
diff --git a/app/meteor-accounts-saml/server/saml_utils.js b/app/meteor-accounts-saml/server/saml_utils.js
index 37313596a222..7cd4ef696851 100644
--- a/app/meteor-accounts-saml/server/saml_utils.js
+++ b/app/meteor-accounts-saml/server/saml_utils.js
@@ -96,9 +96,10 @@ SAML.prototype.generateAuthorizeRequest = function(req) {
request += `
\n`;
}
+ const authnContextComparison = this.options.authnContextComparison || 'exact';
const authnContext = this.options.customAuthnContext || 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport';
request
- += '
'
+ += ``
+ `${ authnContext }\n`
+ '';
@@ -461,6 +462,30 @@ SAML.prototype.mapAttributes = function(attributeStatement, profile) {
}
};
+SAML.prototype.validateNotBeforeNotOnOrAfterAssertions = function(element) {
+ const now = new Date();
+
+ if (element.hasAttribute('NotBefore')) {
+ const notBefore = element.getAttribute('NotBefore');
+
+ const date = new Date(notBefore);
+ if (now < date) {
+ return false;
+ }
+ }
+
+ if (element.hasAttribute('NotOnOrAfter')) {
+ const notOnOrAfter = element.getAttribute('NotOnOrAfter');
+ const date = new Date(notOnOrAfter);
+
+ if (now >= date) {
+ return false;
+ }
+ }
+
+ return true;
+};
+
SAML.prototype.validateResponse = function(samlResponse, relayState, callback) {
const self = this;
const xml = new Buffer(samlResponse, 'base64').toString('utf8');
@@ -543,6 +568,19 @@ SAML.prototype.validateResponse = function(samlResponse, relayState, callback) {
profile.nameIDFormat = nameID.getAttribute('Format');
}
}
+
+ const subjectConfirmation = subject.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'SubjectConfirmation')[0];
+ if (subjectConfirmation) {
+ const subjectConfirmationData = subjectConfirmation.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'SubjectConfirmationData')[0];
+ if (subjectConfirmationData && !this.validateNotBeforeNotOnOrAfterAssertions(subjectConfirmationData)) {
+ return callback(new Error('NotBefore / NotOnOrAfter assertion failed'), null, false);
+ }
+ }
+ }
+
+ const conditions = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Conditions')[0];
+ if (conditions && !this.validateNotBeforeNotOnOrAfterAssertions(conditions)) {
+ return callback(new Error('NotBefore / NotOnOrAfter assertion failed'), null, false);
}
const authnStatement = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'AuthnStatement')[0];
diff --git a/app/metrics/server/lib/metrics.js b/app/metrics/server/lib/metrics.js
index 33bb2f8f12c6..32030be0b1b9 100644
--- a/app/metrics/server/lib/metrics.js
+++ b/app/metrics/server/lib/metrics.js
@@ -97,9 +97,9 @@ const setPrometheusData = async () => {
version: Info.version,
});
- const sessions = Object.values(Meteor.server.sessions);
+ const sessions = Array.from(Meteor.server.sessions.values());
const authenticatedSessions = sessions.filter((s) => s.userId);
- metrics.ddpSessions.set(sessions.length, date);
+ metrics.ddpSessions.set(Meteor.server.sessions.size, date);
metrics.ddpAthenticatedSessions.set(authenticatedSessions.length, date);
metrics.ddpConnectedUsers.set(_.unique(authenticatedSessions.map((s) => s.userId)).length, date);
diff --git a/app/models/server/models/ExportOperations.js b/app/models/server/models/ExportOperations.js
index 72c4be71ca1b..bb0f968b17c1 100644
--- a/app/models/server/models/ExportOperations.js
+++ b/app/models/server/models/ExportOperations.js
@@ -46,6 +46,14 @@ export class ExportOperations extends Base {
return this.find(query, options);
}
+ findOnePending(options) {
+ const query = {
+ status: { $nin: ['completed'] },
+ };
+
+ return this.findOne(query, options);
+ }
+
findAllPendingBeforeMyRequest(requestDay, options) {
const query = {
status: { $nin: ['completed'] },
@@ -63,6 +71,13 @@ export class ExportOperations extends Base {
status: data.status,
fileList: data.fileList,
generatedFile: data.generatedFile,
+ fileId: data.fileId,
+ userNameTable: data.userNameTable,
+ userData: data.userData,
+ generatedUserFile: data.generatedUserFile,
+ generatedAvatar: data.generatedAvatar,
+ exportPath: data.exportPath,
+ assetsPath: data.assetsPath,
},
};
@@ -78,7 +93,9 @@ export class ExportOperations extends Base {
_.extend(exportOperation, data);
- return this.insert(exportOperation);
+ this.insert(exportOperation);
+
+ return exportOperation._id;
}
diff --git a/app/models/server/models/LivechatDepartment.js b/app/models/server/models/LivechatDepartment.js
index db4c57679604..4b36c464cdcd 100644
--- a/app/models/server/models/LivechatDepartment.js
+++ b/app/models/server/models/LivechatDepartment.js
@@ -29,9 +29,15 @@ export class LivechatDepartment extends Base {
}
createOrUpdateDepartment(_id, data = {}, agents) {
- agents = [].concat(agents);
-
- const record = Object.assign(data, { numAgents: agents.length });
+ // We need to allow updating Departments without having to inform agents, so now we'll only
+ // update the agent/numAgents fields when the agent parameter is an Array, otherwise we skipp those fields
+ const hasAgents = agents && Array.isArray(agents);
+ const numAgents = hasAgents && agents.length;
+
+ const record = {
+ ...data,
+ ...hasAgents && { numAgents },
+ };
if (_id) {
this.update({ _id }, { $set: record });
@@ -39,23 +45,25 @@ export class LivechatDepartment extends Base {
_id = this.insert(record);
}
- const savedAgents = _.pluck(LivechatDepartmentAgents.findByDepartmentId(_id).fetch(), 'agentId');
- const agentsToSave = _.pluck(agents, 'agentId');
+ if (hasAgents) {
+ const savedAgents = _.pluck(LivechatDepartmentAgents.findByDepartmentId(_id).fetch(), 'agentId');
+ const agentsToSave = _.pluck(agents, 'agentId');
- // remove other agents
- _.difference(savedAgents, agentsToSave).forEach((agentId) => {
- LivechatDepartmentAgents.removeByDepartmentIdAndAgentId(_id, agentId);
- });
+ // remove other agents
+ _.difference(savedAgents, agentsToSave).forEach((agentId) => {
+ LivechatDepartmentAgents.removeByDepartmentIdAndAgentId(_id, agentId);
+ });
- agents.forEach((agent) => {
- LivechatDepartmentAgents.saveAgent({
- agentId: agent.agentId,
- departmentId: _id,
- username: agent.username,
- count: agent.count ? parseInt(agent.count) : 0,
- order: agent.order ? parseInt(agent.order) : 0,
+ agents.forEach((agent) => {
+ LivechatDepartmentAgents.saveAgent({
+ agentId: agent.agentId,
+ departmentId: _id,
+ username: agent.username,
+ count: agent.count ? parseInt(agent.count) : 0,
+ order: agent.order ? parseInt(agent.order) : 0,
+ });
});
- });
+ }
return _.extend(record, { _id });
}
diff --git a/app/models/server/models/ReadReceipts.js b/app/models/server/models/ReadReceipts.js
index 717f4d91afc4..d830f400669f 100644
--- a/app/models/server/models/ReadReceipts.js
+++ b/app/models/server/models/ReadReceipts.js
@@ -11,6 +11,10 @@ export class ReadReceipts extends Base {
}, {
unique: 1,
});
+
+ this.tryEnsureIndex({
+ messageId: 1,
+ });
}
findByMessageId(messageId) {
diff --git a/app/models/server/models/Subscriptions.js b/app/models/server/models/Subscriptions.js
index 93813847b88a..e886597f1fdb 100644
--- a/app/models/server/models/Subscriptions.js
+++ b/app/models/server/models/Subscriptions.js
@@ -11,16 +11,16 @@ export class Subscriptions extends Base {
constructor(...args) {
super(...args);
+ this.tryEnsureIndex({ rid: 1 });
+ this.tryEnsureIndex({ rid: 1, ls: 1 });
this.tryEnsureIndex({ rid: 1, 'u._id': 1 }, { unique: 1 });
+ this.tryEnsureIndex({ rid: 1, 'u._id': 1, open: 1 });
this.tryEnsureIndex({ rid: 1, 'u.username': 1 });
this.tryEnsureIndex({ rid: 1, alert: 1, 'u._id': 1 });
this.tryEnsureIndex({ rid: 1, roles: 1 });
this.tryEnsureIndex({ 'u._id': 1, name: 1, t: 1 });
this.tryEnsureIndex({ open: 1 });
this.tryEnsureIndex({ alert: 1 });
-
- this.tryEnsureIndex({ rid: 1, 'u._id': 1, open: 1 });
-
this.tryEnsureIndex({ ts: 1 });
this.tryEnsureIndex({ ls: 1 });
this.tryEnsureIndex({ audioNotifications: 1 }, { sparse: 1 });
@@ -807,6 +807,18 @@ export class Subscriptions extends Base {
return this.update(query, update, { multi: true });
}
+ updateFnameByRoomId(rid, fname) {
+ const query = { rid };
+
+ const update = {
+ $set: {
+ fname,
+ },
+ };
+
+ return this.update(query, update, { multi: true });
+ }
+
setUserUsernameByUserId(userId, username) {
const query = { 'u._id': userId };
diff --git a/app/push-notifications/client/views/pushNotificationsFlexTab.js b/app/push-notifications/client/views/pushNotificationsFlexTab.js
index 66c23dac6145..ab03487556b7 100644
--- a/app/push-notifications/client/views/pushNotificationsFlexTab.js
+++ b/app/push-notifications/client/views/pushNotificationsFlexTab.js
@@ -207,12 +207,9 @@ Template.pushNotificationsFlexTab.events({
if (value && value[0] !== 'none') {
const audioVolume = getUserPreference(user, 'notificationsSoundVolume');
- const $audio = $(`audio#${ value[0] }`);
-
- if ($audio && $audio[0] && $audio[0].play) {
- $audio[0].volume = Number((audioVolume / 100).toPrecision(2));
- $audio[0].play();
- }
+ CustomSounds.play(value[0], {
+ volume: Number((audioVolume / 100).toPrecision(2)),
+ });
}
},
diff --git a/app/retention-policy/server/cronPruneMessages.js b/app/retention-policy/server/cronPruneMessages.js
index cccb3c1f5297..ae96f940b97e 100644
--- a/app/retention-policy/server/cronPruneMessages.js
+++ b/app/retention-policy/server/cronPruneMessages.js
@@ -9,15 +9,15 @@ let types = [];
const oldest = new Date('0001-01-01T00:00:00Z');
-let lastPrune = oldest;
const maxTimes = {
c: 0,
p: 0,
d: 0,
};
-const toDays = 1000 * 60 * 60 * 24;
-const gracePeriod = 5000;
+
+const toDays = (d) => d * 1000 * 60 * 60 * 24;
+
function job() {
const now = new Date();
const filesOnly = settings.get('RetentionPolicy_FilesOnly');
@@ -27,11 +27,10 @@ function job() {
// get all rooms with default values
types.forEach((type) => {
const maxAge = maxTimes[type] || 0;
- const latest = new Date(now.getTime() - maxAge * toDays);
+ const latest = new Date(now.getTime() - toDays(maxAge));
Rooms.find({
t: type,
- _updatedAt: { $gte: latest },
$or: [
{ 'retention.enabled': { $eq: true } },
{ 'retention.enabled': { $exists: false } },
@@ -46,25 +45,23 @@ function job() {
'retention.enabled': { $eq: true },
'retention.overrideGlobal': { $eq: true },
'retention.maxAge': { $gte: 0 },
- _updatedAt: { $gte: lastPrune },
}).forEach((room) => {
const { maxAge = 30, filesOnly, excludePinned } = room.retention;
- const latest = new Date(now.getTime() - maxAge * toDays);
+ const latest = new Date(now.getTime() - toDays(maxAge));
cleanRoomHistory({ rid: room._id, latest, oldest, filesOnly, excludePinned, ignoreDiscussion });
});
- lastPrune = new Date(now.getTime() - gracePeriod);
}
function getSchedule(precision) {
switch (precision) {
case '0':
- return '0 */30 * * * *';
+ return '0 */30 * * * *'; // 30 minutes
case '1':
- return '0 0 * * * *';
+ return '0 0 * * * *'; // hour
case '2':
- return '0 0 */6 * * *';
+ return '0 0 */6 * * *'; // 6 hours
case '3':
- return '0 0 0 * * *';
+ return '0 0 0 * * *'; // day
}
}
@@ -84,26 +81,26 @@ function deployCron(precision) {
function reloadPolicy() {
types = [];
- if (settings.get('RetentionPolicy_Enabled')) {
- if (settings.get('RetentionPolicy_AppliesToChannels')) {
- types.push('c');
- }
+ if (!settings.get('RetentionPolicy_Enabled')) {
+ return SyncedCron.remove(pruneCronName);
+ }
+ if (settings.get('RetentionPolicy_AppliesToChannels')) {
+ types.push('c');
+ }
- if (settings.get('RetentionPolicy_AppliesToGroups')) {
- types.push('p');
- }
+ if (settings.get('RetentionPolicy_AppliesToGroups')) {
+ types.push('p');
+ }
- if (settings.get('RetentionPolicy_AppliesToDMs')) {
- types.push('d');
- }
+ if (settings.get('RetentionPolicy_AppliesToDMs')) {
+ types.push('d');
+ }
- maxTimes.c = settings.get('RetentionPolicy_MaxAge_Channels');
- maxTimes.p = settings.get('RetentionPolicy_MaxAge_Groups');
- maxTimes.d = settings.get('RetentionPolicy_MaxAge_DMs');
+ maxTimes.c = settings.get('RetentionPolicy_MaxAge_Channels');
+ maxTimes.p = settings.get('RetentionPolicy_MaxAge_Groups');
+ maxTimes.d = settings.get('RetentionPolicy_MaxAge_DMs');
- return deployCron(settings.get('RetentionPolicy_Precision'));
- }
- return SyncedCron.remove(pruneCronName);
+ return deployCron(settings.get('RetentionPolicy_Precision'));
}
Meteor.startup(function() {
diff --git a/app/search/client/search/search.html b/app/search/client/search/search.html
index 5f2bb57aeb38..dbb6c5a7f179 100644
--- a/app/search/client/search/search.html
+++ b/app/search/client/search/search.html
@@ -20,7 +20,7 @@
{{> icon block="rc-input__icon-svg" icon=provider.icon}}
-
+