diff --git a/res/css/views/settings/_SetIdServer.scss b/res/css/views/settings/_SetIdServer.scss new file mode 100644 index 0000000000..c6fcfc8af5 --- /dev/null +++ b/res/css/views/settings/_SetIdServer.scss @@ -0,0 +1,19 @@ +/* +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. +*/ + +.mx_SetIdServer .mx_Field_input { + width: 300px; +} diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js new file mode 100644 index 0000000000..ba51de46d3 --- /dev/null +++ b/src/components/views/settings/SetIdServer.js @@ -0,0 +1,188 @@ +/* +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 request from 'browser-request'; +import url from 'url'; +import React from 'react'; +import {_t} from "../../../languageHandler"; +import sdk from '../../../index'; +import MatrixClientPeg from "../../../MatrixClientPeg"; +import SdkConfig from "../../../SdkConfig"; +import Field from "../elements/Field"; + +/** + * If a url has no path component, etc. abbreviate it to just the hostname + * + * @param {string} u The url to be abbreviated + * @returns {string} The abbreviated url + */ +function abbreviateUrl(u) { + if (!u) return ''; + + const parsedUrl = url.parse(u); + // if it's something we can't parse as a url then just return it + if (!parsedUrl) return u; + + if (parsedUrl.path == '/') { + // we ignore query / hash parts: these aren't relevant for IS server URLs + return parsedUrl.host; + } + + return u; +} + +function unabbreviateUrl(u) { + if (!u) return ''; + + let longUrl = u; + if (!u.startsWith('https://')) longUrl = 'https://' + u; + const parsed = url.parse(longUrl); + if (parsed.hostname === null) return u; + + return longUrl; +} + +/** + * Check an IS URL is valid, including liveness check + * + * @param {string} isUrl The url to check + * @returns {string} null if url passes all checks, otherwise i18ned error string + */ +async function checkIsUrl(isUrl) { + const parsedUrl = url.parse(isUrl); + + if (parsedUrl.protocol !== 'https:') return _t("Identity Server URL must be HTTPS"); + + // XXX: duplicated logic from js-sdk but it's quite tied up in the validation logic in the + // js-sdk so probably as easy to duplicate it than to separate it out so we can reuse it + return new Promise((resolve) => { + request( + { method: "GET", url: isUrl + '/_matrix/identity/api/v1' }, + (err, response, body) => { + if (err) { + resolve(_t("Could not connect to ID Server")); + } else if (response.status < 200 || response.status >= 300) { + resolve(_t("Not a valid ID Server (status code %(code)s)", {code: response.status})); + } else { + resolve(null); + } + }, + ); + }); +} + +export default class SetIdServer extends React.Component { + constructor() { + super(); + + let defaultIdServer = MatrixClientPeg.get().getIdentityServerUrl(); + if (!defaultIdServer) { + defaultIdServer = SdkConfig.get()['validated_server_config']['idServer'] || ''; + } + + this.state = { + currentClientIdServer: MatrixClientPeg.get().getIdentityServerUrl(), + idServer: defaultIdServer, + error: null, + busy: false, + }; + } + + _onIdentityServerChanged = (ev) => { + const u = ev.target.value; + + this.setState({idServer: u}); + }; + + _getTooltip = () => { + if (this.state.busy) { + const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner'); + return
+ + { _t("Checking Server") } +
; + } else if (this.state.error) { + return this.state.error; + } else { + return null; + } + }; + + _idServerChangeEnabled = () => { + return !!this.state.idServer && !this.state.busy; + }; + + _saveIdServer = async () => { + this.setState({busy: true}); + + const fullUrl = unabbreviateUrl(this.state.idServer); + + const errStr = await checkIsUrl(fullUrl); + if (!errStr) { + MatrixClientPeg.get().setIdentityServerUrl(fullUrl); + localStorage.setItem("mx_is_url", fullUrl); + } + this.setState({ + busy: false, + error: errStr, + currentClientIdServer: MatrixClientPeg.get().getIdentityServerUrl(), + idServer: '', + }); + }; + + render() { + const idServerUrl = this.state.currentClientIdServer; + let sectionTitle; + let bodyText; + if (idServerUrl) { + sectionTitle = _t("Identity Server (%(server)s)", { server: abbreviateUrl(idServerUrl) }); + bodyText = _t( + "You are currently using to discover and be discoverable by " + + "existing contacts you know. You can change your identity server below.", + {}, + { server: sub => {abbreviateUrl(idServerUrl)} }, + ); + } else { + sectionTitle = _t("Identity Server"); + bodyText = _t( + "You are not currently using an Identity Server. " + + "To discover and be discoverable by existing contacts you know, " + + "add one below", + ); + } + + return ( +
+ + {sectionTitle} + + + {bodyText} + + + + + ); + } +}