Merge pull request #2941 from matrix-org/travis/guests/block-ui

Use validated server config for login, registration, and password reset
This commit is contained in:
Travis Ralston 2019-05-10 11:21:21 -06:00 committed by GitHub
commit 6a941aca91
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 509 additions and 560 deletions

View file

@ -35,3 +35,8 @@ limitations under the License.
.mx_ServerConfig_help:link { .mx_ServerConfig_help:link {
opacity: 0.8; opacity: 0.8;
} }
.mx_ServerConfig_error {
display: block;
color: $warning-color;
}

View file

@ -50,8 +50,7 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
import { startAnyRegistrationFlow } from "../../Registration.js"; import { startAnyRegistrationFlow } from "../../Registration.js";
import { messageForSyncError } from '../../utils/ErrorUtils'; import { messageForSyncError } from '../../utils/ErrorUtils';
import ResizeNotifier from "../../utils/ResizeNotifier"; import ResizeNotifier from "../../utils/ResizeNotifier";
import {ValidatedServerConfig} from "../../utils/AutoDiscoveryUtils";
const AutoDiscovery = Matrix.AutoDiscovery;
// Disable warnings for now: we use deprecated bluebird functions // Disable warnings for now: we use deprecated bluebird functions
// and need to migrate, but they spam the console with warnings. // and need to migrate, but they spam the console with warnings.
@ -109,6 +108,7 @@ export default React.createClass({
propTypes: { propTypes: {
config: PropTypes.object, config: PropTypes.object,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig),
ConferenceHandler: PropTypes.any, ConferenceHandler: PropTypes.any,
onNewScreen: PropTypes.func, onNewScreen: PropTypes.func,
registrationUrl: PropTypes.string, registrationUrl: PropTypes.string,
@ -181,16 +181,8 @@ export default React.createClass({
// Parameters used in the registration dance with the IS // Parameters used in the registration dance with the IS
register_client_secret: null, register_client_secret: null,
register_session_id: null, register_session_id: null,
register_hs_url: null,
register_is_url: null,
register_id_sid: null, register_id_sid: null,
// Parameters used for setting up the authentication views
defaultServerName: this.props.config.default_server_name,
defaultHsUrl: this.props.config.default_hs_url,
defaultIsUrl: this.props.config.default_is_url,
defaultServerDiscoveryError: null,
// When showing Modal dialogs we need to set aria-hidden on the root app element // When showing Modal dialogs we need to set aria-hidden on the root app element
// and disable it when there are no dialogs // and disable it when there are no dialogs
hideToSRUsers: false, hideToSRUsers: false,
@ -211,42 +203,16 @@ export default React.createClass({
}; };
}, },
getDefaultServerName: function() { // TODO: TravisR - Remove this or put it somewhere else
return this.state.defaultServerName;
},
getCurrentHsUrl: function() {
if (this.state.register_hs_url) {
return this.state.register_hs_url;
} else if (MatrixClientPeg.get()) {
return MatrixClientPeg.get().getHomeserverUrl();
} else {
return this.getDefaultHsUrl();
}
},
getDefaultHsUrl(defaultToMatrixDotOrg) {
defaultToMatrixDotOrg = typeof(defaultToMatrixDotOrg) !== 'boolean' ? true : defaultToMatrixDotOrg;
if (!this.state.defaultHsUrl && defaultToMatrixDotOrg) return "https://matrix.org";
return this.state.defaultHsUrl;
},
getFallbackHsUrl: function() { getFallbackHsUrl: function() {
return this.props.config.fallback_hs_url; return this.props.config.fallback_hs_url;
}, },
getCurrentIsUrl: function() { getServerProperties() {
if (this.state.register_is_url) { let props = this.state.serverConfig;
return this.state.register_is_url; if (!props) props = this.props.serverConfig; // for unit tests
} else if (MatrixClientPeg.get()) { if (!props) props = SdkConfig.get()["validated_server_config"];
return MatrixClientPeg.get().getIdentityServerUrl(); return {serverConfig: props};
} else {
return this.getDefaultIsUrl();
}
},
getDefaultIsUrl() {
return this.state.defaultIsUrl || "https://vector.im";
}, },
componentWillMount: function() { componentWillMount: function() {
@ -260,40 +226,6 @@ export default React.createClass({
MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit;
} }
// Set up the default URLs (async)
if (this.getDefaultServerName() && !this.getDefaultHsUrl(false)) {
this.setState({loadingDefaultHomeserver: true});
this._tryDiscoverDefaultHomeserver(this.getDefaultServerName());
} else if (this.getDefaultServerName() && this.getDefaultHsUrl(false)) {
// Ideally we would somehow only communicate this to the server admins, but
// given this is at login time we can't really do much besides hope that people
// will check their settings.
this.setState({
defaultServerName: null, // To un-hide any secrets people might be keeping
defaultServerDiscoveryError: _t(
"Invalid configuration: Cannot supply a default homeserver URL and " +
"a default server name",
),
});
}
// Set a default HS with query param `hs_url`
const paramHs = this.props.startingFragmentQueryParams.hs_url;
if (paramHs) {
console.log('Setting register_hs_url ', paramHs);
this.setState({
register_hs_url: paramHs,
});
}
// Set a default IS with query param `is_url`
const paramIs = this.props.startingFragmentQueryParams.is_url;
if (paramIs) {
console.log('Setting register_is_url ', paramIs);
this.setState({
register_is_url: paramIs,
});
}
// a thing to call showScreen with once login completes. this is kept // a thing to call showScreen with once login completes. this is kept
// outside this.state because updating it should never trigger a // outside this.state because updating it should never trigger a
// rerender. // rerender.
@ -374,8 +306,8 @@ export default React.createClass({
return Lifecycle.loadSession({ return Lifecycle.loadSession({
fragmentQueryParams: this.props.startingFragmentQueryParams, fragmentQueryParams: this.props.startingFragmentQueryParams,
enableGuest: this.props.enableGuest, enableGuest: this.props.enableGuest,
guestHsUrl: this.getCurrentHsUrl(), guestHsUrl: this.getServerProperties().serverConfig.hsUrl,
guestIsUrl: this.getCurrentIsUrl(), guestIsUrl: this.getServerProperties().serverConfig.isUrl,
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
}); });
}).then((loadedSession) => { }).then((loadedSession) => {
@ -1823,44 +1755,7 @@ export default React.createClass({
}, },
onServerConfigChange(config) { onServerConfigChange(config) {
const newState = {}; this.setState({serverConfig: config});
if (config.hsUrl) {
newState.register_hs_url = config.hsUrl;
}
if (config.isUrl) {
newState.register_is_url = config.isUrl;
}
this.setState(newState);
},
_tryDiscoverDefaultHomeserver: async function(serverName) {
try {
const discovery = await AutoDiscovery.findClientConfig(serverName);
const state = discovery["m.homeserver"].state;
if (state !== AutoDiscovery.SUCCESS) {
console.error("Failed to discover homeserver on startup:", discovery);
this.setState({
defaultServerDiscoveryError: discovery["m.homeserver"].error,
loadingDefaultHomeserver: false,
});
} else {
const hsUrl = discovery["m.homeserver"].base_url;
const isUrl = discovery["m.identity_server"].state === AutoDiscovery.SUCCESS
? discovery["m.identity_server"].base_url
: "https://vector.im";
this.setState({
defaultHsUrl: hsUrl,
defaultIsUrl: isUrl,
loadingDefaultHomeserver: false,
});
}
} catch (e) {
console.error(e);
this.setState({
defaultServerDiscoveryError: _t("Unknown error discovering homeserver"),
loadingDefaultHomeserver: false,
});
}
}, },
_makeRegistrationUrl: function(params) { _makeRegistrationUrl: function(params) {
@ -1879,8 +1774,7 @@ export default React.createClass({
if ( if (
this.state.view === VIEWS.LOADING || this.state.view === VIEWS.LOADING ||
this.state.view === VIEWS.LOGGING_IN || this.state.view === VIEWS.LOGGING_IN
this.state.loadingDefaultHomeserver
) { ) {
const Spinner = sdk.getComponent('elements.Spinner'); const Spinner = sdk.getComponent('elements.Spinner');
return ( return (
@ -1958,18 +1852,13 @@ export default React.createClass({
sessionId={this.state.register_session_id} sessionId={this.state.register_session_id}
idSid={this.state.register_id_sid} idSid={this.state.register_id_sid}
email={this.props.startingFragmentQueryParams.email} email={this.props.startingFragmentQueryParams.email}
defaultServerName={this.getDefaultServerName()}
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()}
brand={this.props.config.brand} brand={this.props.config.brand}
customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()}
makeRegistrationUrl={this._makeRegistrationUrl} makeRegistrationUrl={this._makeRegistrationUrl}
onLoggedIn={this.onRegistered} onLoggedIn={this.onRegistered}
onLoginClick={this.onLoginClick} onLoginClick={this.onLoginClick}
onServerConfigChange={this.onServerConfigChange} onServerConfigChange={this.onServerConfigChange}
/> {...this.getServerProperties()}
/>
); );
} }
@ -1978,14 +1867,11 @@ export default React.createClass({
const ForgotPassword = sdk.getComponent('structures.auth.ForgotPassword'); const ForgotPassword = sdk.getComponent('structures.auth.ForgotPassword');
return ( return (
<ForgotPassword <ForgotPassword
defaultServerName={this.getDefaultServerName()}
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()}
customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()}
onComplete={this.onLoginClick} onComplete={this.onLoginClick}
onLoginClick={this.onLoginClick} /> onLoginClick={this.onLoginClick}
onServerConfigChange={this.onServerConfigChange}
{...this.getServerProperties()}
/>
); );
} }
@ -1995,16 +1881,11 @@ export default React.createClass({
<Login <Login
onLoggedIn={Lifecycle.setLoggedIn} onLoggedIn={Lifecycle.setLoggedIn}
onRegisterClick={this.onRegisterClick} onRegisterClick={this.onRegisterClick}
defaultServerName={this.getDefaultServerName()}
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()}
customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()}
fallbackHsUrl={this.getFallbackHsUrl()} fallbackHsUrl={this.getFallbackHsUrl()}
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName} defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
onForgotPasswordClick={this.onForgotPasswordClick} onForgotPasswordClick={this.onForgotPasswordClick}
onServerConfigChange={this.onServerConfigChange} onServerConfigChange={this.onServerConfigChange}
{...this.getServerProperties()}
/> />
); );
} }

View file

@ -21,8 +21,8 @@ import { _t } from '../../../languageHandler';
import sdk from '../../../index'; import sdk from '../../../index';
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import SdkConfig from "../../../SdkConfig"; import SdkConfig from "../../../SdkConfig";
import PasswordReset from "../../../PasswordReset"; import PasswordReset from "../../../PasswordReset";
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
// Phases // Phases
// Show controls to configure server details // Show controls to configure server details
@ -40,28 +40,14 @@ module.exports = React.createClass({
displayName: 'ForgotPassword', displayName: 'ForgotPassword',
propTypes: { propTypes: {
// The default server name to use when the user hasn't specified serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
// one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this onServerConfigChange: PropTypes.func.isRequired,
// via `.well-known` discovery. The server name is used instead of the
// HS URL when talking about "your account".
defaultServerName: PropTypes.string,
// An error passed along from higher up explaining that something
// went wrong when finding the defaultHsUrl.
defaultServerDiscoveryError: PropTypes.string,
defaultHsUrl: PropTypes.string,
defaultIsUrl: PropTypes.string,
customHsUrl: PropTypes.string,
customIsUrl: PropTypes.string,
onLoginClick: PropTypes.func, onLoginClick: PropTypes.func,
onComplete: PropTypes.func.isRequired, onComplete: PropTypes.func.isRequired,
}, },
getInitialState: function() { getInitialState: function() {
return { return {
enteredHsUrl: this.props.customHsUrl || this.props.defaultHsUrl,
enteredIsUrl: this.props.customIsUrl || this.props.defaultIsUrl,
phase: PHASE_FORGOT, phase: PHASE_FORGOT,
email: "", email: "",
password: "", password: "",
@ -70,11 +56,11 @@ module.exports = React.createClass({
}; };
}, },
submitPasswordReset: function(hsUrl, identityUrl, email, password) { submitPasswordReset: function(email, password) {
this.setState({ this.setState({
phase: PHASE_SENDING_EMAIL, phase: PHASE_SENDING_EMAIL,
}); });
this.reset = new PasswordReset(hsUrl, identityUrl); this.reset = new PasswordReset(this.props.serverConfig.hsUrl, this.props.serverConfig.isUrl);
this.reset.resetPassword(email, password).done(() => { this.reset.resetPassword(email, password).done(() => {
this.setState({ this.setState({
phase: PHASE_EMAIL_SENT, phase: PHASE_EMAIL_SENT,
@ -103,13 +89,6 @@ module.exports = React.createClass({
onSubmitForm: function(ev) { onSubmitForm: function(ev) {
ev.preventDefault(); ev.preventDefault();
// Don't allow the user to register if there's a discovery error
// Without this, the user could end up registering on the wrong homeserver.
if (this.props.defaultServerDiscoveryError) {
this.setState({errorText: this.props.defaultServerDiscoveryError});
return;
}
if (!this.state.email) { if (!this.state.email) {
this.showErrorDialog(_t('The email address linked to your account must be entered.')); this.showErrorDialog(_t('The email address linked to your account must be entered.'));
} else if (!this.state.password || !this.state.password2) { } else if (!this.state.password || !this.state.password2) {
@ -132,10 +111,7 @@ module.exports = React.createClass({
button: _t('Continue'), button: _t('Continue'),
onFinished: (confirmed) => { onFinished: (confirmed) => {
if (confirmed) { if (confirmed) {
this.submitPasswordReset( this.submitPasswordReset(this.state.email, this.state.password);
this.state.enteredHsUrl, this.state.enteredIsUrl,
this.state.email, this.state.password,
);
} }
}, },
}); });
@ -148,19 +124,13 @@ module.exports = React.createClass({
}); });
}, },
onServerConfigChange: function(config) { async onServerDetailsNextPhaseClick(ev) {
const newState = {};
if (config.hsUrl !== undefined) {
newState.enteredHsUrl = config.hsUrl;
}
if (config.isUrl !== undefined) {
newState.enteredIsUrl = config.isUrl;
}
this.setState(newState);
},
onServerDetailsNextPhaseClick(ev) {
ev.stopPropagation(); ev.stopPropagation();
// TODO: TravisR - Capture the user's input somehow else
if (this._serverConfigRef) {
// Just to make sure the user's input gets captured
await this._serverConfigRef.validateServer();
}
this.setState({ this.setState({
phase: PHASE_FORGOT, phase: PHASE_FORGOT,
}); });
@ -196,13 +166,12 @@ module.exports = React.createClass({
return null; return null;
} }
// TODO: TravisR - Pull out server discovery from ServerConfig to disable the next button?
return <div> return <div>
<ServerConfig <ServerConfig
defaultHsUrl={this.props.defaultHsUrl} ref={r => this._serverConfigRef = r}
defaultIsUrl={this.props.defaultIsUrl} serverConfig={this.props.serverConfig}
customHsUrl={this.state.enteredHsUrl} onServerConfigChange={this.props.onServerConfigChange}
customIsUrl={this.state.enteredIsUrl}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={0} /> delayTimeMs={0} />
<AccessibleButton className="mx_Login_submit" <AccessibleButton className="mx_Login_submit"
onClick={this.onServerDetailsNextPhaseClick} onClick={this.onServerDetailsNextPhaseClick}
@ -221,25 +190,14 @@ module.exports = React.createClass({
errorText = <div className="mx_Login_error">{ err }</div>; errorText = <div className="mx_Login_error">{ err }</div>;
} }
let yourMatrixAccountText = _t('Your Matrix account'); let yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', {
if (this.state.enteredHsUrl === this.props.defaultHsUrl && this.props.defaultServerName) { serverName: this.props.serverConfig.hsName,
yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', { });
serverName: this.props.defaultServerName, if (this.props.serverConfig.hsNameIsDifferent) {
// TODO: TravisR - Use tooltip to underline
yourMatrixAccountText = _t('Your Matrix account on <underlinedServerName />', {}, {
'underlinedServerName': () => <u>{this.props.serverConfig.hsName}</u>,
}); });
} else {
try {
const parsedHsUrl = new URL(this.state.enteredHsUrl);
yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', {
serverName: parsedHsUrl.hostname,
});
} catch (e) {
errorText = <div className="mx_Login_error">{_t(
"The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please " +
"enter a valid URL including the protocol prefix.",
{
hsUrl: this.state.enteredHsUrl,
})}</div>;
}
} }
// If custom URLs are allowed, wire up the server details edit link. // If custom URLs are allowed, wire up the server details edit link.

View file

@ -25,7 +25,7 @@ import sdk from '../../../index';
import Login from '../../../Login'; import Login from '../../../Login';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
import { AutoDiscovery } from "matrix-js-sdk"; import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
// For validating phone numbers without country codes // For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
@ -59,19 +59,14 @@ module.exports = React.createClass({
propTypes: { propTypes: {
onLoggedIn: PropTypes.func.isRequired, onLoggedIn: PropTypes.func.isRequired,
// The default server name to use when the user hasn't specified
// one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this
// via `.well-known` discovery. The server name is used instead of the
// HS URL when talking about where to "sign in to".
defaultServerName: PropTypes.string,
// An error passed along from higher up explaining that something // An error passed along from higher up explaining that something
// went wrong when finding the defaultHsUrl. // went wrong. May be replaced with a different error within the
defaultServerDiscoveryError: PropTypes.string, // Login component.
errorText: PropTypes.string,
// If true, the component will consider itself busy.
busy: PropTypes.bool,
customHsUrl: PropTypes.string,
customIsUrl: PropTypes.string,
defaultHsUrl: PropTypes.string,
defaultIsUrl: PropTypes.string,
// Secondary HS which we try to log into if the user is using // Secondary HS which we try to log into if the user is using
// the default HS but login fails. Useful for migrating to a // the default HS but login fails. Useful for migrating to a
// different homeserver without confusing users. // different homeserver without confusing users.
@ -79,12 +74,13 @@ module.exports = React.createClass({
defaultDeviceDisplayName: PropTypes.string, defaultDeviceDisplayName: PropTypes.string,
// login shouldn't know or care how registration is done. // login shouldn't know or care how registration, password recovery,
// etc is done.
onRegisterClick: PropTypes.func.isRequired, onRegisterClick: PropTypes.func.isRequired,
// login shouldn't care how password recovery is done.
onForgotPasswordClick: PropTypes.func, onForgotPasswordClick: PropTypes.func,
onServerConfigChange: PropTypes.func.isRequired, onServerConfigChange: PropTypes.func.isRequired,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
}, },
getInitialState: function() { getInitialState: function() {
@ -93,9 +89,6 @@ module.exports = React.createClass({
errorText: null, errorText: null,
loginIncorrect: false, loginIncorrect: false,
enteredHsUrl: this.props.customHsUrl || this.props.defaultHsUrl,
enteredIsUrl: this.props.customIsUrl || this.props.defaultIsUrl,
// used for preserving form values when changing homeserver // used for preserving form values when changing homeserver
username: "", username: "",
phoneCountry: null, phoneCountry: null,
@ -105,10 +98,6 @@ module.exports = React.createClass({
phase: PHASE_LOGIN, phase: PHASE_LOGIN,
// The current login flow, such as password, SSO, etc. // The current login flow, such as password, SSO, etc.
currentFlow: "m.login.password", currentFlow: "m.login.password",
// .well-known discovery
discoveryError: "",
findingHomeserver: false,
}; };
}, },
@ -132,6 +121,14 @@ module.exports = React.createClass({
this._unmounted = true; this._unmounted = true;
}, },
componentWillReceiveProps(newProps) {
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
// Ensure that we end up actually logging in to the right place
this._initLoginLogic(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl);
},
onPasswordLoginError: function(errorText) { onPasswordLoginError: function(errorText) {
this.setState({ this.setState({
errorText, errorText,
@ -139,10 +136,17 @@ module.exports = React.createClass({
}); });
}, },
isBusy: function() {
return this.state.busy || this.props.busy;
},
hasError: function() {
return this.state.errorText || this.props.errorText;
},
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) { onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
// Prevent people from submitting their password when homeserver // Prevent people from submitting their password when something isn't right.
// discovery went wrong if (this.isBusy() || this.hasError()) return;
if (this.state.discoveryError || this.props.defaultServerDiscoveryError) return;
this.setState({ this.setState({
busy: true, busy: true,
@ -164,7 +168,7 @@ module.exports = React.createClass({
const usingEmail = username.indexOf("@") > 0; const usingEmail = username.indexOf("@") > 0;
if (error.httpStatus === 400 && usingEmail) { if (error.httpStatus === 400 && usingEmail) {
errorText = _t('This homeserver does not support login using email address.'); errorText = _t('This homeserver does not support login using email address.');
} else if (error.errcode == 'M_RESOURCE_LIMIT_EXCEEDED') { } else if (error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
const errorTop = messageForResourceLimitError( const errorTop = messageForResourceLimitError(
error.data.limit_type, error.data.limit_type,
error.data.admin_contact, { error.data.admin_contact, {
@ -194,11 +198,10 @@ module.exports = React.createClass({
<div> <div>
<div>{ _t('Incorrect username and/or password.') }</div> <div>{ _t('Incorrect username and/or password.') }</div>
<div className="mx_Login_smallError"> <div className="mx_Login_smallError">
{ _t('Please note you are logging into the %(hs)s server, not matrix.org.', {_t(
{ 'Please note you are logging into the %(hs)s server, not matrix.org.',
hs: this.props.defaultHsUrl.replace(/^https?:\/\//, ''), {hs: this.props.serverConfig.hsName},
}) )}
}
</div> </div>
</div> </div>
); );
@ -235,9 +238,9 @@ module.exports = React.createClass({
onUsernameBlur: function(username) { onUsernameBlur: function(username) {
this.setState({ this.setState({
username: username, username: username,
discoveryError: null, errorText: null,
}); });
if (username[0] === "@") { if (username[0] === "@" && false) { // TODO: TravisR - Restore this
const serverName = username.split(':').slice(1).join(':'); const serverName = username.split(':').slice(1).join(':');
try { try {
// we have to append 'https://' to make the URL constructor happy // we have to append 'https://' to make the URL constructor happy
@ -246,7 +249,7 @@ module.exports = React.createClass({
this._tryWellKnownDiscovery(url.hostname); this._tryWellKnownDiscovery(url.hostname);
} catch (e) { } catch (e) {
console.error("Problem parsing URL or unhandled error doing .well-known discovery:", e); console.error("Problem parsing URL or unhandled error doing .well-known discovery:", e);
this.setState({discoveryError: _t("Failed to perform homeserver discovery")}); this.setState({errorText: _t("Failed to perform homeserver discovery")});
} }
} }
}, },
@ -274,32 +277,19 @@ module.exports = React.createClass({
} }
}, },
onServerConfigChange: function(config) {
const self = this;
const newState = {
errorText: null, // reset err messages
};
if (config.hsUrl !== undefined) {
newState.enteredHsUrl = config.hsUrl;
}
if (config.isUrl !== undefined) {
newState.enteredIsUrl = config.isUrl;
}
this.props.onServerConfigChange(config);
this.setState(newState, function() {
self._initLoginLogic(config.hsUrl || null, config.isUrl);
});
},
onRegisterClick: function(ev) { onRegisterClick: function(ev) {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this.props.onRegisterClick(); this.props.onRegisterClick();
}, },
onServerDetailsNextPhaseClick(ev) { async onServerDetailsNextPhaseClick(ev) {
ev.stopPropagation(); ev.stopPropagation();
// TODO: TravisR - Capture the user's input somehow else
if (this._serverConfigRef) {
// Just to make sure the user's input gets captured
await this._serverConfigRef.validateServer();
}
this.setState({ this.setState({
phase: PHASE_LOGIN, phase: PHASE_LOGIN,
}); });
@ -313,64 +303,13 @@ module.exports = React.createClass({
}); });
}, },
_tryWellKnownDiscovery: async function(serverName) {
if (!serverName.trim()) {
// Nothing to discover
this.setState({
discoveryError: "",
findingHomeserver: false,
});
return;
}
this.setState({findingHomeserver: true});
try {
const discovery = await AutoDiscovery.findClientConfig(serverName);
const state = discovery["m.homeserver"].state;
if (state !== AutoDiscovery.SUCCESS && state !== AutoDiscovery.PROMPT) {
this.setState({
discoveryError: discovery["m.homeserver"].error,
findingHomeserver: false,
});
} else if (state === AutoDiscovery.PROMPT) {
this.setState({
discoveryError: "",
findingHomeserver: false,
});
} else if (state === AutoDiscovery.SUCCESS) {
this.setState({
discoveryError: "",
findingHomeserver: false,
});
this.onServerConfigChange({
hsUrl: discovery["m.homeserver"].base_url,
isUrl: discovery["m.identity_server"].state === AutoDiscovery.SUCCESS
? discovery["m.identity_server"].base_url
: "",
});
} else {
console.warn("Unknown state for m.homeserver in discovery response: ", discovery);
this.setState({
discoveryError: _t("Unknown failure discovering homeserver"),
findingHomeserver: false,
});
}
} catch (e) {
console.error(e);
this.setState({
findingHomeserver: false,
discoveryError: _t("Unknown error discovering homeserver"),
});
}
},
_initLoginLogic: function(hsUrl, isUrl) { _initLoginLogic: function(hsUrl, isUrl) {
const self = this; const self = this;
hsUrl = hsUrl || this.state.enteredHsUrl; hsUrl = hsUrl || this.props.serverConfig.hsUrl;
isUrl = isUrl || this.state.enteredIsUrl; isUrl = isUrl || this.props.serverConfig.isUrl;
const fallbackHsUrl = hsUrl === this.props.defaultHsUrl ? this.props.fallbackHsUrl : null; // TODO: TravisR - Only use this if the homeserver is the default homeserver
const fallbackHsUrl = this.props.fallbackHsUrl;
const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, { const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
@ -378,8 +317,6 @@ module.exports = React.createClass({
this._loginLogic = loginLogic; this._loginLogic = loginLogic;
this.setState({ this.setState({
enteredHsUrl: hsUrl,
enteredIsUrl: isUrl,
busy: true, busy: true,
loginIncorrect: false, loginIncorrect: false,
}); });
@ -445,8 +382,8 @@ module.exports = React.createClass({
if (err.cors === 'rejected') { if (err.cors === 'rejected') {
if (window.location.protocol === 'https:' && if (window.location.protocol === 'https:' &&
(this.state.enteredHsUrl.startsWith("http:") || (this.props.serverConfig.hsUrl.startsWith("http:") ||
!this.state.enteredHsUrl.startsWith("http")) !this.props.serverConfig.hsUrl.startsWith("http"))
) { ) {
errorText = <span> errorText = <span>
{ _t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " + { _t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
@ -469,9 +406,9 @@ module.exports = React.createClass({
"is not blocking requests.", {}, "is not blocking requests.", {},
{ {
'a': (sub) => { 'a': (sub) => {
return <a target="_blank" rel="noopener" return <a target="_blank" rel="noopener" href={this.props.serverConfig.hsUrl}>
href={this.state.enteredHsUrl} { sub }
>{ sub }</a>; </a>;
}, },
}, },
) } ) }
@ -495,19 +432,17 @@ module.exports = React.createClass({
} }
const serverDetails = <ServerConfig const serverDetails = <ServerConfig
customHsUrl={this.state.enteredHsUrl} ref={r => this._serverConfigRef = r}
customIsUrl={this.state.enteredIsUrl} serverConfig={this.props.serverConfig}
defaultHsUrl={this.props.defaultHsUrl} onServerConfigChange={this.props.onServerConfigChange}
defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={250} delayTimeMs={250}
/>; />;
let nextButton = null; let nextButton = null;
if (PHASES_ENABLED) { if (PHASES_ENABLED) {
// TODO: TravisR - Pull out server discovery from ServerConfig to disable the next button?
nextButton = <AccessibleButton className="mx_Login_submit" nextButton = <AccessibleButton className="mx_Login_submit"
onClick={this.onServerDetailsNextPhaseClick} onClick={this.onServerDetailsNextPhaseClick}>
>
{_t("Next")} {_t("Next")}
</AccessibleButton>; </AccessibleButton>;
} }
@ -547,13 +482,6 @@ module.exports = React.createClass({
onEditServerDetailsClick = this.onEditServerDetailsClick; onEditServerDetailsClick = this.onEditServerDetailsClick;
} }
// If the current HS URL is the default HS URL, then we can label it
// with the default HS name (if it exists).
let hsName;
if (this.state.enteredHsUrl === this.props.defaultHsUrl) {
hsName = this.props.defaultServerName;
}
return ( return (
<PasswordLogin <PasswordLogin
onSubmit={this.onPasswordLogin} onSubmit={this.onPasswordLogin}
@ -569,10 +497,9 @@ module.exports = React.createClass({
onPhoneNumberBlur={this.onPhoneNumberBlur} onPhoneNumberBlur={this.onPhoneNumberBlur}
onForgotPasswordClick={this.props.onForgotPasswordClick} onForgotPasswordClick={this.props.onForgotPasswordClick}
loginIncorrect={this.state.loginIncorrect} loginIncorrect={this.state.loginIncorrect}
hsName={hsName} serverConfig={this.props.serverConfig}
hsUrl={this.state.enteredHsUrl} disableSubmit={this.isBusy()}
disableSubmit={this.state.findingHomeserver} />
/>
); );
}, },
@ -595,9 +522,9 @@ module.exports = React.createClass({
const AuthPage = sdk.getComponent("auth.AuthPage"); const AuthPage = sdk.getComponent("auth.AuthPage");
const AuthHeader = sdk.getComponent("auth.AuthHeader"); const AuthHeader = sdk.getComponent("auth.AuthHeader");
const AuthBody = sdk.getComponent("auth.AuthBody"); const AuthBody = sdk.getComponent("auth.AuthBody");
const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null; const loader = this.isBusy() ? <div className="mx_Login_loader"><Loader /></div> : null;
const errorText = this.props.defaultServerDiscoveryError || this.state.discoveryError || this.state.errorText; const errorText = this.state.errorText || this.props.errorText;
let errorTextSection; let errorTextSection;
if (errorText) { if (errorText) {

View file

@ -17,16 +17,15 @@ limitations under the License.
*/ */
import Matrix from 'matrix-js-sdk'; import Matrix from 'matrix-js-sdk';
import Promise from 'bluebird'; import Promise from 'bluebird';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t, _td } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
import * as ServerType from '../../views/auth/ServerTypeSelector'; import * as ServerType from '../../views/auth/ServerTypeSelector';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
// Phases // Phases
// Show controls to configure server details // Show controls to configure server details
@ -46,18 +45,7 @@ module.exports = React.createClass({
sessionId: PropTypes.string, sessionId: PropTypes.string,
makeRegistrationUrl: PropTypes.func.isRequired, makeRegistrationUrl: PropTypes.func.isRequired,
idSid: PropTypes.string, idSid: PropTypes.string,
// The default server name to use when the user hasn't specified serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
// one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this
// via `.well-known` discovery. The server name is used instead of the
// HS URL when talking about "your account".
defaultServerName: PropTypes.string,
// An error passed along from higher up explaining that something
// went wrong when finding the defaultHsUrl.
defaultServerDiscoveryError: PropTypes.string,
customHsUrl: PropTypes.string,
customIsUrl: PropTypes.string,
defaultHsUrl: PropTypes.string,
defaultIsUrl: PropTypes.string,
brand: PropTypes.string, brand: PropTypes.string,
email: PropTypes.string, email: PropTypes.string,
// registration shouldn't know or care how login is done. // registration shouldn't know or care how login is done.
@ -66,7 +54,7 @@ module.exports = React.createClass({
}, },
getInitialState: function() { getInitialState: function() {
const serverType = ServerType.getTypeFromHsUrl(this.props.customHsUrl); const serverType = ServerType.getTypeFromServerConfig(this.props.serverConfig);
return { return {
busy: false, busy: false,
@ -87,8 +75,6 @@ module.exports = React.createClass({
// straight back into UI auth // straight back into UI auth
doingUIAuth: Boolean(this.props.sessionId), doingUIAuth: Boolean(this.props.sessionId),
serverType, serverType,
hsUrl: this.props.customHsUrl,
isUrl: this.props.customIsUrl,
// Phase of the overall registration dialog. // Phase of the overall registration dialog.
phase: PHASE_REGISTRATION, phase: PHASE_REGISTRATION,
flows: null, flows: null,
@ -100,18 +86,22 @@ module.exports = React.createClass({
this._replaceClient(); this._replaceClient();
}, },
onServerConfigChange: function(config) { componentWillReceiveProps(newProps) {
const newState = {}; if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
if (config.hsUrl !== undefined) { newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
newState.hsUrl = config.hsUrl;
this._replaceClient(newProps.serverConfig);
// Handle cases where the user enters "https://matrix.org" for their server
// from the advanced option - we should default to FREE at that point.
const serverType = ServerType.getTypeFromServerConfig(newProps.serverConfig);
if (serverType !== this.state.serverType) {
// Reset the phase to default phase for the server type.
this.setState({
serverType,
phase: this.getDefaultPhaseForServerType(serverType),
});
} }
if (config.isUrl !== undefined) {
newState.isUrl = config.isUrl;
}
this.props.onServerConfigChange(config);
this.setState(newState, () => {
this._replaceClient();
});
}, },
getDefaultPhaseForServerType(type) { getDefaultPhaseForServerType(type) {
@ -136,19 +126,17 @@ module.exports = React.createClass({
// the new type. // the new type.
switch (type) { switch (type) {
case ServerType.FREE: { case ServerType.FREE: {
const { hsUrl, isUrl } = ServerType.TYPES.FREE; const { serverConfig } = ServerType.TYPES.FREE;
this.onServerConfigChange({ this.props.onServerConfigChange(serverConfig);
hsUrl,
isUrl,
});
break; break;
} }
case ServerType.PREMIUM: case ServerType.PREMIUM:
// We can accept whatever server config was the default here as this essentially
// acts as a slightly different "custom server"/ADVANCED option.
break;
case ServerType.ADVANCED: case ServerType.ADVANCED:
this.onServerConfigChange({ // Use the default config from the config
hsUrl: this.props.defaultHsUrl, this.props.onServerConfigChange(SdkConfig.get()["validated_server_config"]);
isUrl: this.props.defaultIsUrl,
});
break; break;
} }
@ -158,13 +146,15 @@ module.exports = React.createClass({
}); });
}, },
_replaceClient: async function() { _replaceClient: async function(serverConfig) {
this.setState({ this.setState({
errorText: null, errorText: null,
}); });
if (!serverConfig) serverConfig = this.props.serverConfig;
const {hsUrl, isUrl} = serverConfig;
this._matrixClient = Matrix.createClient({ this._matrixClient = Matrix.createClient({
baseUrl: this.state.hsUrl, baseUrl: hsUrl,
idBaseUrl: this.state.isUrl, idBaseUrl: isUrl,
}); });
try { try {
await this._makeRegisterRequest({}); await this._makeRegisterRequest({});
@ -189,12 +179,6 @@ module.exports = React.createClass({
}, },
onFormSubmit: function(formVals) { onFormSubmit: function(formVals) {
// Don't allow the user to register if there's a discovery error
// Without this, the user could end up registering on the wrong homeserver.
if (this.props.defaultServerDiscoveryError) {
this.setState({errorText: this.props.defaultServerDiscoveryError});
return;
}
this.setState({ this.setState({
errorText: "", errorText: "",
busy: true, busy: true,
@ -207,7 +191,7 @@ module.exports = React.createClass({
if (!success) { if (!success) {
let msg = response.message || response.toString(); let msg = response.message || response.toString();
// can we give a better error message? // can we give a better error message?
if (response.errcode == 'M_RESOURCE_LIMIT_EXCEEDED') { if (response.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
const errorTop = messageForResourceLimitError( const errorTop = messageForResourceLimitError(
response.data.limit_type, response.data.limit_type,
response.data.admin_contact, { response.data.admin_contact, {
@ -302,8 +286,13 @@ module.exports = React.createClass({
}); });
}, },
onServerDetailsNextPhaseClick(ev) { async onServerDetailsNextPhaseClick(ev) {
ev.stopPropagation(); ev.stopPropagation();
// TODO: TravisR - Capture the user's input somehow else
if (this._serverConfigRef) {
// Just to make sure the user's input gets captured
await this._serverConfigRef.validateServer();
}
this.setState({ this.setState({
phase: PHASE_REGISTRATION, phase: PHASE_REGISTRATION,
}); });
@ -371,20 +360,17 @@ module.exports = React.createClass({
break; break;
case ServerType.PREMIUM: case ServerType.PREMIUM:
serverDetails = <ModularServerConfig serverDetails = <ModularServerConfig
customHsUrl={this.state.discoveredHsUrl || this.props.customHsUrl} ref={r => this._serverConfigRef = r}
defaultHsUrl={this.props.defaultHsUrl} serverConfig={this.props.serverConfig}
defaultIsUrl={this.props.defaultIsUrl} onServerConfigChange={this.props.onServerConfigChange}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={250} delayTimeMs={250}
/>; />;
break; break;
case ServerType.ADVANCED: case ServerType.ADVANCED:
serverDetails = <ServerConfig serverDetails = <ServerConfig
customHsUrl={this.state.discoveredHsUrl || this.props.customHsUrl} ref={r => this._serverConfigRef = r}
customIsUrl={this.state.discoveredIsUrl || this.props.customIsUrl} serverConfig={this.props.serverConfig}
defaultHsUrl={this.props.defaultHsUrl} onServerConfigChange={this.props.onServerConfigChange}
defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={250} delayTimeMs={250}
/>; />;
break; break;
@ -392,6 +378,7 @@ module.exports = React.createClass({
let nextButton = null; let nextButton = null;
if (PHASES_ENABLED) { if (PHASES_ENABLED) {
// TODO: TravisR - Pull out server discovery from ServerConfig to disable the next button?
nextButton = <AccessibleButton className="mx_Login_submit" nextButton = <AccessibleButton className="mx_Login_submit"
onClick={this.onServerDetailsNextPhaseClick} onClick={this.onServerDetailsNextPhaseClick}
> >
@ -446,13 +433,6 @@ module.exports = React.createClass({
onEditServerDetailsClick = this.onEditServerDetailsClick; onEditServerDetailsClick = this.onEditServerDetailsClick;
} }
// If the current HS URL is the default HS URL, then we can label it
// with the default HS name (if it exists).
let hsName;
if (this.state.hsUrl === this.props.defaultHsUrl) {
hsName = this.props.defaultServerName;
}
return <RegistrationForm return <RegistrationForm
defaultUsername={this.state.formVals.username} defaultUsername={this.state.formVals.username}
defaultEmail={this.state.formVals.email} defaultEmail={this.state.formVals.email}
@ -462,8 +442,7 @@ module.exports = React.createClass({
onRegisterClick={this.onFormSubmit} onRegisterClick={this.onFormSubmit}
onEditServerDetailsClick={onEditServerDetailsClick} onEditServerDetailsClick={onEditServerDetailsClick}
flows={this.state.flows} flows={this.state.flows}
hsName={hsName} serverConfig={this.props.serverConfig}
hsUrl={this.state.hsUrl}
/>; />;
} }
}, },
@ -474,7 +453,7 @@ module.exports = React.createClass({
const AuthPage = sdk.getComponent('auth.AuthPage'); const AuthPage = sdk.getComponent('auth.AuthPage');
let errorText; let errorText;
const err = this.state.errorText || this.props.defaultServerDiscoveryError; const err = this.state.errorText;
if (err) { if (err) {
errorText = <div className="mx_Login_error">{ err }</div>; errorText = <div className="mx_Login_error">{ err }</div>;
} }

View file

@ -18,9 +18,15 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import SdkConfig from "../../../SdkConfig";
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
import * as ServerType from '../../views/auth/ServerTypeSelector';
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication'; const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
// TODO: TravisR - Can this extend ServerConfig for most things?
/* /*
* Configure the Modular server name. * Configure the Modular server name.
* *
@ -31,65 +37,87 @@ export default class ModularServerConfig extends React.PureComponent {
static propTypes = { static propTypes = {
onServerConfigChange: PropTypes.func, onServerConfigChange: PropTypes.func,
// default URLs are defined in config.json (or the hardcoded defaults) // The current configuration that the user is expecting to change.
// they are used if the user has not overridden them with a custom URL. serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
// In other words, if the custom URL is blank, the default is used.
defaultHsUrl: PropTypes.string, // e.g. https://matrix.org
// This component always uses the default IS URL and doesn't allow it
// to be changed. We still receive it as a prop here to simplify
// consumers by still passing the IS URL via onServerConfigChange.
defaultIsUrl: PropTypes.string, // e.g. https://vector.im
// custom URLs are explicitly provided by the user and override the
// default URLs. The user enters them via the component's input fields,
// which is reflected on these properties whenever on..UrlChanged fires.
// They are persisted in localStorage by MatrixClientPeg, and so can
// override the default URLs when the component initially loads.
customHsUrl: PropTypes.string,
delayTimeMs: PropTypes.number, // time to wait before invoking onChanged delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
} };
static defaultProps = { static defaultProps = {
onServerConfigChange: function() {}, onServerConfigChange: function() {},
customHsUrl: "", customHsUrl: "",
delayTimeMs: 0, delayTimeMs: 0,
} };
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
hsUrl: props.customHsUrl, busy: false,
errorText: "",
hsUrl: props.serverConfig.hsUrl,
isUrl: props.serverConfig.isUrl,
}; };
} }
componentWillReceiveProps(newProps) { componentWillReceiveProps(newProps) {
if (newProps.customHsUrl === this.state.hsUrl) return; if (newProps.serverConfig.hsUrl === this.state.hsUrl &&
newProps.serverConfig.isUrl === this.state.isUrl) return;
this.validateAndApplyServer(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl);
}
async validateAndApplyServer(hsUrl, isUrl) {
// Always try and use the defaults first
const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"];
if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) {
this.setState({busy: false, errorText: ""});
this.props.onServerConfigChange(defaultConfig);
return defaultConfig;
}
this.setState({ this.setState({
hsUrl: newProps.customHsUrl, hsUrl,
}); isUrl,
this.props.onServerConfigChange({ busy: true,
hsUrl: newProps.customHsUrl, errorText: "",
isUrl: this.props.defaultIsUrl,
}); });
try {
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
this.setState({busy: false, errorText: ""});
this.props.onServerConfigChange(result);
return result;
} catch (e) {
console.error(e);
let message = _t("Unable to validate homeserver/identity server");
if (e.translatedMessage) {
message = e.translatedMessage;
}
this.setState({
busy: false,
errorText: message,
});
}
}
async validateServer() {
// TODO: Do we want to support .well-known lookups here?
// If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
// find their homeserver without demanding they use "https://matrix.org"
return this.validateAndApplyServer(this.state.hsUrl, ServerType.TYPES.PREMIUM.identityServerUrl);
} }
onHomeserverBlur = (ev) => { onHomeserverBlur = (ev) => {
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => { this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
this.props.onServerConfigChange({ this.validateServer();
hsUrl: this.state.hsUrl,
isUrl: this.props.defaultIsUrl,
});
}); });
} };
onHomeserverChange = (ev) => { onHomeserverChange = (ev) => {
const hsUrl = ev.target.value; const hsUrl = ev.target.value;
this.setState({ hsUrl }); this.setState({ hsUrl });
} };
_waitThenInvoke(existingTimeoutId, fn) { _waitThenInvoke(existingTimeoutId, fn) {
if (existingTimeoutId) { if (existingTimeoutId) {
@ -116,7 +144,7 @@ export default class ModularServerConfig extends React.PureComponent {
<div className="mx_ServerConfig_fields"> <div className="mx_ServerConfig_fields">
<Field id="mx_ServerConfig_hsUrl" <Field id="mx_ServerConfig_hsUrl"
label={_t("Server Name")} label={_t("Server Name")}
placeholder={this.props.defaultHsUrl} placeholder={this.props.serverConfig.hsUrl}
value={this.state.hsUrl} value={this.state.hsUrl}
onBlur={this.onHomeserverBlur} onBlur={this.onHomeserverBlur}
onChange={this.onHomeserverChange} onChange={this.onHomeserverChange}

View file

@ -1,6 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd Copyright 2017,2019 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -21,11 +21,29 @@ import classNames from 'classnames';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
/** /**
* A pure UI component which displays a username/password form. * A pure UI component which displays a username/password form.
*/ */
class PasswordLogin extends React.Component { export default class PasswordLogin extends React.Component {
static propTypes = {
onSubmit: PropTypes.func.isRequired, // fn(username, password)
onError: PropTypes.func,
onForgotPasswordClick: PropTypes.func, // fn()
initialUsername: PropTypes.string,
initialPhoneCountry: PropTypes.string,
initialPhoneNumber: PropTypes.string,
initialPassword: PropTypes.string,
onUsernameChanged: PropTypes.func,
onPhoneCountryChanged: PropTypes.func,
onPhoneNumberChanged: PropTypes.func,
onPasswordChanged: PropTypes.func,
loginIncorrect: PropTypes.bool,
disableSubmit: PropTypes.bool,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
};
static defaultProps = { static defaultProps = {
onError: function() {}, onError: function() {},
onEditServerDetailsClick: null, onEditServerDetailsClick: null,
@ -40,13 +58,12 @@ class PasswordLogin extends React.Component {
initialPhoneNumber: "", initialPhoneNumber: "",
initialPassword: "", initialPassword: "",
loginIncorrect: false, loginIncorrect: false,
// This is optional and only set if we used a server name to determine
// the HS URL via `.well-known` discovery. The server name is used
// instead of the HS URL when talking about where to "sign in to".
hsName: null,
hsUrl: "",
disableSubmit: false, disableSubmit: false,
} };
static LOGIN_FIELD_EMAIL = "login_field_email";
static LOGIN_FIELD_MXID = "login_field_mxid";
static LOGIN_FIELD_PHONE = "login_field_phone";
constructor(props) { constructor(props) {
super(props); super(props);
@ -195,7 +212,7 @@ class PasswordLogin extends React.Component {
type="text" type="text"
label={SdkConfig.get().disable_custom_urls ? label={SdkConfig.get().disable_custom_urls ?
_t("Username on %(hs)s", { _t("Username on %(hs)s", {
hs: this.props.hsUrl.replace(/^https?:\/\//, ''), hs: this.props.serverConfig.hsName,
}) : _t("Username")} }) : _t("Username")}
value={this.state.username} value={this.state.username}
onChange={this.onUsernameChanged} onChange={this.onUsernameChanged}
@ -258,20 +275,14 @@ class PasswordLogin extends React.Component {
</span>; </span>;
} }
let signInToText = _t('Sign in to your Matrix account'); let signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
if (this.props.hsName) { serverName: this.props.serverConfig.hsName,
signInToText = _t('Sign in to your Matrix account on %(serverName)s', { });
serverName: this.props.hsName, if (this.props.serverConfig.hsNameIsDifferent) {
// TODO: TravisR - Use tooltip to underline
signInToText = _t('Sign in to your Matrix account on <underlinedServerName />', {}, {
'underlinedServerName': () => <u>{this.props.serverConfig.hsName}</u>,
}); });
} else {
try {
const parsedHsUrl = new URL(this.props.hsUrl);
signInToText = _t('Sign in to your Matrix account on %(serverName)s', {
serverName: parsedHsUrl.hostname,
});
} catch (e) {
// ignore
}
} }
let editLink = null; let editLink = null;
@ -353,27 +364,3 @@ class PasswordLogin extends React.Component {
); );
} }
} }
PasswordLogin.LOGIN_FIELD_EMAIL = "login_field_email";
PasswordLogin.LOGIN_FIELD_MXID = "login_field_mxid";
PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone";
PasswordLogin.propTypes = {
onSubmit: PropTypes.func.isRequired, // fn(username, password)
onError: PropTypes.func,
onForgotPasswordClick: PropTypes.func, // fn()
initialUsername: PropTypes.string,
initialPhoneCountry: PropTypes.string,
initialPhoneNumber: PropTypes.string,
initialPassword: PropTypes.string,
onUsernameChanged: PropTypes.func,
onPhoneCountryChanged: PropTypes.func,
onPhoneNumberChanged: PropTypes.func,
onPasswordChanged: PropTypes.func,
loginIncorrect: PropTypes.bool,
hsName: PropTypes.string,
hsUrl: PropTypes.string,
disableSubmit: PropTypes.bool,
};
module.exports = PasswordLogin;

View file

@ -26,6 +26,7 @@ import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import { SAFE_LOCALPART_REGEX } from '../../../Registration'; import { SAFE_LOCALPART_REGEX } from '../../../Registration';
import withValidation from '../elements/Validation'; import withValidation from '../elements/Validation';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
const FIELD_EMAIL = 'field_email'; const FIELD_EMAIL = 'field_email';
const FIELD_PHONE_NUMBER = 'field_phone_number'; const FIELD_PHONE_NUMBER = 'field_phone_number';
@ -51,11 +52,7 @@ module.exports = React.createClass({
onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
onEditServerDetailsClick: PropTypes.func, onEditServerDetailsClick: PropTypes.func,
flows: PropTypes.arrayOf(PropTypes.object).isRequired, flows: PropTypes.arrayOf(PropTypes.object).isRequired,
// This is optional and only set if we used a server name to determine serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
// the HS URL via `.well-known` discovery. The server name is used
// instead of the HS URL when talking about "your account".
hsName: PropTypes.string,
hsUrl: PropTypes.string,
}, },
getDefaultProps: function() { getDefaultProps: function() {
@ -499,20 +496,14 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
let yourMatrixAccountText = _t('Create your Matrix account'); let yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
if (this.props.hsName) { serverName: this.props.serverConfig.hsName,
yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', { });
serverName: this.props.hsName, if (this.props.serverConfig.hsNameIsDifferent) {
// TODO: TravisR - Use tooltip to underline
yourMatrixAccountText = _t('Create your Matrix account on <underlinedServerName />', {}, {
'underlinedServerName': () => <u>{this.props.serverConfig.hsName}</u>,
}); });
} else {
try {
const parsedHsUrl = new URL(this.props.hsUrl);
yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
serverName: parsedHsUrl.hostname,
});
} catch (e) {
// ignore
}
} }
let editLink = null; let editLink = null;

View file

@ -20,6 +20,9 @@ import PropTypes from 'prop-types';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
import SdkConfig from "../../../SdkConfig";
/* /*
* A pure UI component which displays the HS and IS to use. * A pure UI component which displays the HS and IS to use.
@ -29,80 +32,97 @@ export default class ServerConfig extends React.PureComponent {
static propTypes = { static propTypes = {
onServerConfigChange: PropTypes.func, onServerConfigChange: PropTypes.func,
// default URLs are defined in config.json (or the hardcoded defaults) // The current configuration that the user is expecting to change.
// they are used if the user has not overridden them with a custom URL. serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
// In other words, if the custom URL is blank, the default is used.
defaultHsUrl: PropTypes.string, // e.g. https://matrix.org
defaultIsUrl: PropTypes.string, // e.g. https://vector.im
// custom URLs are explicitly provided by the user and override the
// default URLs. The user enters them via the component's input fields,
// which is reflected on these properties whenever on..UrlChanged fires.
// They are persisted in localStorage by MatrixClientPeg, and so can
// override the default URLs when the component initially loads.
customHsUrl: PropTypes.string,
customIsUrl: PropTypes.string,
delayTimeMs: PropTypes.number, // time to wait before invoking onChanged delayTimeMs: PropTypes.number, // time to wait before invoking onChanged
} };
static defaultProps = { static defaultProps = {
onServerConfigChange: function() {}, onServerConfigChange: function() {},
customHsUrl: "",
customIsUrl: "",
delayTimeMs: 0, delayTimeMs: 0,
} };
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
hsUrl: props.customHsUrl, busy: false,
isUrl: props.customIsUrl, errorText: "",
hsUrl: props.serverConfig.hsUrl,
isUrl: props.serverConfig.isUrl,
}; };
} }
componentWillReceiveProps(newProps) { componentWillReceiveProps(newProps) {
if (newProps.customHsUrl === this.state.hsUrl && if (newProps.serverConfig.hsUrl === this.state.hsUrl &&
newProps.customIsUrl === this.state.isUrl) return; newProps.serverConfig.isUrl === this.state.isUrl) return;
this.validateAndApplyServer(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl);
}
async validateServer() {
// TODO: Do we want to support .well-known lookups here?
// If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
// find their homeserver without demanding they use "https://matrix.org"
return this.validateAndApplyServer(this.state.hsUrl, this.state.isUrl);
}
async validateAndApplyServer(hsUrl, isUrl) {
// Always try and use the defaults first
const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"];
if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) {
this.setState({busy: false, errorText: ""});
this.props.onServerConfigChange(defaultConfig);
return defaultConfig;
}
this.setState({ this.setState({
hsUrl: newProps.customHsUrl, hsUrl,
isUrl: newProps.customIsUrl, isUrl,
}); busy: true,
this.props.onServerConfigChange({ errorText: "",
hsUrl: newProps.customHsUrl,
isUrl: newProps.customIsUrl,
}); });
try {
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl);
this.setState({busy: false, errorText: ""});
this.props.onServerConfigChange(result);
return result;
} catch (e) {
console.error(e);
let message = _t("Unable to validate homeserver/identity server");
if (e.translatedMessage) {
message = e.translatedMessage;
}
this.setState({
busy: false,
errorText: message,
});
}
} }
onHomeserverBlur = (ev) => { onHomeserverBlur = (ev) => {
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => { this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {
this.props.onServerConfigChange({ this.validateServer();
hsUrl: this.state.hsUrl,
isUrl: this.state.isUrl,
});
}); });
} };
onHomeserverChange = (ev) => { onHomeserverChange = (ev) => {
const hsUrl = ev.target.value; const hsUrl = ev.target.value;
this.setState({ hsUrl }); this.setState({ hsUrl });
} };
onIdentityServerBlur = (ev) => { onIdentityServerBlur = (ev) => {
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, () => { this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, () => {
this.props.onServerConfigChange({ this.validateServer();
hsUrl: this.state.hsUrl,
isUrl: this.state.isUrl,
});
}); });
} };
onIdentityServerChange = (ev) => { onIdentityServerChange = (ev) => {
const isUrl = ev.target.value; const isUrl = ev.target.value;
this.setState({ isUrl }); this.setState({ isUrl });
} };
_waitThenInvoke(existingTimeoutId, fn) { _waitThenInvoke(existingTimeoutId, fn) {
if (existingTimeoutId) { if (existingTimeoutId) {
@ -114,11 +134,15 @@ export default class ServerConfig extends React.PureComponent {
showHelpPopup = () => { showHelpPopup = () => {
const CustomServerDialog = sdk.getComponent('auth.CustomServerDialog'); const CustomServerDialog = sdk.getComponent('auth.CustomServerDialog');
Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog); Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog);
} };
render() { render() {
const Field = sdk.getComponent('elements.Field'); const Field = sdk.getComponent('elements.Field');
const errorText = this.state.errorText
? <span className='mx_ServerConfig_error'>{this.state.errorText}</span>
: null;
return ( return (
<div className="mx_ServerConfig"> <div className="mx_ServerConfig">
<h3>{_t("Other servers")}</h3> <h3>{_t("Other servers")}</h3>
@ -127,20 +151,23 @@ export default class ServerConfig extends React.PureComponent {
{ sub } { sub }
</a>, </a>,
})} })}
{errorText}
<div className="mx_ServerConfig_fields"> <div className="mx_ServerConfig_fields">
<Field id="mx_ServerConfig_hsUrl" <Field id="mx_ServerConfig_hsUrl"
label={_t("Homeserver URL")} label={_t("Homeserver URL")}
placeholder={this.props.defaultHsUrl} placeholder={this.props.serverConfig.hsUrl}
value={this.state.hsUrl} value={this.state.hsUrl}
onBlur={this.onHomeserverBlur} onBlur={this.onHomeserverBlur}
onChange={this.onHomeserverChange} onChange={this.onHomeserverChange}
disabled={this.state.busy}
/> />
<Field id="mx_ServerConfig_isUrl" <Field id="mx_ServerConfig_isUrl"
label={_t("Identity Server URL")} label={_t("Identity Server URL")}
placeholder={this.props.defaultIsUrl} placeholder={this.props.serverConfig.isUrl}
value={this.state.isUrl} value={this.state.isUrl}
onBlur={this.onIdentityServerBlur} onBlur={this.onIdentityServerBlur}
onChange={this.onIdentityServerChange} onChange={this.onIdentityServerChange}
disabled={this.state.busy}
/> />
</div> </div>
</div> </div>

View file

@ -19,6 +19,8 @@ import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import sdk from '../../../index'; import sdk from '../../../index';
import classnames from 'classnames'; import classnames from 'classnames';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import {makeType} from "../../../utils/TypeUtils";
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication'; const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
@ -32,8 +34,13 @@ export const TYPES = {
label: () => _t('Free'), label: () => _t('Free'),
logo: () => <img src={require('../../../../res/img/matrix-org-bw-logo.svg')} />, logo: () => <img src={require('../../../../res/img/matrix-org-bw-logo.svg')} />,
description: () => _t('Join millions for free on the largest public server'), description: () => _t('Join millions for free on the largest public server'),
hsUrl: 'https://matrix.org', serverConfig: makeType(ValidatedServerConfig, {
isUrl: 'https://vector.im', hsUrl: "https://matrix.org",
hsName: "matrix.org",
hsNameIsDifferent: false,
isUrl: "https://vector.im",
identityEnabled: true,
}),
}, },
PREMIUM: { PREMIUM: {
id: PREMIUM, id: PREMIUM,
@ -44,6 +51,7 @@ export const TYPES = {
{sub} {sub}
</a>, </a>,
}), }),
identityServerUrl: "https://vector.im",
}, },
ADVANCED: { ADVANCED: {
id: ADVANCED, id: ADVANCED,
@ -56,10 +64,11 @@ export const TYPES = {
}, },
}; };
export function getTypeFromHsUrl(hsUrl) { export function getTypeFromServerConfig(config) {
const {hsUrl} = config;
if (!hsUrl) { if (!hsUrl) {
return null; return null;
} else if (hsUrl === TYPES.FREE.hsUrl) { } else if (hsUrl === TYPES.FREE.serverConfig.hsUrl) {
return FREE; return FREE;
} else if (new URL(hsUrl).hostname.endsWith('.modular.im')) { } else if (new URL(hsUrl).hostname.endsWith('.modular.im')) {
// This is an unlikely case to reach, as Modular defaults to hiding the // This is an unlikely case to reach, as Modular defaults to hiding the
@ -76,7 +85,7 @@ export default class ServerTypeSelector extends React.PureComponent {
selected: PropTypes.string, selected: PropTypes.string,
// Handler called when the selected type changes. // Handler called when the selected type changes.
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
} };
constructor(props) { constructor(props) {
super(props); super(props);
@ -106,7 +115,7 @@ export default class ServerTypeSelector extends React.PureComponent {
e.stopPropagation(); e.stopPropagation();
const type = e.currentTarget.dataset.id; const type = e.currentTarget.dataset.id;
this.updateSelectedType(type); this.updateSelectedType(type);
} };
render() { render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');

View file

@ -246,6 +246,8 @@
"%(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 …|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 %(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 …", "%(names)s and %(lastPerson)s are typing …": "%(names)s and %(lastPerson)s are typing …",
"No homeserver URL provided": "No homeserver URL provided",
"Unexpected error resolving homeserver configuration": "Unexpected error resolving homeserver configuration",
"This homeserver has hit its Monthly Active User limit.": "This homeserver has hit its Monthly Active User limit.", "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.", "This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.",
"Please <a>contact your service administrator</a> to continue using the service.": "Please <a>contact your service administrator</a> to continue using the service.", "Please <a>contact your service administrator</a> to continue using the service.": "Please <a>contact your service administrator</a> to continue using the service.",
@ -1306,6 +1308,7 @@
"Code": "Code", "Code": "Code",
"Submit": "Submit", "Submit": "Submit",
"Start authentication": "Start authentication", "Start authentication": "Start authentication",
"Unable to validate homeserver/identity server": "Unable to validate homeserver/identity server",
"Your Modular server": "Your Modular server", "Your Modular server": "Your Modular server",
"Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of <a>modular.im</a>.": "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of <a>modular.im</a>.", "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of <a>modular.im</a>.": "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of <a>modular.im</a>.",
"Server Name": "Server Name", "Server Name": "Server Name",
@ -1318,8 +1321,8 @@
"Username": "Username", "Username": "Username",
"Phone": "Phone", "Phone": "Phone",
"Not sure of your password? <a>Set a new one</a>": "Not sure of your password? <a>Set a new one</a>", "Not sure of your password? <a>Set a new one</a>": "Not sure of your password? <a>Set a new one</a>",
"Sign in to your Matrix account": "Sign in to your Matrix account",
"Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s", "Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s",
"Sign in to your Matrix account on <underlinedServerName />": "Sign in to your Matrix account on <underlinedServerName />",
"Change": "Change", "Change": "Change",
"Sign in with": "Sign in with", "Sign in with": "Sign in with",
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?", "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?",
@ -1339,8 +1342,8 @@
"Email (optional)": "Email (optional)", "Email (optional)": "Email (optional)",
"Confirm": "Confirm", "Confirm": "Confirm",
"Phone (optional)": "Phone (optional)", "Phone (optional)": "Phone (optional)",
"Create your Matrix account": "Create your Matrix account",
"Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s", "Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s",
"Create your Matrix account on <underlinedServerName />": "Create your Matrix account on <underlinedServerName />",
"Use an email address to recover your account.": "Use an email address to recover your account.", "Use an email address to recover your account.": "Use an email address to recover your account.",
"Other users can invite you to rooms using your contact details.": "Other users can invite you to rooms using your contact details.", "Other users can invite you to rooms using your contact details.": "Other users can invite you to rooms using your contact details.",
"Other servers": "Other servers", "Other servers": "Other servers",
@ -1407,7 +1410,6 @@
"This homeserver does not support communities": "This homeserver does not support communities", "This homeserver does not support communities": "This homeserver does not support communities",
"Failed to load %(groupId)s": "Failed to load %(groupId)s", "Failed to load %(groupId)s": "Failed to load %(groupId)s",
"Filter room names": "Filter room names", "Filter room names": "Filter room names",
"Invalid configuration: Cannot supply a default homeserver URL and a default server name": "Invalid configuration: Cannot supply a default homeserver URL and a default server name",
"Failed to reject invitation": "Failed to reject invitation", "Failed to reject invitation": "Failed to reject invitation",
"This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.", "This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.",
"Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?", "Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?",
@ -1423,7 +1425,6 @@
"Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.", "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.",
"You are logged in to another account": "You are logged in to another account", "You are logged in to another account": "You are logged in to another account",
"Thank you for verifying your email! The account you're logged into here (%(sessionUserId)s) appears to be different from the account you've verified an email for (%(verifiedUserId)s). If you would like to log in to %(verifiedUserId2)s, please log out first.": "Thank you for verifying your email! The account you're logged into here (%(sessionUserId)s) appears to be different from the account you've verified an email for (%(verifiedUserId)s). If you would like to log in to %(verifiedUserId2)s, please log out first.", "Thank you for verifying your email! The account you're logged into here (%(sessionUserId)s) appears to be different from the account you've verified an email for (%(verifiedUserId)s). If you would like to log in to %(verifiedUserId2)s, please log out first.": "Thank you for verifying your email! The account you're logged into here (%(sessionUserId)s) appears to be different from the account you've verified an email for (%(verifiedUserId)s). If you would like to log in to %(verifiedUserId2)s, please log out first.",
"Unknown error discovering homeserver": "Unknown error discovering homeserver",
"Logout": "Logout", "Logout": "Logout",
"Your Communities": "Your Communities", "Your Communities": "Your Communities",
"Did you know: you can use communities to filter your Riot.im experience!": "Did you know: you can use communities to filter your Riot.im experience!", "Did you know: you can use communities to filter your Riot.im experience!": "Did you know: you can use communities to filter your Riot.im experience!",
@ -1491,9 +1492,8 @@
"A new password must be entered.": "A new password must be entered.", "A new password must be entered.": "A new password must be entered.",
"New passwords must match each other.": "New passwords must match each other.", "New passwords must match each other.": "New passwords must match each other.",
"Changing your password will reset any end-to-end encryption keys on all of your devices, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another device before resetting your password.": "Changing your password will reset any end-to-end encryption keys on all of your devices, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another device before resetting your password.", "Changing your password will reset any end-to-end encryption keys on all of your devices, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another device before resetting your password.": "Changing your password will reset any end-to-end encryption keys on all of your devices, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another device before resetting your password.",
"Your Matrix account": "Your Matrix account",
"Your Matrix account on %(serverName)s": "Your Matrix account on %(serverName)s", "Your Matrix account on %(serverName)s": "Your Matrix account on %(serverName)s",
"The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please enter a valid URL including the protocol prefix.": "The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please enter a valid URL including the protocol prefix.", "Your Matrix account on <underlinedServerName />": "Your Matrix account on <underlinedServerName />",
"A verification email will be sent to your inbox to confirm setting your new password.": "A verification email will be sent to your inbox to confirm setting your new password.", "A verification email will be sent to your inbox to confirm setting your new password.": "A verification email will be sent to your inbox to confirm setting your new password.",
"Send Reset Email": "Send Reset Email", "Send Reset Email": "Send Reset Email",
"Sign in instead": "Sign in instead", "Sign in instead": "Sign in instead",
@ -1517,7 +1517,6 @@
"Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.", "Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.",
"Failed to perform homeserver discovery": "Failed to perform homeserver discovery", "Failed to perform homeserver discovery": "Failed to perform homeserver discovery",
"The phone number entered looks invalid": "The phone number entered looks invalid", "The phone number entered looks invalid": "The phone number entered looks invalid",
"Unknown failure discovering homeserver": "Unknown failure discovering homeserver",
"This homeserver doesn't offer any login flows which are supported by this client.": "This homeserver doesn't offer any login flows which are supported by this client.", "This homeserver doesn't offer any login flows which are supported by this client.": "This homeserver doesn't offer any login flows which are supported by this client.",
"Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.", "Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.",
"Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.", "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.",

View file

@ -32,6 +32,18 @@ counterpart.setSeparator('|');
// Fall back to English // Fall back to English
counterpart.setFallbackLocale('en'); counterpart.setFallbackLocale('en');
/**
* Helper function to create an error which has an English message
* with a translatedMessage property for use by the consumer.
* @param {string} message Message to translate.
* @returns {Error} The constructed error.
*/
export function newTranslatableError(message) {
const error = new Error(message);
error.translatedMessage = _t(message);
return error;
}
// Function which only purpose is to mark that a string is translatable // Function which only purpose is to mark that a string is translatable
// Does not actually do anything. It's helpful for automatic extraction of translatable strings // Does not actually do anything. It's helpful for automatic extraction of translatable strings
export function _td(s) { export function _td(s) {

View file

@ -0,0 +1,104 @@
/*
Copyright 2019 New Vector Ltd
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 {AutoDiscovery} from "matrix-js-sdk";
import {_td, newTranslatableError} from "../languageHandler";
import {makeType} from "./TypeUtils";
import SdkConfig from "../SdkConfig";
export class ValidatedServerConfig {
hsUrl: string;
hsName: string;
hsNameIsDifferent: string;
isUrl: string;
identityEnabled: boolean;
}
export default class AutoDiscoveryUtils {
static async validateServerConfigWithStaticUrls(homeserverUrl: string, identityUrl: string): ValidatedServerConfig {
if (!homeserverUrl) {
throw newTranslatableError(_td("No homeserver URL provided"));
}
const wellknownConfig = {
"m.homeserver": {
base_url: homeserverUrl,
},
"m.identity_server": {
base_url: identityUrl,
},
};
const result = await AutoDiscovery.fromDiscoveryConfig(wellknownConfig);
const url = new URL(homeserverUrl);
const serverName = url.hostname;
return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result);
}
static async validateServerName(serverName: string): ValidatedServerConfig {
const result = await AutoDiscovery.findClientConfig(serverName);
return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result);
}
static buildValidatedConfigFromDiscovery(serverName: string, discoveryResult): 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".
console.error("Ended up in a state of not knowing which homeserver to connect to.");
throw newTranslatableError(_td("Unexpected error resolving homeserver configuration"));
}
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"));
}
const isResult = discoveryResult['m.identity_server'];
let preferredIdentityUrl = "https://vector.im";
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"));
}
const preferredHomeserverUrl = hsResult["base_url"];
let preferredHomeserverName = serverName ? serverName : hsResult["server_name"];
const url = new URL(preferredHomeserverUrl);
if (!preferredHomeserverName) preferredHomeserverName = url.hostname;
// It should have been set by now, so check it
if (!preferredHomeserverName) {
console.error("Failed to parse homeserver name from homeserver URL");
throw newTranslatableError(_td("Unexpected error resolving homeserver configuration"));
}
return makeType(ValidatedServerConfig, {
hsUrl: preferredHomeserverUrl,
hsName: preferredHomeserverName,
hsNameIsDifferent: url.hostname !== preferredHomeserverName,
isUrl: preferredIdentityUrl,
identityEnabled: !SdkConfig.get()['disable_identity_server'],
});
}
}

30
src/utils/TypeUtils.js Normal file
View file

@ -0,0 +1,30 @@
/*
Copyright 2019 New Vector Ltd
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.
*/
/**
* Creates a class of a given type using the objects defined. This
* is a stopgap function while we don't have TypeScript interfaces.
* In future, we'd define the `type` as an interface and just cast
* it instead of cheating like we are here.
* @param {Type} Type The type of class to construct.
* @param {*} opts The options (properties) to set on the object.
* @returns {*} The created object.
*/
export function makeType(Type: any, opts: any) {
const c = new Type();
Object.assign(c, opts);
return c;
}

View file

@ -22,6 +22,7 @@ import ReactTestUtils from 'react-dom/test-utils';
import sdk from 'matrix-react-sdk'; import sdk from 'matrix-react-sdk';
import SdkConfig from '../../../../src/SdkConfig'; import SdkConfig from '../../../../src/SdkConfig';
import * as TestUtils from '../../../test-utils'; import * as TestUtils from '../../../test-utils';
import {mkServerConfig} from "../../../test-utils";
const Login = sdk.getComponent( const Login = sdk.getComponent(
'structures.auth.Login', 'structures.auth.Login',
@ -44,8 +45,7 @@ describe('Login', function() {
function render() { function render() {
return ReactDOM.render(<Login return ReactDOM.render(<Login
defaultHsUrl="https://matrix.org" serverConfig={mkServerConfig("https://matrix.org", "https://vector.im")}
defaultIsUrl="https://vector.im"
onLoggedIn={() => {}} onLoggedIn={() => {}}
onRegisterClick={() => {}} onRegisterClick={() => {}}
onServerConfigChange={() => {}} onServerConfigChange={() => {}}

View file

@ -22,6 +22,7 @@ import ReactTestUtils from 'react-dom/test-utils';
import sdk from 'matrix-react-sdk'; import sdk from 'matrix-react-sdk';
import SdkConfig from '../../../../src/SdkConfig'; import SdkConfig from '../../../../src/SdkConfig';
import * as TestUtils from '../../../test-utils'; import * as TestUtils from '../../../test-utils';
import {mkServerConfig} from "../../../test-utils";
const Registration = sdk.getComponent( const Registration = sdk.getComponent(
'structures.auth.Registration', 'structures.auth.Registration',
@ -44,8 +45,7 @@ describe('Registration', function() {
function render() { function render() {
return ReactDOM.render(<Registration return ReactDOM.render(<Registration
defaultHsUrl="https://matrix.org" serverConfig={mkServerConfig("https://matrix.org", "https://vector.im")}
defaultIsUrl="https://vector.im"
makeRegistrationUrl={() => {}} makeRegistrationUrl={() => {}}
onLoggedIn={() => {}} onLoggedIn={() => {}}
onLoginClick={() => {}} onLoginClick={() => {}}

View file

@ -7,6 +7,8 @@ import PropTypes from 'prop-types';
import peg from '../src/MatrixClientPeg'; import peg from '../src/MatrixClientPeg';
import dis from '../src/dispatcher'; import dis from '../src/dispatcher';
import jssdk from 'matrix-js-sdk'; import jssdk from 'matrix-js-sdk';
import {makeType} from "../src/utils/TypeUtils";
import {ValidatedServerConfig} from "../src/utils/AutoDiscoveryUtils";
const MatrixEvent = jssdk.MatrixEvent; const MatrixEvent = jssdk.MatrixEvent;
/** /**
@ -260,6 +262,16 @@ export function mkStubRoom(roomId = null) {
}; };
} }
export function mkServerConfig(hsUrl, isUrl) {
return makeType(ValidatedServerConfig, {
hsUrl,
hsName: "TEST_ENVIRONMENT",
hsNameIsDifferent: false, // yes, we lie
isUrl,
identityEnabled: true,
});
}
export function getDispatchForStore(store) { export function getDispatchForStore(store) {
// Mock the dispatcher by gut-wrenching. Stores can only __emitChange whilst a // Mock the dispatcher by gut-wrenching. Stores can only __emitChange whilst a
// dispatcher `_isDispatching` is true. // dispatcher `_isDispatching` is true.