Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 6.12.1 #33245

Merged
merged 5 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/bump-patch-1725994766358.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Bump @rocket.chat/meteor version.
5 changes: 5 additions & 0 deletions .changeset/four-cherries-kneel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Allow to use the token from `room.v` when requesting transcript instead of visitor token. Visitors may change their tokens at any time, rendering old conversations impossible to access for them (or for APIs depending on token) as the visitor token won't match the `room.v` token.
5 changes: 5 additions & 0 deletions .changeset/many-rules-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates)
5 changes: 5 additions & 0 deletions .changeset/pink-swans-teach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

fixed retention policy max age settings not being respected after upgrade
6 changes: 6 additions & 0 deletions .changeset/short-drinks-itch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/message-parser': patch
'@rocket.chat/peggy-loader': patch
---

Improved the performance of the message parser
28 changes: 21 additions & 7 deletions apps/meteor/app/authorization/server/functions/canDeleteMessage.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { IUser } from '@rocket.chat/core-typings';
import type { IUser, IRoom } from '@rocket.chat/core-typings';
import { Rooms } from '@rocket.chat/models';

import { getValue } from '../../../settings/server/raw';
import { canAccessRoomAsync } from './canAccessRoom';
import { hasPermissionAsync } from './hasPermission';

const elapsedTime = (ts: Date): number => {
Expand All @@ -13,6 +14,25 @@ export const canDeleteMessageAsync = async (
uid: string,
{ u, rid, ts }: { u: Pick<IUser, '_id' | 'username'>; rid: string; ts: Date },
): Promise<boolean> => {
const room = await Rooms.findOneById<Pick<IRoom, '_id' | 'ro' | 'unmuted' | 't' | 'teamId' | 'prid'>>(rid, {
projection: {
_id: 1,
ro: 1,
unmuted: 1,
t: 1,
teamId: 1,
prid: 1,
},
});

if (!room) {
return false;
}

if (!(await canAccessRoomAsync(room, { _id: uid }))) {
return false;
}

const forceDelete = await hasPermissionAsync(uid, 'force-delete-message', rid);

if (forceDelete) {
Expand Down Expand Up @@ -45,12 +65,6 @@ export const canDeleteMessageAsync = async (
}
}

const room = await Rooms.findOneById(rid, { projection: { ro: 1, unmuted: 1 } });

if (!room) {
return false;
}

if (room.ro === true && !(await hasPermissionAsync(uid, 'post-readonly', rid))) {
// Unless the user was manually unmuted
if (u.username && !(room.unmuted || []).includes(u.username)) {
Expand Down
17 changes: 7 additions & 10 deletions apps/meteor/app/livechat/server/lib/sendTranscript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
type IUser,
type MessageTypesValues,
type IOmnichannelSystemMessage,
type ILivechatVisitor,
isFileAttachment,
isFileImageAttachment,
} from '@rocket.chat/core-typings';
import colors from '@rocket.chat/fuselage-tokens/colors';
import { Logger } from '@rocket.chat/logger';
import { LivechatRooms, LivechatVisitors, Messages, Uploads, Users } from '@rocket.chat/models';
import { LivechatRooms, Messages, Uploads, Users } from '@rocket.chat/models';
import { check } from 'meteor/check';
import moment from 'moment-timezone';

Expand All @@ -22,7 +23,7 @@

const logger = new Logger('Livechat-SendTranscript');

export async function sendTranscript({

Check warning on line 26 in apps/meteor/app/livechat/server/lib/sendTranscript.ts

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

Async function 'sendTranscript' has a complexity of 32. Maximum allowed is 31
token,
rid,
email,
Expand All @@ -41,16 +42,12 @@

const room = await LivechatRooms.findOneById(rid);

const visitor = await LivechatVisitors.getVisitorByToken(token, {
projection: { _id: 1, token: 1, language: 1, username: 1, name: 1 },
});

if (!visitor) {
throw new Error('error-invalid-token');
const visitor = room?.v as ILivechatVisitor;
if (token !== visitor?.token) {
throw new Error('error-invalid-visitor');
}

// @ts-expect-error - Visitor typings should include language?
const userLanguage = visitor?.language || settings.get('Language') || 'en';
const userLanguage = settings.get<string>('Language') || 'en';
const timezone = getTimezone(user);
logger.debug(`Transcript will be sent using ${timezone} as timezone`);

Expand All @@ -59,7 +56,7 @@
}

// allow to only user to send transcripts from their own chats
if (room.t !== 'l' || !room.v || room.v.token !== token) {
if (room.t !== 'l') {
throw new Error('error-invalid-room');
}

Expand Down
38 changes: 36 additions & 2 deletions apps/meteor/app/otr/server/methods/updateOTRAck.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { api } from '@rocket.chat/core-services';
import type { IOTRMessage } from '@rocket.chat/core-typings';
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Rooms } from '@rocket.chat/models';
import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';

import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom';

declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
Expand All @@ -11,10 +15,40 @@ declare module '@rocket.chat/ddp-client' {
}

Meteor.methods<ServerMethods>({
updateOTRAck({ message, ack }) {
if (!Meteor.userId()) {
async updateOTRAck({ message, ack }) {
const uid = Meteor.userId();
if (!uid) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'updateOTRAck' });
}

check(ack, String);
check(message, {
_id: String,
rid: String,
msg: String,
t: String,
ts: Date,
u: {
_id: String,
username: String,
name: String,
},
});

if (message?.t !== 'otr') {
throw new Meteor.Error('error-invalid-message', 'Invalid message type', { method: 'updateOTRAck' });
}

const room = await Rooms.findOneByIdAndType(message.rid, 'd', { projection: { t: 1, _id: 1, uids: 1 } });

if (!room) {
throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'updateOTRAck' });
}

if (!(await canAccessRoomAsync(room, { _id: uid })) || (room.uids && (!message.u._id || !room.uids.includes(message.u._id)))) {
throw new Meteor.Error('error-invalid-user', 'Invalid user, not in room', { method: 'updateOTRAck' });
}

const acknowledgeMessage: IOTRMessage = { ...message, otrAck: ack };
void api.broadcast('otrAckUpdate', { roomId: message.rid, acknowledgeMessage });
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { Box, Callout, Chip, Margins } from '@rocket.chat/fuselage';
import { ExternalLink } from '@rocket.chat/ui-client';
import type { TranslationKey } from '@rocket.chat/ui-contexts';
import { useTranslation } from '@rocket.chat/ui-contexts';
import DOMPurify from 'dompurify';
import React from 'react';

import ScreenshotCarouselAnchor from '../../../components/ScreenshotCarouselAnchor';
import type { AppInfo } from '../../../definitions/AppInfo';
import { purifyOptions } from '../../../lib/purifyOptions';
import AppDetailsAPIs from './AppDetailsAPIs';
import { normalizeUrl } from './normalizeUrl';

Expand Down Expand Up @@ -61,7 +63,14 @@ const AppDetails = ({ app }: AppDetailsProps) => {
<Box fontScale='h4' mbe={8} color='titles-labels'>
{t('Description')}
</Box>
<Box dangerouslySetInnerHTML={{ __html: isMarkdown ? detailedDescription.rendered : description }} withRichContent />
<Box
dangerouslySetInnerHTML={{
__html: isMarkdown
? DOMPurify.sanitize(detailedDescription.rendered, purifyOptions)
: DOMPurify.sanitize(description, purifyOptions),
}}
withRichContent
/>
</Box>

<Box is='section'>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Accordion, Box } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import DOMPurify from 'dompurify';
import type { ReactElement } from 'react';
import React from 'react';

import { useTimeAgo } from '../../../../../hooks/useTimeAgo';
import { purifyOptions } from '../../../lib/purifyOptions';

type IRelease = {
version: string;
Expand Down Expand Up @@ -36,7 +38,7 @@ const AppReleasesItem = ({ release, ...props }: ReleaseItemProps): ReactElement
return (
<Accordion.Item title={title} {...props}>
{release.detailedChangelog?.rendered ? (
<Box dangerouslySetInnerHTML={{ __html: release.detailedChangelog?.rendered }} color='default' />
<Box dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(release.detailedChangelog?.rendered, purifyOptions) }} color='default' />
) : (
<Box color='default'>{t('No_release_information_provided')}</Box>
)}
Expand Down
50 changes: 50 additions & 0 deletions apps/meteor/client/views/marketplace/lib/purifyOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
export const purifyOptions = {
ALLOWED_TAGS: [
'b',
'i',
'em',
'strong',
'br',
'p',
'ul',
'ol',
'li',
'article',
'aside',
'figure',
'section',
'summary',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'hgroup',
'div',
'hr',
'span',
'wbr',
'abbr',
'acronym',
'cite',
'code',
'dfn',
'figcaption',
'mark',
's',
'samp',
'sub',
'sup',
'var',
'time',
'q',
'del',
'ins',
'rp',
'rt',
'ruby',
'bdi',
'bdo',
],
};
4 changes: 4 additions & 0 deletions apps/meteor/server/models/raw/Rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,10 @@ export class RoomsRaw extends BaseRaw<IRoom> implements IRoomsModel {
return this.findOne(query, options);
}

findOneByIdAndType(roomId: IRoom['_id'], type: IRoom['t'], options: FindOptions<IRoom> = {}): Promise<IRoom | null> {
return this.findOne({ _id: roomId, t: type }, options);
}

setCallStatus(_id: IRoom['_id'], status: IRoom['callStatus']): Promise<UpdateResult> {
const query: Filter<IRoom> = {
_id,
Expand Down
14 changes: 12 additions & 2 deletions apps/meteor/server/startup/migrations/xrun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Settings } from '@rocket.chat/models';
import type { UpdateResult } from 'mongodb';

import { upsertPermissions } from '../../../app/authorization/server/functions/upsertPermissions';
import { settings } from '../../../app/settings/server';
import { migrateDatabase, onServerVersionChange } from '../../lib/migrations';
import { ensureCloudWorkspaceRegistered } from '../cloudRegistration';

Expand All @@ -23,22 +24,31 @@ const moveRetentionSetting = async () => {
{ _id: { $in: Array.from(maxAgeSettingMap.keys()) }, value: { $ne: -1 } },
{ projection: { _id: 1, value: 1 } },
).forEach(({ _id, value }) => {
if (!maxAgeSettingMap.has(_id)) {
const newSettingId = maxAgeSettingMap.get(_id);
if (!newSettingId) {
throw new Error(`moveRetentionSetting - Setting ${_id} equivalent does not exist`);
}

const newValue = convertDaysToMs(Number(value));

promises.push(
Settings.updateOne(
{
_id: maxAgeSettingMap.get(_id),
},
{
$set: {
value: convertDaysToMs(Number(value)),
value: newValue,
},
},
),
);

const currentCache = settings.getSetting(newSettingId);
if (!currentCache) {
return;
}
settings.set({ ...currentCache, value: newValue });
});

await Promise.all(promises);
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/tests/data/livechat/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ export const createLivechatRoom = async (visitorToken: string, extraRoomParams?:
return response.body.room;
};

export const createVisitor = (department?: string, visitorName?: string): Promise<ILivechatVisitor> =>
export const createVisitor = (department?: string, visitorName?: string, customEmail?: string): Promise<ILivechatVisitor> =>
new Promise((resolve, reject) => {
const token = getRandomVisitorToken();
const email = `${token}@${token}.com`;
const email = customEmail || `${token}@${token}.com`;
const phone = `${Math.floor(Math.random() * 10000000000)}`;
void request.get(api(`livechat/visitor/${token}`)).end((err: Error, res: DummyResponse<ILivechatVisitor>) => {
if (!err && res && res.body && res.body.visitor) {
Expand Down
21 changes: 21 additions & 0 deletions apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,27 @@ describe('LIVECHAT - Utils', () => {
.send({ token: visitor.token, rid: room._id, email: '[email protected]' });
expect(body).to.have.property('success', true);
});
it('should allow a visitor to get a transcript even if token changed by using an old token that matches room.v', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
await closeOmnichannelRoom(room._id);
const visitor2 = await createVisitor(undefined, undefined, visitor.visitorEmails?.[0].address);
const room2 = await createLivechatRoom(visitor2.token);
await closeOmnichannelRoom(room2._id);

expect(visitor.token !== visitor2.token).to.be.true;
const { body } = await request
.post(api('livechat/transcript'))
.set(credentials)
.send({ token: visitor.token, rid: room._id, email: '[email protected]' });
expect(body).to.have.property('success', true);

const { body: body2 } = await request
.post(api('livechat/transcript'))
.set(credentials)
.send({ token: visitor2.token, rid: room2._id, email: '[email protected]' });
expect(body2).to.have.property('success', true);
});
});

describe('livechat/transcript/:rid', () => {
Expand Down
Loading
Loading