Merge branches 'develop' and 't3chguy/m.relates_to' of github.com:matrix-org/matrix-react-sdk into t3chguy/m.relates_to

This commit is contained in:
Michael Telatynski 2018-02-10 11:23:51 +00:00
commit a15d024ad3
No known key found for this signature in database
GPG key ID: 3F879DA5AD802A5E
10 changed files with 110 additions and 23 deletions

View file

@ -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) 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) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.2...v0.11.3)

View file

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "0.11.3", "version": "0.11.4",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd 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"); 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.
@ -25,6 +25,7 @@ import escape from 'lodash/escape';
import emojione from 'emojione'; import emojione from 'emojione';
import classNames from 'classnames'; import classNames from 'classnames';
import MatrixClientPeg from './MatrixClientPeg'; import MatrixClientPeg from './MatrixClientPeg';
import url from 'url';
emojione.imagePathSVG = 'emojione/svg/'; emojione.imagePathSVG = 'emojione/svg/';
// Store PNG path for displaying many flags at once (for increased performance over 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 EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; 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 * Return true if the given string contains emoji
* Uses a much, much simpler regex than emojione's so will give false * Uses a much, much simpler regex than emojione's so will give false
@ -152,6 +155,25 @@ export function sanitizedHtmlNode(insaneHtml) {
return <div dangerouslySetInnerHTML={{ __html: saneHtml }} dir="auto" />; return <div dangerouslySetInnerHTML={{ __html: saneHtml }} dir="auto" />;
} }
/**
* 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 = { const sanitizeHtmlParams = {
allowedTags: [ allowedTags: [
'font', // custom to matrix for IRC-style font coloring '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 // 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'], selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
// URL schemes we permit // URL schemes we permit
allowedSchemes: ['http', 'https', 'ftp', 'mailto', 'magnet'], allowedSchemes: PERMITTED_URL_SCHEMES,
allowProtocolRelative: false, allowProtocolRelative: false,

View file

@ -618,18 +618,26 @@ export default React.createClass({
}, },
_startRegistration: function(params) { _startRegistration: function(params) {
this.setStateForNewView({ const newState = {
view: VIEWS.REGISTER, 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 // Only honour params if they are all present, otherwise we reset
// user just clicked 'register' // HS and IS URLs when switching to registration.
register_client_secret: params.client_secret, if (params.client_secret &&
register_session_id: params.session_id, params.session_id &&
register_hs_url: params.hs_url, params.hs_url &&
register_is_url: params.is_url, params.is_url &&
register_id_sid: params.sid, 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'); 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) { _makeRegistrationUrl: function(params) {
if (this.props.startingFragmentQueryParams.referrer) { if (this.props.startingFragmentQueryParams.referrer) {
params.referrer = this.props.startingFragmentQueryParams.referrer; params.referrer = this.props.startingFragmentQueryParams.referrer;
@ -1589,6 +1608,7 @@ export default React.createClass({
onLoginClick={this.onLoginClick} onLoginClick={this.onLoginClick}
onRegisterClick={this.onRegisterClick} onRegisterClick={this.onRegisterClick}
onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null} onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null}
onServerConfigChange={this.onServerConfigChange}
/> />
); );
} }
@ -1623,6 +1643,7 @@ export default React.createClass({
onForgotPasswordClick={this.onForgotPasswordClick} onForgotPasswordClick={this.onForgotPasswordClick}
enableGuest={this.props.enableGuest} enableGuest={this.props.enableGuest}
onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null} onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null}
onServerConfigChange={this.onServerConfigChange}
/> />
); );
} }

View file

@ -264,12 +264,19 @@ module.exports = React.createClass({
isPeeking: true, // this will change to false if peeking fails isPeeking: true, // this will change to false if peeking fails
}); });
MatrixClientPeg.get().peekInRoom(roomId).then((room) => { MatrixClientPeg.get().peekInRoom(roomId).then((room) => {
if (this.unmounted) {
return;
}
this.setState({ this.setState({
room: room, room: room,
peekLoading: false, peekLoading: false,
}); });
this._onRoomLoaded(room); this._onRoomLoaded(room);
}, (err) => { }, (err) => {
if (this.unmounted) {
return;
}
// Stop peeking if anything went wrong // Stop peeking if anything went wrong
this.setState({ this.setState({
isPeeking: false, isPeeking: false,
@ -286,7 +293,7 @@ module.exports = React.createClass({
} else { } else {
throw err; throw err;
} }
}).done(); });
} }
} else if (room) { } else if (room) {
// Stop peeking because we have joined this room previously // Stop peeking because we have joined this room previously

View file

@ -58,6 +58,7 @@ module.exports = React.createClass({
// login shouldn't care how password recovery is done. // login shouldn't care how password recovery is done.
onForgotPasswordClick: PropTypes.func, onForgotPasswordClick: PropTypes.func,
onCancelClick: PropTypes.func, onCancelClick: PropTypes.func,
onServerConfigChange: PropTypes.func.isRequired,
}, },
getInitialState: function() { getInitialState: function() {
@ -218,6 +219,8 @@ module.exports = React.createClass({
if (config.isUrl !== undefined) { if (config.isUrl !== undefined) {
newState.enteredIdentityServerUrl = config.isUrl; newState.enteredIdentityServerUrl = config.isUrl;
} }
this.props.onServerConfigChange(config);
this.setState(newState, function() { this.setState(newState, function() {
self._initLoginLogic(config.hsUrl || null, config.isUrl); self._initLoginLogic(config.hsUrl || null, config.isUrl);
}); });

View file

@ -61,6 +61,7 @@ module.exports = React.createClass({
// registration shouldn't know or care how login is done. // registration shouldn't know or care how login is done.
onLoginClick: PropTypes.func.isRequired, onLoginClick: PropTypes.func.isRequired,
onCancelClick: PropTypes.func, onCancelClick: PropTypes.func,
onServerConfigChange: PropTypes.func.isRequired,
}, },
getInitialState: function() { getInitialState: function() {
@ -131,6 +132,7 @@ module.exports = React.createClass({
if (config.isUrl !== undefined) { if (config.isUrl !== undefined) {
newState.isUrl = config.isUrl; newState.isUrl = config.isUrl;
} }
this.props.onServerConfigChange(config);
this.setState(newState, function() { this.setState(newState, function() {
this._replaceClient(); this._replaceClient();
}); });

View file

@ -17,9 +17,12 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import { KeyCode } from '../../../Keyboard'; import { KeyCode } from '../../../Keyboard';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import sdk from '../../../index'; import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
/** /**
* Basic container for modal dialogs. * Basic container for modal dialogs.
@ -51,6 +54,20 @@ export default React.createClass({
children: PropTypes.node, children: PropTypes.node,
}, },
childContextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
getChildContext: function() {
return {
matrixClient: this._matrixClient,
};
},
componentWillMount() {
this._matrixClient = MatrixClientPeg.get();
},
_onKeyDown: function(e) { _onKeyDown: function(e) {
if (this.props.onKeyDown) { if (this.props.onKeyDown) {
this.props.onKeyDown(e); this.props.onKeyDown(e);

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector 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.
@ -18,6 +19,7 @@ import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import sdk from '../../../index';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'IncomingCallBox', displayName: 'IncomingCallBox',
@ -26,14 +28,16 @@ module.exports = React.createClass({
incomingCall: PropTypes.object, incomingCall: PropTypes.object,
}, },
onAnswerClick: function() { onAnswerClick: function(e) {
e.stopPropagation();
dis.dispatch({ dis.dispatch({
action: 'answer', action: 'answer',
room_id: this.props.incomingCall.roomId, room_id: this.props.incomingCall.roomId,
}); });
}, },
onRejectClick: function() { onRejectClick: function(e) {
e.stopPropagation();
dis.dispatch({ dis.dispatch({
action: 'hangup', action: 'hangup',
room_id: this.props.incomingCall.roomId, room_id: this.props.incomingCall.roomId,
@ -59,6 +63,7 @@ module.exports = React.createClass({
} }
} }
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return ( return (
<div className="mx_IncomingCallBox" id="incomingCallBox"> <div className="mx_IncomingCallBox" id="incomingCallBox">
<img className="mx_IncomingCallBox_chevron" src="img/chevron-left.png" width="9" height="16" /> <img className="mx_IncomingCallBox_chevron" src="img/chevron-left.png" width="9" height="16" />
@ -67,14 +72,14 @@ module.exports = React.createClass({
</div> </div>
<div className="mx_IncomingCallBox_buttons"> <div className="mx_IncomingCallBox_buttons">
<div className="mx_IncomingCallBox_buttons_cell"> <div className="mx_IncomingCallBox_buttons_cell">
<div className="mx_IncomingCallBox_buttons_decline" onClick={this.onRejectClick}> <AccessibleButton className="mx_IncomingCallBox_buttons_decline" onClick={this.onRejectClick}>
{ _t("Decline") } { _t("Decline") }
</div> </AccessibleButton>
</div> </div>
<div className="mx_IncomingCallBox_buttons_cell"> <div className="mx_IncomingCallBox_buttons_cell">
<div className="mx_IncomingCallBox_buttons_accept" onClick={this.onAnswerClick}> <AccessibleButton className="mx_IncomingCallBox_buttons_accept" onClick={this.onAnswerClick}>
{ _t("Accept") } { _t("Accept") }
</div> </AccessibleButton>
</div> </div>
</div> </div>
</div> </div>

View file

@ -35,13 +35,17 @@ module.exports = function(f, minIntervalMs) {
if (self.lastCall < now - minIntervalMs) { if (self.lastCall < now - minIntervalMs) {
f.apply(this); 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) { } else if (self.scheduledCall === undefined) {
self.scheduledCall = setTimeout( self.scheduledCall = setTimeout(
() => { () => {
self.scheduledCall = undefined; self.scheduledCall = undefined;
f.apply(this); f.apply(this);
self.lastCall = now; // get time again as per above
self.lastCall = Date.now();
}, },
(self.lastCall + minIntervalMs) - now, (self.lastCall + minIntervalMs) - now,
); );