From 9ef096fd9cca048cdda31d41f113650e2c6a25a3 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 11 Sep 2024 13:42:29 -0300 Subject: [PATCH 1/6] chore: publish preview github pages (#33248) --- .github/workflows/ci-preview.yml | 44 +++++++++++++++++++++++ .gitignore | 2 ++ _templates/package/new/package.json.ejs.t | 1 + apps/uikit-playground/package.json | 3 +- turbo.json | 8 +++++ 5 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci-preview.yml diff --git a/.github/workflows/ci-preview.yml b/.github/workflows/ci-preview.yml new file mode 100644 index 000000000000..18b4b0bef373 --- /dev/null +++ b/.github/workflows/ci-preview.yml @@ -0,0 +1,44 @@ +# .github/workflows/ci-preview.yml +name: Deploy PR previews +concurrency: preview-${{ github.ref }} +on: + pull_request: + types: + - opened + - reopened + - synchronize + - closed + push: + branches: + - main + - master + - develop +jobs: + deploy-preview: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: rharkor/caching-for-turbo@v1.5 + if: github.event.action != 'closed' + + - name: Setup NodeJS + uses: ./.github/actions/setup-node + if: github.event.action != 'closed' + with: + node-version: 14.21.3 + cache-modules: true + install: true + + - name: Build + if: github.event.action != 'closed' + run: | + yarn turbo run build-preview + yarn turbo run .:build-preview-move + + - uses: rossjrw/pr-preview-action@v1 + with: + source-dir: .preview + preview-branch: gh-pages + umbrella-dir: pr-preview + action: auto diff --git a/.gitignore b/.gitignore index 4e6e4bb29da9..dbad2c29a22c 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,8 @@ yarn-error.log* .history .envrc +.preview + *.sublime-workspace **/.vim/ diff --git a/_templates/package/new/package.json.ejs.t b/_templates/package/new/package.json.ejs.t index 6bee52f55927..b2827ee3fb89 100644 --- a/_templates/package/new/package.json.ejs.t +++ b/_templates/package/new/package.json.ejs.t @@ -19,6 +19,7 @@ to: packages/<%= name %>/package.json "test": "jest", "build": "rm -rf dist && tsc -p tsconfig.json", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput" + "build-preview": "mkdir -p ../../.preview && cp -r ./dist ../../.preview/<%= name.toLowerCase() %>" }, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index d309c2d54599..233e71b719e5 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -5,7 +5,8 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build", + "build-preview": "tsc && vite build", + ".:build-preview-move": "mkdir -p ../../.preview/ && cp -r ./dist ../../.preview/uikit-playground", "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, diff --git a/turbo.json b/turbo.json index 7ce868eb3707..0ae903f9250d 100644 --- a/turbo.json +++ b/turbo.json @@ -2,6 +2,14 @@ "$schema": "https://turborepo.org/schema.json", "globalDependencies": ["tsconfig.base.json", "tsconfig.base.server.json", "tsconfig.base.client.json"], "tasks": { + "build-preview": { + "dependsOn": ["^build"], + "outputs": ["storybook-static/**", ".storybook/**", "dist/**"] + }, + ".:build-preview-move": { + "dependsOn": ["^build-preview"], + "cache": false + }, "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] From fa8ac7aeaf5403720c2004cbae93a2ef3df51b78 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Wed, 11 Sep 2024 15:22:23 -0300 Subject: [PATCH 2/6] fix: Infinite loading when uploading a private app (#33181) --- .changeset/wet-hats-walk.md | 5 +++++ .../client/views/marketplace/hooks/useInstallApp.tsx | 8 ++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 .changeset/wet-hats-walk.md diff --git a/.changeset/wet-hats-walk.md b/.changeset/wet-hats-walk.md new file mode 100644 index 000000000000..4c3565e75523 --- /dev/null +++ b/.changeset/wet-hats-walk.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed issue that caused an infinite loading state when uploading a private app to Rocket.Chat diff --git a/apps/meteor/client/views/marketplace/hooks/useInstallApp.tsx b/apps/meteor/client/views/marketplace/hooks/useInstallApp.tsx index f10d3e989e0a..3b784e24c375 100644 --- a/apps/meteor/client/views/marketplace/hooks/useInstallApp.tsx +++ b/apps/meteor/client/views/marketplace/hooks/useInstallApp.tsx @@ -114,8 +114,11 @@ export const useInstallApp = (file: File, url: string): { install: () => void; i }; const extractManifestFromAppFile = async (appFile: File) => { - const manifest = await getManifestFromZippedApp(appFile); - return manifest; + try { + return getManifestFromZippedApp(appFile); + } catch (error) { + handleInstallError(error as Error); + } }; const install = async () => { @@ -158,6 +161,7 @@ export const useInstallApp = (file: File, url: string): { install: () => void; i handleEnableUnlimitedApps={() => { openExternalLink(manageSubscriptionUrl); setModal(null); + setInstalling(false); }} />, ); From cfc84ab29f8304e56858309e7ed14291f246634b Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 11 Sep 2024 17:35:47 -0300 Subject: [PATCH 3/6] chore: add index to preview page (#33261) Co-authored-by: Diego Sampaio --- ...iew.yml => ci-deploy-gh-pages-preview.yml} | 7 +--- .github/workflows/ci-deploy-gh-pages.yml | 38 +++++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) rename .github/workflows/{ci-preview.yml => ci-deploy-gh-pages-preview.yml} (91%) create mode 100644 .github/workflows/ci-deploy-gh-pages.yml diff --git a/.github/workflows/ci-preview.yml b/.github/workflows/ci-deploy-gh-pages-preview.yml similarity index 91% rename from .github/workflows/ci-preview.yml rename to .github/workflows/ci-deploy-gh-pages-preview.yml index 18b4b0bef373..17f247ddad94 100644 --- a/.github/workflows/ci-preview.yml +++ b/.github/workflows/ci-deploy-gh-pages-preview.yml @@ -8,11 +8,7 @@ on: - reopened - synchronize - closed - push: - branches: - - main - - master - - develop + jobs: deploy-preview: runs-on: ubuntu-latest @@ -35,6 +31,7 @@ jobs: run: | yarn turbo run build-preview yarn turbo run .:build-preview-move + npx indexifier .preview --html --extensions .html > .preview/index.html - uses: rossjrw/pr-preview-action@v1 with: diff --git a/.github/workflows/ci-deploy-gh-pages.yml b/.github/workflows/ci-deploy-gh-pages.yml new file mode 100644 index 000000000000..b381e05ae5d8 --- /dev/null +++ b/.github/workflows/ci-deploy-gh-pages.yml @@ -0,0 +1,38 @@ +# .github/workflows/ci-preview-deploy.yml +name: Deploy GitHub Pages +concurrency: preview-deploy-${{ github.ref }} +on: + push: + branches: + - main + - master + - develop +jobs: + deploy-preview: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: rharkor/caching-for-turbo@v1.5 + + - name: Setup NodeJS + uses: ./.github/actions/setup-node + with: + node-version: 14.21.3 + cache-modules: true + install: true + + - name: Build + run: | + yarn turbo run build-preview + yarn turbo run .:build-preview-move + npx indexifier .preview --html --extensions .html > .preview/index.html + mv .preview ${{ github.ref_name }} + mkdir .preview + mv ${{ github.ref_name }} .preview + + - name: Deploy + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: .preview + keep_files: true From 18b1b992a196c7ae8803380a0ddf4d67b8b08f59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Guimar=C3=A3es=20Ribeiro?= <43561537+rique223@users.noreply.github.com> Date: Wed, 11 Sep 2024 18:56:50 -0300 Subject: [PATCH 4/6] refactor: Upgrade `Fuselage` and remove unnecessary `FieldError` styling (#33205) --- .../views/admin/users/AdminUserForm.tsx | 2 +- packages/fuselage-ui-kit/package.json | 8 +++---- packages/gazzodown/package.json | 4 ++-- packages/ui-avatar/package.json | 2 +- packages/ui-client/package.json | 2 +- packages/ui-video-conf/package.json | 4 ++-- packages/web-ui-registration/package.json | 2 +- yarn.lock | 22 +++++++++---------- 8 files changed, 23 insertions(+), 23 deletions(-) diff --git a/apps/meteor/client/views/admin/users/AdminUserForm.tsx b/apps/meteor/client/views/admin/users/AdminUserForm.tsx index 023d3df851e5..a55cc29eb648 100644 --- a/apps/meteor/client/views/admin/users/AdminUserForm.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserForm.tsx @@ -237,7 +237,7 @@ const AdminUserForm = ({ userData, onReload, context, refetchUserFormData, roleD /> {errors?.email && ( - + {errors.email.message} )} diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 7c644744c3cf..7277e183eae6 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -50,10 +50,10 @@ "@rocket.chat/icons": "*", "@rocket.chat/prettier-config": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-avatar": "6.0.0", - "@rocket.chat/ui-contexts": "10.0.0", - "@rocket.chat/ui-kit": "0.36.1", - "@rocket.chat/ui-video-conf": "10.0.0", + "@rocket.chat/ui-avatar": "*", + "@rocket.chat/ui-contexts": "*", + "@rocket.chat/ui-kit": "*", + "@rocket.chat/ui-video-conf": "*", "@tanstack/react-query": "*", "react": "*", "react-dom": "*" diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index 8b751c895b0f..d4a18fb09a76 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -66,8 +66,8 @@ "@rocket.chat/fuselage-tokens": "*", "@rocket.chat/message-parser": "0.31.29", "@rocket.chat/styled": "*", - "@rocket.chat/ui-client": "10.0.0", - "@rocket.chat/ui-contexts": "10.0.0", + "@rocket.chat/ui-client": "*", + "@rocket.chat/ui-contexts": "*", "katex": "*", "react": "*" }, diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index 66a50ce8c8e1..42860fd8692a 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -31,7 +31,7 @@ ], "peerDependencies": { "@rocket.chat/fuselage": "*", - "@rocket.chat/ui-contexts": "10.0.0", + "@rocket.chat/ui-contexts": "*", "react": "~17.0.2" }, "volta": { diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index d9cdb9f24ade..0e969e173ca1 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -60,7 +60,7 @@ "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", - "@rocket.chat/ui-contexts": "10.0.0", + "@rocket.chat/ui-contexts": "*", "react": "~17.0.2" }, "volta": { diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index 40253050d9bf..d10c05edad2c 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -40,8 +40,8 @@ "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-avatar": "6.0.0", - "@rocket.chat/ui-contexts": "10.0.0", + "@rocket.chat/ui-avatar": "*", + "@rocket.chat/ui-contexts": "*", "react": "^17.0.2", "react-dom": "^17.0.2" }, diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index e12b653ebfeb..2023641c15ab 100644 --- a/packages/web-ui-registration/package.json +++ b/packages/web-ui-registration/package.json @@ -47,7 +47,7 @@ "peerDependencies": { "@rocket.chat/layout": "*", "@rocket.chat/tools": "0.2.2", - "@rocket.chat/ui-contexts": "10.0.0", + "@rocket.chat/ui-contexts": "*", "@tanstack/react-query": "*", "react": "*", "react-hook-form": "*", diff --git a/yarn.lock b/yarn.lock index aa2b104e5190..65ebae84019b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8934,10 +8934,10 @@ __metadata: "@rocket.chat/icons": "*" "@rocket.chat/prettier-config": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": 6.0.0 - "@rocket.chat/ui-contexts": 10.0.0 - "@rocket.chat/ui-kit": 0.36.1 - "@rocket.chat/ui-video-conf": 10.0.0 + "@rocket.chat/ui-avatar": "*" + "@rocket.chat/ui-contexts": "*" + "@rocket.chat/ui-kit": "*" + "@rocket.chat/ui-video-conf": "*" "@tanstack/react-query": "*" react: "*" react-dom: "*" @@ -9021,8 +9021,8 @@ __metadata: "@rocket.chat/fuselage-tokens": "*" "@rocket.chat/message-parser": 0.31.29 "@rocket.chat/styled": "*" - "@rocket.chat/ui-client": 10.0.0 - "@rocket.chat/ui-contexts": 10.0.0 + "@rocket.chat/ui-client": "*" + "@rocket.chat/ui-contexts": "*" katex: "*" react: "*" languageName: unknown @@ -10228,7 +10228,7 @@ __metadata: typescript: ~5.5.4 peerDependencies: "@rocket.chat/fuselage": "*" - "@rocket.chat/ui-contexts": 10.0.0 + "@rocket.chat/ui-contexts": "*" react: ~17.0.2 languageName: unknown linkType: soft @@ -10278,7 +10278,7 @@ __metadata: "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" - "@rocket.chat/ui-contexts": 10.0.0 + "@rocket.chat/ui-contexts": "*" react: ~17.0.2 languageName: unknown linkType: soft @@ -10448,8 +10448,8 @@ __metadata: "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": 6.0.0 - "@rocket.chat/ui-contexts": 10.0.0 + "@rocket.chat/ui-avatar": "*" + "@rocket.chat/ui-contexts": "*" react: ^17.0.2 react-dom: ^17.0.2 languageName: unknown @@ -10536,7 +10536,7 @@ __metadata: peerDependencies: "@rocket.chat/layout": "*" "@rocket.chat/tools": 0.2.2 - "@rocket.chat/ui-contexts": 10.0.0 + "@rocket.chat/ui-contexts": "*" "@tanstack/react-query": "*" react: "*" react-hook-form: "*" From 41a1816e2e997b0e47d859f20b0fc25990d31d29 Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Thu, 12 Sep 2024 09:42:32 -0300 Subject: [PATCH 5/6] fix: Retention Policy cached settings not updated during upgrade procedure (#33237) --- .changeset/pink-swans-teach.md | 5 +++++ apps/meteor/server/startup/migrations/xrun.ts | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 .changeset/pink-swans-teach.md diff --git a/.changeset/pink-swans-teach.md b/.changeset/pink-swans-teach.md new file mode 100644 index 000000000000..7c85572a78d5 --- /dev/null +++ b/.changeset/pink-swans-teach.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fixed retention policy max age settings not being respected after upgrade diff --git a/apps/meteor/server/startup/migrations/xrun.ts b/apps/meteor/server/startup/migrations/xrun.ts index 7f6c8a68ad55..0344649f9993 100644 --- a/apps/meteor/server/startup/migrations/xrun.ts +++ b/apps/meteor/server/startup/migrations/xrun.ts @@ -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'; @@ -23,10 +24,13 @@ 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( { @@ -34,11 +38,17 @@ const moveRetentionSetting = async () => { }, { $set: { - value: convertDaysToMs(Number(value)), + value: newValue, }, }, ), ); + + const currentCache = settings.getSetting(newSettingId); + if (!currentCache) { + return; + } + settings.set({ ...currentCache, value: newValue }); }); await Promise.all(promises); From 7cb6de00336dcec878edcb223e7bb6158a848565 Mon Sep 17 00:00:00 2001 From: "Julio A." <52619625+julio-cfa@users.noreply.github.com> Date: Thu, 12 Sep 2024 17:06:20 +0200 Subject: [PATCH 6/6] fix: imported fixes (#33246) --- .changeset/many-rules-shout.md | 5 + .../server/functions/canDeleteMessage.ts | 28 +- .../app/otr/server/methods/updateOTRAck.ts | 38 +- .../tabs/AppDetails/AppDetails.tsx | 11 +- .../tabs/AppReleases/AppReleasesItem.tsx | 4 +- .../views/marketplace/lib/purifyOptions.ts | 50 ++ apps/meteor/server/models/raw/Rooms.ts | 4 + apps/meteor/tests/end-to-end/api/methods.ts | 454 ++++++++++++++++++ packages/gazzodown/package.json | 2 + .../gazzodown/src/emoji/EmojiRenderer.tsx | 11 +- packages/gazzodown/src/katex/KatexBlock.tsx | 1 + packages/gazzodown/src/katex/KatexElement.tsx | 1 + .../model-typings/src/models/IRoomsModel.ts | 1 + 13 files changed, 595 insertions(+), 15 deletions(-) create mode 100644 .changeset/many-rules-shout.md create mode 100644 apps/meteor/client/views/marketplace/lib/purifyOptions.ts diff --git a/.changeset/many-rules-shout.md b/.changeset/many-rules-shout.md new file mode 100644 index 000000000000..eacb88108a0f --- /dev/null +++ b/.changeset/many-rules-shout.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) diff --git a/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts b/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts index 7cd953a52bb2..fea37fd1c2a5 100644 --- a/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts +++ b/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts @@ -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 => { @@ -13,6 +14,25 @@ export const canDeleteMessageAsync = async ( uid: string, { u, rid, ts }: { u: Pick; rid: string; ts: Date }, ): Promise => { + const room = await Rooms.findOneById>(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) { @@ -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)) { diff --git a/apps/meteor/app/otr/server/methods/updateOTRAck.ts b/apps/meteor/app/otr/server/methods/updateOTRAck.ts index 4fbd182e9d27..64e5e97fa4e5 100644 --- a/apps/meteor/app/otr/server/methods/updateOTRAck.ts +++ b/apps/meteor/app/otr/server/methods/updateOTRAck.ts @@ -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 { @@ -11,10 +15,40 @@ declare module '@rocket.chat/ddp-client' { } Meteor.methods({ - 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 }); }, diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppDetails/AppDetails.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppDetails/AppDetails.tsx index 8d17f669db83..5f3e427b8391 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppDetails/AppDetails.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppDetails/AppDetails.tsx @@ -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'; @@ -61,7 +63,14 @@ const AppDetails = ({ app }: AppDetailsProps) => { {t('Description')} - + diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppReleases/AppReleasesItem.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppReleases/AppReleasesItem.tsx index bc27053ea1d2..974b6d148e61 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppReleases/AppReleasesItem.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppReleases/AppReleasesItem.tsx @@ -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; @@ -36,7 +38,7 @@ const AppReleasesItem = ({ release, ...props }: ReleaseItemProps): ReactElement return ( {release.detailedChangelog?.rendered ? ( - + ) : ( {t('No_release_information_provided')} )} diff --git a/apps/meteor/client/views/marketplace/lib/purifyOptions.ts b/apps/meteor/client/views/marketplace/lib/purifyOptions.ts new file mode 100644 index 000000000000..cef1a2c8c707 --- /dev/null +++ b/apps/meteor/client/views/marketplace/lib/purifyOptions.ts @@ -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', + ], +}; diff --git a/apps/meteor/server/models/raw/Rooms.ts b/apps/meteor/server/models/raw/Rooms.ts index 96cd5a3a3acf..ec3bd6fe8d40 100644 --- a/apps/meteor/server/models/raw/Rooms.ts +++ b/apps/meteor/server/models/raw/Rooms.ts @@ -903,6 +903,10 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { return this.findOne(query, options); } + findOneByIdAndType(roomId: IRoom['_id'], type: IRoom['t'], options: FindOptions = {}): Promise { + return this.findOne({ _id: roomId, t: type }, options); + } + setCallStatus(_id: IRoom['_id'], status: IRoom['callStatus']): Promise { const query: Filter = { _id, diff --git a/apps/meteor/tests/end-to-end/api/methods.ts b/apps/meteor/tests/end-to-end/api/methods.ts index 197a71f8e8f2..08945994e438 100644 --- a/apps/meteor/tests/end-to-end/api/methods.ts +++ b/apps/meteor/tests/end-to-end/api/methods.ts @@ -2602,6 +2602,158 @@ describe('Meteor.methods', () => { updateSetting('Message_AllowEditing_BlockEditInMinutes', 0), ]); }); + + describe('message deletion when user is not part of the room', () => { + let ridTestRoom: IRoom['_id']; + let messageIdTestRoom: IMessage['_id']; + let testUser: TestUser; + let testUserCredentials: Credentials; + + before('create room, add new owner, and leave room', async () => { + testUser = await createUser(); + testUserCredentials = await login(testUser.username, password); + const channelName = `methods-test-channel-${Date.now()}`; + + await request + .post(api('groups.create')) + .set(testUserCredentials) + .send({ + name: channelName, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('group._id'); + expect(res.body).to.have.nested.property('group.name', channelName); + expect(res.body).to.have.nested.property('group.t', 'p'); + expect(res.body).to.have.nested.property('group.msgs', 0); + ridTestRoom = res.body.group._id; + }); + + await request + .post(methodCall('sendMessage')) + .set(testUserCredentials) + .send({ + message: JSON.stringify({ + method: 'sendMessage', + params: [ + { + _id: `${Date.now() + Math.random()}`, + rid: ridTestRoom, + msg: 'just a random test message', + }, + ], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('result').that.is.an('object'); + messageIdTestRoom = data.result._id; + }); + + await request + .post(methodCall('addUsersToRoom')) + .set(testUserCredentials) + .send({ + message: JSON.stringify({ + method: 'addUsersToRoom', + params: [ + { + rid: ridTestRoom, + users: ['rocket.cat'], + }, + ], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + }); + + await request + .post(api('groups.addOwner')) + .set(testUserCredentials) + .send({ + roomId: ridTestRoom, + userId: 'rocket.cat', + }) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + }); + + await request + .post(api('groups.leave')) + .set(testUserCredentials) + .send({ + roomId: ridTestRoom, + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + }); + }); + + it('should not delete a message if the user is no longer member of the room', async () => { + await request + .post(methodCall('deleteMessage')) + .set(testUserCredentials) + .send({ + message: JSON.stringify({ + method: 'deleteMessage', + params: [{ _id: messageIdTestRoom, rid: ridTestRoom }], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('msg', 'result'); + expect(data).to.have.a.property('id', 'id'); + expect(data.error).to.have.a.property('error', 'error-action-not-allowed'); + }); + }); + + it('should not delete a message if the user was never part of the room', async () => { + await request + .post(methodCall('deleteMessage')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'deleteMessage', + params: [{ _id: messageIdTestRoom, rid: ridTestRoom }], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('msg', 'result'); + expect(data).to.have.a.property('id', 'id'); + expect(data.error).to.have.a.property('error', 'error-action-not-allowed'); + }); + }); + + after(() => Promise.all([deleteRoom({ type: 'p', roomId: ridTestRoom }), deleteUser(testUser)])); + }); }); describe('[@setUserActiveStatus]', () => { @@ -3348,6 +3500,7 @@ describe('Meteor.methods', () => { .end(done); }); }); + (IS_EE ? describe : describe.skip)('[@auditGetAuditions] EE', () => { let testUser: TestUser; let testUserCredentials: Credentials; @@ -3451,4 +3604,305 @@ describe('Meteor.methods', () => { }); }); }); + + describe('UpdateOTRAck', () => { + let testUser: TestUser; + let testUser2: TestUser; + let testUserCredentials: Credentials; + let dmTestId: IRoom['_id']; + + before(async () => { + testUser = await createUser(); + testUser2 = await createUser(); + testUserCredentials = await login(testUser.username, password); + }); + + before('create direct conversation between both users', (done) => { + void request + .post(methodCall('createDirectMessage')) + .set(testUserCredentials) + .send({ + message: JSON.stringify({ + method: 'createDirectMessage', + params: [testUser2.username], + id: 'id', + msg: 'method', + }), + }) + .end((_err, res) => { + const result = JSON.parse(res.body.message); + expect(result.result).to.be.an('object'); + expect(result.result).to.have.property('rid').that.is.an('string'); + + dmTestId = result.result.rid; + done(); + }); + }); + + after(() => Promise.all([deleteRoom({ type: 'd', roomId: dmTestId }), deleteUser(testUser), deleteUser(testUser2)])); + + it('should fail if required parameters are not present', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + // rid: 'test', + msg: 'test', + t: 'otr', + ts: { $date: 1725447664093 }, + u: { + _id: 'test', + username: 'test', + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error'); + expect(data.error).to.have.a.property('message', "Match error: Missing key 'rid'"); + }); + }); + + it('should fail if required parameters have a different type', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + rid: { $ne: 'test' }, + msg: 'test', + t: 'otr', + ts: { $date: 1725447664093 }, + u: { + _id: 'test', + username: 'test', + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error'); + expect(data.error).to.have.a.property('message', 'Match error: Expected string, got object in field rid'); + }); + }); + + it('should fail if "t" is not "otr"', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + rid: 'test', + msg: 'test', + t: 'notOTR', + ts: { $date: 1725447664093 }, + u: { + _id: 'test', + username: 'test', + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error'); + expect(data.error).to.have.a.property('message', 'Invalid message type [error-invalid-message]'); + }); + }); + + it('should fail if room does not exist', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + rid: 'test', + msg: 'test', + t: 'otr', + ts: { $date: 1725447664093 }, + u: { + _id: 'test', + username: 'test', + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error'); + expect(data.error).to.have.a.property('message', 'Invalid room [error-invalid-room]'); + }); + }); + + it('should fail if room is not a DM', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + rid: 'GENERAL', + msg: 'test', + t: 'otr', + ts: { $date: 1725447664093 }, + u: { + _id: 'test', + username: 'test', + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error'); + expect(data.error).to.have.a.property('message', 'Invalid room [error-invalid-room]'); + }); + }); + + it('should fail if user is not part of DM room', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + rid: dmTestId, + msg: 'test', + t: 'otr', + ts: { $date: 1725447664093 }, + u: { + _id: testUser._id, + username: testUser.username, + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error'); + expect(data.error).to.have.a.property('message', 'Invalid user, not in room [error-invalid-user]'); + }); + }); + + it('should pass if all parameters are present and user is part of DM room', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(testUserCredentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + rid: dmTestId, + msg: 'test', + t: 'otr', + ts: { $date: 1725447664093 }, + u: { + _id: testUser._id, + username: testUser.username, + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + expect(res.body).to.have.a.property('success', true); + }); + }); + }); }); diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index d4a18fb09a76..f6c48c7f8e47 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -72,7 +72,9 @@ "react": "*" }, "dependencies": { + "@types/dompurify": "^3.0.5", "date-fns": "^3.3.1", + "dompurify": "^3.1.6", "highlight.js": "^11.5.1", "react-error-boundary": "^3.1.4" }, diff --git a/packages/gazzodown/src/emoji/EmojiRenderer.tsx b/packages/gazzodown/src/emoji/EmojiRenderer.tsx index 7a4ca5324930..84116361157c 100644 --- a/packages/gazzodown/src/emoji/EmojiRenderer.tsx +++ b/packages/gazzodown/src/emoji/EmojiRenderer.tsx @@ -1,5 +1,6 @@ import { MessageEmoji, ThreadMessageEmoji } from '@rocket.chat/fuselage'; import type * as MessageParser from '@rocket.chat/message-parser'; +import DOMPurify from 'dompurify'; import { ReactElement, useMemo, useContext, memo } from 'react'; import { MarkupInteractionContext } from '../MarkupInteractionContext'; @@ -14,10 +15,12 @@ const EmojiRenderer = ({ big = false, preview = false, ...emoji }: EmojiProps): const fallback = useMemo(() => ('unicode' in emoji ? emoji.unicode : `:${emoji.shortCode ?? emoji.value.value}:`), [emoji]); + const sanitizedFallback = DOMPurify.sanitize(fallback); + const descriptors = useMemo(() => { - const detected = detectEmoji?.(fallback); + const detected = detectEmoji?.(sanitizedFallback); return detected?.length !== 0 ? detected : undefined; - }, [detectEmoji, fallback]); + }, [detectEmoji, sanitizedFallback]); return ( <> @@ -34,8 +37,8 @@ const EmojiRenderer = ({ big = false, preview = false, ...emoji }: EmojiProps): )} )) ?? ( - - {fallback} + + {sanitizedFallback} )} diff --git a/packages/gazzodown/src/katex/KatexBlock.tsx b/packages/gazzodown/src/katex/KatexBlock.tsx index 5913185d3969..267b310b3897 100644 --- a/packages/gazzodown/src/katex/KatexBlock.tsx +++ b/packages/gazzodown/src/katex/KatexBlock.tsx @@ -15,6 +15,7 @@ const KatexBlock = ({ code }: KatexBlockProps): ReactElement => { macros: { '\\href': '\\@secondoftwo', }, + maxSize: 100, }), [code], ); diff --git a/packages/gazzodown/src/katex/KatexElement.tsx b/packages/gazzodown/src/katex/KatexElement.tsx index 3595f698f7ae..099c2f82cf8c 100644 --- a/packages/gazzodown/src/katex/KatexElement.tsx +++ b/packages/gazzodown/src/katex/KatexElement.tsx @@ -15,6 +15,7 @@ const KatexElement = ({ code }: KatexElementProps): ReactElement => { macros: { '\\href': '\\@secondoftwo', }, + maxSize: 100, }), [code], ); diff --git a/packages/model-typings/src/models/IRoomsModel.ts b/packages/model-typings/src/models/IRoomsModel.ts index 498a3c6b4bbc..9097a89c1413 100644 --- a/packages/model-typings/src/models/IRoomsModel.ts +++ b/packages/model-typings/src/models/IRoomsModel.ts @@ -192,6 +192,7 @@ export interface IRoomsModel extends IBaseModel { setE2eKeyId(roomId: string, e2eKeyId: string, options?: FindOptions): Promise; findOneByImportId(importId: string, options?: FindOptions): Promise; findOneByNameAndNotId(name: string, rid: string): Promise; + findOneByIdAndType(roomId: IRoom['_id'], type: IRoom['t'], options?: FindOptions): Promise; findOneByDisplayName(displayName: string, options?: FindOptions): Promise; findOneByNameAndType( name: string,