/* Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ import React, { createRef, KeyboardEventHandler } from "react"; import { MatrixError } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../languageHandler"; import withValidation, { IFieldState, IValidationResult } from "./Validation"; import Field, { IValidateOpts } from "./Field"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; interface IProps { domain?: string; value: string; label?: string; placeholder?: string; disabled?: boolean; // if roomId is passed then the entered alias is checked to point to this roomId, else must be unassigned roomId?: string; onKeyDown?: KeyboardEventHandler; onChange?(value: string): void; } interface IState { isValid: boolean; } // Controlled form component wrapping Field for inputting a room alias scoped to a given domain export default class RoomAliasField extends React.PureComponent { public static contextType = MatrixClientContext; public declare context: React.ContextType; private fieldRef = createRef(); public constructor(props: IProps, context: React.ContextType) { super(props, context); this.state = { isValid: true, }; } private asFullAlias(localpart: string): string { const hashAlias = `#${localpart}`; if (this.props.domain) { return `${hashAlias}:${this.props.domain}`; } return hashAlias; } private get domainProps(): { prefix: JSX.Element; postfix: JSX.Element; value: string; maxlength: number; } { const { domain } = this.props; const prefix = #; const postfix = domain ? {`:${domain}`} : ; const maxlength = domain ? 255 - domain.length - 2 : 255 - 1; // 2 for # and : const value = domain ? this.props.value.substring(1, this.props.value.length - domain.length - 1) : this.props.value.substring(1); return { prefix, postfix, value, maxlength }; } public render(): React.ReactNode { const { prefix, postfix, value, maxlength } = this.domainProps; return ( ); } private onChange = (ev: React.ChangeEvent): void => { this.props.onChange?.(this.asFullAlias(ev.target.value)); }; private onValidate = async (fieldState: IFieldState): Promise => { const result = await this.validationRules(fieldState); this.setState({ isValid: !!result.valid }); return result; }; private validationRules = withValidation({ rules: [ { key: "hasDomain", test: async ({ value }): Promise => { // Ignore if we have passed domain if (!value || this.props.domain) { return true; } if (value.split(":").length < 2) { return false; } return true; }, invalid: () => _t("room_settings|general|alias_field_has_domain_invalid"), }, { key: "hasLocalpart", test: async ({ value }): Promise => { if (!value || this.props.domain) { return true; } const split = value.split(":"); if (split.length < 2) { return true; // hasDomain check will fail here instead } // Define the value invalid if there's no first part (roomname) if (split[0].length < 1) { return false; } return true; }, invalid: () => _t("room_settings|general|alias_field_has_localpart_invalid"), }, { key: "safeLocalpart", test: async ({ value }): Promise => { if (!value) { return true; } if (!this.props.domain) { return true; } else { const fullAlias = this.asFullAlias(value); const hasColon = this.props.domain ? !value.includes(":") : true; // XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668 // NOTE: We could probably use linkifyjs to parse those aliases here? return ( !value.includes("#") && hasColon && !value.includes(",") && encodeURI(fullAlias) === fullAlias ); } }, invalid: () => _t("room_settings|general|alias_field_safe_localpart_invalid"), }, { key: "required", test: async ({ value, allowEmpty }) => allowEmpty || !!value, invalid: () => _t("room_settings|general|alias_field_required_invalid"), }, this.props.roomId ? { key: "matches", final: true, test: async ({ value }): Promise => { if (!value) { return true; } const client = this.context; try { const result = await client.getRoomIdForAlias(this.asFullAlias(value)); return result.room_id === this.props.roomId; } catch (err) { console.log(err); return false; } }, invalid: () => _t("room_settings|general|alias_field_matches_invalid"), } : { key: "taken", final: true, test: async ({ value }): Promise => { if (!value) { return true; } const client = this.context; try { await client.getRoomIdForAlias(this.asFullAlias(value)); // we got a room id, so the alias is taken return false; } catch (err) { console.log(err); // any server error code will do, // either it M_NOT_FOUND or the alias is invalid somehow, // in which case we don't want to show the invalid message return err instanceof MatrixError; } }, valid: () => _t("room_settings|general|alias_field_taken_valid"), invalid: () => this.props.domain ? _t("room_settings|general|alias_field_taken_invalid_domain") : _t("room_settings|general|alias_field_taken_invalid"), }, ], }); public get isValid(): boolean { return this.state.isValid; } public async validate(options: IValidateOpts): Promise { const val = await this.fieldRef.current?.validate(options); return val ?? false; } public focus(): void { this.fieldRef.current?.focus(); } }