Skip to content
  •  
  •  
  •  
5 changes: 5 additions & 0 deletions .changeset/chubby-garlics-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/apps': patch
---

Replaces {} with Object.create(null) to ensure defense-in-depth against prototype pollution
6 changes: 6 additions & 0 deletions .changeset/neat-planets-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/rest-typings': minor
'@rocket.chat/meteor': minor
---

Adds custom-sounds.delete API endpoint.
48 changes: 48 additions & 0 deletions apps/meteor/app/api/server/v1/custom-sounds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ import {
isCustomSoundsGetOneProps,
isCustomSoundsListProps,
isCustomSoundsCreateProps,
isCustomSoundsDeleteProps,
isCustomSoundsUpdateProps,
ajv,
validateBadRequestErrorResponse,
validateNotFoundErrorResponse,
validateForbiddenErrorResponse,
validateUnauthorizedErrorResponse,
validateInternalErrorResponse,
} from '@rocket.chat/rest-typings';
import { escapeRegExp } from '@rocket.chat/string-helpers';

import { MAX_CUSTOM_SOUND_SIZE_BYTES, CUSTOM_SOUND_ALLOWED_MIME_TYPES } from '../../../../lib/constants';
import { SystemLogger } from '../../../../server/lib/logger/system';
import { deleteCustomSound } from '../../../custom-sounds/server/lib/deleteCustomSound';
import { insertOrUpdateSound } from '../../../custom-sounds/server/lib/insertOrUpdateSound';
import { uploadCustomSound } from '../../../custom-sounds/server/lib/uploadCustomSound';
import { getExtension, getMimeTypeFromFileName } from '../../../utils/lib/mimeTypes';
Expand Down Expand Up @@ -58,6 +61,18 @@ const updateCustomSoundsResponse = ajv.compile<{ success: boolean }>({
required: ['success'],
});

const deleteCustomSoundsResponse = ajv.compile<void>({
additionalProperties: false,
type: 'object',
properties: {
success: {
type: 'boolean',
description: 'Indicates if the request was successful.',
},
},
required: ['success'],
});

const customSoundsEndpoints = API.v1
.get(
'custom-sounds.list',
Expand Down Expand Up @@ -280,6 +295,39 @@ const customSoundsEndpoints = API.v1
return API.v1.failure(error instanceof Error ? error.message : 'Unknown error');
}
},
)
.post(
'custom-sounds.delete',
{
response: {
200: deleteCustomSoundsResponse,
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
404: validateNotFoundErrorResponse,
500: validateInternalErrorResponse,
},
authRequired: true,
body: isCustomSoundsDeleteProps,
permissionsRequired: ['manage-sounds'],
},
async function action() {
const { _id } = this.bodyParams;

try {
await deleteCustomSound(_id);

return API.v1.success();
} catch (error: unknown) {
this.logger.error({ error });

if (error instanceof Meteor.Error && error.error === 'Custom_Sound_Error_Invalid_Sound') {
return API.v1.failure(error.error);
}

return API.v1.internalError();
}
},
);

export type CustomSoundEndpoints = ExtractRoutesFromAPI<typeof customSoundsEndpoints>;
Expand Down
20 changes: 20 additions & 0 deletions apps/meteor/app/custom-sounds/server/lib/deleteCustomSound.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { api } from '@rocket.chat/core-services';
import { CustomSounds } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';

import { RocketChatFileCustomSoundsInstance } from '../startup/custom-sounds';

export const deleteCustomSound = async (_id: string): Promise<void> => {
const sound = await CustomSounds.findOneById(_id);

if (!sound) {
throw new Meteor.Error('Custom_Sound_Error_Invalid_Sound', 'Invalid sound', {
method: 'deleteCustomSound',
});
}

await RocketChatFileCustomSoundsInstance.deleteFile(`${sound._id}.${sound.extension}`);
await CustomSounds.removeById(_id);

void api.broadcast('notify.deleteCustomSound', { soundData: sound });
};
26 changes: 7 additions & 19 deletions apps/meteor/app/custom-sounds/server/methods/deleteCustomSound.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { api } from '@rocket.chat/core-services';
import type { ICustomSound } from '@rocket.chat/core-typings';
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { CustomSounds } from '@rocket.chat/models';
import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';

