Merge pull request #2525 from Bilb/fix-displayname-use-bytes

fix: displayName allowed length based on bytes rather than char
pull/2555/head
Audric Ackermann 3 years ago committed by GitHub
commit 53c57efb89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -363,6 +363,7 @@
"notificationPreview": "Preview", "notificationPreview": "Preview",
"recoveryPhraseEmpty": "Enter your recovery phrase", "recoveryPhraseEmpty": "Enter your recovery phrase",
"displayNameEmpty": "Please enter a display name", "displayNameEmpty": "Please enter a display name",
"displayNameTooLong": "Display name is too long",
"members": "$count$ members", "members": "$count$ members",
"activeMembers": "$count$ active members", "activeMembers": "$count$ active members",
"join": "Join", "join": "Join",

@ -1,4 +1,4 @@
import React, { useEffect, useRef } from 'react'; import React, { useRef } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { SessionIconButton } from './icon/'; import { SessionIconButton } from './icon/';
@ -63,17 +63,11 @@ export const SessionWrapperModal = (props: SessionWrapperModalType) => {
} }
}; };
useEffect(() => {
document.addEventListener('mousedown', handleClick);
return () => {
document.removeEventListener('mousedown', handleClick);
};
}, []);
return ( return (
<div <div
className={classNames('loki-dialog modal', additionalClassName ? additionalClassName : null)} className={classNames('loki-dialog modal', additionalClassName ? additionalClassName : null)}
onClick={handleClick}
role="dialog"
> >
<div className="session-confirm-wrapper"> <div className="session-confirm-wrapper">
<div ref={modalRef} className="session-modal"> <div ref={modalRef} className="session-modal">

@ -2,8 +2,8 @@ import React from 'react';
import { Flex } from '../basic/Flex'; import { Flex } from '../basic/Flex';
import styled from 'styled-components'; import styled from 'styled-components';
import { noop } from 'lodash';
import { SessionIcon, SessionIconType } from '../icon'; import { SessionIcon, SessionIconType } from '../icon';
import { noop } from 'lodash';
export enum SessionToastType { export enum SessionToastType {
Info = 'info', Info = 'info',
@ -44,6 +44,8 @@ const IconDiv = styled.div`
padding-inline-end: var(--margins-xs); padding-inline-end: var(--margins-xs);
`; `;
// tslint:disable: use-simple-attributes
export const SessionToast = (props: Props) => { export const SessionToast = (props: Props) => {
const { title, description, type, icon } = props; const { title, description, type, icon } = props;
@ -71,14 +73,10 @@ export const SessionToast = (props: Props) => {
} }
} }
const onToastClick = props?.onToastClick || noop;
return ( return (
// tslint:disable-next-line: use-simple-attributes <Flex container={true} alignItems="center" onClick={onToastClick} data-testid="session-toast">
<Flex
container={true}
alignItems="center"
onClick={props?.onToastClick || noop}
data-testid="session-toast"
>
<IconDiv> <IconDiv>
<SessionIcon iconType={toastIcon} iconSize={toastIconSize} /> <SessionIcon iconType={toastIcon} iconSize={toastIconSize} />
</IconDiv> </IconDiv>

@ -16,12 +16,12 @@ import { uploadOurAvatar } from '../../interactions/conversationInteractions';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton'; import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
import { SessionSpinner } from '../basic/SessionSpinner'; import { SessionSpinner } from '../basic/SessionSpinner';
import { SessionIconButton } from '../icon'; import { SessionIconButton } from '../icon';
import { MAX_USERNAME_LENGTH } from '../registration/RegistrationStages';
import { SessionWrapperModal } from '../SessionWrapperModal'; import { SessionWrapperModal } from '../SessionWrapperModal';
import { pickFileForAvatar } from '../../types/attachments/VisualAttachment'; import { pickFileForAvatar } from '../../types/attachments/VisualAttachment';
import { sanitizeSessionUsername } from '../../session/utils/String'; import { sanitizeSessionUsername } from '../../session/utils/String';
import { setLastProfileUpdateTimestamp } from '../../util/storage'; import { setLastProfileUpdateTimestamp } from '../../util/storage';
import { ConversationTypeEnum } from '../../models/conversationAttributes'; import { ConversationTypeEnum } from '../../models/conversationAttributes';
import { MAX_USERNAME_BYTES } from '../../session/constants';
interface State { interface State {
profileName: string; profileName: string;
@ -214,7 +214,7 @@ export class EditProfileDialog extends React.Component<{}, State> {
value={this.state.profileName} value={this.state.profileName}
placeholder={placeholderText} placeholder={placeholderText}
onChange={this.onNameEdited} onChange={this.onNameEdited}
maxLength={MAX_USERNAME_LENGTH} maxLength={MAX_USERNAME_BYTES}
tabIndex={0} tabIndex={0}
required={true} required={true}
aria-required={true} aria-required={true}
@ -240,10 +240,18 @@ export class EditProfileDialog extends React.Component<{}, State> {
} }
private onNameEdited(event: ChangeEvent<HTMLInputElement>) { private onNameEdited(event: ChangeEvent<HTMLInputElement>) {
const newName = sanitizeSessionUsername(event.target.value); const displayName = event.target.value;
this.setState({ try {
profileName: newName, const newName = sanitizeSessionUsername(displayName);
}); this.setState({
profileName: newName,
});
} catch (e) {
this.setState({
profileName: displayName,
});
ToastUtils.pushToastError('nameTooLong', window.i18n('displayNameTooLong'));
}
} }
private onKeyUp(event: any) { private onKeyUp(event: any) {
@ -266,26 +274,37 @@ export class EditProfileDialog extends React.Component<{}, State> {
*/ */
private onClickOK() { private onClickOK() {
const { newAvatarObjectUrl, profileName } = this.state; const { newAvatarObjectUrl, profileName } = this.state;
const newName = profileName ? profileName.trim() : ''; try {
const newName = profileName ? profileName.trim() : '';
if (newName.length === 0 || newName.length > MAX_USERNAME_BYTES) {
return;
}
// this throw if the length in bytes is too long
const sanitizedName = sanitizeSessionUsername(newName);
const trimName = sanitizedName.trim();
this.setState(
{
profileName: trimName,
loading: true,
},
async () => {
await commitProfileEdits(newName, newAvatarObjectUrl);
this.setState({
loading: false,
mode: 'default',
updatedProfileName: this.state.profileName,
});
}
);
} catch (e) {
ToastUtils.pushToastError('nameTooLong', window.i18n('displayNameTooLong'));
if (newName.length === 0 || newName.length > MAX_USERNAME_LENGTH) {
return; return;
} }
this.setState(
{
loading: true,
},
async () => {
await commitProfileEdits(newName, newAvatarObjectUrl);
this.setState({
loading: false,
mode: 'default',
updatedProfileName: this.state.profileName,
});
}
);
} }
private closeDialog() { private closeDialog() {

@ -39,7 +39,6 @@ import { getSwarmPollingInstance } from '../../session/apis/snode_api';
import { forceRefreshRandomSnodePool } from '../../session/apis/snode_api/snodePool'; import { forceRefreshRandomSnodePool } from '../../session/apis/snode_api/snodePool';
import { Avatar, AvatarSize } from '../avatar/Avatar'; import { Avatar, AvatarSize } from '../avatar/Avatar';
import { SessionIconButton } from '../icon'; import { SessionIconButton } from '../icon';
import { SessionToastContainer } from '../SessionToastContainer';
import { LeftPaneSectionContainer } from './LeftPaneSectionContainer'; import { LeftPaneSectionContainer } from './LeftPaneSectionContainer';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { UserUtils } from '../../session/utils'; import { UserUtils } from '../../session/utils';
@ -277,8 +276,6 @@ export const ActionsPanel = () => {
<Section type={SectionType.Message} /> <Section type={SectionType.Message} />
<Section type={SectionType.Settings} /> <Section type={SectionType.Settings} />
<SessionToastContainer />
<Section type={SectionType.PathIndicator} /> <Section type={SectionType.PathIndicator} />
<Section type={SectionType.Moon} /> <Section type={SectionType.Moon} />
</LeftPaneSectionContainer> </LeftPaneSectionContainer>

@ -12,6 +12,7 @@ import { CallInFullScreenContainer } from '../calling/CallInFullScreenContainer'
import { DraggableCallContainer } from '../calling/DraggableCallContainer'; import { DraggableCallContainer } from '../calling/DraggableCallContainer';
import { IncomingCallDialog } from '../calling/IncomingCallDialog'; import { IncomingCallDialog } from '../calling/IncomingCallDialog';
import { ModalContainer } from '../dialog/ModalContainer'; import { ModalContainer } from '../dialog/ModalContainer';
import { SessionToastContainer } from '../SessionToastContainer';
import { ActionsPanel } from './ActionsPanel'; import { ActionsPanel } from './ActionsPanel';
import { LeftPaneMessageSection } from './LeftPaneMessageSection'; import { LeftPaneMessageSection } from './LeftPaneMessageSection';
import { LeftPaneSettingSection } from './LeftPaneSettingSection'; import { LeftPaneSettingSection } from './LeftPaneSettingSection';
@ -71,6 +72,7 @@ export const LeftPane = () => {
<div className="module-left-pane-session"> <div className="module-left-pane-session">
<ModalContainer /> <ModalContainer />
<CallContainer /> <CallContainer />
<SessionToastContainer />
<ActionsPanel /> <ActionsPanel />
<StyledLeftPane className="module-left-pane"> <StyledLeftPane className="module-left-pane">

@ -17,7 +17,6 @@ import {
import { fromHex } from '../../session/utils/String'; import { fromHex } from '../../session/utils/String';
import { setSignInByLinking, setSignWithRecoveryPhrase, Storage } from '../../util/storage'; import { setSignInByLinking, setSignWithRecoveryPhrase, Storage } from '../../util/storage';
export const MAX_USERNAME_LENGTH = 26;
// tslint:disable: use-simple-attributes // tslint:disable: use-simple-attributes
export async function resetRegistration() { export async function resetRegistration() {

@ -1,7 +1,7 @@
import classNames from 'classnames'; import classNames from 'classnames';
import React from 'react'; import React from 'react';
import { MAX_USERNAME_BYTES } from '../../session/constants';
import { SessionInput } from '../basic/SessionInput'; import { SessionInput } from '../basic/SessionInput';
import { MAX_USERNAME_LENGTH } from './RegistrationStages';
const DisplayNameInput = (props: { const DisplayNameInput = (props: {
stealAutoFocus?: boolean; stealAutoFocus?: boolean;
@ -17,7 +17,7 @@ const DisplayNameInput = (props: {
type="text" type="text"
placeholder={window.i18n('enterDisplayName')} placeholder={window.i18n('enterDisplayName')}
value={props.displayName} value={props.displayName}
maxLength={MAX_USERNAME_LENGTH} maxLength={MAX_USERNAME_BYTES}
onValueChanged={props.onDisplayNameChanged} onValueChanged={props.onDisplayNameChanged}
onEnterPressed={props.handlePressEnter} onEnterPressed={props.handlePressEnter}
inputDataTestId="display-name-input" inputDataTestId="display-name-input"

@ -1,4 +1,5 @@
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import { ToastUtils } from '../../session/utils';
import { sanitizeSessionUsername } from '../../session/utils/String'; import { sanitizeSessionUsername } from '../../session/utils/String';
import { Flex } from '../basic/Flex'; import { Flex } from '../basic/Flex';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton'; import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
@ -95,6 +96,23 @@ const SignInButtons = (props: {
); );
}; };
export function sanitizeDisplayNameOrToast(
displayName: string,
setDisplayName: (sanitized: string) => void,
setDisplayNameError: (error: string | undefined) => void
) {
try {
const sanitizedName = sanitizeSessionUsername(displayName);
const trimName = sanitizedName.trim();
setDisplayName(sanitizedName);
setDisplayNameError(!trimName ? window.i18n('displayNameEmpty') : undefined);
} catch (e) {
setDisplayName(displayName);
setDisplayNameError(window.i18n('displayNameTooLong'));
ToastUtils.pushToastError('toolong', window.i18n('displayNameTooLong'));
}
}
export const SignInTab = () => { export const SignInTab = () => {
const { setRegistrationPhase, signInMode, setSignInMode } = useContext(RegistrationContext); const { setRegistrationPhase, signInMode, setSignInMode } = useContext(RegistrationContext);
@ -148,10 +166,7 @@ export const SignInTab = () => {
displayName={displayName} displayName={displayName}
handlePressEnter={continueYourSession} handlePressEnter={continueYourSession}
onDisplayNameChanged={(name: string) => { onDisplayNameChanged={(name: string) => {
const sanitizedName = sanitizeSessionUsername(name); sanitizeDisplayNameOrToast(name, setDisplayName, setDisplayNameError);
const trimName = sanitizedName.trim();
setDisplayName(sanitizedName);
setDisplayNameError(!trimName ? window.i18n('displayNameEmpty') : undefined);
}} }}
onSeedChanged={(seed: string) => { onSeedChanged={(seed: string) => {
setRecoveryPhrase(seed); setRecoveryPhrase(seed);

@ -1,12 +1,11 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { sanitizeSessionUsername } from '../../session/utils/String';
import { Flex } from '../basic/Flex'; import { Flex } from '../basic/Flex';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton'; import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
import { SessionIdEditable } from '../basic/SessionIdEditable'; import { SessionIdEditable } from '../basic/SessionIdEditable';
import { SessionIconButton } from '../icon'; import { SessionIconButton } from '../icon';
import { RegistrationContext, RegistrationPhase, signUp } from './RegistrationStages'; import { RegistrationContext, RegistrationPhase, signUp } from './RegistrationStages';
import { RegistrationUserDetails } from './RegistrationUserDetails'; import { RegistrationUserDetails } from './RegistrationUserDetails';
import { SignInMode } from './SignInTab'; import { sanitizeDisplayNameOrToast, SignInMode } from './SignInTab';
import { TermsAndConditions } from './TermsAndConditions'; import { TermsAndConditions } from './TermsAndConditions';
export enum SignUpMode { export enum SignUpMode {
@ -144,10 +143,7 @@ export const SignUpTab = () => {
displayName={displayName} displayName={displayName}
handlePressEnter={signUpWithDetails} handlePressEnter={signUpWithDetails}
onDisplayNameChanged={(name: string) => { onDisplayNameChanged={(name: string) => {
const sanitizedName = sanitizeSessionUsername(name); sanitizeDisplayNameOrToast(name, setDisplayName, setDisplayNameError);
const trimName = sanitizedName.trim();
setDisplayName(sanitizedName);
setDisplayNameError(!trimName ? window.i18n('displayNameEmpty') : undefined);
}} }}
stealAutoFocus={true} stealAutoFocus={true}
/> />

@ -60,3 +60,5 @@ export const UI = {
export const QUOTED_TEXT_MAX_LENGTH = 150; export const QUOTED_TEXT_MAX_LENGTH = 150;
export const DEFAULT_RECENT_REACTS = ['😂', '🥰', '😢', '😡', '😮', '😈']; export const DEFAULT_RECENT_REACTS = ['😂', '🥰', '😢', '😡', '😮', '😈'];
export const MAX_USERNAME_BYTES = 64;

@ -1,4 +1,5 @@
import ByteBuffer from 'bytebuffer'; import ByteBuffer from 'bytebuffer';
import { MAX_USERNAME_BYTES } from '../constants';
export type Encoding = 'base64' | 'hex' | 'binary' | 'utf8'; export type Encoding = 'base64' | 'hex' | 'binary' | 'utf8';
export type BufferType = ByteBuffer | Buffer | ArrayBuffer | Uint8Array; export type BufferType = ByteBuffer | Buffer | ArrayBuffer | Uint8Array;
@ -54,10 +55,19 @@ const forbiddenDisplayCharRegex = /\uFFD2*/g;
* *
* This function removes any forbidden char from a given display name. * This function removes any forbidden char from a given display name.
* This does not trim it as otherwise, a user cannot type User A as when he hits the space, it gets trimmed right away. * This does not trim it as otherwise, a user cannot type User A as when he hits the space, it gets trimmed right away.
* The trimming should hence happen after calling this and on saving the display name * The trimming should hence happen after calling this and on saving the display name.
*
* This functions makes sure that the MAX_USERNAME_BYTES is verified for utf8 byte length
* @param inputName the input to sanitize * @param inputName the input to sanitize
* @returns a sanitized string, untrimmed * @returns a sanitized string, untrimmed
*/ */
export const sanitizeSessionUsername = (inputName: string) => { export const sanitizeSessionUsername = (inputName: string) => {
return inputName.replace(forbiddenDisplayCharRegex, ''); const validChars = inputName.replace(forbiddenDisplayCharRegex, '');
const lengthBytes = encode(validChars, 'utf8').byteLength;
if (lengthBytes > MAX_USERNAME_BYTES) {
throw new Error('Display name is too long');
}
return validChars;
}; };

@ -495,6 +495,7 @@ export type LocalizerKeys =
| 'trustThisContactDialogDescription' | 'trustThisContactDialogDescription'
| 'unknownCountry' | 'unknownCountry'
| 'searchFor...' | 'searchFor...'
| 'displayNameTooLong'
| 'joinedTheGroup' | 'joinedTheGroup'
| 'editGroupName' | 'editGroupName'
| 'reportIssue'; | 'reportIssue';

Loading…
Cancel
Save