diff --git a/.changeset/breezy-horses-marry.md b/.changeset/breezy-horses-marry.md new file mode 100644 index 0000000000000..eacb88108a0f7 --- /dev/null +++ b/.changeset/breezy-horses-marry.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) diff --git a/.changeset/fifty-apricots-clean.md b/.changeset/fifty-apricots-clean.md new file mode 100644 index 0000000000000..e8f867f1ffff8 --- /dev/null +++ b/.changeset/fifty-apricots-clean.md @@ -0,0 +1,9 @@ +--- +"@rocket.chat/omnichannel-services": patch +--- + +Fixes a behavior when running microservices that caused queue worker to process just the first 60 seconds of request. + +This was due to a mistakenly bound context. Queue Worker was changed to start doing work only after it received the first request. + +However, with the introduction of ASL and actual context on calls, the worker registration was absorbing the context of the call that created them, causing service calls happening inside the callbacks to fail because of a timeout. diff --git a/.changeset/kind-ducks-thank.md b/.changeset/kind-ducks-thank.md new file mode 100644 index 0000000000000..67a94a0fe0a31 --- /dev/null +++ b/.changeset/kind-ducks-thank.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes apps actions showing in toolbar without an icon diff --git a/apps/meteor/app/cors/server/cors.ts b/apps/meteor/app/cors/server/cors.ts index 3090530140168..effbb712681b1 100644 --- a/apps/meteor/app/cors/server/cors.ts +++ b/apps/meteor/app/cors/server/cors.ts @@ -4,6 +4,7 @@ import type { UrlWithParsedQuery } from 'url'; import url from 'url'; import { Logger } from '@rocket.chat/logger'; +import { OAuthApps } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import type { StaticFiles } from 'meteor/webapp'; import { WebApp, WebAppInternals } from 'meteor/webapp'; @@ -48,10 +49,13 @@ WebApp.rawConnectHandlers.use(async (_req: http.IncomingMessage, res: http.Serve } if (settings.get('Enable_CSP')) { + const legacyZapierAvailable = Boolean(await OAuthApps.findOneById('zapier')); + // eslint-disable-next-line @typescript-eslint/naming-convention const cdn_prefixes = [ settings.get('CDN_PREFIX'), settings.get('CDN_PREFIX_ALL') ? null : settings.get('CDN_JSCSS_PREFIX'), + legacyZapierAvailable && 'https://cdn.zapier.com', ] .filter(Boolean) .join(' '); @@ -68,6 +72,7 @@ WebApp.rawConnectHandlers.use(async (_req: http.IncomingMessage, res: http.Serve settings.get('Accounts_OAuth_Apple') && 'https://appleid.cdn-apple.com', settings.get('PiwikAnalytics_enabled') && settings.get('PiwikAnalytics_url'), settings.get('GoogleAnalytics_enabled') && 'https://www.google-analytics.com', + legacyZapierAvailable && 'https://zapier.com', ...settings .get('Extra_CSP_Domains') .split(/[ \n\,]/gim) diff --git a/apps/meteor/app/oauth2-server-config/server/index.ts b/apps/meteor/app/oauth2-server-config/server/index.ts index be26bdb2facb1..3914cac5eaadc 100644 --- a/apps/meteor/app/oauth2-server-config/server/index.ts +++ b/apps/meteor/app/oauth2-server-config/server/index.ts @@ -1,5 +1,4 @@ import './oauth/oauth2-server'; -import './oauth/default-services'; import './admin/functions/addOAuthApp'; import './admin/methods/updateOAuthApp'; import './admin/methods/deleteOAuthApp'; diff --git a/apps/meteor/app/oauth2-server-config/server/oauth/default-services.ts b/apps/meteor/app/oauth2-server-config/server/oauth/default-services.ts deleted file mode 100644 index cd2d4e6c862d0..0000000000000 --- a/apps/meteor/app/oauth2-server-config/server/oauth/default-services.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { OAuthApps } from '@rocket.chat/models'; - -async function run(): Promise { - if (!(await OAuthApps.findOneById('zapier'))) { - await OAuthApps.insertOne({ - _id: 'zapier', - name: 'Zapier', - active: true, - clientId: 'zapier', - clientSecret: 'RTK6TlndaCIolhQhZ7_KHIGOKj41RnlaOq_o-7JKwLr', - redirectUri: 'https://zapier.com/dashboard/auth/oauth/return/RocketChatDevAPI/', - _createdAt: new Date(), - _createdBy: { - _id: 'system', - username: 'system', - }, - }); - } -} - -void run(); diff --git a/apps/meteor/client/components/FingerprintChangeModal.tsx b/apps/meteor/client/components/FingerprintChangeModal.tsx index 834f130bb9f1a..354d13df06c24 100644 --- a/apps/meteor/client/components/FingerprintChangeModal.tsx +++ b/apps/meteor/client/components/FingerprintChangeModal.tsx @@ -1,4 +1,5 @@ import { Box } from '@rocket.chat/fuselage'; +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; @@ -26,14 +27,17 @@ const FingerprintChangeModal = ({ onConfirm, onCancel, onClose }: FingerprintCha is='p' mbe={16} dangerouslySetInnerHTML={{ - __html: t('Unique_ID_change_detected_description'), + __html: DOMPurify.sanitize(t('Unique_ID_change_detected_description')), }} /> diff --git a/apps/meteor/client/components/FingerprintChangeModalConfirmation.tsx b/apps/meteor/client/components/FingerprintChangeModalConfirmation.tsx index 7b0d93146a599..7d262d7929355 100644 --- a/apps/meteor/client/components/FingerprintChangeModalConfirmation.tsx +++ b/apps/meteor/client/components/FingerprintChangeModalConfirmation.tsx @@ -1,4 +1,5 @@ import { Box } from '@rocket.chat/fuselage'; +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; @@ -29,14 +30,19 @@ const FingerprintChangeModalConfirmation = ({ is='p' mbe={16} dangerouslySetInnerHTML={{ - __html: newWorkspace ? t('Confirm_new_workspace_description') : t('Confirm_configuration_update_description'), + __html: newWorkspace + ? DOMPurify.sanitize(t('Confirm_new_workspace_description')) + : DOMPurify.sanitize(t('Confirm_configuration_update_description')), }} /> diff --git a/apps/meteor/client/components/RawText.tsx b/apps/meteor/client/components/RawText.tsx index a4220371666f4..58ae89e4d041c 100644 --- a/apps/meteor/client/components/RawText.tsx +++ b/apps/meteor/client/components/RawText.tsx @@ -1,6 +1,9 @@ +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; /** @deprecated */ -const RawText = ({ children }: { children: string }): ReactElement => ; +const RawText = ({ children }: { children: string }): ReactElement => ( + +); export default RawText; diff --git a/apps/meteor/client/components/RoomIcon/OmnichannelRoomIcon/provider/OmnichannelRoomIconProvider.tsx b/apps/meteor/client/components/RoomIcon/OmnichannelRoomIcon/provider/OmnichannelRoomIconProvider.tsx index 57febd38b9e36..305359f1be9be 100644 --- a/apps/meteor/client/components/RoomIcon/OmnichannelRoomIcon/provider/OmnichannelRoomIconProvider.tsx +++ b/apps/meteor/client/components/RoomIcon/OmnichannelRoomIcon/provider/OmnichannelRoomIconProvider.tsx @@ -1,3 +1,4 @@ +import DOMPurify from 'dompurify'; import type { ReactNode } from 'react'; import { useCallback, useMemo } from 'react'; import { createPortal } from 'react-dom'; @@ -84,7 +85,11 @@ export const OmnichannelRoomIconProvider = ({ children }: OmnichannelRoomIconPro xmlns='http://www.w3.org/2000/svg' xmlnsXlink='http://www.w3.org/1999/xlink' style={{ display: 'none' }} - dangerouslySetInnerHTML={{ __html: svgIcons.join('') }} + dangerouslySetInnerHTML={{ + __html: DOMPurify.sanitize(svgIcons.join(''), { + USE_PROFILES: { svg: true, svgFilters: true }, + }), + }} />, document.body, 'custom-icons', diff --git a/apps/meteor/client/components/UrlChangeModal.tsx b/apps/meteor/client/components/UrlChangeModal.tsx index cf1df67adf00d..75a5310861c8e 100644 --- a/apps/meteor/client/components/UrlChangeModal.tsx +++ b/apps/meteor/client/components/UrlChangeModal.tsx @@ -1,4 +1,5 @@ import { Box } from '@rocket.chat/fuselage'; +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; @@ -19,18 +20,22 @@ const UrlChangeModal = ({ onConfirm, siteUrl, currentUrl, onClose }: UrlChangeMo is='p' mbe={16} dangerouslySetInnerHTML={{ - __html: t('The_setting_s_is_configured_to_s_and_you_are_accessing_from_s', { - postProcess: 'sprintf', - sprintf: [t('Site_Url'), siteUrl, currentUrl], - }), + __html: DOMPurify.sanitize( + t('The_setting_s_is_configured_to_s_and_you_are_accessing_from_s', { + postProcess: 'sprintf', + sprintf: [t('Site_Url'), siteUrl, currentUrl], + }), + ), }} />

diff --git a/apps/meteor/client/lib/utils/createToken.ts b/apps/meteor/client/lib/utils/createToken.ts index 0795f8103fd18..25360b773ba9c 100644 --- a/apps/meteor/client/lib/utils/createToken.ts +++ b/apps/meteor/client/lib/utils/createToken.ts @@ -1 +1,14 @@ -export const createToken = (): string => Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); +export const createToken = (): string => { + const array = new Uint8Array(16); + if (typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValues) { + window.crypto.getRandomValues(array); + } else { + // Use Node.js crypto + const { randomBytes } = require('crypto'); // eslint-disable-line @typescript-eslint/no-var-requires + const buffer = randomBytes(16); + array.set(buffer); + } + return Array.from(array) + .map((byte) => byte.toString(16).padStart(2, '0')) + .join(''); +}; diff --git a/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx index 741d0740d6338..8c8e4c6c814da 100644 --- a/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx +++ b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx @@ -2,6 +2,7 @@ import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; import { isDirectMessageRoom, isMultipleDirectMessageRoom, isOmnichannelRoom, isVideoConfMessage } from '@rocket.chat/core-typings'; import { Badge, Sidebar, SidebarItemAction, SidebarItemActions, Margins } from '@rocket.chat/fuselage'; import { useLayout } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import type { TFunction } from 'i18next'; import type { AllHTMLAttributes, ComponentType, ReactElement, ReactNode } from 'react'; import { memo, useMemo } from 'react'; @@ -147,7 +148,9 @@ function SideBarItemTemplateWithData({ const { enabled: isPriorityEnabled } = useOmnichannelPriorities(); const message = extended && getMessage(room, lastMessage, t); - const subtitle = message ? : null; + const subtitle = message ? ( + + ) : null; const threadUnread = tunread.length > 0; const variant = diff --git a/apps/meteor/client/sidebar/footer/SidebarFooterDefault.tsx b/apps/meteor/client/sidebar/footer/SidebarFooterDefault.tsx index 66e5150ab2e14..291799c361d36 100644 --- a/apps/meteor/client/sidebar/footer/SidebarFooterDefault.tsx +++ b/apps/meteor/client/sidebar/footer/SidebarFooterDefault.tsx @@ -2,6 +2,7 @@ import { css } from '@rocket.chat/css-in-js'; import { Box, SidebarDivider, Palette, SidebarFooter as Footer } from '@rocket.chat/fuselage'; import { useSetting } from '@rocket.chat/ui-contexts'; import { useThemeMode } from '@rocket.chat/ui-theming'; +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; import { SidebarFooterWatermark } from './SidebarFooterWatermark'; @@ -32,7 +33,7 @@ const SidebarFooterDefault = (): ReactElement => { width='auto' className={sidebarFooterStyle} dangerouslySetInnerHTML={{ - __html: logo, + __html: DOMPurify.sanitize(logo), }} /> diff --git a/apps/meteor/client/sidebarv2/footer/SidebarFooterDefault.tsx b/apps/meteor/client/sidebarv2/footer/SidebarFooterDefault.tsx index 66e5150ab2e14..291799c361d36 100644 --- a/apps/meteor/client/sidebarv2/footer/SidebarFooterDefault.tsx +++ b/apps/meteor/client/sidebarv2/footer/SidebarFooterDefault.tsx @@ -2,6 +2,7 @@ import { css } from '@rocket.chat/css-in-js'; import { Box, SidebarDivider, Palette, SidebarFooter as Footer } from '@rocket.chat/fuselage'; import { useSetting } from '@rocket.chat/ui-contexts'; import { useThemeMode } from '@rocket.chat/ui-theming'; +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; import { SidebarFooterWatermark } from './SidebarFooterWatermark'; @@ -32,7 +33,7 @@ const SidebarFooterDefault = (): ReactElement => { width='auto' className={sidebarFooterStyle} dangerouslySetInnerHTML={{ - __html: logo, + __html: DOMPurify.sanitize(logo), }} /> diff --git a/apps/meteor/client/views/account/preferences/PreferencesMyDataSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesMyDataSection.tsx index 53e91211a49d0..bffd31cda9042 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesMyDataSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesMyDataSection.tsx @@ -1,5 +1,6 @@ import { AccordionItem, ButtonGroup, Button, Box } from '@rocket.chat/fuselage'; import { useSetModal, useToastMessageDispatch, useMethod } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -23,7 +24,7 @@ const PreferencesMyDataSection = () => { setModal( } + text={} onCancel={() => setModal(null)} />, ); @@ -41,7 +42,7 @@ const PreferencesMyDataSection = () => { setModal( } + text={} onCancel={() => setModal(null)} />, ); @@ -55,7 +56,7 @@ const PreferencesMyDataSection = () => { setModal( } + text={} onCancel={() => setModal(null)} />, ); diff --git a/apps/meteor/client/views/account/security/EndToEnd.tsx b/apps/meteor/client/views/account/security/EndToEnd.tsx index 6eb16d62aea92..c892f4e93cb0a 100644 --- a/apps/meteor/client/views/account/security/EndToEnd.tsx +++ b/apps/meteor/client/views/account/security/EndToEnd.tsx @@ -1,6 +1,7 @@ import { Box, PasswordInput, Field, FieldGroup, FieldLabel, FieldRow, FieldError, FieldHint, Button, Divider } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useMethod, useTranslation, useLogout } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import { Accounts } from 'meteor/accounts-base'; import type { ComponentProps, ReactElement } from 'react'; import { useCallback, useEffect } from 'react'; @@ -76,7 +77,7 @@ const EndToEnd = (props: ComponentProps): ReactElement => { is='p' fontScale='p1' id={e2ePasswordExplanationId} - dangerouslySetInnerHTML={{ __html: t('E2E_Encryption_Password_Explanation') }} + dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(t('E2E_Encryption_Password_Explanation')) }} /> @@ -160,7 +161,7 @@ const EndToEnd = (props: ComponentProps): ReactElement => { {t('Reset_E2E_Key')} - + diff --git a/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensTable.tsx b/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensTable.tsx index 833b133f0c087..d1c3365b9df18 100644 --- a/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensTable.tsx +++ b/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensTable.tsx @@ -1,5 +1,6 @@ import { Box, Pagination, States, StatesAction, StatesActions, StatesIcon, StatesSubtitle, StatesTitle } from '@rocket.chat/fuselage'; import { useSetModal, useToastMessageDispatch, useUserId, useMethod } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import type { ReactElement, RefObject } from 'react'; import { useMemo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -68,10 +69,12 @@ const AccountTokensTable = (): ReactElement => { , diff --git a/apps/meteor/client/views/account/tokens/AccountTokensTable/AddToken.tsx b/apps/meteor/client/views/account/tokens/AccountTokensTable/AddToken.tsx index a4d6ef483ca54..fc1d715b5249e 100644 --- a/apps/meteor/client/views/account/tokens/AccountTokensTable/AddToken.tsx +++ b/apps/meteor/client/views/account/tokens/AccountTokensTable/AddToken.tsx @@ -1,6 +1,7 @@ import type { SelectOption } from '@rocket.chat/fuselage'; import { Box, TextInput, Button, Margins, Select } from '@rocket.chat/fuselage'; import { useSetModal, useToastMessageDispatch, useUserId, useMethod } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import { useCallback, useMemo, useEffect } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -50,10 +51,12 @@ const AddToken = ({ reload }: AddTokenProps) => { setModal(null)} onClose={() => setModal(null)}> , diff --git a/apps/meteor/client/views/admin/integrations/NewBot.tsx b/apps/meteor/client/views/admin/integrations/NewBot.tsx index 2294f9cb91555..efa50938534fb 100644 --- a/apps/meteor/client/views/admin/integrations/NewBot.tsx +++ b/apps/meteor/client/views/admin/integrations/NewBot.tsx @@ -1,10 +1,23 @@ import { Box } from '@rocket.chat/fuselage'; +import DOMPurify from 'dompurify'; import { useTranslation } from 'react-i18next'; const NewBot = () => { const { t } = useTranslation(); - return ; + return ( + + ); }; export default NewBot; diff --git a/apps/meteor/client/views/admin/integrations/NewZapier.tsx b/apps/meteor/client/views/admin/integrations/NewZapier.tsx index 565b371f18f61..1842047aa8c52 100644 --- a/apps/meteor/client/views/admin/integrations/NewZapier.tsx +++ b/apps/meteor/client/views/admin/integrations/NewZapier.tsx @@ -2,6 +2,9 @@ import { Box, Skeleton, Margins, Callout } from '@rocket.chat/fuselage'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useOAuthAppQuery } from '../../oauth/hooks/useOAuthAppQuery'; +import PageLoading from '../../root/PageLoading'; + const blogSpotStyleScriptImport = (src: string) => new Promise((resolve) => { const script = document.createElement('script'); @@ -17,6 +20,8 @@ const blogSpotStyleScriptImport = (src: string) => const NewZapier = ({ ...props }) => { const { t } = useTranslation(); + const oauthAppQuery = useOAuthAppQuery('zapier'); + const zapierAvailable = !oauthAppQuery.isLoading && !oauthAppQuery.isError && oauthAppQuery.data; const [script, setScript] = useState(); useEffect(() => { @@ -28,7 +33,7 @@ const NewZapier = ({ ...props }) => { setScript(scriptEl as HTMLScriptElement); }; - if (!script) { + if (!script && zapierAvailable) { importZapier(); } @@ -37,25 +42,39 @@ const NewZapier = ({ ...props }) => { script.parentNode?.removeChild(script); } }; - }, [script]); + }, [script, zapierAvailable]); + + if (oauthAppQuery.isLoading) { + return ; + } return ( <> - - {t('Install_Zapier_from_marketplace')} + + {t(!zapierAvailable ? 'Install_Zapier_from_marketplace_new_workspaces' : 'Install_Zapier_from_marketplace')} - {!script && ( - - - - - - - - - + {zapierAvailable && ( + <> + {!script && ( + + + + + + + + + + )} + + )} - ); }; diff --git a/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx b/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx index f1f641fccfeb5..4df8be1cbeb2f 100644 --- a/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx +++ b/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx @@ -19,6 +19,7 @@ import { } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useAbsoluteUrl } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import { useMemo } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -112,7 +113,7 @@ const IncomingWebhookForm = ({ webhookData }: { webhookData?: Serialized

-										
+										
 									
@@ -180,10 +181,12 @@ const IncomingWebhookForm = ({ webhookData }: { webhookData?: Serialized {errors?.channel && ( @@ -272,7 +275,7 @@ const IncomingWebhookForm = ({ webhookData }: { webhookData?: Serialized{t('You_can_use_an_emoji_as_avatar')} diff --git a/apps/meteor/client/views/admin/integrations/outgoing/OutgoingWebhookForm.tsx b/apps/meteor/client/views/admin/integrations/outgoing/OutgoingWebhookForm.tsx index 3259857f009d9..15db3e86f795f 100644 --- a/apps/meteor/client/views/admin/integrations/outgoing/OutgoingWebhookForm.tsx +++ b/apps/meteor/client/views/admin/integrations/outgoing/OutgoingWebhookForm.tsx @@ -18,6 +18,7 @@ import { } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import { useMemo } from 'react'; import { useFormContext, Controller } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -179,13 +180,18 @@ const OutgoingWebhookForm = () => { - + )} {showTriggerWords && ( @@ -226,10 +232,12 @@ const OutgoingWebhookForm = () => { @@ -359,7 +367,7 @@ const OutgoingWebhookForm = () => { {t('You_can_use_an_emoji_as_avatar')} @@ -435,7 +443,7 @@ const OutgoingWebhookForm = () => {
-										
+										
 									
@@ -485,7 +493,10 @@ const OutgoingWebhookForm = () => { )} /> - +
{event === 'sendMessage' && ( diff --git a/apps/meteor/client/views/admin/integrations/outgoing/history/HistoryItem.tsx b/apps/meteor/client/views/admin/integrations/outgoing/history/HistoryItem.tsx index f016ce36fe7c0..d9581600110c8 100644 --- a/apps/meteor/client/views/admin/integrations/outgoing/history/HistoryItem.tsx +++ b/apps/meteor/client/views/admin/integrations/outgoing/history/HistoryItem.tsx @@ -2,6 +2,7 @@ import type { IIntegrationHistory, Serialized } from '@rocket.chat/core-typings' import { Button, Icon, Box, AccordionItem, Field, FieldGroup, FieldLabel, FieldRow } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { useMethod } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import { useTranslation } from 'react-i18next'; import { outgoingEvents } from '../../../../../../app/integrations/lib/outgoingEvents'; @@ -110,7 +111,7 @@ const HistoryItem = ({ data }: { data: Serialized }) => {
-									
+									
 								
@@ -122,7 +123,7 @@ const HistoryItem = ({ data }: { data: Serialized }) => {
-									
+									
 								
@@ -134,7 +135,7 @@ const HistoryItem = ({ data }: { data: Serialized }) => {
-									
+									
 								
@@ -156,7 +157,7 @@ const HistoryItem = ({ data }: { data: Serialized }) => {
-									
+									
 								
@@ -168,7 +169,7 @@ const HistoryItem = ({ data }: { data: Serialized }) => {
-									
+									
 								
@@ -180,7 +181,7 @@ const HistoryItem = ({ data }: { data: Serialized }) => {
-									
+									
 								
@@ -192,7 +193,7 @@ const HistoryItem = ({ data }: { data: Serialized }) => {
-									
+									
 								
diff --git a/apps/meteor/client/views/admin/mailer/MailerPage.tsx b/apps/meteor/client/views/admin/mailer/MailerPage.tsx index 9f6942368faa7..1a38a80c5d2cc 100644 --- a/apps/meteor/client/views/admin/mailer/MailerPage.tsx +++ b/apps/meteor/client/views/admin/mailer/MailerPage.tsx @@ -15,6 +15,7 @@ import { import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; +import DOMPurify from 'dompurify'; import { Controller, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -175,7 +176,7 @@ const MailerPage = () => { {errors.emailBody.message} )} - +
diff --git a/apps/meteor/client/views/admin/settings/Setting/Setting.tsx b/apps/meteor/client/views/admin/settings/Setting/Setting.tsx index 9fb0a5317b053..484eb34cacfa3 100644 --- a/apps/meteor/client/views/admin/settings/Setting/Setting.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/Setting.tsx @@ -3,6 +3,7 @@ import { isSettingColor, isSetting } from '@rocket.chat/core-typings'; import { Box, Button, Tag } from '@rocket.chat/fuselage'; import { useDebouncedCallback } from '@rocket.chat/fuselage-hooks'; import { useSettingStructure } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; import { useEffect, useMemo, useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -108,7 +109,8 @@ function Setting({ className = undefined, settingId, sectionChanged }: SettingPr ); const callout = useMemo( - () => alert && , + () => + alert && , [alert, i18n, t], ); diff --git a/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/OAuthGroupPage.tsx b/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/OAuthGroupPage.tsx index 50f523571a0e5..0bfb6a8101a2c 100644 --- a/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/OAuthGroupPage.tsx +++ b/apps/meteor/client/views/admin/settings/groups/OAuthGroupPage/OAuthGroupPage.tsx @@ -2,6 +2,7 @@ import type { ISetting } from '@rocket.chat/core-typings'; import { Button } from '@rocket.chat/fuselage'; import { capitalize } from '@rocket.chat/string-helpers'; import { useToastMessageDispatch, useAbsoluteUrl, useMethod, useTranslation, useSetModal } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; import { memo, useEffect, useState } from 'react'; @@ -118,7 +119,7 @@ function OAuthGroupPage({ _id, onClickBack, ...group }: OAuthGroupPageProps): Re help={ } diff --git a/apps/meteor/client/views/admin/users/AdminUserForm.tsx b/apps/meteor/client/views/admin/users/AdminUserForm.tsx index 78d71d91393e3..326a3b6b8e20f 100644 --- a/apps/meteor/client/views/admin/users/AdminUserForm.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserForm.tsx @@ -30,6 +30,7 @@ import { useTranslation, } from '@rocket.chat/ui-contexts'; import { useMutation, useQueryClient } from '@tanstack/react-query'; +import DOMPurify from 'dompurify'; import { useMemo, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; @@ -270,13 +271,15 @@ const AdminUserForm = ({ userData, onReload, context, refetchUserFormData, roleD {isVerificationNeeded && !isSmtpEnabled && ( )} {!isVerificationNeeded && ( )} @@ -429,7 +432,7 @@ const AdminUserForm = ({ userData, onReload, context, refetchUserFormData, roleD {!isSmtpEnabled && ( )} diff --git a/apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordRadios.tsx b/apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordRadios.tsx index 7b06f59307221..aa47bc24b67fc 100644 --- a/apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordRadios.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserSetRandomPasswordRadios.tsx @@ -1,5 +1,6 @@ import { Box, FieldHint, FieldLabel, FieldRow, RadioButton } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import DOMPurify from 'dompurify'; import type { Control, UseFormSetValue } from 'react-hook-form'; import { Controller } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -58,7 +59,7 @@ const AdminUserSetRandomPasswordRadios = ({ {!isSmtpEnabled && ( diff --git a/apps/meteor/client/views/banners/LegacyBanner.tsx b/apps/meteor/client/views/banners/LegacyBanner.tsx index aaf947d195ae1..1227a2a2f8317 100644 --- a/apps/meteor/client/views/banners/LegacyBanner.tsx +++ b/apps/meteor/client/views/banners/LegacyBanner.tsx @@ -1,4 +1,5 @@ import { Banner, Icon } from '@rocket.chat/fuselage'; +import DOMPurify from 'dompurify'; import { useCallback, useEffect } from 'react'; import type { LegacyBannerPayload } from '../../lib/banners'; @@ -50,7 +51,9 @@ const LegacyBanner = ({ config }: LegacyBannerProps) => { onClose={handleClose} > {typeof text === 'function' ? text() : text} - {html &&
} + {html && ( +
+ )} ); }; diff --git a/apps/meteor/client/views/composer/EmojiPicker/EmojiElement.tsx b/apps/meteor/client/views/composer/EmojiPicker/EmojiElement.tsx index 22c54d7b9eda5..e0ae6fad46113 100644 --- a/apps/meteor/client/views/composer/EmojiPicker/EmojiElement.tsx +++ b/apps/meteor/client/views/composer/EmojiPicker/EmojiElement.tsx @@ -1,5 +1,6 @@ import { css } from '@rocket.chat/css-in-js'; import { IconButton } from '@rocket.chat/fuselage'; +import DOMPurify from 'dompurify'; import type { MouseEvent, AllHTMLAttributes } from 'react'; import { memo } from 'react'; @@ -26,7 +27,7 @@ const EmojiElement = ({ emoji, image, onClick, small = false, ...props }: EmojiE } `; - const emojiElement =
; + const emojiElement =
; return ( { let toneEmoji; @@ -23,7 +24,7 @@ const ToneItem = ({ tone }: { tone: number }) => { toneEmoji = ''; } - return ; + return ; }; export default ToneItem; diff --git a/apps/meteor/client/views/e2e/EnterE2EPasswordModal.tsx b/apps/meteor/client/views/e2e/EnterE2EPasswordModal.tsx index 8227cd6da79ef..888afa1d7b3e6 100644 --- a/apps/meteor/client/views/e2e/EnterE2EPasswordModal.tsx +++ b/apps/meteor/client/views/e2e/EnterE2EPasswordModal.tsx @@ -1,5 +1,6 @@ import { Box, PasswordInput, Field, FieldGroup, FieldRow, FieldError } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import DOMPurify from 'dompurify'; import type { ChangeEvent, ReactElement } from 'react'; import { useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -48,7 +49,7 @@ const EnterE2EPasswordModal = ({ onClose={onClose} onCancel={onCancel} > - + diff --git a/apps/meteor/client/views/e2e/SaveE2EPasswordModal.tsx b/apps/meteor/client/views/e2e/SaveE2EPasswordModal.tsx index c9e7fa1a8abec..1511b71686d5f 100644 --- a/apps/meteor/client/views/e2e/SaveE2EPasswordModal.tsx +++ b/apps/meteor/client/views/e2e/SaveE2EPasswordModal.tsx @@ -1,6 +1,7 @@ import { Box, CodeSnippet } from '@rocket.chat/fuselage'; import { useClipboard } from '@rocket.chat/fuselage-hooks'; import { ExternalLink } from '@rocket.chat/ui-client'; +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; @@ -31,7 +32,7 @@ const SaveE2EPasswordModal = ({ randomPassword, onClose, onCancel, onConfirm }: annotation={t('You_can_do_from_account_preferences')} >

- + {t('Learn_more_about_E2EE')} diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogsItemEntry.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogsItemEntry.tsx index 91c18baa01d16..1743fa6013e7e 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogsItemEntry.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogsItemEntry.tsx @@ -1,4 +1,5 @@ import { Box } from '@rocket.chat/fuselage'; +import DOMPurify from 'dompurify'; import { useTranslation } from 'react-i18next'; import { useHighlightedCode } from '../../../../../hooks/useHighlightedCode'; @@ -22,7 +23,7 @@ const AppLogsItemEntry = ({ severity, timestamp, caller, args }: AppLogsItemEntr

 					
 				
diff --git a/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx b/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx index 08ccddf9ca8c1..40c54dfe1499f 100644 --- a/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx +++ b/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx @@ -1,12 +1,11 @@ import type { Box } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { GenericMenu } from '@rocket.chat/ui-client'; -import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; -import { useLayout } from '@rocket.chat/ui-contexts'; import type { ComponentProps } from 'react'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; +import { useRoomToolboxActions } from './hooks/useRoomToolboxActions'; import { HeaderToolbarAction, HeaderToolbarDivider } from '../../../../components/Header'; import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; import type { RoomToolboxActionConfig } from '../../contexts/RoomToolboxContext'; @@ -15,48 +14,13 @@ type RoomToolboxProps = { className?: ComponentProps['className']; }; -type MenuActionsProps = { - id: string; - items: GenericMenuItemProps[]; -}[]; - const RoomToolbox = ({ className }: RoomToolboxProps) => { const { t } = useTranslation(); - const { roomToolboxExpanded } = useLayout(); const toolbox = useRoomToolbox(); - const { actions, openTab } = toolbox; - - const featuredActions = actions.filter((action) => action.featured); - const normalActions = actions.filter((action) => !action.featured); - const visibleActions = !roomToolboxExpanded ? [] : normalActions.slice(0, 6); - - const hiddenActions = (!roomToolboxExpanded ? actions : normalActions.slice(6)) - .filter((item) => !item.disabled && !item.featured) - .map((item) => ({ - 'key': item.id, - 'content': t(item.title), - 'onClick': - item.action ?? - ((): void => { - openTab(item.id); - }), - 'data-qa-id': `ToolBoxAction-${item.icon}`, - ...item, - })) - .reduce((acc, item) => { - const group = item.type ? item.type : ''; - const section = acc.find((section: { id: string }) => section.id === group); - if (section) { - section.items.push(item); - return acc; - } - - const newSection = { id: group, key: item.key, title: group === 'apps' ? t('Apps') : '', items: [item] }; - acc.push(newSection); + const { featuredActions, hiddenActions, visibleActions } = useRoomToolboxActions(toolbox); - return acc; - }, [] as MenuActionsProps); + const showKebabMenu = hiddenActions.length > 0; const renderDefaultToolboxItem: RoomToolboxActionConfig['renderToolboxItem'] = useEffectEvent( ({ id, className, index, icon, title, toolbox: { tab }, action, disabled, tooltip }) => { @@ -92,9 +56,7 @@ const RoomToolbox = ({ className }: RoomToolboxProps) => { {featuredActions.map(mapToToolboxItem)} {featuredActions.length > 0 && } {visibleActions.map(mapToToolboxItem)} - {(normalActions.length > 6 || !roomToolboxExpanded) && !!hiddenActions.length && ( - - )} + {showKebabMenu && } ); }; diff --git a/apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.spec.ts b/apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.spec.ts new file mode 100644 index 0000000000000..555cf915aa18f --- /dev/null +++ b/apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.spec.ts @@ -0,0 +1,172 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { renderHook } from '@testing-library/react'; + +import { useRoomToolboxActions } from './useRoomToolboxActions'; +import type { RoomToolboxActionConfig } from '../../../contexts/RoomToolboxContext'; + +describe('useRoomToolboxActions', () => { + it('should return an empty array if there are no actions', () => { + const { result } = renderHook(() => useRoomToolboxActions({ actions: [], openTab: () => undefined }), { + legacyRoot: true, + wrapper: mockAppRoot().build(), + }); + expect(result.current.featuredActions).toEqual([]); + expect(result.current.hiddenActions).toEqual([]); + expect(result.current.visibleActions).toEqual([]); + }); + + it('should return apps actions only inside hiddenActions', () => { + const { result } = renderHook(() => useRoomToolboxActions({ actions: appsActions, openTab: () => undefined }), { + legacyRoot: true, + wrapper: mockAppRoot().build(), + }); + const appsSection = result.current.hiddenActions[0]; + const appsItems = appsSection.items; + + expect(appsSection).toBeDefined(); + expect(appsSection).toHaveProperty('id', 'apps'); + expect(appsItems).toMatchObject(appsActions); + }); + + it('should return max of 6 items on visibleActions and the rest items inside hiddenActions', () => { + const { result } = renderHook(() => useRoomToolboxActions({ actions, openTab: () => undefined }), { + legacyRoot: true, + wrapper: mockAppRoot().build(), + }); + expect(result.current.hiddenActions.length).toBeGreaterThan(0); + expect(result.current.visibleActions.length).toBe(6); + }); + + it('should return featured items inside featuredActions', () => { + const { result } = renderHook(() => useRoomToolboxActions({ actions, openTab: () => undefined }), { + legacyRoot: true, + wrapper: mockAppRoot().build(), + }); + expect(result.current.featuredActions).toMatchObject(actions.filter((action) => action.featured)); + }); +}); + +const appsActions: RoomToolboxActionConfig[] = [ + { + id: 'app1', + title: 'app-42212581-0966-44aa-8366-b3e92aa00df4.action_button_label_files', + groups: ['group', 'channel', 'live', 'team', 'direct', 'direct_multiple'], + type: 'apps', + }, + { + id: 'app2', + title: 'app-42212581-0966-44aa-8366-b3e92aa00df4.action_button_label_files', + groups: ['group', 'channel', 'live', 'team', 'direct', 'direct_multiple'], + type: 'apps', + }, +]; + +const actions: RoomToolboxActionConfig[] = [ + { + id: 'team-info', + groups: ['team'], + anonymous: true, + full: true, + title: 'Teams_Info', + icon: 'info-circled', + order: 1, + }, + { + id: 'thread', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'team'], + full: true, + title: 'Threads', + icon: 'thread', + order: 2, + }, + { + id: 'team-channels', + groups: ['team'], + anonymous: true, + full: true, + title: 'Team_Channels', + icon: 'hash', + order: 2, + }, + { + id: 'discussions', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'team'], + title: 'Discussions', + icon: 'discussion', + full: true, + order: 3, + }, + { + id: 'start-call', + title: 'Call', + icon: 'phone', + groups: ['direct', 'direct_multiple', 'group', 'team', 'channel', 'direct'], + disabled: false, + full: true, + order: 4, + featured: true, + }, + { + id: 'rocket-search', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'live', 'team'], + title: 'Search_Messages', + icon: 'magnifier', + order: 5, + }, + { + id: 'mentions', + groups: ['channel', 'group', 'team'], + title: 'Mentions', + icon: 'at', + order: 6, + type: 'organization', + }, + { + id: 'members-list', + groups: ['channel', 'group', 'team'], + title: 'Teams_members', + icon: 'members', + order: 7, + }, + { + id: 'uploaded-files-list', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'live', 'team'], + title: 'Files', + icon: 'clip', + order: 8, + type: 'organization', + }, + { + id: 'pinned-messages', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'team'], + title: 'Pinned_Messages', + icon: 'pin', + order: 9, + type: 'organization', + }, + { + id: 'starred-messages', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'team'], + title: 'Starred_Messages', + icon: 'star', + order: 10, + type: 'organization', + }, + { + id: 'keyboard-shortcut-list', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'team'], + title: 'Keyboard_Shortcuts_Title', + icon: 'keyboard', + order: 99, + type: 'customization', + }, + { + id: 'clean-history', + groups: ['channel', 'group', 'team', 'direct_multiple', 'direct'], + full: true, + title: 'Prune_Messages', + icon: 'eraser', + order: 250, + type: 'customization', + }, +]; diff --git a/apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.ts b/apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.ts new file mode 100644 index 0000000000000..fb328fcf2f148 --- /dev/null +++ b/apps/meteor/client/views/room/Header/RoomToolbox/hooks/useRoomToolboxActions.ts @@ -0,0 +1,49 @@ +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import { useLayout } from '@rocket.chat/ui-contexts'; +import { useTranslation } from 'react-i18next'; + +import type { RoomToolboxContextValue } from '../../../contexts/RoomToolboxContext'; + +type MenuActionsProps = { + id: string; + items: GenericMenuItemProps[]; +}[]; + +export const useRoomToolboxActions = ({ actions, openTab }: Pick) => { + const { t } = useTranslation(); + const { roomToolboxExpanded } = useLayout(); + + const normalActions = actions.filter((action) => !action.featured && action.type !== 'apps'); + const featuredActions = actions.filter((action) => action.featured); + const appsActions = actions.filter((action) => action.type === 'apps'); + const visibleActions = !roomToolboxExpanded ? [] : normalActions.slice(0, 6); + + const hiddenActions = (!roomToolboxExpanded ? actions : [...appsActions, ...normalActions.slice(6)]) + .filter((item) => !item.disabled && !item.featured) + .map((item) => ({ + 'key': item.id, + 'content': t(item.title), + 'onClick': + item.action ?? + ((): void => { + openTab(item.id); + }), + 'data-qa-id': `ToolBoxAction-${item.icon}`, + ...item, + })) + .reduce((acc, item) => { + const group = item.type ? item.type : ''; + const section = acc.find((section: { id: string }) => section.id === group); + if (section) { + section.items.push(item); + return acc; + } + + const newSection = { id: group, key: item.key, title: group === 'apps' ? t('Apps') : '', items: [item] }; + acc.push(newSection); + + return acc; + }, [] as MenuActionsProps); + + return { hiddenActions, featuredActions, visibleActions }; +}; diff --git a/apps/meteor/client/views/room/HeaderV2/RoomToolbox/RoomToolbox.tsx b/apps/meteor/client/views/room/HeaderV2/RoomToolbox/RoomToolbox.tsx index 08ccddf9ca8c1..40c54dfe1499f 100644 --- a/apps/meteor/client/views/room/HeaderV2/RoomToolbox/RoomToolbox.tsx +++ b/apps/meteor/client/views/room/HeaderV2/RoomToolbox/RoomToolbox.tsx @@ -1,12 +1,11 @@ import type { Box } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { GenericMenu } from '@rocket.chat/ui-client'; -import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; -import { useLayout } from '@rocket.chat/ui-contexts'; import type { ComponentProps } from 'react'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; +import { useRoomToolboxActions } from './hooks/useRoomToolboxActions'; import { HeaderToolbarAction, HeaderToolbarDivider } from '../../../../components/Header'; import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; import type { RoomToolboxActionConfig } from '../../contexts/RoomToolboxContext'; @@ -15,48 +14,13 @@ type RoomToolboxProps = { className?: ComponentProps['className']; }; -type MenuActionsProps = { - id: string; - items: GenericMenuItemProps[]; -}[]; - const RoomToolbox = ({ className }: RoomToolboxProps) => { const { t } = useTranslation(); - const { roomToolboxExpanded } = useLayout(); const toolbox = useRoomToolbox(); - const { actions, openTab } = toolbox; - - const featuredActions = actions.filter((action) => action.featured); - const normalActions = actions.filter((action) => !action.featured); - const visibleActions = !roomToolboxExpanded ? [] : normalActions.slice(0, 6); - - const hiddenActions = (!roomToolboxExpanded ? actions : normalActions.slice(6)) - .filter((item) => !item.disabled && !item.featured) - .map((item) => ({ - 'key': item.id, - 'content': t(item.title), - 'onClick': - item.action ?? - ((): void => { - openTab(item.id); - }), - 'data-qa-id': `ToolBoxAction-${item.icon}`, - ...item, - })) - .reduce((acc, item) => { - const group = item.type ? item.type : ''; - const section = acc.find((section: { id: string }) => section.id === group); - if (section) { - section.items.push(item); - return acc; - } - - const newSection = { id: group, key: item.key, title: group === 'apps' ? t('Apps') : '', items: [item] }; - acc.push(newSection); + const { featuredActions, hiddenActions, visibleActions } = useRoomToolboxActions(toolbox); - return acc; - }, [] as MenuActionsProps); + const showKebabMenu = hiddenActions.length > 0; const renderDefaultToolboxItem: RoomToolboxActionConfig['renderToolboxItem'] = useEffectEvent( ({ id, className, index, icon, title, toolbox: { tab }, action, disabled, tooltip }) => { @@ -92,9 +56,7 @@ const RoomToolbox = ({ className }: RoomToolboxProps) => { {featuredActions.map(mapToToolboxItem)} {featuredActions.length > 0 && } {visibleActions.map(mapToToolboxItem)} - {(normalActions.length > 6 || !roomToolboxExpanded) && !!hiddenActions.length && ( - - )} + {showKebabMenu && } ); }; diff --git a/apps/meteor/client/views/room/HeaderV2/RoomToolbox/hooks/useRoomToolboxActions.spec.ts b/apps/meteor/client/views/room/HeaderV2/RoomToolbox/hooks/useRoomToolboxActions.spec.ts new file mode 100644 index 0000000000000..555cf915aa18f --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/RoomToolbox/hooks/useRoomToolboxActions.spec.ts @@ -0,0 +1,172 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { renderHook } from '@testing-library/react'; + +import { useRoomToolboxActions } from './useRoomToolboxActions'; +import type { RoomToolboxActionConfig } from '../../../contexts/RoomToolboxContext'; + +describe('useRoomToolboxActions', () => { + it('should return an empty array if there are no actions', () => { + const { result } = renderHook(() => useRoomToolboxActions({ actions: [], openTab: () => undefined }), { + legacyRoot: true, + wrapper: mockAppRoot().build(), + }); + expect(result.current.featuredActions).toEqual([]); + expect(result.current.hiddenActions).toEqual([]); + expect(result.current.visibleActions).toEqual([]); + }); + + it('should return apps actions only inside hiddenActions', () => { + const { result } = renderHook(() => useRoomToolboxActions({ actions: appsActions, openTab: () => undefined }), { + legacyRoot: true, + wrapper: mockAppRoot().build(), + }); + const appsSection = result.current.hiddenActions[0]; + const appsItems = appsSection.items; + + expect(appsSection).toBeDefined(); + expect(appsSection).toHaveProperty('id', 'apps'); + expect(appsItems).toMatchObject(appsActions); + }); + + it('should return max of 6 items on visibleActions and the rest items inside hiddenActions', () => { + const { result } = renderHook(() => useRoomToolboxActions({ actions, openTab: () => undefined }), { + legacyRoot: true, + wrapper: mockAppRoot().build(), + }); + expect(result.current.hiddenActions.length).toBeGreaterThan(0); + expect(result.current.visibleActions.length).toBe(6); + }); + + it('should return featured items inside featuredActions', () => { + const { result } = renderHook(() => useRoomToolboxActions({ actions, openTab: () => undefined }), { + legacyRoot: true, + wrapper: mockAppRoot().build(), + }); + expect(result.current.featuredActions).toMatchObject(actions.filter((action) => action.featured)); + }); +}); + +const appsActions: RoomToolboxActionConfig[] = [ + { + id: 'app1', + title: 'app-42212581-0966-44aa-8366-b3e92aa00df4.action_button_label_files', + groups: ['group', 'channel', 'live', 'team', 'direct', 'direct_multiple'], + type: 'apps', + }, + { + id: 'app2', + title: 'app-42212581-0966-44aa-8366-b3e92aa00df4.action_button_label_files', + groups: ['group', 'channel', 'live', 'team', 'direct', 'direct_multiple'], + type: 'apps', + }, +]; + +const actions: RoomToolboxActionConfig[] = [ + { + id: 'team-info', + groups: ['team'], + anonymous: true, + full: true, + title: 'Teams_Info', + icon: 'info-circled', + order: 1, + }, + { + id: 'thread', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'team'], + full: true, + title: 'Threads', + icon: 'thread', + order: 2, + }, + { + id: 'team-channels', + groups: ['team'], + anonymous: true, + full: true, + title: 'Team_Channels', + icon: 'hash', + order: 2, + }, + { + id: 'discussions', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'team'], + title: 'Discussions', + icon: 'discussion', + full: true, + order: 3, + }, + { + id: 'start-call', + title: 'Call', + icon: 'phone', + groups: ['direct', 'direct_multiple', 'group', 'team', 'channel', 'direct'], + disabled: false, + full: true, + order: 4, + featured: true, + }, + { + id: 'rocket-search', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'live', 'team'], + title: 'Search_Messages', + icon: 'magnifier', + order: 5, + }, + { + id: 'mentions', + groups: ['channel', 'group', 'team'], + title: 'Mentions', + icon: 'at', + order: 6, + type: 'organization', + }, + { + id: 'members-list', + groups: ['channel', 'group', 'team'], + title: 'Teams_members', + icon: 'members', + order: 7, + }, + { + id: 'uploaded-files-list', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'live', 'team'], + title: 'Files', + icon: 'clip', + order: 8, + type: 'organization', + }, + { + id: 'pinned-messages', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'team'], + title: 'Pinned_Messages', + icon: 'pin', + order: 9, + type: 'organization', + }, + { + id: 'starred-messages', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'team'], + title: 'Starred_Messages', + icon: 'star', + order: 10, + type: 'organization', + }, + { + id: 'keyboard-shortcut-list', + groups: ['channel', 'group', 'direct', 'direct_multiple', 'team'], + title: 'Keyboard_Shortcuts_Title', + icon: 'keyboard', + order: 99, + type: 'customization', + }, + { + id: 'clean-history', + groups: ['channel', 'group', 'team', 'direct_multiple', 'direct'], + full: true, + title: 'Prune_Messages', + icon: 'eraser', + order: 250, + type: 'customization', + }, +]; diff --git a/apps/meteor/client/views/room/HeaderV2/RoomToolbox/hooks/useRoomToolboxActions.ts b/apps/meteor/client/views/room/HeaderV2/RoomToolbox/hooks/useRoomToolboxActions.ts new file mode 100644 index 0000000000000..fb328fcf2f148 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/RoomToolbox/hooks/useRoomToolboxActions.ts @@ -0,0 +1,49 @@ +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import { useLayout } from '@rocket.chat/ui-contexts'; +import { useTranslation } from 'react-i18next'; + +import type { RoomToolboxContextValue } from '../../../contexts/RoomToolboxContext'; + +type MenuActionsProps = { + id: string; + items: GenericMenuItemProps[]; +}[]; + +export const useRoomToolboxActions = ({ actions, openTab }: Pick) => { + const { t } = useTranslation(); + const { roomToolboxExpanded } = useLayout(); + + const normalActions = actions.filter((action) => !action.featured && action.type !== 'apps'); + const featuredActions = actions.filter((action) => action.featured); + const appsActions = actions.filter((action) => action.type === 'apps'); + const visibleActions = !roomToolboxExpanded ? [] : normalActions.slice(0, 6); + + const hiddenActions = (!roomToolboxExpanded ? actions : [...appsActions, ...normalActions.slice(6)]) + .filter((item) => !item.disabled && !item.featured) + .map((item) => ({ + 'key': item.id, + 'content': t(item.title), + 'onClick': + item.action ?? + ((): void => { + openTab(item.id); + }), + 'data-qa-id': `ToolBoxAction-${item.icon}`, + ...item, + })) + .reduce((acc, item) => { + const group = item.type ? item.type : ''; + const section = acc.find((section: { id: string }) => section.id === group); + if (section) { + section.items.push(item); + return acc; + } + + const newSection = { id: group, key: item.key, title: group === 'apps' ? t('Apps') : '', items: [item] }; + acc.push(newSection); + + return acc; + }, [] as MenuActionsProps); + + return { hiddenActions, featuredActions, visibleActions }; +}; diff --git a/apps/meteor/client/views/room/contextualBar/MessageSearchTab/components/MessageSearchForm.tsx b/apps/meteor/client/views/room/contextualBar/MessageSearchTab/components/MessageSearchForm.tsx index 1da4c0c4a9789..e409df9ee677c 100644 --- a/apps/meteor/client/views/room/contextualBar/MessageSearchTab/components/MessageSearchForm.tsx +++ b/apps/meteor/client/views/room/contextualBar/MessageSearchTab/components/MessageSearchForm.tsx @@ -2,6 +2,7 @@ import type { IMessageSearchProvider } from '@rocket.chat/core-typings'; import { Box, Field, FieldLabel, FieldHint, Icon, TextInput, ToggleSwitch, Callout } from '@rocket.chat/fuselage'; import { useDebouncedCallback, useEffectEvent, useUniqueId } from '@rocket.chat/fuselage-hooks'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import { useEffect } from 'react'; import { useForm, useWatch } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -57,7 +58,9 @@ const MessageSearchForm = ({ provider, onSearch }: MessageSearchFormProps) => { autoComplete='off' {...register('searchText')} /> - {provider.description && } + {provider.description && ( + + )}
{globalSearchEnabled && ( diff --git a/apps/meteor/tests/end-to-end/api/oauthapps.ts b/apps/meteor/tests/end-to-end/api/oauthapps.ts index 5e42069d99341..39fa944223475 100644 --- a/apps/meteor/tests/end-to-end/api/oauthapps.ts +++ b/apps/meteor/tests/end-to-end/api/oauthapps.ts @@ -50,79 +50,6 @@ describe('[OAuthApps]', () => { }); }); - describe('[/oauth-apps.get]', () => { - before(() => updatePermission('manage-oauth-apps', ['admin'])); - after(() => updatePermission('manage-oauth-apps', ['admin'])); - - it('should return a single oauthApp by id', () => { - return request - .get(api('oauth-apps.get')) - .query({ appId: 'zapier' }) - .set(credentials) - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('oauthApp'); - expect(res.body.oauthApp._id).to.be.equal('zapier'); - expect(res.body.oauthApp).to.have.property('clientSecret'); - }); - }); - it('should return a single oauthApp by client id', () => { - return request - .get(api('oauth-apps.get')) - .query({ clientId: 'zapier' }) - .set(credentials) - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('oauthApp'); - expect(res.body.oauthApp._id).to.be.equal('zapier'); - expect(res.body.oauthApp).to.have.property('clientSecret'); - }); - }); - it('should return only non sensitive information if user does not have the permission to manage oauth apps when searching by clientId', async () => { - await updatePermission('manage-oauth-apps', []); - await request - .get(api('oauth-apps.get')) - .query({ clientId: 'zapier' }) - .set(credentials) - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('oauthApp'); - expect(res.body.oauthApp._id).to.be.equal('zapier'); - expect(res.body.oauthApp.clientId).to.be.equal('zapier'); - expect(res.body.oauthApp).to.not.have.property('clientSecret'); - }); - }); - it('should return only non sensitive information if user does not have the permission to manage oauth apps when searching by appId', async () => { - await updatePermission('manage-oauth-apps', []); - await request - .get(api('oauth-apps.get')) - .query({ appId: 'zapier' }) - .set(credentials) - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('oauthApp'); - expect(res.body.oauthApp._id).to.be.equal('zapier'); - expect(res.body.oauthApp.clientId).to.be.equal('zapier'); - expect(res.body.oauthApp).to.not.have.property('clientSecret'); - }); - }); - it('should fail returning an oauth app when an invalid id is provided (avoid NoSQL injections)', () => { - return request - .get(api('oauth-apps.get')) - .query({ _id: '{ "$ne": "" }' }) - .set(credentials) - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', 'OAuth app not found.'); - }); - }); - }); - describe('[/oauth-apps.create]', () => { it('should return an error when the user does not have the necessary permission', async () => { await updatePermission('manage-oauth-apps', []); @@ -220,6 +147,213 @@ describe('[OAuthApps]', () => { }); }); + describe('[/oauth-apps.get]', () => { + let clientId = ''; + let _id = ''; + let clientSecret = ''; + + before(async () => { + await updatePermission('manage-oauth-apps', ['admin']); + + const res = await request + .post(api('oauth-apps.create')) + .set(credentials) + .send({ + name: `new app ${Date.now()}`, + redirectUri: 'http://localhost:3000', + active: true, + }); + + if (res.statusCode !== 200 || !res.body?.success || !res.body.application?._id || !res.body.application?.clientId) { + console.error(res); + throw new Error('Failed to create oauth app for tests'); + } + + clientId = res.body.application.clientId; + _id = res.body.application._id; + clientSecret = res.body.application.clientSecret; + createdAppsIds.push(_id); + }); + after(() => updatePermission('manage-oauth-apps', ['admin'])); + + it('should return a single oauthApp by client id', () => { + return request + .get(api('oauth-apps.get')) + .query({ clientId }) + .set(credentials) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('oauthApp'); + expect(res.body.oauthApp._id).to.be.equal(_id); + expect(res.body.oauthApp).to.have.property('clientSecret'); + + if (clientSecret) { + expect(res.body.oauthApp.clientSecret).to.be.equal(clientSecret); + } + }); + }); + + it('should return a single oauthApp by _id', () => { + return request + .get(api('oauth-apps.get')) + .query({ _id }) + .set(credentials) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('oauthApp'); + expect(res.body.oauthApp._id).to.be.equal(_id); + expect(res.body.oauthApp.clientId).to.be.equal(clientId); + expect(res.body.oauthApp).to.have.property('clientSecret'); + if (clientSecret) { + expect(res.body.oauthApp.clientSecret).to.be.equal(clientSecret); + } + }); + }); + + it('should return a single oauthApp by appId (deprecated)', () => { + return request + .get(api('oauth-apps.get')) + .query({ appId: _id }) + .set(credentials) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('oauthApp'); + expect(res.body.oauthApp._id).to.be.equal(_id); + expect(res.body.oauthApp.clientId).to.be.equal(clientId); + expect(res.body.oauthApp).to.have.property('clientSecret'); + if (clientSecret) { + expect(res.body.oauthApp.clientSecret).to.be.equal(clientSecret); + } + }); + }); + + it('should return only non sensitive information if user does not have the permission to manage oauth apps when searching by clientId', async () => { + await updatePermission('manage-oauth-apps', []); + await request + .get(api('oauth-apps.get')) + .query({ clientId }) + .set(credentials) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('oauthApp'); + expect(res.body.oauthApp._id).to.be.equal(_id); + expect(res.body.oauthApp.clientId).to.be.equal(clientId); + expect(res.body.oauthApp).to.not.have.property('clientSecret'); + }); + }); + + it('should return only non sensitive information if user does not have the permission to manage oauth apps when searching by _id', async () => { + await updatePermission('manage-oauth-apps', []); + await request + .get(api('oauth-apps.get')) + .query({ _id }) + .set(credentials) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('oauthApp'); + expect(res.body.oauthApp._id).to.be.equal(_id); + expect(res.body.oauthApp.clientId).to.be.equal(clientId); + expect(res.body.oauthApp).to.not.have.property('clientSecret'); + }); + }); + + it('should return only non sensitive information if user does not have the permission to manage oauth apps when searching by appId (deprecated)', async () => { + await updatePermission('manage-oauth-apps', []); + await request + .get(api('oauth-apps.get')) + .query({ appId: _id }) + .set(credentials) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('oauthApp'); + expect(res.body.oauthApp._id).to.be.equal(_id); + expect(res.body.oauthApp.clientId).to.be.equal(clientId); + expect(res.body.oauthApp).to.not.have.property('clientSecret'); + }); + }); + + it('should fail returning an oauth app when an invalid id is provided (avoid NoSQL injections)', () => { + return request + .get(api('oauth-apps.get')) + .query({ _id: { $ne: '' } }) + .set(credentials) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + expect(res.body.error).to.include('must be string').and.include('must match exactly one schema in oneOf [invalid-params]'); + }); + }); + + it('should fail returning an oauth app when an invalid id string is provided (avoid NoSQL injections)', () => { + return request + .get(api('oauth-apps.get')) + .query({ _id: '{ "$ne": "" }' }) + .set(credentials) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'OAuth app not found.'); + }); + }); + + it('should fail returning an oauth app when an invalid clientId is provided (avoid NoSQL injections)', () => { + return request + .get(api('oauth-apps.get')) + .query({ clientId: { $ne: '' } }) + .set(credentials) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + expect(res.body.error).to.include('must be string').and.include('must match exactly one schema in oneOf [invalid-params]'); + }); + }); + + it('should fail returning an oauth app when an invalid clientId string is provided (avoid NoSQL injections)', () => { + return request + .get(api('oauth-apps.get')) + .query({ clientId: '{ "$ne": "" }' }) + .set(credentials) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'OAuth app not found.'); + }); + }); + + it('should fail returning an oauth app when an invalid appId is provided (avoid NoSQL injections; deprecated)', () => { + return request + .get(api('oauth-apps.get')) + .query({ appId: { $ne: '' } }) + .set(credentials) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + expect(res.body.error).to.include('must be string').and.include('must match exactly one schema in oneOf [invalid-params]'); + }); + }); + + it('should fail returning an oauth app when an invalid appId string is provided (avoid NoSQL injections; deprecated)', () => { + return request + .get(api('oauth-apps.get')) + .query({ appId: '{ "$ne": "" }' }) + .set(credentials) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'OAuth app not found.'); + }); + }); + }); + describe('[/oauth-apps.update]', () => { let appId: IOAuthApps['_id']; diff --git a/ee/packages/omnichannel-services/src/QueueWorker.ts b/ee/packages/omnichannel-services/src/QueueWorker.ts index 5908512404a2b..5950bbd128ea3 100644 --- a/ee/packages/omnichannel-services/src/QueueWorker.ts +++ b/ee/packages/omnichannel-services/src/QueueWorker.ts @@ -11,14 +11,12 @@ export class QueueWorker extends ServiceClass implements IQueueWorkerService { protected retryCount = 5; // Default delay is 5 seconds - protected retryDelay = 5000; + protected retryDelay = Number(process.env.RETRY_DELAY) || 5000; protected queue: MessageQueue; private logger: Logger; - private queueStarted = false; - constructor( private readonly db: Db, loggerClass: typeof Logger, @@ -28,7 +26,7 @@ export class QueueWorker extends ServiceClass implements IQueueWorkerService { // eslint-disable-next-line new-cap this.logger = new loggerClass('QueueWorker'); this.queue = new MessageQueue(); - this.queue.pollingInterval = 5000; + this.queue.pollingInterval = Number(process.env.POLLING_INTERVAL) || 5000; } isServiceNotFoundMessage(message: string): boolean { @@ -46,6 +44,7 @@ export class QueueWorker extends ServiceClass implements IQueueWorkerService { try { await this.createIndexes(); + this.registerWorkers(); } catch (e) { this.logger.fatal(e, 'Fatal error occurred when registering workers'); process.exit(1); @@ -55,7 +54,7 @@ export class QueueWorker extends ServiceClass implements IQueueWorkerService { async createIndexes(): Promise { this.logger.info('Creating indexes for queue worker'); - // Library doesnt create indexes by itself, for some reason + // Library doesn't create indexes by itself, for some reason // This should create the indexes we need and improve queue perf on reading await this.db.collection(this.queue.collectionName).createIndex({ type: 1 }); await this.db.collection(this.queue.collectionName).createIndex({ rejectedTime: 1 }, { sparse: true }); @@ -105,8 +104,6 @@ export class QueueWorker extends ServiceClass implements IQueueWorkerService { this.logger.info('Registering workers of type "workComplete"'); this.queue.registerWorker('workComplete', this.workerCallback.bind(this)); - - this.queueStarted = true; } private matchServiceCall(service: string): boolean { @@ -123,10 +120,6 @@ export class QueueWorker extends ServiceClass implements IQueueWorkerService { // This is a "generic" job that allows you to call any service async queueWork>(queue: Actions, to: string, data: T): Promise { this.logger.info(`Queueing work for ${to}`); - if (!this.queueStarted) { - this.registerWorkers(); - } - if (!this.matchServiceCall(to)) { // We don't want to queue calls to invalid service names throw new Error(`Invalid service name ${to}`); @@ -150,8 +143,4 @@ export class QueueWorker extends ServiceClass implements IQueueWorkerService { ]) .toArray(); } - - async isQueueStarted(): Promise { - return this.queueStarted; - } } diff --git a/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyCreator.ts b/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyCreator.ts index 00b4640295e51..f4509990edc99 100644 --- a/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyCreator.ts +++ b/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyCreator.ts @@ -17,6 +17,7 @@ import type { ILivechatMessageBuilder } from '@rocket.chat/apps-engine/definitio import type { UIHelper as _UIHelper } from '@rocket.chat/apps-engine/server/misc/UIHelper.ts'; import * as Messenger from '../../messenger.ts'; +import { randomBytes } from 'node:crypto'; import { BlockBuilder } from '../builders/BlockBuilder.ts'; import { MessageBuilder } from '../builders/MessageBuilder.ts'; @@ -45,7 +46,7 @@ export class ModifyCreator implements IModifyCreator { get: (_target: unknown, prop: string) => { // It's not worthwhile to make an asynchronous request for such a simple method if (prop === 'createToken') { - return () => Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); + return () => randomBytes(16).toString('hex'); } if (prop === 'toJSON') { diff --git a/packages/apps-engine/src/server/accessors/LivechatCreator.ts b/packages/apps-engine/src/server/accessors/LivechatCreator.ts index 0462230b97b04..b0cfbf5be4810 100644 --- a/packages/apps-engine/src/server/accessors/LivechatCreator.ts +++ b/packages/apps-engine/src/server/accessors/LivechatCreator.ts @@ -1,3 +1,5 @@ +import { randomBytes } from 'crypto'; + import type { ILivechatCreator } from '../../definition/accessors'; import type { IExtraRoomParams } from '../../definition/accessors/ILivechatCreator'; import type { ILivechatRoom } from '../../definition/livechat/ILivechatRoom'; @@ -27,6 +29,6 @@ export class LivechatCreator implements ILivechatCreator { } public createToken(): string { - return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); + return randomBytes(16).toString('hex'); // Ensures 128 bits of entropy } } diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index a7f3bbfe7a69a..d8fa2449a2127 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -5513,7 +5513,9 @@ "This_is_a_desktop_notification": "This is a desktop notification", "This_is_a_deprecated_feature_alert": "This is a deprecated feature. It may not work as expected and will not get new updates.", "Zapier_integration_has_been_deprecated": "The Zapier integration has been deprecated, may not work as expected and will not receive updates", + "Zapier_integration_is_not_available": "The Zapier integration has been deprecated and is no longer available for new Rocket.Chat workspaces", "Install_Zapier_from_marketplace": "Install the Zapier app from Marketplace to avoid disruptions", + "Install_Zapier_from_marketplace_new_workspaces": "Install the Zapier app from Marketplace to configure new integrations", "Input": "Input", "This_is_a_push_test_messsage": "This is a push test message", "This_message_was_rejected_by__peer__peer": "This message was rejected by {{peer}} peer.", diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index a8587428ef527..ef0602ab7e753 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -38,6 +38,7 @@ "@storybook/react": "^8.4.4", "@storybook/react-webpack5": "^8.4.4", "@testing-library/react": "~16.0.1", + "@types/dompurify": "^3.0.5", "@types/jest": "~29.5.14", "@types/react": "~17.0.83", "@types/react-dom": "~17.0.26", @@ -67,5 +68,8 @@ }, "volta": { "extends": "../../package.json" + }, + "dependencies": { + "dompurify": "^3.2.0" } } diff --git a/packages/ui-client/src/components/EmojiPicker/EmojiPickerPreview.tsx b/packages/ui-client/src/components/EmojiPicker/EmojiPickerPreview.tsx index dd6a15a468b0e..332f6c831f55e 100644 --- a/packages/ui-client/src/components/EmojiPicker/EmojiPickerPreview.tsx +++ b/packages/ui-client/src/components/EmojiPicker/EmojiPickerPreview.tsx @@ -1,5 +1,6 @@ import { css } from '@rocket.chat/css-in-js'; import { Box } from '@rocket.chat/fuselage'; +import DOMPurify from 'dompurify'; import type { AllHTMLAttributes } from 'react'; const EmojiPickerPreview = ({ emoji, name, ...props }: { emoji: string; name: string } & Omit, 'is'>) => { @@ -12,7 +13,7 @@ const EmojiPickerPreview = ({ emoji, name, ...props }: { emoji: string; name: st return ( - + {name} diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index 028983ac29fee..720b698ceda9a 100644 --- a/packages/web-ui-registration/package.json +++ b/packages/web-ui-registration/package.json @@ -36,6 +36,7 @@ "@storybook/theming": "^8.4.4", "@tanstack/react-query": "patch:@tanstack/react-query@npm%3A5.60.5#~/.yarn/patches/@tanstack-react-query-npm-5.60.5-04c500b172.patch", "@testing-library/react": "~16.0.1", + "@types/dompurify": "^3.0.5", "@types/react": "~17.0.83", "babel-loader": "~9.2.1", "eslint": "~8.45.0", @@ -57,5 +58,8 @@ }, "volta": { "extends": "../../package.json" + }, + "dependencies": { + "dompurify": "^3.2.0" } } diff --git a/packages/web-ui-registration/src/components/LoginTerms.tsx b/packages/web-ui-registration/src/components/LoginTerms.tsx index 67dc916fec2d4..d582e0e7321c8 100644 --- a/packages/web-ui-registration/src/components/LoginTerms.tsx +++ b/packages/web-ui-registration/src/components/LoginTerms.tsx @@ -1,6 +1,7 @@ import { Box } from '@rocket.chat/fuselage'; import { HorizontalWizardLayoutCaption } from '@rocket.chat/layout'; import { useSetting } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; @@ -10,7 +11,12 @@ export const LoginTerms = (): ReactElement => { return ( - + ); };