import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { RocketChatFileCustomSoundsInstance } from '../startup/custom-sounds';
import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger';
import { deleteCustomSound } from '../lib/deleteCustomSound';

declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
Expand All @@ -16,24 +16,12 @@ declare module '@rocket.chat/ddp-client' {

Meteor.methods<ServerMethods>({
async deleteCustomSound(_id) {
let sound = null;

if (this.userId && (await hasPermissionAsync(this.userId, 'manage-sounds'))) {
sound = await CustomSounds.findOneById(_id);
} else {
methodDeprecationLogger.method('deleteCustomSound', '9.0.0', '/v1/custom-sounds.delete');
if (!this.userId || !(await hasPermissionAsync(this.userId, 'manage-sounds'))) {
throw new Meteor.Error('not_authorized');
}

if (sound == null) {
throw new Meteor.Error('Custom_Sound_Error_Invalid_Sound', 'Invalid sound', {
method: 'deleteCustomSound',
});
}

await RocketChatFileCustomSoundsInstance.deleteFile(`${sound._id}.${sound.extension}`);
await CustomSounds.removeById(_id);
void api.broadcast('notify.deleteCustomSound', { soundData: sound });

check(_id, String);
await deleteCustomSound(_id);
return true;
},
});
3 changes: 1 addition & 2 deletions apps/meteor/client/components/Emoji.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import styled from '@rocket.chat/styled';
import type { ReactElement } from 'react';

import { getEmojiClassNameAndDataTitle } from '../lib/utils/renderEmoji';

Expand All @@ -20,7 +19,7 @@ const EmojiComponent = styled('span', ({ fillContainer: _fillContainer, ...props
: ''}
`;

function Emoji({ emojiHandle, className = undefined, fillContainer }: EmojiProps): ReactElement {
function Emoji({ emojiHandle, className = undefined, fillContainer }: EmojiProps) {
const { className: emojiClassName, image, ...props } = getEmojiClassNameAndDataTitle(emojiHandle);

return (
Expand Down
3 changes: 1 addition & 2 deletions apps/meteor/client/components/FingerprintChangeModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Box } from '@rocket.chat/fuselage';
import { ExternalLink, GenericModal } from '@rocket.chat/ui-client';
import type { ReactElement } from 'react';
import { Trans, useTranslation } from 'react-i18next';

import { links } from '../lib/links';
Expand All @@ -11,7 +10,7 @@ type FingerprintChangeModalProps = {
onClose: () => void;
};

const FingerprintChangeModal = ({ onConfirm, onCancel, onClose }: FingerprintChangeModalProps): ReactElement => {
const FingerprintChangeModal = ({ onConfirm, onCancel, onClose }: FingerprintChangeModalProps) => {
const { t } = useTranslation();
return (
<GenericModal
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Box } from '@rocket.chat/fuselage';
import { ExternalLink, GenericModal } from '@rocket.chat/ui-client';
import type { ReactElement } from 'react';
import { Trans, useTranslation } from 'react-i18next';

import { links } from '../lib/links';
Expand All @@ -12,12 +11,7 @@ type FingerprintChangeModalConfirmationProps = {
newWorkspace: boolean;
};

const FingerprintChangeModalConfirmation = ({
onConfirm,
onCancel,
onClose,
newWorkspace,
}: FingerprintChangeModalConfirmationProps): ReactElement => {
const FingerprintChangeModalConfirmation = ({ onConfirm, onCancel, onClose, newWorkspace }: FingerprintChangeModalConfirmationProps) => {
const { t } = useTranslation();
return (
<GenericModal
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/client/components/GazzodownText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { ChannelMention, UserMention } from '@rocket.chat/gazzodown';
import { MarkupInteractionContext } from '@rocket.chat/gazzodown';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import { useLayout, useRouter, useUserPreference, useUserId, useUserCard } from '@rocket.chat/ui-contexts';
import type { UIEvent } from 'react';
import type { UIEvent, ReactNode } from 'react';
import { useCallback, memo, useMemo } from 'react';

import { normalizeUsername } from '../../lib/utils/normalizeUsername';
Expand All @@ -14,7 +14,7 @@ import { useMessageListHighlights, useMessageListShowRealName } from './message/
import { useGoToRoom } from '../views/room/hooks/useGoToRoom';

type GazzodownTextProps = {
children: JSX.Element;
children: ReactNode;
mentions?: {
type?: 'user' | 'team';
_id: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { Box, ModalHeroImage } from '@rocket.chat/fuselage';
import { GenericModal } from '@rocket.chat/ui-client';
import type { ReactElement, ComponentProps } from 'react';
import type { ComponentProps, ReactNode } from 'react';
import { useTranslation } from 'react-i18next';

type GenericUpsellModalProps = Omit<ComponentProps<typeof GenericModal>, 'variant' | 'children' | 'onClose' | 'onDismiss'> & {
subtitle?: string | ReactElement;
description?: string | ReactElement;
subtitle?: ReactNode;
description?: ReactNode;
img: ComponentProps<typeof ModalHeroImage>['src'];

imgWidth?: ComponentProps<typeof ModalHeroImage>['width'];
imgHeight?: ComponentProps<typeof ModalHeroImage>['height'];
imgAlt?: string;
Expand Down
3 changes: 1 addition & 2 deletions apps/meteor/client/components/ListSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Box, Skeleton } from '@rocket.chat/fuselage';
import type { ReactElement } from 'react';
import { memo, useMemo } from 'react';

const availablePercentualWidths = [47, 68, 75, 82];
Expand All @@ -8,7 +7,7 @@ type ListSkeletonProps = {
listCount?: number;
};

const ListSkeleton = ({ listCount = 2 }: ListSkeletonProps): ReactElement => {
const ListSkeleton = ({ listCount = 2 }: ListSkeletonProps) => {
const widths = useMemo(
() => Array.from({ length: listCount }, (_, index) => `${availablePercentualWidths[index % availablePercentualWidths.length]}%`),
[listCount],
Expand Down
3 changes: 1 addition & 2 deletions apps/meteor/client/components/LocalTime.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { ReactElement } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';

Expand All @@ -8,7 +7,7 @@ type LocalTimeProps = {
utcOffset: number;
};

const LocalTime = ({ utcOffset }: LocalTimeProps): ReactElement => {
const LocalTime = ({ utcOffset }: LocalTimeProps) => {
const time = useUTCClock(utcOffset);
const { t } = useTranslation();

Expand Down
3 changes: 1 addition & 2 deletions apps/meteor/client/components/NotFoundState.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { Box, States, StatesAction, StatesActions, StatesIcon, StatesSubtitle, StatesTitle } from '@rocket.chat/fuselage';
import { useRouter } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';

type NotFoundProps = {
title: string;
subtitle: string;
};

const NotFoundState = ({ title, subtitle }: NotFoundProps): ReactElement => {
const NotFoundState = ({ title, subtitle }: NotFoundProps) => {
const { t } = useTranslation();
const router = useRouter();

Expand Down
3 changes: 1 addition & 2 deletions apps/meteor/client/components/PageSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Box, Button, ButtonGroup, Skeleton } from '@rocket.chat/fuselage';
import { Page, PageHeader, PageContent } from '@rocket.chat/ui-client';
import type { ReactElement } from 'react';

const PageSkeleton = (): ReactElement => (
const PageSkeleton = () => (
<Page>
<PageHeader title={<Skeleton width='x320' maxWidth='full' />}>
<ButtonGroup>
Expand Down
5 changes: 1 addition & 4 deletions apps/meteor/client/components/RawText.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import DOMPurify from 'dompurify';
import type { ReactElement } from 'react';

/** @deprecated */
const RawText = ({ children }: { children: string }): ReactElement => (
<span dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(children) }} />
);
const RawText = ({ children }: { children: string }) => <span dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(children) }} />;

export default RawText;
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import { RoomAvatar } from '@rocket.chat/ui-avatar';
import { useEndpoint } from '@rocket.chat/ui-contexts';
import { keepPreviousData, useQuery } from '@tanstack/react-query';
import type { ReactElement } from 'react';
import type { ReactNode } from 'react';
import { forwardRef, memo, useMemo, useState } from 'react';

type LabelType = { name: string; avatarETag?: string; type: IRoom['t']; encrypted?: IRoom['encrypted'] };
Expand All @@ -19,7 +19,7 @@ const generateQuery = (

type RoomAutoCompleteProps = Omit<AutoCompleteProps<LabelType>, 'filter'> & {
scope?: 'admin' | 'regular';
renderRoomIcon?: (props: { encrypted: IRoom['encrypted']; type: IRoom['t'] }) => ReactElement | null;
renderRoomIcon?: (props: { encrypted: IRoom['encrypted']; type: IRoom['t'] }) => ReactNode;
setSelectedRoom?: (room: IRoom | undefined) => void;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import { RoomAvatar } from '@rocket.chat/ui-avatar';
import { useEndpoint } from '@rocket.chat/ui-contexts';
import { keepPreviousData, useQuery } from '@tanstack/react-query';
import type { ReactElement, ComponentProps } from 'react';
import type { ComponentProps } from 'react';
import { memo, useMemo, useState } from 'react';

const generateQuery = (
Expand All @@ -16,7 +16,7 @@ type RoomAutoCompleteProps = Omit<ComponentProps<typeof AutoComplete>, 'filter'>
readOnly?: boolean;
};

const RoomAutoCompleteMultiple = ({ value, onChange, ...props }: RoomAutoCompleteProps): ReactElement => {
const RoomAutoCompleteMultiple = ({ value, onChange, ...props }: RoomAutoCompleteProps) => {
const [filter, setFilter] = useState('');
const filterDebounced = useDebouncedValue(filter, 300);
const autocomplete = useEndpoint('GET', '/v1/rooms.autocomplete.channelAndPrivate');
Expand Down Expand Up @@ -50,15 +50,15 @@ const RoomAutoCompleteMultiple = ({ value, onChange, ...props }: RoomAutoComplet
filter={filter}
setFilter={setFilter}
multiple
renderSelected={({ selected: { value, label }, onRemove, ...props }): ReactElement => (
renderSelected={({ selected: { value, label }, onRemove, ...props }) => (
<Chip {...props} key={value} value={value} onClick={onRemove}>
<RoomAvatar size='x20' room={{ ...label, type: label?.type || 'c', _id: value }} />
<Box is='span' margin='none' mis={4}>
{label?.name}
</Box>
</Chip>
)}
renderItem={({ value, label, ...props }): ReactElement => (
renderItem={({ value, label, ...props }) => (
<Option
key={value}
{...props}
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/client/components/RoomIcon/RoomIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import { Icon } from '@rocket.chat/fuselage';
import type { ComponentProps, ReactElement } from 'react';
import type { ComponentProps } from 'react';
import { isValidElement } from 'react';

import { OmnichannelRoomIcon } from './OmnichannelRoomIcon';
Expand All @@ -17,7 +17,7 @@ export const RoomIcon = ({
size?: ComponentProps<typeof Icon>['size'];
isIncomingCall?: boolean;
placement?: 'sidebar' | 'default';
}): ReactElement | null => {
}) => {
const iconPropsOrReactNode = useRoomIcon(room);

if (isIncomingCall) {
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/client/components/Sidebar/ListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Option, OptionColumn, OptionContent, OptionIcon, OptionInput } from '@rocket.chat/fuselage';
import type { ComponentProps, ReactElement, ReactNode } from 'react';
import type { ComponentProps, ReactNode } from 'react';

type ListItemCommonProps = {
text: ReactNode;
Expand All @@ -20,7 +20,7 @@ type ListItemConditionalProps =

type ListItemProps = ListItemCommonProps & ListItemConditionalProps;

const ListItem = ({ icon, text, input, children, gap, ...props }: ListItemProps): ReactElement => (
const ListItem = ({ icon, text, input, children, gap, ...props }: ListItemProps) => (
<Option {...props}>
{icon && <OptionIcon name={icon} />}
{gap && <OptionColumn />}
Expand Down
Loading
Loading