diff --git a/CHANGELOG.md b/CHANGELOG.md index 87459882c9..055e25b805 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +Changes in [0.11.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.4) (2018-02-09) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.3...v0.11.4) + + * Add isUrlPermitted function to sanity check URLs + Changes in [0.11.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.3) (2017-12-04) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.2...v0.11.3) diff --git a/package.json b/package.json index f81e72e556..bb8db64d28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.11.3", + "version": "0.11.4", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 0c262fe89a..5c6cbd6c1b 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -1,6 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 New Vector Ltd +Copyright 2017, 2018 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. @@ -25,6 +25,7 @@ import escape from 'lodash/escape'; import emojione from 'emojione'; import classNames from 'classnames'; import MatrixClientPeg from './MatrixClientPeg'; +import url from 'url'; emojione.imagePathSVG = 'emojione/svg/'; // Store PNG path for displaying many flags at once (for increased performance over SVG) @@ -44,6 +45,8 @@ const SYMBOL_PATTERN = /([\u2100-\u2bff])/; const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi"); const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; +const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet']; + /* * Return true if the given string contains emoji * Uses a much, much simpler regex than emojione's so will give false @@ -152,6 +155,25 @@ export function sanitizedHtmlNode(insaneHtml) { return
; } +/** + * Tests if a URL from an untrusted source may be safely put into the DOM + * The biggest threat here is javascript: URIs. + * Note that the HTML sanitiser library has its own internal logic for + * doing this, to which we pass the same list of schemes. This is used in + * other places we need to sanitise URLs. + * @return true if permitted, otherwise false + */ +export function isUrlPermitted(inputUrl) { + try { + const parsed = url.parse(inputUrl); + if (!parsed.protocol) return false; + // URL parser protocol includes the trailing colon + return PERMITTED_URL_SCHEMES.includes(parsed.protocol.slice(0, -1)); + } catch (e) { + return false; + } +} + const sanitizeHtmlParams = { allowedTags: [ 'font', // custom to matrix for IRC-style font coloring @@ -172,7 +194,7 @@ const sanitizeHtmlParams = { // Lots of these won't come up by default because we don't allow them selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'], // URL schemes we permit - allowedSchemes: ['http', 'https', 'ftp', 'mailto', 'magnet'], + allowedSchemes: PERMITTED_URL_SCHEMES, allowProtocolRelative: false, diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index d6d0b00c84..b37da0144f 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -618,18 +618,26 @@ export default React.createClass({ }, _startRegistration: function(params) { - this.setStateForNewView({ + const newState = { view: VIEWS.REGISTER, - // these params may be undefined, but if they are, - // unset them from our state: we don't want to - // resume a previous registration session if the - // user just clicked 'register' - register_client_secret: params.client_secret, - register_session_id: params.session_id, - register_hs_url: params.hs_url, - register_is_url: params.is_url, - register_id_sid: params.sid, - }); + }; + + // Only honour params if they are all present, otherwise we reset + // HS and IS URLs when switching to registration. + if (params.client_secret && + params.session_id && + params.hs_url && + params.is_url && + params.sid + ) { + newState.register_client_secret = params.client_secret; + newState.register_session_id = params.session_id; + newState.register_hs_url = params.hs_url; + newState.register_is_url = params.is_url; + newState.register_id_sid = params.sid; + } + + this.setStateForNewView(newState); this.notifyNewScreen('register'); }, @@ -1501,6 +1509,17 @@ export default React.createClass({ } }, + onServerConfigChange(config) { + const newState = {}; + if (config.hsUrl) { + newState.register_hs_url = config.hsUrl; + } + if (config.isUrl) { + newState.register_is_url = config.isUrl; + } + this.setState(newState); + }, + _makeRegistrationUrl: function(params) { if (this.props.startingFragmentQueryParams.referrer) { params.referrer = this.props.startingFragmentQueryParams.referrer; @@ -1589,6 +1608,7 @@ export default React.createClass({ onLoginClick={this.onLoginClick} onRegisterClick={this.onRegisterClick} onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null} + onServerConfigChange={this.onServerConfigChange} /> ); } @@ -1623,6 +1643,7 @@ export default React.createClass({ onForgotPasswordClick={this.onForgotPasswordClick} enableGuest={this.props.enableGuest} onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null} + onServerConfigChange={this.onServerConfigChange} /> ); } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 27a70fdb10..5304f38901 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -264,12 +264,19 @@ module.exports = React.createClass({ isPeeking: true, // this will change to false if peeking fails }); MatrixClientPeg.get().peekInRoom(roomId).then((room) => { + if (this.unmounted) { + return; + } this.setState({ room: room, peekLoading: false, }); this._onRoomLoaded(room); }, (err) => { + if (this.unmounted) { + return; + } + // Stop peeking if anything went wrong this.setState({ isPeeking: false, @@ -286,7 +293,7 @@ module.exports = React.createClass({ } else { throw err; } - }).done(); + }); } } else if (room) { // Stop peeking because we have joined this room previously diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index f4c08e8362..5042ca1fd0 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -58,6 +58,7 @@ module.exports = React.createClass({ // login shouldn't care how password recovery is done. onForgotPasswordClick: PropTypes.func, onCancelClick: PropTypes.func, + onServerConfigChange: PropTypes.func.isRequired, }, getInitialState: function() { @@ -218,6 +219,8 @@ module.exports = React.createClass({ if (config.isUrl !== undefined) { newState.enteredIdentityServerUrl = config.isUrl; } + + this.props.onServerConfigChange(config); this.setState(newState, function() { self._initLoginLogic(config.hsUrl || null, config.isUrl); }); diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index b8a85c5f82..62a3ee4f68 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -61,6 +61,7 @@ module.exports = React.createClass({ // registration shouldn't know or care how login is done. onLoginClick: PropTypes.func.isRequired, onCancelClick: PropTypes.func, + onServerConfigChange: PropTypes.func.isRequired, }, getInitialState: function() { @@ -131,6 +132,7 @@ module.exports = React.createClass({ if (config.isUrl !== undefined) { newState.isUrl = config.isUrl; } + this.props.onServerConfigChange(config); this.setState(newState, function() { this._replaceClient(); }); diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index e879808dc2..66e5fcb0c0 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -17,9 +17,12 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import { MatrixClient } from 'matrix-js-sdk'; + import { KeyCode } from '../../../Keyboard'; import AccessibleButton from '../elements/AccessibleButton'; import sdk from '../../../index'; +import MatrixClientPeg from '../../../MatrixClientPeg'; /** * Basic container for modal dialogs. @@ -51,6 +54,20 @@ export default React.createClass({ children: PropTypes.node, }, + childContextTypes: { + matrixClient: PropTypes.instanceOf(MatrixClient), + }, + + getChildContext: function() { + return { + matrixClient: this._matrixClient, + }; + }, + + componentWillMount() { + this._matrixClient = MatrixClientPeg.get(); + }, + _onKeyDown: function(e) { if (this.props.onKeyDown) { this.props.onKeyDown(e); diff --git a/src/components/views/voip/IncomingCallBox.js b/src/components/views/voip/IncomingCallBox.js index 8d75029baa..6cbaabe602 100644 --- a/src/components/views/voip/IncomingCallBox.js +++ b/src/components/views/voip/IncomingCallBox.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 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. @@ -18,6 +19,7 @@ import PropTypes from 'prop-types'; import MatrixClientPeg from '../../../MatrixClientPeg'; import dis from '../../../dispatcher'; import { _t } from '../../../languageHandler'; +import sdk from '../../../index'; module.exports = React.createClass({ displayName: 'IncomingCallBox', @@ -26,14 +28,16 @@ module.exports = React.createClass({ incomingCall: PropTypes.object, }, - onAnswerClick: function() { + onAnswerClick: function(e) { + e.stopPropagation(); dis.dispatch({ action: 'answer', room_id: this.props.incomingCall.roomId, }); }, - onRejectClick: function() { + onRejectClick: function(e) { + e.stopPropagation(); dis.dispatch({ action: 'hangup', room_id: this.props.incomingCall.roomId, @@ -59,6 +63,7 @@ module.exports = React.createClass({ } } + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return (
@@ -67,14 +72,14 @@ module.exports = React.createClass({
-
+ { _t("Decline") } -
+
-
+ { _t("Accept") } -
+
diff --git a/src/ratelimitedfunc.js b/src/ratelimitedfunc.js index d8c30f5d03..20f6db79b8 100644 --- a/src/ratelimitedfunc.js +++ b/src/ratelimitedfunc.js @@ -35,13 +35,17 @@ module.exports = function(f, minIntervalMs) { if (self.lastCall < now - minIntervalMs) { f.apply(this); - self.lastCall = now; + // get the time again now the function has finished, so if it + // took longer than the delay time to execute, it doesn't + // immediately become eligible to run again. + self.lastCall = Date.now(); } else if (self.scheduledCall === undefined) { self.scheduledCall = setTimeout( () => { self.scheduledCall = undefined; f.apply(this); - self.lastCall = now; + // get time again as per above + self.lastCall = Date.now(); }, (self.lastCall + minIntervalMs) - now, );