diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 2ea39ad657..e235446b16 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -22,7 +22,7 @@ import sdk from '../../../index'; import Modal from "../../../Modal"; import SdkConfig from "../../../SdkConfig"; import PasswordReset from "../../../PasswordReset"; -import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; +import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; // Phases // Show controls to configure server details @@ -53,9 +53,40 @@ module.exports = React.createClass({ password: "", password2: "", errorText: null, + + // We perform liveliness checks later, but for now suppress the errors. + // We also track the server dead errors independently of the regular errors so + // that we can render it differently, and override any other error the user may + // be seeing. + serverIsAlive: true, + serverDeadError: "", }; }, + componentWillMount: function() { + this._checkServerLiveliness(this.props.serverConfig); + }, + + componentWillReceiveProps: async function(newProps) { + if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl && + newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return; + + // Do a liveliness check on the new URLs + this._checkServerLiveliness(newProps.serverConfig); + }, + + _checkServerLiveliness: async function(serverConfig) { + try { + await AutoDiscoveryUtils.validateServerConfigWithStaticUrls( + serverConfig.hsUrl, + serverConfig.isUrl, + ); + this.setState({serverIsAlive: true}); + } catch (e) { + this.setState(AutoDiscoveryUtils.authComponentStateForError(e)); + } + }, + submitPasswordReset: function(email, password) { this.setState({ phase: PHASE_SENDING_EMAIL, @@ -89,6 +120,8 @@ module.exports = React.createClass({ onSubmitForm: function(ev) { ev.preventDefault(); + if (!this.state.serverIsAlive) return; + if (!this.state.email) { this.showErrorDialog(_t('The email address linked to your account must be entered.')); } else if (!this.state.password || !this.state.password2) { @@ -173,11 +206,21 @@ module.exports = React.createClass({ const Field = sdk.getComponent('elements.Field'); let errorText = null; - const err = this.state.errorText || this.props.defaultServerDiscoveryError; + const err = this.state.errorText; if (err) { errorText =
{ err }
; } + let serverDeadSection; + if (!this.state.serverIsAlive) { + // TODO: TravisR - Design from Nad + serverDeadSection = ( +
+ {this.state.serverDeadError} +
+ ); + } + let yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', { serverName: this.props.serverConfig.hsName, }); @@ -207,11 +250,12 @@ module.exports = React.createClass({ } return
+ {errorText} + {serverDeadSection}

{yourMatrixAccountText} {editLink}

