Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 4a23630

Browse files
authored
Fix soft crash around inviting invalid MXIDs in start DM on first message flow (#9281)
* Fix soft crash around inviting invalid MXIDs * Make ts --strict happier * Prevent suggesting invalid MXIDs * Add tests * Fix coverage * Fix coverage * Make tsc --strict happier * Fix test * Add tests
1 parent 4fec436 commit 4a23630

File tree

6 files changed

+194
-57
lines changed

6 files changed

+194
-57
lines changed

cypress/e2e/crypto/crypto.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const checkDMRoom = () => {
5050

5151
const startDMWithBob = function(this: CryptoTestContext) {
5252
cy.get('.mx_RoomList [aria-label="Start chat"]').click();
53-
cy.get('[data-test-id="invite-dialog-input"]').type(this.bob.getUserId());
53+
cy.get('[data-testid="invite-dialog-input"]').type(this.bob.getUserId());
5454
cy.contains(".mx_InviteDialog_tile_nameStack_name", "Bob").click();
5555
cy.contains(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name", "Bob").should("exist");
5656
cy.get(".mx_InviteDialog_goButton").click();

src/components/views/dialogs/InviteDialog.tsx

Lines changed: 49 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ import { Icon as InfoIcon } from "../../../../res/img/element-icons/info.svg";
2525
import { Icon as EmailPillAvatarIcon } from "../../../../res/img/icon-email-pill-avatar.svg";
2626
import { _t, _td } from "../../../languageHandler";
2727
import { MatrixClientPeg } from "../../../MatrixClientPeg";
28-
import { makeRoomPermalink, makeUserPermalink } from "../../../utils/permalinks/Permalinks";
28+
import {
29+
getHostnameFromMatrixServerName,
30+
getServerName,
31+
makeRoomPermalink,
32+
makeUserPermalink,
33+
} from "../../../utils/permalinks/Permalinks";
2934
import DMRoomMap from "../../../utils/DMRoomMap";
3035
import SdkConfig from "../../../SdkConfig";
3136
import * as Email from "../../../email";
@@ -69,7 +74,7 @@ import {
6974
startDmOnFirstMessage,
7075
ThreepidMember,
7176
} from "../../../utils/direct-messages";
72-
import { AnyInviteKind, KIND_CALL_TRANSFER, KIND_DM, KIND_INVITE } from './InviteDialogTypes';
77+
import { KIND_CALL_TRANSFER, KIND_DM, KIND_INVITE } from './InviteDialogTypes';
7378
import Modal from '../../../Modal';
7479
import dis from "../../../dispatcher/dispatcher";
7580

@@ -243,26 +248,37 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
243248
}
244249
}
245250

246-
interface IInviteDialogProps {
251+
interface BaseProps {
247252
// Takes a boolean which is true if a user / users were invited /
248253
// a call transfer was initiated or false if the dialog was cancelled
249254
// with no action taken.
250255
onFinished: (success: boolean) => void;
251256

252-
// The kind of invite being performed. Assumed to be KIND_DM if
253-
// not provided.
254-
kind: AnyInviteKind;
257+
// Initial value to populate the filter with
258+
initialText?: string;
259+
}
260+
261+
interface InviteDMProps extends BaseProps {
262+
// The kind of invite being performed. Assumed to be KIND_DM if not provided.
263+
kind?: typeof KIND_DM;
264+
}
265+
266+
interface InviteRoomProps extends BaseProps {
267+
kind: typeof KIND_INVITE;
255268

256269
// The room ID this dialog is for. Only required for KIND_INVITE.
257270
roomId: string;
271+
}
272+
273+
interface InviteCallProps extends BaseProps {
274+
kind: typeof KIND_CALL_TRANSFER;
258275

259276
// The call to transfer. Only required for KIND_CALL_TRANSFER.
260277
call: MatrixCall;
261-
262-
// Initial value to populate the filter with
263-
initialText: string;
264278
}
265279

280+
type Props = InviteDMProps | InviteRoomProps | InviteCallProps;
281+
266282
interface IInviteDialogState {
267283
targets: Member[]; // array of Member objects (see interface above)
268284
filterText: string;
@@ -283,14 +299,13 @@ interface IInviteDialogState {
283299
errorText: string;
284300
}
285301

286-
export default class InviteDialog extends React.PureComponent<IInviteDialogProps, IInviteDialogState> {
302+
export default class InviteDialog extends React.PureComponent<Props, IInviteDialogState> {
287303
static defaultProps = {
288304
kind: KIND_DM,
289305
initialText: "",
290306
};
291307

292-
private closeCopiedTooltip: () => void;
293-
private debounceTimer: number = null; // actually number because we're in the browser
308+
private debounceTimer: number | null = null; // actually number because we're in the browser
294309
private editorRef = createRef<HTMLInputElement>();
295310
private numberEntryFieldRef: React.RefObject<Field> = createRef();
296311
private unmounted = false;
@@ -316,7 +331,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
316331

317332
this.state = {
318333
targets: [], // array of Member objects (see interface above)
319-
filterText: this.props.initialText,
334+
filterText: this.props.initialText || "",
320335
recents: InviteDialog.buildRecents(alreadyInvited),
321336
numRecentsShown: INITIAL_ROOMS_SHOWN,
322337
suggestions: this.buildSuggestions(alreadyInvited),
@@ -343,9 +358,6 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
343358

344359
componentWillUnmount() {
345360
this.unmounted = true;
346-
// if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close
347-
// the tooltip otherwise, such as pressing Escape or clicking X really quickly
348-
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
349361
}
350362

351363
private onConsultFirstChange = (ev) => {
@@ -466,6 +478,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
466478
};
467479

468480
private inviteUsers = async () => {
481+
if (this.props.kind !== KIND_INVITE) return;
469482
this.setState({ busy: true });
470483
this.convertFilter();
471484
const targets = this.convertFilter();
@@ -499,6 +512,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
499512
};
500513

501514
private transferCall = async () => {
515+
if (this.props.kind !== KIND_CALL_TRANSFER) return;
502516
if (this.state.currentTabId == TabId.UserDirectory) {
503517
this.convertFilter();
504518
const targets = this.convertFilter();
@@ -565,7 +579,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
565579
this.props.onFinished(false);
566580
};
567581

568-
private updateSuggestions = async (term) => {
582+
private updateSuggestions = async (term: string) => {
569583
MatrixClientPeg.get().searchUserDirectory({ term }).then(async r => {
570584
if (term !== this.state.filterText) {
571585
// Discard the results - we were probably too slow on the server-side to make
@@ -595,13 +609,17 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
595609
logger.warn("Non-fatal error trying to make an invite for a user ID");
596610
logger.warn(e);
597611

598-
// Add a result anyways, just without a profile. We stick it at the
599-
// top so it is most obviously presented to the user.
600-
r.results.splice(0, 0, {
601-
user_id: term,
602-
display_name: term,
603-
avatar_url: null,
604-
});
612+
// Reuse logic from Permalinks as a basic MXID validity check
613+
const serverName = getServerName(term);
614+
const domain = getHostnameFromMatrixServerName(serverName);
615+
if (domain) {
616+
// Add a result anyways, just without a profile. We stick it at the
617+
// top so it is most obviously presented to the user.
618+
r.results.splice(0, 0, {
619+
user_id: term,
620+
display_name: term,
621+
});
622+
}
605623
}
606624
}
607625

@@ -940,7 +958,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
940958
disabled={this.state.busy || (this.props.kind == KIND_CALL_TRANSFER && this.state.targets.length > 0)}
941959
autoComplete="off"
942960
placeholder={hasPlaceholder ? _t("Search") : null}
943-
data-test-id="invite-dialog-input"
961+
data-testid="invite-dialog-input"
944962
/>
945963
);
946964
return (
@@ -1107,14 +1125,15 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
11071125
</CopyableText>
11081126
</div>;
11091127
} else if (this.props.kind === KIND_INVITE) {
1110-
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
1128+
const roomId = this.props.roomId;
1129+
const room = MatrixClientPeg.get()?.getRoom(roomId);
11111130
const isSpace = room?.isSpaceRoom();
11121131
title = isSpace
11131132
? _t("Invite to %(spaceName)s", {
1114-
spaceName: room.name || _t("Unnamed Space"),
1133+
spaceName: room?.name || _t("Unnamed Space"),
11151134
})
11161135
: _t("Invite to %(roomName)s", {
1117-
roomName: room.name || _t("Unnamed Room"),
1136+
roomName: room?.name || _t("Unnamed Room"),
11181137
});
11191138

11201139
let helpTextUntranslated;
@@ -1140,14 +1159,14 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
11401159
userId: () =>
11411160
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{ userId }</a>,
11421161
a: (sub) =>
1143-
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{ sub }</a>,
1162+
<a href={makeRoomPermalink(roomId)} rel="noreferrer noopener" target="_blank">{ sub }</a>,
11441163
});
11451164

11461165
buttonText = _t("Invite");
11471166
goButtonFn = this.inviteUsers;
11481167

11491168
if (cli.isRoomEncrypted(this.props.roomId)) {
1150-
const room = cli.getRoom(this.props.roomId);
1169+
const room = cli.getRoom(this.props.roomId)!;
11511170
const visibilityEvent = room.currentState.getStateEvents(
11521171
"m.room.history_visibility", "",
11531172
);
@@ -1185,8 +1204,6 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
11851204
{ _t("Transfer") }
11861205
</AccessibleButton>
11871206
</div>;
1188-
} else {
1189-
logger.error("Unknown kind of InviteDialog: " + this.props.kind);
11901207
}
11911208

11921209
const goButton = this.props.kind == KIND_CALL_TRANSFER ? null : <AccessibleButton

src/components/views/dialogs/ShareDialog.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,6 @@ interface IState {
7171
}
7272

7373
export default class ShareDialog extends React.PureComponent<IProps, IState> {
74-
protected closeCopiedTooltip: () => void;
75-
7674
constructor(props) {
7775
super(props);
7876

@@ -100,12 +98,6 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
10098
});
10199
};
102100

103-
componentWillUnmount() {
104-
// if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close
105-
// the tooltip otherwise, such as pressing Escape or clicking X really quickly
106-
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
107-
}
108-
109101
private getUrl() {
110102
let matrixToUrl;
111103

src/utils/permalinks/Permalinks.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ export class RoomPermalinkCreator {
180180
if (plEvent) {
181181
const content = plEvent.getContent();
182182
if (content) {
183-
const users = content.users;
183+
const users: Record<string, number> = content.users;
184184
if (users) {
185185
const entries = Object.entries(users);
186186
const allowedEntries = entries.filter(([userId]) => {
@@ -189,9 +189,11 @@ export class RoomPermalinkCreator {
189189
return false;
190190
}
191191
const serverName = getServerName(userId);
192-
return !isHostnameIpAddress(serverName) &&
193-
!isHostInRegex(serverName, this.bannedHostsRegexps) &&
194-
isHostInRegex(serverName, this.allowedHostsRegexps);
192+
193+
const domain = getHostnameFromMatrixServerName(serverName) ?? serverName;
194+
return !isHostnameIpAddress(domain) &&
195+
!isHostInRegex(domain, this.bannedHostsRegexps) &&
196+
isHostInRegex(domain, this.allowedHostsRegexps);
195197
});
196198
const maxEntry = allowedEntries.reduce((max, entry) => {
197199
return (entry[1] > max[1]) ? entry : max;
@@ -250,14 +252,15 @@ export class RoomPermalinkCreator {
250252
.sort((a, b) => this.populationMap[b] - this.populationMap[a]);
251253

252254
for (let i = 0; i < serversByPopulation.length && candidates.size < MAX_SERVER_CANDIDATES; i++) {
253-
const server = serversByPopulation[i];
255+
const serverName = serversByPopulation[i];
256+
const domain = getHostnameFromMatrixServerName(serverName) ?? "";
254257
if (
255-
!candidates.has(server) &&
256-
!isHostnameIpAddress(server) &&
257-
!isHostInRegex(server, this.bannedHostsRegexps) &&
258-
isHostInRegex(server, this.allowedHostsRegexps)
258+
!candidates.has(serverName) &&
259+
!isHostnameIpAddress(domain) &&
260+
!isHostInRegex(domain, this.bannedHostsRegexps) &&
261+
isHostInRegex(domain, this.allowedHostsRegexps)
259262
) {
260-
candidates.add(server);
263+
candidates.add(serverName);
261264
}
262265
}
263266

@@ -441,25 +444,28 @@ export function parsePermalink(fullUrl: string): PermalinkParts {
441444
return null; // not a permalink we can handle
442445
}
443446

444-
function getServerName(userId: string): string {
447+
export function getServerName(userId: string): string {
445448
return userId.split(":").splice(1).join(":");
446449
}
447450

448-
function getHostnameFromMatrixDomain(domain: string): string {
449-
if (!domain) return null;
450-
return new URL(`https://${domain}`).hostname;
451+
export function getHostnameFromMatrixServerName(serverName: string): string | null {
452+
if (!serverName) return null;
453+
try {
454+
return new URL(`https://${serverName}`).hostname;
455+
} catch (e) {
456+
console.error("Error encountered while extracting hostname from server name", e);
457+
return null;
458+
}
451459
}
452460

453461
function isHostInRegex(hostname: string, regexps: RegExp[]): boolean {
454-
hostname = getHostnameFromMatrixDomain(hostname);
455462
if (!hostname) return true; // assumed
456463
if (regexps.length > 0 && !regexps[0].test) throw new Error(regexps[0].toString());
457464

458465
return regexps.some(h => h.test(hostname));
459466
}
460467

461468
function isHostnameIpAddress(hostname: string): boolean {
462-
hostname = getHostnameFromMatrixDomain(hostname);
463469
if (!hostname) return false;
464470

465471
// is-ip doesn't want IPv6 addresses surrounded by brackets, so

0 commit comments

Comments
 (0)