Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Extend slash command '/topic' to display the room topic #2532

Merged
merged 18 commits into from
Feb 7, 2019
Merged
44 changes: 40 additions & 4 deletions src/HtmlUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,21 @@ limitations under the License.

import ReplyThread from "./components/views/elements/ReplyThread";

const React = require('react');
const sanitizeHtml = require('sanitize-html');
const highlight = require('highlight.js');
const linkifyMatrix = require('./linkify-matrix');
import React from 'react';
import sanitizeHtml from 'sanitize-html';
import highlight from 'highlight.js';
import * as linkify from 'linkifyjs';
import linkifyMatrix from './linkify-matrix';
import _linkifyElement from 'linkifyjs/element';
import _linkifyString from 'linkifyjs/string';
import escape from 'lodash/escape';
import emojione from 'emojione';
import classNames from 'classnames';
import MatrixClientPeg from './MatrixClientPeg';
import url from 'url';

linkifyMatrix(linkify);

emojione.imagePathSVG = 'emojione/svg/';
// Store PNG path for displaying many flags at once (for increased performance over SVG)
emojione.imagePathPNG = 'emojione/png/';
Expand Down Expand Up @@ -508,3 +513,34 @@ export function emojifyText(text) {
__html: unicodeToImage(escape(text)),
};
}

/**
* Linkifies the given string. This is a wrapper around 'linkifyjs/string'.
*
* @param {string} str
* @returns {string}
*/
export function linkifyString(str) {
return _linkifyString(str);
}

/**
* Linkifies the given DOM element. This is a wrapper around 'linkifyjs/element'.
*
* @param {object} element DOM element to linkify
* @param {object} [options] Options for linkifyElement. Default: linkifyMatrix.options
* @returns {object}
*/
export function linkifyElement(element, options = linkifyMatrix.options) {
return _linkifyElement(element, options);
}

/**
* Linkify the given string and sanitize the HTML afterwards.
*
* @param {string} dirtyHtml The HTML string to sanitize and linkify
* @returns {string}
*/
export function linkifyAndSanitizeHtml(dirtyHtml) {
return sanitizeHtml(linkifyString(dirtyHtml), sanitizeHtmlParams);
}
38 changes: 24 additions & 14 deletions src/SlashCommands.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import SettingsStore, {SettingLevel} from './settings/SettingsStore';
import {MATRIXTO_URL_PATTERN} from "./linkify-matrix";
import * as querystring from "querystring";
import MultiInviter from './utils/MultiInviter';

import { linkifyAndSanitizeHtml } from './HtmlUtils';

