From 83af732d05d44b8c6389775c5616833bfc375dfb Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 19 Aug 2019 22:53:37 -0600 Subject: [PATCH 1/7] Rename and export abbreviateIdentityUrl --- src/components/views/settings/SetIdServer.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index c5e979da04..135d3c32fd 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -32,14 +32,14 @@ import {SERVICE_TYPES} from "matrix-js-sdk"; * @param {string} u The url to be abbreviated * @returns {string} The abbreviated url */ -function abbreviateUrl(u) { +export function abbreviateIdentityUrl(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 == '/') { + if (parsedUrl.path === '/') { // we ignore query / hash parts: these aren't relevant for IS server URLs return parsedUrl.host; } @@ -93,7 +93,7 @@ export default class SetIdServer extends React.Component { if (!MatrixClientPeg.get().getIdentityServerUrl() && SdkConfig.get()['validated_server_config']['isUrl']) { // If no ID server is configured but there's one in the config, prepopulate // the field to help the user. - defaultIdServer = abbreviateUrl(SdkConfig.get()['validated_server_config']['isUrl']); + defaultIdServer = abbreviateIdentityUrl(SdkConfig.get()['validated_server_config']['isUrl']); } this.state = { @@ -124,7 +124,7 @@ export default class SetIdServer extends React.Component { const fullUrl = MatrixClientPeg.get().getIdentityServerUrl(); let abbr = ''; - if (fullUrl) abbr = abbreviateUrl(fullUrl); + if (fullUrl) abbr = abbreviateIdentityUrl(fullUrl); this.setState({ currentClientIdServer: fullUrl, @@ -234,15 +234,15 @@ export default class SetIdServer extends React.Component { "server . You will need to reconnect to to stop " + "sharing them.", {}, { - idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}, + idserver: sub => {abbreviateIdentityUrl(this.state.currentClientIdServer)}, // XXX: https://github.com/vector-im/riot-web/issues/9086 - idserver2: sub => {abbreviateUrl(this.state.currentClientIdServer)}, + idserver2: sub => {abbreviateIdentityUrl(this.state.currentClientIdServer)}, }, ); } else { message = _t( "Disconnect from the identity server ?", {}, - {idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}}, + {idserver: sub => {abbreviateIdentityUrl(this.state.currentClientIdServer)}}, ); } @@ -272,7 +272,7 @@ export default class SetIdServer extends React.Component { if (SdkConfig.get()['validated_server_config']['isUrl']) { // Prepopulate the client's default so the user at least has some idea of // a valid value they might enter - newFieldVal = abbreviateUrl(SdkConfig.get()['validated_server_config']['isUrl']); + newFieldVal = abbreviateIdentityUrl(SdkConfig.get()['validated_server_config']['isUrl']); } this.setState({ @@ -290,12 +290,12 @@ export default class SetIdServer extends React.Component { let sectionTitle; let bodyText; if (idServerUrl) { - sectionTitle = _t("Identity Server (%(server)s)", { server: abbreviateUrl(idServerUrl) }); + sectionTitle = _t("Identity Server (%(server)s)", { server: abbreviateIdentityUrl(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)} }, + { server: sub => {abbreviateIdentityUrl(idServerUrl)} }, ); } else { sectionTitle = _t("Identity Server"); From 525b4cad0f88a184d392b9adfe202b0d4c4be8ed Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 19 Aug 2019 22:54:23 -0600 Subject: [PATCH 2/7] Support IS token handling without checking terms This is so we can optionally do our own terms handling. --- src/IdentityAuthClient.js | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js index d3b4d8a6de..075ae93709 100644 --- a/src/IdentityAuthClient.js +++ b/src/IdentityAuthClient.js @@ -65,7 +65,7 @@ export default class IdentityAuthClient { } // Returns a promise that resolves to the access_token string from the IS - async getAccessToken() { + async getAccessToken(check=true) { if (!this.authEnabled) { // The current IS doesn't support authentication return null; @@ -77,7 +77,7 @@ export default class IdentityAuthClient { } if (!token) { - token = await this.registerForToken(); + token = await this.registerForToken(check); if (token) { this.accessToken = token; this._writeToken(); @@ -85,18 +85,20 @@ export default class IdentityAuthClient { return token; } - try { - await this._checkToken(token); - } catch (e) { - if (e instanceof TermsNotSignedError) { - // Retrying won't help this - throw e; - } - // Retry in case token expired - token = await this.registerForToken(); - if (token) { - this.accessToken = token; - this._writeToken(); + if (check) { + try { + await this._checkToken(token); + } catch (e) { + if (e instanceof TermsNotSignedError) { + // Retrying won't help this + throw e; + } + // Retry in case token expired + token = await this.registerForToken(); + if (token) { + this.accessToken = token; + this._writeToken(); + } } } @@ -126,12 +128,12 @@ export default class IdentityAuthClient { // See also https://github.com/vector-im/riot-web/issues/10455. } - async registerForToken() { + async registerForToken(check=true) { try { const hsOpenIdToken = await MatrixClientPeg.get().getOpenIdToken(); const { access_token: identityAccessToken } = await this._matrixClient.registerWithIdentityServer(hsOpenIdToken); - await this._checkToken(identityAccessToken); + if (check) await this._checkToken(identityAccessToken); return identityAccessToken; } catch (e) { if (e.cors === "rejected" || e.httpStatus === 404) { From 417de0cac7adbcd0aaadc6a563a3199131950d2b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 19 Aug 2019 22:59:33 -0600 Subject: [PATCH 3/7] Add an inline terms agreement component Handles agreement of terms in an inline way. --- res/css/_components.scss | 1 + .../views/terms/_InlineTermsAgreement.scss | 45 +++++++ .../views/terms/InlineTermsAgreement.js | 119 ++++++++++++++++++ src/i18n/strings/en_EN.json | 2 + 4 files changed, 167 insertions(+) create mode 100644 res/css/views/terms/_InlineTermsAgreement.scss create mode 100644 src/components/views/terms/InlineTermsAgreement.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 579369a509..b8811c742f 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -180,6 +180,7 @@ @import "./views/settings/tabs/user/_PreferencesUserSettingsTab.scss"; @import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss"; @import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss"; +@import "./views/terms/_InlineTermsAgreement.scss"; @import "./views/verification/_VerificationShowSas.scss"; @import "./views/voip/_CallView.scss"; @import "./views/voip/_IncomingCallbox.scss"; diff --git a/res/css/views/terms/_InlineTermsAgreement.scss b/res/css/views/terms/_InlineTermsAgreement.scss new file mode 100644 index 0000000000..e00dcf31d1 --- /dev/null +++ b/res/css/views/terms/_InlineTermsAgreement.scss @@ -0,0 +1,45 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_InlineTermsAgreement_cbContainer { + margin-bottom: 10px; + font-size: 14px; + + a { + color: $accent-color; + text-decoration: none; + } + + .mx_InlineTermsAgreement_checkbox { + margin-top: 10px; + + input { + vertical-align: text-bottom; + } + } +} + +.mx_InlineTermsAgreement_link { + display: inline-block; + mask-image: url('$(res)/img/external-link.svg'); + background-color: $accent-color; + mask-repeat: no-repeat; + mask-size: contain; + width: 12px; + height: 12px; + margin-left: 3px; + vertical-align: middle; +} diff --git a/src/components/views/terms/InlineTermsAgreement.js b/src/components/views/terms/InlineTermsAgreement.js new file mode 100644 index 0000000000..c88612dacb --- /dev/null +++ b/src/components/views/terms/InlineTermsAgreement.js @@ -0,0 +1,119 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import PropTypes from "prop-types"; +import {_t, pickBestLanguage} from "../../../languageHandler"; +import sdk from "../../../.."; + +export default class InlineTermsAgreement extends React.Component { + static propTypes = { + policiesAndServicePairs: PropTypes.array.isRequired, // array of service/policy pairs + agreedUrls: PropTypes.array.isRequired, // array of URLs the user has accepted + onFinished: PropTypes.func.isRequired, // takes an argument of accepted URLs + introElement: PropTypes.node, + }; + + constructor() { + super(); + + this.state = { + policies: [], + busy: false, + }; + } + + componentDidMount() { + // Build all the terms the user needs to accept + const policies = []; // { checked, url, name } + for (const servicePolicies of this.props.policiesAndServicePairs) { + const availablePolicies = Object.values(servicePolicies.policies); + for (const policy of availablePolicies) { + const language = pickBestLanguage(Object.keys(policy).filter(p => p !== 'version')); + const renderablePolicy = { + checked: false, + url: policy[language].url, + name: policy[language].name, + }; + policies.push(renderablePolicy); + } + } + + this.setState({policies}); + } + + _togglePolicy = (index) => { + const policies = JSON.parse(JSON.stringify(this.state.policies)); // deep & cheap clone + policies[index].checked = !policies[index].checked; + this.setState({policies}); + }; + + _onContinue = () => { + const hasUnchecked = !!this.state.policies.some(p => !p.checked); + if (hasUnchecked) return; + + this.setState({busy: true}); + this.props.onFinished(this.state.policies.map(p => p.url)); + }; + + _renderCheckboxes() { + const rendered = []; + for (let i = 0; i < this.state.policies.length; i++) { + const policy = this.state.policies[i]; + const introText = _t( + "Accept to continue:", {}, { + policyLink: () => { + return ( + + {policy.name} + + + ); + }, + }, + ); + rendered.push( +
+
{introText}
+
+ this._togglePolicy(i)} checked={policy.checked} /> + {_t("Accept")} +
+
+ ); + } + return rendered; + } + + render() { + const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton"); + const hasUnchecked = !!this.state.policies.some(p => !p.checked); + + return ( +
+ {this.props.introElement} + {this._renderCheckboxes()} + + {_t("Continue")} + +
+ ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d38965bea4..469a31bb43 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -457,6 +457,7 @@ "Headphones": "Headphones", "Folder": "Folder", "Pin": "Pin", + "Accept to continue:": "Accept to continue:", "Failed to upload profile picture!": "Failed to upload profile picture!", "Upload new:": "Upload new:", "No display name": "No display name", @@ -582,6 +583,7 @@ "Set a new account password...": "Set a new account password...", "Language and region": "Language and region", "Theme": "Theme", + "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.", "Account management": "Account management", "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!", "Deactivate Account": "Deactivate Account", From 318182953298baa12893db462f6be7426cd02961 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 19 Aug 2019 23:00:05 -0600 Subject: [PATCH 4/7] Use new InlineTermsAgreement component on IS Discovery section Fixes https://github.com/vector-im/riot-web/issues/10522 --- .../tabs/user/_GeneralUserSettingsTab.scss | 4 + .../tabs/user/GeneralUserSettingsTab.js | 80 ++++++++++++++++++- src/i18n/strings/en_EN.json | 2 +- 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss index 16467165cf..ae55733192 100644 --- a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss @@ -28,3 +28,7 @@ limitations under the License. .mx_GeneralUserSettingsTab_languageInput { @mixin mx_Settings_fullWidthField; } + +.mx_GeneralUserSettingsTab_warningIcon { + vertical-align: middle; +} diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 7a8d123fcd..2f9752bb86 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -33,6 +33,10 @@ import MatrixClientPeg from "../../../../../MatrixClientPeg"; import sdk from "../../../../.."; import Modal from "../../../../../Modal"; import dis from "../../../../../dispatcher"; +import {Service, startTermsFlow} from "../../../../../Terms"; +import {SERVICE_TYPES} from "matrix-js-sdk"; +import IdentityAuthClient from "../../../../../IdentityAuthClient"; +import {abbreviateIdentityUrl} from "../../SetIdServer"; export default class GeneralUserSettingsTab extends React.Component { static propTypes = { @@ -47,6 +51,14 @@ export default class GeneralUserSettingsTab extends React.Component { theme: SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"), haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()), serverRequiresIdServer: null, + idServerHasUnsignedTerms: false, + requiredPolicyInfo: { // This object is passed along to a component for handling + hasTerms: false, + // policiesAndServices, // From the startTermsFlow callback + // agreedUrls, // From the startTermsFlow callback + // resolve, // Promise resolve function for startTermsFlow callback + // reject, // Promise reject function for startTermsFlow callback + }, }; this.dispatcherRef = dis.register(this._onAction); @@ -55,6 +67,9 @@ export default class GeneralUserSettingsTab extends React.Component { async componentWillMount() { const serverRequiresIdServer = await MatrixClientPeg.get().doesServerRequireIdServerParam(); this.setState({serverRequiresIdServer}); + + // Check to see if terms need accepting + this._checkTerms(); } componentWillUnmount() { @@ -64,9 +79,49 @@ export default class GeneralUserSettingsTab extends React.Component { _onAction = (payload) => { if (payload.action === 'id_server_changed') { this.setState({haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl())}); + this._checkTerms(); } }; + async _checkTerms() { + if (!this.state.haveIdServer) { + this.setState({idServerHasUnsignedTerms: false}); + return; + } + + // By starting the terms flow we get the logic for checking which terms the user has signed + // for free. So we might as well use that for our own purposes. + const authClient = new IdentityAuthClient(); + console.log("Getting access token..."); + const idAccessToken = await authClient.getAccessToken(/*check=*/false); + console.log("Got access token: " + idAccessToken); + startTermsFlow([new Service( + SERVICE_TYPES.IS, + MatrixClientPeg.get().getIdentityServerUrl(), + idAccessToken, + )], (policiesAndServices, agreedUrls, extraClassNames) => { + return new Promise((resolve, reject) => { + this.setState({ + idServerName: abbreviateIdentityUrl(MatrixClientPeg.get().getIdentityServerUrl()), + requiredPolicyInfo: { + hasTerms: true, + policiesAndServices, + agreedUrls, + resolve, + reject, + }, + }); + }); + }).then(() => { + // User accepted all terms + this.setState({ + requiredPolicyInfo: { + hasTerms: false, + }, + }); + }); + } + _onLanguageChange = (newLanguage) => { if (this.state.language === newLanguage) return; @@ -198,6 +253,23 @@ export default class GeneralUserSettingsTab extends React.Component { } _renderDiscoverySection() { + if (this.state.requiredPolicyInfo.hasTerms) { + const InlineTermsAgreement = sdk.getComponent("views.terms.InlineTermsAgreement"); + const intro = + {_t( + "Agree to the identity server (%(serverName)s) Terms of Service to " + + "allow yourself to be discoverable by email address or phone number.", + {serverName: this.state.idServerName}, + )} + ; + return ; + } + const EmailAddresses = sdk.getComponent("views.settings.discovery.EmailAddresses"); const PhoneNumbers = sdk.getComponent("views.settings.discovery.PhoneNumbers"); const SetIdServer = sdk.getComponent("views.settings.SetIdServer"); @@ -246,6 +318,12 @@ export default class GeneralUserSettingsTab extends React.Component { } render() { + const discoWarning = this.state.requiredPolicyInfo.hasTerms + ? {_t("Warning")} + : null; + return (
{_t("General")}
@@ -253,7 +331,7 @@ export default class GeneralUserSettingsTab extends React.Component { {this._renderAccountSection()} {this._renderLanguageSection()} {this._renderThemeSection()} -
{_t("Discovery")}
+
{discoWarning} {_t("Discovery")}
{this._renderDiscoverySection()} {this._renderIntegrationManagerSection() /* Has its own title */}
{_t("Deactivate account")}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 469a31bb43..de10eb6735 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -587,6 +587,7 @@ "Account management": "Account management", "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!", "Deactivate Account": "Deactivate Account", + "Warning": "Warning", "General": "General", "Discovery": "Discovery", "Deactivate account": "Deactivate account", @@ -1050,7 +1051,6 @@ "Checking for an update...": "Checking for an update...", "No update available.": "No update available.", "Downloading update...": "Downloading update...", - "Warning": "Warning", "Unknown Address": "Unknown Address", "NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted", "Warning: This widget might use cookies.": "Warning: This widget might use cookies.", From 7cd2fb371806c9e3ffa6fd6b58ebb1a8822d4f78 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 19 Aug 2019 23:03:01 -0600 Subject: [PATCH 5/7] Appease the linter It really wants a trailing comma. --- src/components/views/terms/InlineTermsAgreement.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/terms/InlineTermsAgreement.js b/src/components/views/terms/InlineTermsAgreement.js index c88612dacb..a22359933f 100644 --- a/src/components/views/terms/InlineTermsAgreement.js +++ b/src/components/views/terms/InlineTermsAgreement.js @@ -92,7 +92,7 @@ export default class InlineTermsAgreement extends React.Component { this._togglePolicy(i)} checked={policy.checked} /> {_t("Accept")}
- + , ); } return rendered; From c758b5d3f186dd244441373b081c037a9a0e6654 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 21 Aug 2019 08:43:42 -0600 Subject: [PATCH 6/7] We don't use reject --- .../views/settings/tabs/user/GeneralUserSettingsTab.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 2f9752bb86..18c18f61d8 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -57,7 +57,6 @@ export default class GeneralUserSettingsTab extends React.Component { // policiesAndServices, // From the startTermsFlow callback // agreedUrls, // From the startTermsFlow callback // resolve, // Promise resolve function for startTermsFlow callback - // reject, // Promise reject function for startTermsFlow callback }, }; @@ -108,7 +107,6 @@ export default class GeneralUserSettingsTab extends React.Component { policiesAndServices, agreedUrls, resolve, - reject, }, }); }); From 2dc28a608f2a3a4b48547bae6fb6de387902893e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 21 Aug 2019 08:46:10 -0600 Subject: [PATCH 7/7] Move URL abbreviation to its own util file --- src/components/views/settings/SetIdServer.js | 49 ++++--------------- .../tabs/user/GeneralUserSettingsTab.js | 4 +- src/utils/UrlUtils.js | 49 +++++++++++++++++++ 3 files changed, 60 insertions(+), 42 deletions(-) create mode 100644 src/utils/UrlUtils.js diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 135d3c32fd..9c2a59bc82 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -25,38 +25,7 @@ import dis from "../../../dispatcher"; import { getThreepidBindStatus } from '../../../boundThreepids'; import IdentityAuthClient from "../../../IdentityAuthClient"; import {SERVICE_TYPES} from "matrix-js-sdk"; - -/** - * 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 - */ -export function abbreviateIdentityUrl(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; -} +import {abbreviateUrl, unabbreviateUrl} from "../../../utils/UrlUtils"; /** * Check an IS URL is valid, including liveness check @@ -93,7 +62,7 @@ export default class SetIdServer extends React.Component { if (!MatrixClientPeg.get().getIdentityServerUrl() && SdkConfig.get()['validated_server_config']['isUrl']) { // If no ID server is configured but there's one in the config, prepopulate // the field to help the user. - defaultIdServer = abbreviateIdentityUrl(SdkConfig.get()['validated_server_config']['isUrl']); + defaultIdServer = abbreviateUrl(SdkConfig.get()['validated_server_config']['isUrl']); } this.state = { @@ -124,7 +93,7 @@ export default class SetIdServer extends React.Component { const fullUrl = MatrixClientPeg.get().getIdentityServerUrl(); let abbr = ''; - if (fullUrl) abbr = abbreviateIdentityUrl(fullUrl); + if (fullUrl) abbr = abbreviateUrl(fullUrl); this.setState({ currentClientIdServer: fullUrl, @@ -234,15 +203,15 @@ export default class SetIdServer extends React.Component { "server . You will need to reconnect to to stop " + "sharing them.", {}, { - idserver: sub => {abbreviateIdentityUrl(this.state.currentClientIdServer)}, + idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}, // XXX: https://github.com/vector-im/riot-web/issues/9086 - idserver2: sub => {abbreviateIdentityUrl(this.state.currentClientIdServer)}, + idserver2: sub => {abbreviateUrl(this.state.currentClientIdServer)}, }, ); } else { message = _t( "Disconnect from the identity server ?", {}, - {idserver: sub => {abbreviateIdentityUrl(this.state.currentClientIdServer)}}, + {idserver: sub => {abbreviateUrl(this.state.currentClientIdServer)}}, ); } @@ -272,7 +241,7 @@ export default class SetIdServer extends React.Component { if (SdkConfig.get()['validated_server_config']['isUrl']) { // Prepopulate the client's default so the user at least has some idea of // a valid value they might enter - newFieldVal = abbreviateIdentityUrl(SdkConfig.get()['validated_server_config']['isUrl']); + newFieldVal = abbreviateUrl(SdkConfig.get()['validated_server_config']['isUrl']); } this.setState({ @@ -290,12 +259,12 @@ export default class SetIdServer extends React.Component { let sectionTitle; let bodyText; if (idServerUrl) { - sectionTitle = _t("Identity Server (%(server)s)", { server: abbreviateIdentityUrl(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 => {abbreviateIdentityUrl(idServerUrl)} }, + { server: sub => {abbreviateUrl(idServerUrl)} }, ); } else { sectionTitle = _t("Identity Server"); diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 18c18f61d8..e37fa003f7 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -36,7 +36,7 @@ import dis from "../../../../../dispatcher"; import {Service, startTermsFlow} from "../../../../../Terms"; import {SERVICE_TYPES} from "matrix-js-sdk"; import IdentityAuthClient from "../../../../../IdentityAuthClient"; -import {abbreviateIdentityUrl} from "../../SetIdServer"; +import {abbreviateUrl} from "../../../../../utils/UrlUtils"; export default class GeneralUserSettingsTab extends React.Component { static propTypes = { @@ -101,7 +101,7 @@ export default class GeneralUserSettingsTab extends React.Component { )], (policiesAndServices, agreedUrls, extraClassNames) => { return new Promise((resolve, reject) => { this.setState({ - idServerName: abbreviateIdentityUrl(MatrixClientPeg.get().getIdentityServerUrl()), + idServerName: abbreviateUrl(MatrixClientPeg.get().getIdentityServerUrl()), requiredPolicyInfo: { hasTerms: true, policiesAndServices, diff --git a/src/utils/UrlUtils.js b/src/utils/UrlUtils.js new file mode 100644 index 0000000000..7b207c128e --- /dev/null +++ b/src/utils/UrlUtils.js @@ -0,0 +1,49 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import url from "url"; + +/** + * 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 + */ +export 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; +} + +export 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; +}