- {errorText}
- + {_t('Sign in instead')} diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index b556057cdb..c38d59caeb 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -94,6 +94,13 @@ module.exports = React.createClass({ phase: PHASE_LOGIN, // The current login flow, such as password, SSO, etc. currentFlow: "m.login.password", + + // We perform liveliness checks later, but for now suppress the errors. + // We also track the server dead errors independently of the regular errors so + // that we can render it differently, and override any other error the user may + // be seeing. + serverIsAlive: true, + serverDeadError: "", }; }, @@ -233,7 +240,7 @@ module.exports = React.createClass({ username: username, busy: doWellknownLookup, // unset later by the result of onServerConfigChange errorText: null, - canTryLogin: true, + canTryLogin: this.state.serverIsAlive, }); if (doWellknownLookup) { const serverName = username.split(':').slice(1).join(':'); @@ -247,7 +254,19 @@ module.exports = React.createClass({ if (e.translatedMessage) { message = e.translatedMessage; } - this.setState({errorText: message, busy: false, canTryLogin: false}); + + let errorText = message; + let discoveryState = {}; + if (AutoDiscoveryUtils.isLivelinessError(e)) { + errorText = this.state.errorText; + discoveryState = this._stateForDiscoveryError(e); + } + + this.setState({ + busy: false, + errorText, + ...discoveryState, + }); } } }, @@ -272,7 +291,7 @@ module.exports = React.createClass({ } else { this.setState({ errorText: null, - canTryLogin: true, + canTryLogin: this.state.serverIsAlive, }); } }, @@ -297,13 +316,25 @@ module.exports = React.createClass({ }); }, - _initLoginLogic: function(hsUrl, isUrl) { - const self = this; + _stateForDiscoveryError: function(err) { + return { + canTryLogin: false, + ...AutoDiscoveryUtils.authComponentStateForError(err), + }; + }, + + _initLoginLogic: async function(hsUrl, isUrl) { hsUrl = hsUrl || this.props.serverConfig.hsUrl; isUrl = isUrl || this.props.serverConfig.isUrl; - // TODO: TravisR - Only use this if the homeserver is the default homeserver - const fallbackHsUrl = this.props.fallbackHsUrl; + let isDefaultServer = false; + if (this.props.serverConfig.isDefault + && hsUrl === this.props.serverConfig.hsUrl + && isUrl === this.props.serverConfig.isUrl) { + isDefaultServer = true; + } + + const fallbackHsUrl = isDefaultServer ? this.props.fallbackHsUrl : null; const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, { defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, @@ -315,6 +346,19 @@ module.exports = React.createClass({ loginIncorrect: false, }); + // Do a quick liveliness check on the URLs + try { + await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl); + this.setState({serverIsAlive: true, errorText: "", canTryLogin: true}); + } catch (e) { + const discoveryState = this._stateForDiscoveryError(e); + this.setState({ + busy: false, + ...discoveryState, + }); + return; // Server is dead - do not continue. + } + loginLogic.getFlows().then((flows) => { // look for a flow where we understand all of the steps. for (let i = 0; i < flows.length; i++ ) { @@ -339,14 +383,14 @@ module.exports = React.createClass({ "supported by this client.", ), }); - }, function(err) { - self.setState({ - errorText: self._errorTextFromError(err), + }, (err) => { + this.setState({ + errorText: this._errorTextFromError(err), loginIncorrect: false, canTryLogin: false, }); - }).finally(function() { - self.setState({ + }).finally(() => { + this.setState({ busy: false, }); }).done(); @@ -485,7 +529,7 @@ module.exports = React.createClass({ onForgotPasswordClick={this.props.onForgotPasswordClick} loginIncorrect={this.state.loginIncorrect} serverConfig={this.props.serverConfig} - disableSubmit={this.isBusy()} + disableSubmit={this.isBusy() || !this.state.serverIsAlive} /> ); }, @@ -522,6 +566,16 @@ module.exports = React.createClass({ ); } + let serverDeadSection; + if (!this.state.serverIsAlive) { + // TODO: TravisR - Design from Nad + serverDeadSection = ( +
+ {this.state.serverDeadError} +
+ ); + } + return ( @@ -531,6 +585,7 @@ module.exports = React.createClass({ {loader} { errorTextSection } + { serverDeadSection } { this.renderServerComponent() } { this.renderLoginComponentForStep() }
diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 6e4f076091..bf46c7520d 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -26,7 +26,7 @@ import { _t, _td } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; import * as ServerType from '../../views/auth/ServerTypeSelector'; -import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; +import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; // Phases // Show controls to configure server details @@ -79,6 +79,13 @@ module.exports = React.createClass({ // Phase of the overall registration dialog. phase: PHASE_REGISTRATION, flows: null, + + // We perform liveliness checks later, but for now suppress the errors. + // We also track the server dead errors independently of the regular errors so + // that we can render it differently, and override any other error the user may + // be seeing. + serverIsAlive: true, + serverDeadError: "", }; }, @@ -152,6 +159,19 @@ module.exports = React.createClass({ errorText: null, }); if (!serverConfig) serverConfig = this.props.serverConfig; + + // Do a liveliness check on the URLs + try { + await AutoDiscoveryUtils.validateServerConfigWithStaticUrls( + serverConfig.hsUrl, + serverConfig.isUrl, + ); + this.setState({serverIsAlive: true}); + } catch (e) { + this.setState(AutoDiscoveryUtils.authComponentStateForError(e)); + return; // Server is dead - do not continue. + } + const {hsUrl, isUrl} = serverConfig; this._matrixClient = Matrix.createClient({ baseUrl: hsUrl, @@ -447,6 +467,7 @@ module.exports = React.createClass({ onEditServerDetailsClick={onEditServerDetailsClick} flows={this.state.flows} serverConfig={this.props.serverConfig} + canSubmit={this.state.serverIsAlive} />; } }, @@ -462,6 +483,16 @@ module.exports = React.createClass({ errorText =
{ err }
; } + let serverDeadSection; + if (!this.state.serverIsAlive) { + // TODO: TravisR - Design from Nad + serverDeadSection = ( +
+ {this.state.serverDeadError} +
+ ); + } + const signIn =
{ _t('Sign in instead') } ; @@ -480,6 +511,7 @@ module.exports = React.createClass({

{ _t('Create your account') }

{ errorText } + { serverDeadSection } { this.renderServerComponent() } { this.renderRegisterComponent() } { goBack } diff --git a/src/components/views/auth/ModularServerConfig.js b/src/components/views/auth/ModularServerConfig.js index 5a3bc23596..b5af58adf1 100644 --- a/src/components/views/auth/ModularServerConfig.js +++ b/src/components/views/auth/ModularServerConfig.js @@ -108,6 +108,8 @@ export default class ModularServerConfig extends React.PureComponent { busy: false, errorText: message, }); + + return null; } } @@ -132,7 +134,8 @@ export default class ModularServerConfig extends React.PureComponent { onSubmit = async (ev) => { ev.preventDefault(); ev.stopPropagation(); - await this.validateServer(); + const result = await this.validateServer(); + if (!result) return; // Do not continue. if (this.props.onAfterSubmit) { this.props.onAfterSubmit(); diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index b1af6ea42c..ccbfc507c6 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -53,11 +53,13 @@ module.exports = React.createClass({ onEditServerDetailsClick: PropTypes.func, flows: PropTypes.arrayOf(PropTypes.object).isRequired, serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, + canSubmit: PropTypes.bool, }, getDefaultProps: function() { return { onValidationChange: console.error, + canSubmit: true, }; }, @@ -80,6 +82,8 @@ module.exports = React.createClass({ onSubmit: async function(ev) { ev.preventDefault(); + if (!this.props.canSubmit) return; + const allFieldsValid = await this.verifyFieldsBeforeSubmit(); if (!allFieldsValid) { return; @@ -540,7 +544,7 @@ module.exports = React.createClass({ } const registerButton = ( - + ); return ( diff --git a/src/components/views/auth/ServerConfig.js b/src/components/views/auth/ServerConfig.js index 3967f49f18..8d2e2e7bba 100644 --- a/src/components/views/auth/ServerConfig.js +++ b/src/components/views/auth/ServerConfig.js @@ -109,6 +109,8 @@ export default class ServerConfig extends React.PureComponent { busy: false, errorText: message, }); + + return null; } } @@ -137,7 +139,8 @@ export default class ServerConfig extends React.PureComponent { onSubmit = async (ev) => { ev.preventDefault(); ev.stopPropagation(); - await this.validateServer(); + const result = await this.validateServer(); + if (!result) return; // Do not continue. if (this.props.onAfterSubmit) { this.props.onAfterSubmit(); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7673b5f6ab..2b5efc38ee 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -249,8 +249,11 @@ "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …", "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …", "%(names)s and %(lastPerson)s are typing …": "%(names)s and %(lastPerson)s are typing …", + "Server failed liveliness check": "Server failed liveliness check", + "Server failed syntax check": "Server failed syntax check", "No homeserver URL provided": "No homeserver URL provided", "Unexpected error resolving homeserver configuration": "Unexpected error resolving homeserver configuration", + "Unexpected error resolving identity server configuration": "Unexpected error resolving identity server configuration", "This homeserver has hit its Monthly Active User limit.": "This homeserver has hit its Monthly Active User limit.", "This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.", "Please contact your service administrator to continue using the service.": "Please contact your service administrator to continue using the service.", @@ -304,7 +307,6 @@ "Custom user status messages": "Custom user status messages", "Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)", "Render simple counters in room header": "Render simple counters in room header", - "Custom Notification Sounds": "Custom Notification Sounds", "Edit messages after they have been sent (refresh to apply changes)": "Edit messages after they have been sent (refresh to apply changes)", "React to messages with emoji (refresh to apply changes)": "React to messages with emoji (refresh to apply changes)", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", diff --git a/src/utils/AutoDiscoveryUtils.js b/src/utils/AutoDiscoveryUtils.js index 0850039344..405215c237 100644 --- a/src/utils/AutoDiscoveryUtils.js +++ b/src/utils/AutoDiscoveryUtils.js @@ -15,10 +15,15 @@ limitations under the License. */ import {AutoDiscovery} from "matrix-js-sdk"; -import {_td, newTranslatableError} from "../languageHandler"; +import {_t, _td, newTranslatableError} from "../languageHandler"; import {makeType} from "./TypeUtils"; import SdkConfig from "../SdkConfig"; +const LIVLINESS_DISCOVERY_ERRORS = [ + AutoDiscovery.ERROR_INVALID_HOMESERVER, + AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER, +]; + export class ValidatedServerConfig { hsUrl: string; hsName: string; @@ -31,7 +36,52 @@ export class ValidatedServerConfig { } export default class AutoDiscoveryUtils { - static async validateServerConfigWithStaticUrls(homeserverUrl: string, identityUrl: string): ValidatedServerConfig { + /** + * Checks if a given error or error message is considered an error + * relating to the liveliness of the server. Must be an error returned + * from this AutoDiscoveryUtils class. + * @param {string|Error} error The error to check + * @returns {boolean} True if the error is a liveliness error. + */ + static isLivelinessError(error: string|Error): boolean { + if (!error) return false; + return !!LIVLINESS_DISCOVERY_ERRORS.find(e => e === error || e === error.message); + } + + /** + * Gets the common state for auth components (login, registration, forgot + * password) for a given validation error. + * @param {Error} err The error encountered. + * @returns {{serverDeadError: (string|*), serverIsAlive: boolean}} The state + * for the component, given the error. + */ + static authComponentStateForError(err: Error): {serverIsAlive: boolean, serverDeadError: string} { + if (AutoDiscoveryUtils.isLivelinessError(err)) { + // TODO: TravisR - Copy from Nad + return { + serverIsAlive: false, + serverDeadError: _t("Server failed liveliness check"), + }; + } else { + // TODO: TravisR - Copy from Nad + return { + serverIsAlive: false, + serverDeadError: _t("Server failed syntax check"), + }; + } + } + + /** + * Validates a server configuration, using a pair of URLs as input. + * @param {string} homeserverUrl The homeserver URL. + * @param {string} identityUrl The identity server URL. + * @param {boolean} syntaxOnly If true, errors relating to liveliness of the servers will + * not be raised. + * @returns {Promise} Resolves to the validated configuration. + */ + static async validateServerConfigWithStaticUrls( + homeserverUrl: string, identityUrl: string, syntaxOnly = false): ValidatedServerConfig { + if (!homeserverUrl) { throw newTranslatableError(_td("No homeserver URL provided")); } @@ -50,15 +100,33 @@ export default class AutoDiscoveryUtils { const url = new URL(homeserverUrl); const serverName = url.hostname; - return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result); + return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result, syntaxOnly); } - static async validateServerName(serverName: string): ValidatedServerConfig { + /** + * Validates a server configuration, using a homeserver domain name as input. + * @param {string} serverName The homeserver domain name (eg: "matrix.org") to validate. + * @param {boolean} syntaxOnly If true, errors relating to liveliness of the servers will + * not be raised. + * @returns {Promise} Resolves to the validated configuration. + */ + static async validateServerName(serverName: string, syntaxOnly=false): ValidatedServerConfig { const result = await AutoDiscovery.findClientConfig(serverName); return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result); } - static buildValidatedConfigFromDiscovery(serverName: string, discoveryResult): ValidatedServerConfig { + /** + * Validates a server configuration, using a pre-calculated AutoDiscovery result as + * input. + * @param {string} serverName The domain name the AutoDiscovery result is for. + * @param {*} discoveryResult The AutoDiscovery result. + * @param {boolean} syntaxOnly If true, errors relating to liveliness of the servers will + * not be raised. + * @returns {Promise} Resolves to the validated configuration. + */ + static buildValidatedConfigFromDiscovery( + serverName: string, discoveryResult, syntaxOnly=false): ValidatedServerConfig { + if (!discoveryResult || !discoveryResult["m.homeserver"]) { // This shouldn't happen without major misconfiguration, so we'll log a bit of information // in the log so we can find this bit of codee but otherwise tell teh user "it broke". @@ -68,19 +136,27 @@ export default class AutoDiscoveryUtils { const hsResult = discoveryResult['m.homeserver']; if (hsResult.state !== AutoDiscovery.SUCCESS) { - if (AutoDiscovery.ALL_ERRORS.indexOf(hsResult.error) !== -1) { - throw newTranslatableError(hsResult.error); - } - throw newTranslatableError(_td("Unexpected error resolving homeserver configuration")); + console.error("Error processing homeserver config:", hsResult); + if (!syntaxOnly || !AutoDiscoveryUtils.isLivelinessError(hsResult.error)) { + if (AutoDiscovery.ALL_ERRORS.indexOf(hsResult.error) !== -1) { + throw newTranslatableError(hsResult.error); + } + throw newTranslatableError(_td("Unexpected error resolving homeserver configuration")); + } // else the error is not related to syntax - continue anyways. } const isResult = discoveryResult['m.identity_server']; - let preferredIdentityUrl = "https://vector.im"; + let preferredIdentityUrl = "https://vector.im"; // We already know this is an IS, so don't validate it. if (isResult && isResult.state === AutoDiscovery.SUCCESS) { preferredIdentityUrl = isResult["base_url"]; } else if (isResult && isResult.state !== AutoDiscovery.PROMPT) { console.error("Error determining preferred identity server URL:", isResult); - throw newTranslatableError(_td("Unexpected error resolving homeserver configuration")); + if (!syntaxOnly || !AutoDiscoveryUtils.isLivelinessError(isResult.error)) { + if (AutoDiscovery.ALL_ERRORS.indexOf(isResult.error) !== -1) { + throw newTranslatableError(isResult.error); + } + throw newTranslatableError(_td("Unexpected error resolving identity server configuration")); + } // else the error is not related to syntax - continue anyways. } const preferredHomeserverUrl = hsResult["base_url"];