class Command {
constructor({name, args='', description, runFn, hideCompletionAfterSpace=false}) {
Expand Down Expand Up @@ -137,13 +137,26 @@ export const CommandMap = {

topic: new Command({
name: 'topic',
args: '<topic>',
description: _td('Sets the room topic'),
args: '[<topic>]',
description: _td('Gets or sets the room topic'),
runFn: function(roomId, args) {
const cli = MatrixClientPeg.get();
if (args) {
return success(MatrixClientPeg.get().setRoomTopic(roomId, args));
return success(cli.setRoomTopic(roomId, args));
}
return reject(this.getUsage());
const room = cli.getRoom(roomId);
if (!room) return reject('Bad room ID: ' + roomId);

const topicEvents = room.currentState.getStateEvents('m.room.topic', '');
const topic = topicEvents && topicEvents.getContent().topic;
const topicHtml = topic ? linkifyAndSanitizeHtml(topic) : _t('This room has no topic.');

const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
Modal.createTrackedDialog('Slash Commands', 'Topic', InfoDialog, {
boeserwolf marked this conversation as resolved.
Show resolved Hide resolved
title: room.name,
description: <div dangerouslySetInnerHTML={{ __html: topicHtml }} />,
});
return success();
},
}),

Expand Down Expand Up @@ -391,13 +404,12 @@ export const CommandMap = {
ignoredUsers.push(userId); // de-duped internally in the js-sdk
return success(
cli.setIgnoredUsers(ignoredUsers).then(() => {
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
Modal.createTrackedDialog('Slash Commands', 'User ignored', QuestionDialog, {
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
Modal.createTrackedDialog('Slash Commands', 'User ignored', InfoDialog, {
title: _t('Ignored user'),
description: <div>
<p>{ _t('You are now ignoring %(userId)s', {userId}) }</p>
</div>,
hasCancelButton: false,
});
}),
);
Expand All @@ -423,13 +435,12 @@ export const CommandMap = {
if (index !== -1) ignoredUsers.splice(index, 1);
return success(
cli.setIgnoredUsers(ignoredUsers).then(() => {
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
Modal.createTrackedDialog('Slash Commands', 'User unignored', QuestionDialog, {
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
Modal.createTrackedDialog('Slash Commands', 'User unignored', InfoDialog, {
title: _t('Unignored user'),
description: <div>
<p>{ _t('You are no longer ignoring %(userId)s', {userId}) }</p>
</div>,
hasCancelButton: false,
});
}),
);
Expand Down Expand Up @@ -546,8 +557,8 @@ export const CommandMap = {
return cli.setDeviceVerified(userId, deviceId, true);
}).then(() => {
// Tell the user we verified everything
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
Modal.createTrackedDialog('Slash Commands', 'Verified key', QuestionDialog, {
const InfoDialog = sdk.getComponent('dialogs.InfoDialog');
Modal.createTrackedDialog('Slash Commands', 'Verified key', InfoDialog, {
title: _t('Verified key'),
description: <div>
<p>
Expand All @@ -558,7 +569,6 @@ export const CommandMap = {
}
</p>
</div>,
hasCancelButton: false,
});
}),
);
Expand Down
9 changes: 2 additions & 7 deletions src/components/structures/RoomDirectory.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,7 @@ const Modal = require('../../Modal');
const sdk = require('../../index');
const dis = require('../../dispatcher');

const linkify = require('linkifyjs');
const linkifyString = require('linkifyjs/string');
const linkifyMatrix = require('../../linkify-matrix');
const sanitizeHtml = require('sanitize-html');
import { linkifyAndSanitizeHtml } from '../../HtmlUtils';
import Promise from 'bluebird';

import { _t } from '../../languageHandler';
Expand All @@ -37,8 +34,6 @@ import {instanceForInstanceId, protocolNameForInstanceId} from '../../utils/Dire
const MAX_NAME_LENGTH = 80;
const MAX_TOPIC_LENGTH = 160;

linkifyMatrix(linkify);

module.exports = React.createClass({
displayName: 'RoomDirectory',

Expand Down Expand Up @@ -438,7 +433,7 @@ module.exports = React.createClass({
if (topic.length > MAX_TOPIC_LENGTH) {
topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`;
}
topic = linkifyString(sanitizeHtml(topic));
topic = linkifyAndSanitizeHtml(topic);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this was linkifyString(sanitizeHtml(..)) previously but linkifyAndSanitizeHtml() does it the other way around: sanitizeHtml(linkifyString(...)): I suspect this is probably fine but obviously if it's wrong it's a very big deal. Is there a reason to change it?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For security reasons, the specified (insecure) string should always be sanitized last. F. e., someone could figure out how to do nasty things during linkification, thus bypassing the HTML sanitizer completely. Anyways, linkifyString() should always produce valid HTML that passes the sanitizer.

Of course, I could be on the wrong track here, so please feel free to correct me. :-)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thought, although in practice linkifyString happens to escape HTML as it's intended for parsing plaintext strings, so this will end up escaping all HTML attributes.

So Riot, by doing it the other way around, was also sanitising the HTML and then completely escaping it, meaning no HTML could actually be used, so you found a bug there.

In a further plot twist, the topic is not actually HTML at all: I don't know why RoomDirectory was using sanitizeHtml and dangerouslySetInnerHTML, since in RoomHeader, it's just escaped normally as a plain text string and linkified. However, because of the above bug, RoomDirectory was escaping the sanitised HTML and ended up being consistent with the room header...

Sorry you've unwittingly got tangled up in this mess: If we were to fix this, I would probably make a LinkifedText component which takes the linkification out of https://github.com/matrix-org/matrix-react-sdk/blob/master/src/components/views/rooms/RoomHeader.js#L307 / https://github.com/matrix-org/matrix-react-sdk/blob/master/src/components/views/rooms/RoomHeader.js#L81 ie. using React's normal string escaping. and then pointing linkify at the DOM element.

That said, this PR has the same behaviour as before so we could also merge this and then fix it later: you've put more than enough work in to this PR!

One more thing: your parameter to linkifyAndSanitizeHtml isn't used in either of the calls: I would remove it and always use our sanitisation options (it's also a bug in Riot that it was using sanitize-html's default rather than our own, but it didn't matter anyway because the HTML all got escaped anyway).


rows.push(
<tr key={ rooms[i].room_id }
Expand Down
64 changes: 64 additions & 0 deletions src/components/views/dialogs/InfoDialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd.
boeserwolf marked this conversation as resolved.
Show resolved Hide resolved
Copyright 2019 Bastian Masanek, Noxware IT <matrix@noxware.de>

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 sdk from '../../../index';
import { _t } from '../../../languageHandler';

export default React.createClass({
displayName: 'InfoDialog',
propTypes: {
title: PropTypes.string,
description: PropTypes.node,
button: PropTypes.string,
onFinished: PropTypes.func,
},

getDefaultProps: function() {
return {
title: '',
description: '',
};
},

onFinished: function() {
this.props.onFinished();
},

render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return (
<BaseDialog className="mx_InfoDialog" onFinished={this.props.onFinished}
title={this.props.title}
contentId='mx_Dialog_content'
hasCancel={false}
>
<div className="mx_Dialog_content" id="mx_Dialog_content">
{ this.props.description }
</div>
<DialogButtons primaryButton={this.props.button || _t('OK')}
onPrimaryButtonClick={this.onFinished}
hasCancel={false}
>
</DialogButtons>
</BaseDialog>
);
},
});
7 changes: 1 addition & 6 deletions src/components/views/messages/TextualBody.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import highlight from 'highlight.js';
import * as HtmlUtils from '../../../HtmlUtils';
import * as linkify from 'linkifyjs';
import linkifyElement from 'linkifyjs/element';
import linkifyMatrix from '../../../linkify-matrix';
import sdk from '../../../index';
import ScalarAuthClient from '../../../ScalarAuthClient';
import Modal from '../../../Modal';
Expand All @@ -38,8 +35,6 @@ import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
import ReplyThread from "../elements/ReplyThread";
import {host as matrixtoHost} from '../../../matrix-to';

linkifyMatrix(linkify);

module.exports = React.createClass({
displayName: 'TextualBody',

Expand Down Expand Up @@ -98,7 +93,7 @@ module.exports = React.createClass({
// are still sent as plaintext URLs. If these are ever pillified in the composer,
// we should be pillify them here by doing the linkifying BEFORE the pillifying.
this.pillifyLinks(this.refs.content.children);
linkifyElement(this.refs.content, linkifyMatrix.options);
HtmlUtils.linkifyElement(this.refs.content);
this.calculateUrlPreview();

if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") {
Expand Down
12 changes: 4 additions & 8 deletions src/components/views/rooms/LinkPreviewWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,15 @@ limitations under the License.

'use strict';

const React = require('react');
import React from 'react';
import PropTypes from 'prop-types';
import { linkifyElement } from '../../../HtmlUtils';

const sdk = require('../../../index');
const MatrixClientPeg = require('../../../MatrixClientPeg');
const ImageUtils = require('../../../ImageUtils');
const Modal = require('../../../Modal');

const linkify = require('linkifyjs');
const linkifyElement = require('linkifyjs/element');
const linkifyMatrix = require('../../../linkify-matrix');
linkifyMatrix(linkify);

module.exports = React.createClass({
displayName: 'LinkPreviewWidget',

Expand Down Expand Up @@ -62,13 +58,13 @@ module.exports = React.createClass({

componentDidMount: function() {
if (this.refs.description) {
linkifyElement(this.refs.description, linkifyMatrix.options);
linkifyElement(this.refs.description);
}
},

componentDidUpdate: function() {
if (this.refs.description) {
linkifyElement(this.refs.description, linkifyMatrix.options);
linkifyElement(this.refs.description);
}
},

Expand Down
8 changes: 2 additions & 6 deletions src/components/views/rooms/RoomDetailRow.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,11 @@ limitations under the License.
import sdk from '../../../index';
import React from 'react';
import { _t } from '../../../languageHandler';
import * as linkify from 'linkifyjs';
import linkifyElement from 'linkifyjs/element';
import linkifyMatrix from '../../../linkify-matrix';
import { linkifyElement } from '../../../HtmlUtils';
import { ContentRepo } from 'matrix-js-sdk';
import MatrixClientPeg from '../../../MatrixClientPeg';
import PropTypes from 'prop-types';

linkifyMatrix(linkify);

export function getDisplayAliasForRoom(room) {
return room.canonicalAlias || (room.aliases ? room.aliases[0] : "");
}
Expand Down Expand Up @@ -53,7 +49,7 @@ export default React.createClass({

_linkifyTopic: function() {
if (this.refs.topic) {
linkifyElement(this.refs.topic, linkifyMatrix.options);
linkifyElement(this.refs.topic);
}
},

Expand Down
8 changes: 2 additions & 6 deletions src/components/views/rooms/RoomHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,13 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
import Modal from "../../../Modal";
import RateLimitedFunc from '../../../ratelimitedfunc';

import * as linkify from 'linkifyjs';
import linkifyElement from 'linkifyjs/element';
import linkifyMatrix from '../../../linkify-matrix';
import { linkifyElement } from '../../../HtmlUtils';
import AccessibleButton from '../elements/AccessibleButton';
import ManageIntegsButton from '../elements/ManageIntegsButton';
import {CancelButton} from './SimpleRoomHeader';
import SettingsStore from "../../../settings/SettingsStore";
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';

linkifyMatrix(linkify);

module.exports = React.createClass({
displayName: 'RoomHeader',

Expand Down Expand Up @@ -78,7 +74,7 @@ module.exports = React.createClass({

componentDidUpdate: function() {
if (this.refs.topic) {
linkifyElement(this.refs.topic, linkifyMatrix.options);
linkifyElement(this.refs.topic);
}
},

Expand Down
2 changes: 1 addition & 1 deletion src/components/views/verification/VerificationShowSas.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default class VerificationShowSas extends React.Component {
"Verify this user by confirming the following number appears on their screen.",
)}</p>
<p>{_t(
"For maximum security, we reccommend you do this in person or use another " +
"For maximum security, we recommend you do this in person or use another " +
boeserwolf marked this conversation as resolved.
Show resolved Hide resolved
"trusted means of communication.",
)}</p>
<HexVerify text={this.props.sas}
Expand Down
Loading