diff --git a/res/css/structures/_GenericErrorPage.scss b/res/css/structures/_GenericErrorPage.scss index 44ea73444e4..2b9e9f5e7d3 100644 --- a/res/css/structures/_GenericErrorPage.scss +++ b/res/css/structures/_GenericErrorPage.scss @@ -12,7 +12,7 @@ right: 0; margin: auto; width: 500px; - height: 200px; + height: 125px; border: 1px solid #f22; padding: 10px 10px 20px; background-color: #fcc; diff --git a/res/css/structures/auth/_Login.scss b/res/css/structures/auth/_Login.scss index 4eff5c33e4e..9ba46c09ab2 100644 --- a/res/css/structures/auth/_Login.scss +++ b/res/css/structures/auth/_Login.scss @@ -62,6 +62,11 @@ limitations under the License. margin-bottom: 12px; } +.mx_Login_error.mx_Login_serverError { + text-align: left; + font-weight: normal; +} + .mx_Login_type_container { display: flex; align-items: center; diff --git a/src/components/structures/GenericErrorPage.js b/src/components/structures/GenericErrorPage.js index 3d8e68cea78..ab7d4f93111 100644 --- a/src/components/structures/GenericErrorPage.js +++ b/src/components/structures/GenericErrorPage.js @@ -16,22 +16,18 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import {_t} from "../../languageHandler"; export default class GenericErrorPage extends React.PureComponent { static propTypes = { + title: PropTypes.object.isRequired, // jsx for title message: PropTypes.object.isRequired, // jsx to display }; render() { return
-

{_t("Error loading Riot")}

+

{this.props.title}

{this.props.message}

-

{_t( - "If this is unexpected, please contact your system administrator " + - "or technical support representative.", - )}

; } diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 2ea39ad657b..3380ea6dacf 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: 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, @@ -86,9 +117,11 @@ module.exports = React.createClass({ }); }, - onSubmitForm: function(ev) { + onSubmitForm: async function(ev) { ev.preventDefault(); + await this._checkServerLiveliness(this.props.serverConfig); + 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,20 @@ 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) { + serverDeadSection = ( +
+ {this.state.serverDeadError} +
+ ); + } + let yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', { serverName: this.props.serverConfig.hsName, }); @@ -207,11 +249,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 b556057cdbe..9abf188ac32 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: "", }; }, @@ -138,7 +145,7 @@ module.exports = React.createClass({ onPasswordLogin: function(username, phoneCountry, phoneNumber, password) { // Prevent people from submitting their password when something isn't right. - if (this.isBusy() || !this.state.canTryLogin) return; + if (this.isBusy()) return; this.setState({ busy: true, @@ -149,6 +156,7 @@ module.exports = React.createClass({ this._loginLogic.loginViaPassword( username, phoneCountry, phoneNumber, password, ).then((data) => { + this.setState({serverIsAlive: true}); // it must be, we logged in. this.props.onLoggedIn(data); }, (error) => { if (this._unmounted) { @@ -247,7 +255,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 = AutoDiscoveryUtils.authComponentStateForError(e); + } + + this.setState({ + busy: false, + errorText, + ...discoveryState, + }); } } }, @@ -297,13 +317,18 @@ module.exports = React.createClass({ }); }, - _initLoginLogic: function(hsUrl, isUrl) { - const self = this; + _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 +340,18 @@ 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: ""}); + } catch (e) { + this.setState({ + busy: false, + ...AutoDiscoveryUtils.authComponentStateForError(e), + }); + 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 +376,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(); @@ -522,6 +559,15 @@ module.exports = React.createClass({ ); } + let serverDeadSection; + if (!this.state.serverIsAlive) { + serverDeadSection = ( +
+ {this.state.serverDeadError} +
+ ); + } + return ( @@ -531,6 +577,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 6e4f076091c..cee809de133 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,15 @@ module.exports = React.createClass({ errorText =
{ err }
; } + let serverDeadSection; + if (!this.state.serverIsAlive) { + serverDeadSection = ( +
+ {this.state.serverDeadError} +
+ ); + } + const signIn =
{ _t('Sign in instead') } ; @@ -480,6 +510,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 5a3bc23596f..b5af58adf19 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 b1af6ea42cf..ccbfc507c60 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 3967f49f189..8d2e2e7bbae 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 7673b5f6ab8..1e883075332 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -249,8 +249,13 @@ "%(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 …", + "Cannot reach homeserver": "Cannot reach homeserver", + "Ensure you have a stable internet connection, or get in touch with the server admin": "Ensure you have a stable internet connection, or get in touch with the server admin", + "Your Riot is misconfigured": "Your Riot is misconfigured", + "Ask your Riot admin to check your config for incorrect or duplicate entries.": "Ask your Riot admin to check your config for incorrect or duplicate entries.", "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 +309,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", @@ -1385,8 +1389,6 @@ "You must register to use this functionality": "You must register to use this functionality", "You must join the room to see its files": "You must join the room to see its files", "There are no visible files in this room": "There are no visible files in this room", - "Error loading Riot": "Error loading Riot", - "If this is unexpected, please contact your system administrator or technical support representative.": "If this is unexpected, please contact your system administrator or technical support representative.", "

HTML for your community's page

\n

\n Use the long description to introduce new members to the community, or distribute\n some important links\n

\n

\n You can even use 'img' tags\n

\n": "

HTML for your community's page

\n

\n Use the long description to introduce new members to the community, or distribute\n some important links\n

\n

\n You can even use 'img' tags\n

\n", "Add rooms to the community summary": "Add rooms to the community summary", "Which rooms would you like to add to this summary?": "Which rooms would you like to add to this summary?", diff --git a/src/utils/AutoDiscoveryUtils.js b/src/utils/AutoDiscoveryUtils.js index 08500393441..55cf84b9ad4 100644 --- a/src/utils/AutoDiscoveryUtils.js +++ b/src/utils/AutoDiscoveryUtils.js @@ -14,11 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from 'react'; 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 +37,65 @@ 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} { + let title = _t("Cannot reach homeserver"); + let body = _t("Ensure you have a stable internet connection, or get in touch with the server admin"); + if (!AutoDiscoveryUtils.isLivelinessError(err)) { + title = _t("Your Riot is misconfigured"); + body = _t( + "Ask your Riot admin to check your config for incorrect or duplicate entries.", + {}, { + a: (sub) => { + return {sub}; + }, + }, + ); + } + + return { + serverIsAlive: false, + serverDeadError: ( +
+ {title} +
{body}
+
+ ), + }; + } + + /** + * 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 +114,32 @@ 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 +149,31 @@ 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']; + // Note: In the cases where we rely on this pre-populated "https://vector.im" (namely + // lack of identity server provided by the discovery method), we intentionally do not + // validate it. We already know the IS is an IS, and this helps some off-the-grid usage + // of Riot. let preferredIdentityUrl = "https://vector.im"; + const isResult = discoveryResult['m.identity_server']; 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"];