diff --git a/res/css/_components.scss b/res/css/_components.scss index d30684993d..4c2829b68c 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -70,6 +70,7 @@ @import "./views/dialogs/_SetPasswordDialog.scss"; @import "./views/dialogs/_SettingsDialog.scss"; @import "./views/dialogs/_ShareDialog.scss"; +@import "./views/dialogs/_TermsDialog.scss"; @import "./views/dialogs/_UnknownDeviceDialog.scss"; @import "./views/dialogs/_UploadConfirmDialog.scss"; @import "./views/dialogs/_UserSettingsDialog.scss"; diff --git a/res/css/views/dialogs/_TermsDialog.scss b/res/css/views/dialogs/_TermsDialog.scss new file mode 100644 index 0000000000..60dec57b66 --- /dev/null +++ b/res/css/views/dialogs/_TermsDialog.scss @@ -0,0 +1,35 @@ +/* +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_TermsDialog_termsTableHeader { + font-weight: bold; + text-align: left; +} + +.mx_TermsDialog_termsTable { + font-size: 12px; +} + +.mx_TermsDialog_service, .mx_TermsDialog_summary { + padding-right: 10px; +} + +.mx_TermsDialog_link { + mask-image: url('$(res)/img/external-link.svg'); + background-color: $accent-color; + width: 10px; + height: 10px; +} diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.js index e2b2bf0eb2..bab6ce3f67 100644 --- a/src/ScalarAuthClient.js +++ b/src/ScalarAuthClient.js @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +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. @@ -16,11 +17,14 @@ limitations under the License. import Promise from 'bluebird'; import SettingsStore from "./settings/SettingsStore"; +import { Service, presentTermsForServices, TermsNotSignedError } from './Terms'; const request = require('browser-request'); const SdkConfig = require('./SdkConfig'); const MatrixClientPeg = require('./MatrixClientPeg'); +import * as Matrix from 'matrix-js-sdk'; + // The version of the integration manager API we're intending to work with const imApiVersion = "1.1"; @@ -55,23 +59,11 @@ class ScalarAuthClient { if (!token) { return this.registerForToken(); } else { - return this.validateToken(token).then(userId => { - const me = MatrixClientPeg.get().getUserId(); - if (userId !== me) { - throw new Error("Scalar token is owned by someone else: " + me); - } - return token; - }).catch(err => { - console.error(err); - - // Something went wrong - try to get a new token. - console.warn("Registering for new scalar token"); - return this.registerForToken(); - }); + return this._checkToken(token); } } - validateToken(token) { + _getAccountName(token) { const url = SdkConfig.get().integrations_rest_url + "/account"; return new Promise(function(resolve, reject) { @@ -83,8 +75,10 @@ class ScalarAuthClient { }, (err, response, body) => { if (err) { reject(err); + } else if (body && body.errcode === 'M_TERMS_NOT_SIGNED') { + reject(new TermsNotSignedError()); } else if (response.statusCode / 100 !== 2) { - reject({statusCode: response.statusCode}); + reject(body); } else if (!body || !body.user_id) { reject(new Error("Missing user_id in response")); } else { @@ -94,11 +88,35 @@ class ScalarAuthClient { }); } + _checkToken(token) { + return this._getAccountName(token).then(userId => { + const me = MatrixClientPeg.get().getUserId(); + if (userId !== me) { + throw new Error("Scalar token is owned by someone else: " + me); + } + return token; + }).catch((e) => { + if (e instanceof TermsNotSignedError) { + console.log("Integrations manager requires new terms to be agreed to"); + return presentTermsForServices([new Service( + Matrix.SERVICETYPES.IM, + SdkConfig.get().integrations_rest_url, + token, + )]).then(() => { + return token; + }); + } + }); + } + registerForToken() { // Get openid bearer token from the HS as the first part of our dance return MatrixClientPeg.get().getOpenIdToken().then((tokenObject) => { // Now we can send that to scalar and exchange it for a scalar token return this.exchangeForScalarToken(tokenObject); + }).then((tokenObject) => { + // Validate it (this mostly checks to see if the IM needs us to agree to some terms) + return this._checkToken(tokenObject); }).then((tokenObject) => { window.localStorage.setItem("mx_scalar_token", tokenObject); return tokenObject; diff --git a/src/components/views/dialogs/TermsDialog.js b/src/components/views/dialogs/TermsDialog.js new file mode 100644 index 0000000000..de93957603 --- /dev/null +++ b/src/components/views/dialogs/TermsDialog.js @@ -0,0 +1,201 @@ +/* +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'; +import React from 'react'; +import PropTypes from 'prop-types'; +import sdk from '../../../index'; +import { _t, pickBestLanguage } from '../../../languageHandler'; + +import Matrix from 'matrix-js-sdk'; + +class TermsCheckbox extends React.Component { + static propTypes = { + onChange: PropTypes.func.isRequired, + url: PropTypes.string.isRequired, + checked: PropTypes.bool.isRequired, + } + + onChange = (ev) => { + this.props.onChange(this.props.url, ev.target.checked); + } + + render() { + return ; + } +} + +export default class TermsDialog extends React.Component { + static propTypes = { + /** + * Array of TermsWithService + */ + termsWithServices: PropTypes.arrayOf(PropTypes.object).isRequired, + + /** + * Called with: + * * success {bool} True if the user accepted any douments, false if cancelled + * * agreedUrls {string[]} List of agreed URLs + */ + onFinished: PropTypes.func.isRequired, + } + + constructor() { + super(); + this.state = { + // url -> boolean + agreedUrls: {}, + }; + } + + _onCancelClick = () => { + this.props.onFinished(false); + } + + _onNextClick = () => { + this.props.onFinished(true, Object.keys(this.state.agreedUrls).filter((url) => this.state.agreedUrls[url])); + } + + _nameForServiceType(serviceType, host) { + switch (serviceType) { + case Matrix.SERVICETYPES.IS: + return
{_t("To continue you need to accept the Terms of this service.")}
+ +{_t("Service")} | +{_t("Summary")} | +{_t("Terms")} | +{_t("Accept")} | +
---|