From e0cea74c7ec9dc7891be1ab604e4f9904cbb2c40 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Wed, 2 Nov 2016 16:26:10 +0000 Subject: [PATCH 01/18] Encrypt attachments in encrypted rooms, decrypt image attachments when displaying them --- package.json | 1 + src/ContentMessages.js | 49 ++++++++++++++++++++- src/components/views/messages/MImageBody.js | 44 +++++++++++++++++- 3 files changed, 91 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 498514a2b0..3bcccd668e 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ }, "dependencies": { "babel-runtime": "^6.11.6", + "browser-encrypt-attachment": "0.0.0", "browser-request": "^0.3.3", "classnames": "^2.1.2", "draft-js": "^0.8.1", diff --git a/src/ContentMessages.js b/src/ContentMessages.js index fd18b22d30..a3f6d548c3 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -23,6 +23,8 @@ var MatrixClientPeg = require('./MatrixClientPeg'); var sdk = require('./index'); var Modal = require('./Modal'); +var encrypt = require("browser-encrypt-attachment"); + function infoForImageFile(imageFile) { var deferred = q.defer(); @@ -81,6 +83,24 @@ function infoForVideoFile(videoFile) { return deferred.promise; } +/** + * Read the file as an ArrayBuffer. + * @return {Promise} A promise that resolves with an ArrayBuffer when the file + * is read. + */ +function readFileAsArrayBuffer(file) { + var deferred = q.defer(); + var reader = new FileReader(); + reader.onload = function(e) { + deferred.resolve(e.target.result); + }; + reader.onerror = function(e) { + deferred.reject(e); + }; + reader.readAsArrayBuffer(file); + return deferred.promise; +} + class ContentMessages { constructor() { @@ -137,10 +157,26 @@ class ContentMessages { this.inprogress.push(upload); dis.dispatch({action: 'upload_started'}); + var encryptInfo = null; var error; var self = this; return def.promise.then(function() { - upload.promise = matrixClient.uploadContent(file); + if (matrixClient.isRoomEncrypted(room_id)) { + // If the room is encrypted then encrypt the file before uploading it. + // First read the file into memory. + upload.promise = readFileAsArrayBuffer(file).then(function(data) { + // Then encrypt the file. + return encrypt.encryptAttachment(data); + }).then(function(encryptResult) { + // Record the information needed to decrypt the attachment. + encryptInfo = encryptResult.info; + // Pass the encrypted data as a Blob to the uploader. + var blob = new Blob([encryptResult.data]); + return matrixClient.uploadContent(blob); + }); + } else { + upload.promise = matrixClient.uploadContent(file); + } return upload.promise; }).progress(function(ev) { if (ev) { @@ -149,7 +185,16 @@ class ContentMessages { dis.dispatch({action: 'upload_progress', upload: upload}); } }).then(function(url) { - content.url = url; + if (encryptInfo === null) { + // If the attachment isn't encrypted then include the URL directly. + content.url = url; + } else { + // If the attachment is encrypted then bundle the URL along + // with the information needed to decrypt the attachment and + // add it under a file key. + encryptInfo.url = url; + content.file = encryptInfo; + } return matrixClient.sendMessage(roomId, content); }, function(err) { error = err; diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 526fc6a3a5..087a337bd2 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -19,12 +19,18 @@ limitations under the License. var React = require('react'); var filesize = require('filesize'); +// Pull in the encryption lib so that we can decrypt attachments. +var encrypt = require("browser-encrypt-attachment"); +// Pull in a fetch polyfill so we can download encrypted attachments. +require("isomorphic-fetch"); + var MatrixClientPeg = require('../../../MatrixClientPeg'); var ImageUtils = require('../../../ImageUtils'); var Modal = require('../../../Modal'); var sdk = require('../../../index'); var dis = require("../../../dispatcher"); + module.exports = React.createClass({ displayName: 'MImageBody', @@ -85,6 +91,33 @@ module.exports = React.createClass({ componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); this.fixupHeight(); + var content = this.props.mxEvent.getContent(); + if (content.file !== undefined) { + // TODO: hook up an error handler to the promise. + this.decryptFile(content.file); + } + }, + + decryptFile: function(file) { + var url = MatrixClientPeg.get().mxcUrlToHttp(file.url); + var self = this; + // Download the encrypted file as an array buffer. + return fetch(url).then(function (response) { + return response.arrayBuffer(); + }).then(function (responseData) { + // Decrypt the array buffer using the information taken from + // the event content. + return encrypt.decryptAttachment(responseData, file); + }).then(function(dataArray) { + // Turn the array into a Blob and use createObjectURL to make + // a url that we can use as an img src. + var blob = new Blob([dataArray]); + var blobUrl = window.URL.createObjectURL(blob); + self.refs.image.src = blobUrl; + self.refs.image.onload = function() { + window.URL.revokeObjectURL(blobUrl); + }; + }); }, componentWillUnmount: function() { @@ -148,7 +181,16 @@ module.exports = React.createClass({ } var thumbUrl = this._getThumbUrl(); - if (thumbUrl) { + if (content.file !== undefined) { + // Need to decrypt the attachment + // The attachment is decrypted in componentDidMount. + return ( + + {content.body} + + ); + } else if (thumbUrl) { return ( From 46301727c3e9e5e4c9f31ded5810a2bad4137cca Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 3 Nov 2016 15:53:52 +0000 Subject: [PATCH 02/18] Actually add isomorphic-fetch --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 3bcccd668e..26ef006ed0 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "fuse.js": "^2.2.0", "glob": "^5.0.14", "highlight.js": "^8.9.1", + "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.3", "lodash": "^4.13.1", "marked": "^0.3.5", From e949d91162de3c10cb079c2f4de7141231c99c1c Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Thu, 3 Nov 2016 16:39:20 +0000 Subject: [PATCH 03/18] Handle decryption errors --- src/components/views/messages/MImageBody.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 087a337bd2..c07051455e 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -92,9 +92,14 @@ module.exports = React.createClass({ this.dispatcherRef = dis.register(this.onAction); this.fixupHeight(); var content = this.props.mxEvent.getContent(); + var self = this; if (content.file !== undefined) { // TODO: hook up an error handler to the promise. - this.decryptFile(content.file); + this.decryptFile(content.file).catch(function (err) { + console.warn("Unable to decrypt attachment: ", err) + // Set a placeholder image when we can't decrypt the image. + self.refs.image.src = "img/warning.svg"; + }); } }, @@ -184,9 +189,10 @@ module.exports = React.createClass({ if (content.file !== undefined) { // Need to decrypt the attachment // The attachment is decrypted in componentDidMount. + // For now add an img tag with a spinner. return ( - {content.body} ); From 842b5aed56e43529e7e689ba4b3940c95e954adb Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 3 Nov 2016 17:12:29 +0000 Subject: [PATCH 04/18] The variable is called roomId here --- src/ContentMessages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ContentMessages.js b/src/ContentMessages.js index a3f6d548c3..d88d769c91 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -161,7 +161,7 @@ class ContentMessages { var error; var self = this; return def.promise.then(function() { - if (matrixClient.isRoomEncrypted(room_id)) { + if (matrixClient.isRoomEncrypted(roomId)) { // If the room is encrypted then encrypt the file before uploading it. // First read the file into memory. upload.promise = readFileAsArrayBuffer(file).then(function(data) { From 12fc70c6717db10f552e07828a5982ae6f6dfc72 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 4 Nov 2016 11:52:47 +0000 Subject: [PATCH 05/18] Include the mimetype with the file info. Store the objectURL in state so that it can be used normally by the exising templates --- src/ContentMessages.js | 3 + src/components/views/messages/MImageBody.js | 80 ++++++++++++++------- 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/src/ContentMessages.js b/src/ContentMessages.js index d88d769c91..9059874f81 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -193,6 +193,9 @@ class ContentMessages { // with the information needed to decrypt the attachment and // add it under a file key. encryptInfo.url = url; + if (file.type) { + encryptInfo.mimetype = file.type; + } content.file = encryptInfo; } return matrixClient.sendMessage(roomId, content); diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index c07051455e..06b8a4d9bf 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -39,11 +39,18 @@ module.exports = React.createClass({ mxEvent: React.PropTypes.object.isRequired, }, + getInitialState: function() { + return { + decryptedUrl: null, + }; + }, + + onClick: function onClick(ev) { if (ev.button == 0 && !ev.metaKey) { ev.preventDefault(); var content = this.props.mxEvent.getContent(); - var httpUrl = MatrixClientPeg.get().mxcUrlToHttp(content.url); + var httpUrl = this._getContentUrl(); var ImageView = sdk.getComponent("elements.ImageView"); var params = { src: httpUrl, @@ -83,9 +90,23 @@ module.exports = React.createClass({ imgElement.src = this._getThumbUrl(); }, + _getContentUrl: function() { + var content = this.props.mxEvent.getContent(); + if (content.file !== undefined) { + return this.state.decryptedUrl; + } else { + return MatrixClientPeg.get().mxcUrlToHttp(content.url); + } + }, + _getThumbUrl: function() { var content = this.props.mxEvent.getContent(); - return MatrixClientPeg.get().mxcUrlToHttp(content.url, 800, 600); + if (content.file !== undefined) { + // TODO: Decrypt and use the thumbnail file if one is present. + return this.state.decryptedUrl; + } else { + return MatrixClientPeg.get().mxcUrlToHttp(content.url, 800, 600); + } }, componentDidMount: function() { @@ -93,7 +114,7 @@ module.exports = React.createClass({ this.fixupHeight(); var content = this.props.mxEvent.getContent(); var self = this; - if (content.file !== undefined) { + if (content.file !== undefined && this.state.decryptedUrl === null) { // TODO: hook up an error handler to the promise. this.decryptFile(content.file).catch(function (err) { console.warn("Unable to decrypt attachment: ", err) @@ -114,19 +135,23 @@ module.exports = React.createClass({ // the event content. return encrypt.decryptAttachment(responseData, file); }).then(function(dataArray) { - // Turn the array into a Blob and use createObjectURL to make + // Turn the array into a Blob and use createObjectUrl to make // a url that we can use as an img src. - var blob = new Blob([dataArray]); - var blobUrl = window.URL.createObjectURL(blob); - self.refs.image.src = blobUrl; - self.refs.image.onload = function() { - window.URL.revokeObjectURL(blobUrl); - }; + var blob = new Blob([dataArray], {type: file.mimetype}); + if (!self._unmounted) { + self.setState({ + decryptedUrl: window.URL.createObjectURL(blob), + }); + } }); }, componentWillUnmount: function() { dis.unregister(this.dispatcherRef); + this._unmounted = true; + if (this.state.decryptedUrl) { + window.URL.revokeObjectURL(this.state.decryptedUrl); + } }, onAction: function(payload) { @@ -161,11 +186,27 @@ module.exports = React.createClass({ var content = this.props.mxEvent.getContent(); var cli = MatrixClientPeg.get(); + if (content.file !== undefined && this.state.decryptedUrl === null) { + + // Need to decrypt the attachment + // The attachment is decrypted in componentDidMount. + // For now add an img tag with a spinner. + return ( + + {content.body} + + ); + } + + var contentUrl = this._getContentUrl(); + var thumbUrl = this._getThumbUrl(); + var download; if (this.props.tileShape === "file_grid") { download = (
- + {content.body}
@@ -177,7 +218,7 @@ module.exports = React.createClass({ else { download = (
- + Download {content.body} ({ content.info && content.info.size ? filesize(content.info.size) : "Unknown size" }) @@ -185,21 +226,10 @@ module.exports = React.createClass({ ); } - var thumbUrl = this._getThumbUrl(); - if (content.file !== undefined) { - // Need to decrypt the attachment - // The attachment is decrypted in componentDidMount. - // For now add an img tag with a spinner. + if (thumbUrl) { return ( - {content.body} - - ); - } else if (thumbUrl) { - return ( - - + {content.body} Date: Fri, 4 Nov 2016 12:46:45 +0000 Subject: [PATCH 06/18] Move decryptFile into a utility function so that it can be shared between different components --- src/components/views/messages/MImageBody.js | 35 ++++------------- src/utils/DecryptFile.js | 42 +++++++++++++++++++++ 2 files changed, 50 insertions(+), 27 deletions(-) create mode 100644 src/utils/DecryptFile.js diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 06b8a4d9bf..6ae8a1b54c 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -19,16 +19,13 @@ limitations under the License. var React = require('react'); var filesize = require('filesize'); -// Pull in the encryption lib so that we can decrypt attachments. -var encrypt = require("browser-encrypt-attachment"); -// Pull in a fetch polyfill so we can download encrypted attachments. -require("isomorphic-fetch"); var MatrixClientPeg = require('../../../MatrixClientPeg'); var ImageUtils = require('../../../ImageUtils'); var Modal = require('../../../Modal'); var sdk = require('../../../index'); var dis = require("../../../dispatcher"); +var DecryptFile = require('../../../utils/DecryptFile'); module.exports = React.createClass({ @@ -116,7 +113,13 @@ module.exports = React.createClass({ var self = this; if (content.file !== undefined && this.state.decryptedUrl === null) { // TODO: hook up an error handler to the promise. - this.decryptFile(content.file).catch(function (err) { + DecryptFile.decryptFile(content.file).then(function(blob) { + if (!self._unmounted) { + self.setState({ + decryptedUrl: window.URL.createObjectURL(blob), + }); + } + }).catch(function (err) { console.warn("Unable to decrypt attachment: ", err) // Set a placeholder image when we can't decrypt the image. self.refs.image.src = "img/warning.svg"; @@ -124,28 +127,6 @@ module.exports = React.createClass({ } }, - decryptFile: function(file) { - var url = MatrixClientPeg.get().mxcUrlToHttp(file.url); - var self = this; - // Download the encrypted file as an array buffer. - return fetch(url).then(function (response) { - return response.arrayBuffer(); - }).then(function (responseData) { - // Decrypt the array buffer using the information taken from - // the event content. - return encrypt.decryptAttachment(responseData, file); - }).then(function(dataArray) { - // Turn the array into a Blob and use createObjectUrl to make - // a url that we can use as an img src. - var blob = new Blob([dataArray], {type: file.mimetype}); - if (!self._unmounted) { - self.setState({ - decryptedUrl: window.URL.createObjectURL(blob), - }); - } - }); - }, - componentWillUnmount: function() { dis.unregister(this.dispatcherRef); this._unmounted = true; diff --git a/src/utils/DecryptFile.js b/src/utils/DecryptFile.js new file mode 100644 index 0000000000..254c807158 --- /dev/null +++ b/src/utils/DecryptFile.js @@ -0,0 +1,42 @@ +/* +Copyright 2016 OpenMarket Ltd + +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. +*/ + +'use struct'; + +// Pull in the encryption lib so that we can decrypt attachments. +var encrypt = require("browser-encrypt-attachment"); +// Pull in a fetch polyfill so we can download encrypted attachments. +require("isomorphic-fetch"); +// Grab the client so that we can turn mxc:// URLs into https:// URLS. +var MatrixClientPeg = require('../MatrixClientPeg'); + + +export function decryptFile(file) { + var url = MatrixClientPeg.get().mxcUrlToHttp(file.url); + var self = this; + // Download the encrypted file as an array buffer. + return fetch(url).then(function (response) { + return response.arrayBuffer(); + }).then(function (responseData) { + // Decrypt the array buffer using the information taken from + // the event content. + return encrypt.decryptAttachment(responseData, file); + }).then(function(dataArray) { + // Turn the array into a Blob and give it the correct MIME-type. + var blob = new Blob([dataArray], {type: file.mimetype}); + return blob; + }); +} From 48cfd4f399ff4c7226a4996afb83fa65167fbbdf Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 4 Nov 2016 13:05:34 +0000 Subject: [PATCH 07/18] Decypt m.video events --- src/components/views/messages/MImageBody.js | 1 - src/components/views/messages/MVideoBody.js | 100 ++++++++++++++++++-- 2 files changed, 94 insertions(+), 7 deletions(-) diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 6ae8a1b54c..8b6794d8cb 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -165,7 +165,6 @@ module.exports = React.createClass({ render: function() { var TintableSvg = sdk.getComponent("elements.TintableSvg"); var content = this.props.mxEvent.getContent(); - var cli = MatrixClientPeg.get(); if (content.file !== undefined && this.state.decryptedUrl === null) { diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index d29a3ea53e..df707ddcb6 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -19,13 +19,22 @@ limitations under the License. var React = require('react'); var filesize = require('filesize'); + var MatrixClientPeg = require('../../../MatrixClientPeg'); var Modal = require('../../../Modal'); var sdk = require('../../../index'); +var DecryptFile = require("../../../utils/DecryptFile") module.exports = React.createClass({ displayName: 'MVideoBody', + getInitialState: function() { + return { + decryptedUrl: null, + decryptedThumbnailUrl: null, + }; + }, + thumbScale: function(fullWidth, fullHeight, thumbWidth, thumbHeight) { if (!fullWidth || !fullHeight) { // Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even @@ -48,9 +57,88 @@ module.exports = React.createClass({ } }, + _getContentUrl: function() { + var content = this.props.mxEvent.getContent(); + if (content.file !== undefined) { + return this.state.decryptedUrl; + } else { + return MatrixClientPeg.get().mxcUrlToHttp(content.url); + } + }, + + _getThumbUrl: function() { + var content = this.props.mxEvent.getContent(); + if (content.file !== undefined) { + return this.state.decryptedThumbnailUrl; + } else { + return MatrixClientPeg.get().mxcUrlToHttp(content.url, 800, 600); + } + }, + + componentDidMount: function() { + var content = this.props.mxEvent.getContent(); + var self = this; + + if (content.file !== undefined && this.state.decryptedUrl === null) { + var thumbnailPromise = Promise.resolve(null); + if (content.info.thumbnail_file) { + thumbnailPromise = DecryptFile.decryptFile( + content.info.thumbnail_file + ); + } + thumbnailPromise.then(function(thumbnailBlob) { + DecryptFile.decryptFile( + content.file + ).then(function(contentBlob) { + if (self._unmounted) { + return; + } + var contentUrl = window.URL.createObjectURL(contentBlob); + var thumbUrl = null; + if (thumbnailBlob) { + thumbUrl = window.URL.createObjectURL(thumbnailBlob); + } + self.setState({ + decryptedUrl: contentUrl, + decryptedThumbnailUrl: thumbUrl, + }); + }); + }).catch(function (err) { + console.warn("Unable to decrypt attachment: ", err) + // Set a placeholder image when we can't decrypt the image. + self.refs.image.src = "img/warning.svg"; + }); + } + }, + + componentWillUnmount: function() { + this._unmounted = true; + if (this.state.decryptedUrl) { + window.URL.revokeObjectURL(this.state.decryptedUrl); + } + if (this.state.decryptedThumbnailUrl) { + window.URL.revokeObjectURL(this.state.decryptedThumbnailUrl); + } + }, + + render: function() { var content = this.props.mxEvent.getContent(); - var cli = MatrixClientPeg.get(); + + if (content.file !== undefined && this.state.decryptedUrl === null) { + // Need to decrypt the attachment + // The attachment is decrypted in componentDidMount. + // For now add an img tag with a spinner. + return ( + + {content.body} + + ); + } + + var contentUrl = this._getContentUrl(); + var thumbUrl = this._getThumbUrl(); var height = null; var width = null; @@ -63,8 +151,8 @@ module.exports = React.createClass({ height = Math.floor(content.info.h * scale); } - if (content.info.thumbnail_url) { - poster = cli.mxcUrlToHttp(content.info.thumbnail_url); + if (thumbUrl) { + poster = thumbUrl; preload = "none"; } } @@ -73,7 +161,7 @@ module.exports = React.createClass({ if (this.props.tileShape === "file_grid") { download = (
- + {content.body}
@@ -86,7 +174,7 @@ module.exports = React.createClass({ var TintableSvg = sdk.getComponent("elements.TintableSvg"); download = (
- + Download {content.body} ({ content.info && content.info.size ? filesize(content.info.size) : "Unknown size" }) @@ -96,7 +184,7 @@ module.exports = React.createClass({ return ( - From 71c002d588849b17af5ee256062cb3487f3d4e70 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 4 Nov 2016 13:08:17 +0000 Subject: [PATCH 08/18] Remove spurious TODO --- src/components/views/messages/MImageBody.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 8b6794d8cb..2f6f9224ea 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -112,7 +112,6 @@ module.exports = React.createClass({ var content = this.props.mxEvent.getContent(); var self = this; if (content.file !== undefined && this.state.decryptedUrl === null) { - // TODO: hook up an error handler to the promise. DecryptFile.decryptFile(content.file).then(function(blob) { if (!self._unmounted) { self.setState({ From 91123431dd5d35a59f761a1246877a5fc4d49f0a Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 4 Nov 2016 13:13:25 +0000 Subject: [PATCH 09/18] Fix unencrypted video thumbnail --- src/components/views/messages/MVideoBody.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index df707ddcb6..6f9a270800 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -70,8 +70,10 @@ module.exports = React.createClass({ var content = this.props.mxEvent.getContent(); if (content.file !== undefined) { return this.state.decryptedThumbnailUrl; + } else if (content.info.thumbnail_url) { + return MatrixClientPeg.get().mxcUrlToHttp(content.info.thumbnail_url); } else { - return MatrixClientPeg.get().mxcUrlToHttp(content.url, 800, 600); + return null; } }, From 94bfe31857ac835d98e05050e502f9a7a25950d2 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 4 Nov 2016 13:45:00 +0000 Subject: [PATCH 10/18] Decrypt m.audio attachments --- src/components/views/messages/MAudioBody.js | 55 +++++++++++++++++++-- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js index 6113fa7c6c..375b2431c3 100644 --- a/src/components/views/messages/MAudioBody.js +++ b/src/components/views/messages/MAudioBody.js @@ -21,28 +21,75 @@ import MFileBody from './MFileBody'; import MatrixClientPeg from '../../../MatrixClientPeg'; import sdk from '../../../index'; +import { decryptFile } from '../../../utils/DecryptFile'; export default class MAudioBody extends React.Component { constructor(props) { super(props); this.state = { - playing: false + playing: false, + decryptedUrl: null, } } - onPlayToggle() { this.setState({ playing: !this.state.playing }); } + _getContentUrl() { + var content = this.props.mxEvent.getContent(); + if (content.file !== undefined) { + return this.state.decryptedUrl; + } else { + return MatrixClientPeg.get().mxcUrlToHttp(content.url); + } + } + + componentDidMount() { + var content = this.props.mxEvent.getContent(); + if (content.file !== undefined && this.state.decryptedUrl === null) { + decryptFile(content.file).then((blob) => { + if (!this._unmounted) { + this.setState({ + decryptedUrl: window.URL.createObjectURL(blob), + }); + } + }).catch((err) => { + console.warn("Unable to decrypt attachment: ", err) + // Set a placeholder image when we can't decrypt the image. + this.refs.image.src = "img/warning.svg"; + }); + } + } + + componentWillUnmount() { + this._unmounted = true; + if (this.state.decryptedUrl) { + window.URL.revokeObjectURL(this.state.decryptedUrl); + } + } + render() { var content = this.props.mxEvent.getContent(); - var cli = MatrixClientPeg.get(); + + if (content.file !== undefined && this.state.decryptedUrl === null) { + // Need to decrypt the attachment + // The attachment is decrypted in componentDidMount. + // For now add an img tag with a spinner. + return ( + + {content.body} + + ); + } + + var contentUrl = this._getContentUrl(); return ( - ); From 6ea00852906921d0e772e9e6fd3649b296187cf9 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 4 Nov 2016 14:00:26 +0000 Subject: [PATCH 11/18] Decrypt m.file attachments --- src/components/views/messages/MFileBody.js | 64 ++++++++++++++++++++-- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index c37cd32c4e..855cdac59c 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -20,11 +20,17 @@ var React = require('react'); var filesize = require('filesize'); var MatrixClientPeg = require('../../../MatrixClientPeg'); var sdk = require('../../../index'); -var dis = require("../../../dispatcher"); +var DecryptFile = require('../../../utils/DecryptFile'); module.exports = React.createClass({ displayName: 'MFileBody', + getInitialState: function() { + return { + decryptedUrl: null, + }; + }, + presentableTextForFile: function(content) { var linkText = 'Attachment'; if (content.body && content.body.length > 0) { @@ -47,21 +53,67 @@ module.exports = React.createClass({ return linkText; }, + _getContentUrl: function() { + var content = this.props.mxEvent.getContent(); + if (content.file !== undefined) { + return this.state.decryptedUrl; + } else { + return MatrixClientPeg.get().mxcUrlToHttp(content.url); + } + }, + + componentDidMount: function() { + var content = this.props.mxEvent.getContent(); + var self = this; + if (content.file !== undefined && this.state.decryptedUrl === null) { + DecryptFile.decryptFile(content.file).then(function(blob) { + if (!self._unmounted) { + self.setState({ + decryptedUrl: window.URL.createObjectURL(blob), + }); + } + }).catch(function (err) { + console.warn("Unable to decrypt attachment: ", err) + // Set a placeholder image when we can't decrypt the image. + self.refs.image.src = "img/warning.svg"; + }); + } + }, + + componentWillUnmount: function() { + this._unmounted = true; + if (this.state.decryptedUrl) { + window.URL.revokeObjectURL(this.state.decryptedUrl); + } + }, + render: function() { var content = this.props.mxEvent.getContent(); - var cli = MatrixClientPeg.get(); - var httpUrl = cli.mxcUrlToHttp(content.url); var text = this.presentableTextForFile(content); var TintableSvg = sdk.getComponent("elements.TintableSvg"); + if (content.file !== undefined && this.state.decryptedUrl === null) { - if (httpUrl) { + // Need to decrypt the attachment + // The attachment is decrypted in componentDidMount. + // For now add an img tag with a spinner. + return ( + + {content.body} + + ); + } + + var contentUrl = this._getContentUrl(); + + if (contentUrl) { if (this.props.tileShape === "file_grid") { return (
- + { content.body && content.body.length > 0 ? content.body : "Attachment" }
@@ -75,7 +127,7 @@ module.exports = React.createClass({ return (
- + Download {text} From 8f778f54fd10428836c99d08346cf89f038dd0ce Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 4 Nov 2016 15:39:39 +0000 Subject: [PATCH 12/18] Use data:// URI rather than blob: URI to avoid XSS --- src/components/views/messages/MAudioBody.js | 17 ++++---------- src/components/views/messages/MFileBody.js | 18 +++++---------- src/components/views/messages/MImageBody.js | 14 ++++-------- src/components/views/messages/MVideoBody.js | 25 +++------------------ src/utils/DecryptFile.js | 22 +++++++++++++++++- 5 files changed, 37 insertions(+), 59 deletions(-) diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js index 375b2431c3..d20c594f6f 100644 --- a/src/components/views/messages/MAudioBody.js +++ b/src/components/views/messages/MAudioBody.js @@ -49,12 +49,10 @@ export default class MAudioBody extends React.Component { componentDidMount() { var content = this.props.mxEvent.getContent(); if (content.file !== undefined && this.state.decryptedUrl === null) { - decryptFile(content.file).then((blob) => { - if (!this._unmounted) { - this.setState({ - decryptedUrl: window.URL.createObjectURL(blob), - }); - } + decryptFile(content.file).then((url) => { + this.setState({ + decryptedUrl: url + }); }).catch((err) => { console.warn("Unable to decrypt attachment: ", err) // Set a placeholder image when we can't decrypt the image. @@ -63,13 +61,6 @@ export default class MAudioBody extends React.Component { } } - componentWillUnmount() { - this._unmounted = true; - if (this.state.decryptedUrl) { - window.URL.revokeObjectURL(this.state.decryptedUrl); - } - } - render() { var content = this.props.mxEvent.getContent(); diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index 855cdac59c..531c382d9a 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -22,6 +22,7 @@ var MatrixClientPeg = require('../../../MatrixClientPeg'); var sdk = require('../../../index'); var DecryptFile = require('../../../utils/DecryptFile'); + module.exports = React.createClass({ displayName: 'MFileBody', @@ -66,12 +67,10 @@ module.exports = React.createClass({ var content = this.props.mxEvent.getContent(); var self = this; if (content.file !== undefined && this.state.decryptedUrl === null) { - DecryptFile.decryptFile(content.file).then(function(blob) { - if (!self._unmounted) { - self.setState({ - decryptedUrl: window.URL.createObjectURL(blob), - }); - } + DecryptFile.decryptFile(content.file).then(function(url) { + self.setState({ + decryptedUrl: url, + }); }).catch(function (err) { console.warn("Unable to decrypt attachment: ", err) // Set a placeholder image when we can't decrypt the image. @@ -80,13 +79,6 @@ module.exports = React.createClass({ } }, - componentWillUnmount: function() { - this._unmounted = true; - if (this.state.decryptedUrl) { - window.URL.revokeObjectURL(this.state.decryptedUrl); - } - }, - render: function() { var content = this.props.mxEvent.getContent(); diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 2f6f9224ea..e6ad13dc9e 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -112,12 +112,10 @@ module.exports = React.createClass({ var content = this.props.mxEvent.getContent(); var self = this; if (content.file !== undefined && this.state.decryptedUrl === null) { - DecryptFile.decryptFile(content.file).then(function(blob) { - if (!self._unmounted) { - self.setState({ - decryptedUrl: window.URL.createObjectURL(blob), - }); - } + DecryptFile.decryptFile(content.file).then(function(url) { + self.setState({ + decryptedUrl: url, + }); }).catch(function (err) { console.warn("Unable to decrypt attachment: ", err) // Set a placeholder image when we can't decrypt the image. @@ -128,10 +126,6 @@ module.exports = React.createClass({ componentWillUnmount: function() { dis.unregister(this.dispatcherRef); - this._unmounted = true; - if (this.state.decryptedUrl) { - window.URL.revokeObjectURL(this.state.decryptedUrl); - } }, onAction: function(payload) { diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index 6f9a270800..821f10be88 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -88,21 +88,13 @@ module.exports = React.createClass({ content.info.thumbnail_file ); } - thumbnailPromise.then(function(thumbnailBlob) { + thumbnailPromise.then(function(thumbnailUrl) { DecryptFile.decryptFile( content.file - ).then(function(contentBlob) { - if (self._unmounted) { - return; - } - var contentUrl = window.URL.createObjectURL(contentBlob); - var thumbUrl = null; - if (thumbnailBlob) { - thumbUrl = window.URL.createObjectURL(thumbnailBlob); - } + ).then(function(contentUrl) { self.setState({ decryptedUrl: contentUrl, - decryptedThumbnailUrl: thumbUrl, + decryptedThumbnailUrl: thumbnailUrl, }); }); }).catch(function (err) { @@ -113,17 +105,6 @@ module.exports = React.createClass({ } }, - componentWillUnmount: function() { - this._unmounted = true; - if (this.state.decryptedUrl) { - window.URL.revokeObjectURL(this.state.decryptedUrl); - } - if (this.state.decryptedThumbnailUrl) { - window.URL.revokeObjectURL(this.state.decryptedThumbnailUrl); - } - }, - - render: function() { var content = this.props.mxEvent.getContent(); diff --git a/src/utils/DecryptFile.js b/src/utils/DecryptFile.js index 254c807158..06a098c5fd 100644 --- a/src/utils/DecryptFile.js +++ b/src/utils/DecryptFile.js @@ -22,6 +22,26 @@ var encrypt = require("browser-encrypt-attachment"); require("isomorphic-fetch"); // Grab the client so that we can turn mxc:// URLs into https:// URLS. var MatrixClientPeg = require('../MatrixClientPeg'); +var q = require('q'); + + +/** + * Read blob as a data:// URI. + * @return {Promise} A promise that resolves with the data:// URI. + */ + +function readBlobAsDataUri(file) { + var deferred = q.defer(); + var reader = new FileReader(); + reader.onload = function(e) { + deferred.resolve(e.target.result); + }; + reader.onerror = function(e) { + deferred.reject(e); + }; + reader.readAsDataURL(file); + return deferred.promise; +} export function decryptFile(file) { @@ -37,6 +57,6 @@ export function decryptFile(file) { }).then(function(dataArray) { // Turn the array into a Blob and give it the correct MIME-type. var blob = new Blob([dataArray], {type: file.mimetype}); - return blob; + return readBlobAsDataUri(blob); }); } From 1529396ef5c98e0025aa042ea7a4196c7644176d Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 4 Nov 2016 18:09:12 +0000 Subject: [PATCH 13/18] Make everything use MFileBody for downloads, ensure that encrypted attachments are actually downloaded --- src/components/views/messages/MFileBody.js | 36 +++++++++++++++++++-- src/components/views/messages/MImageBody.js | 28 ++-------------- src/components/views/messages/MVideoBody.js | 28 ++-------------- 3 files changed, 37 insertions(+), 55 deletions(-) diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index 531c382d9a..313577054a 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -100,13 +100,43 @@ module.exports = React.createClass({ var contentUrl = this._getContentUrl(); + var fileName = content.body && content.body.length > 0 ? content.body : "Attachment"; + + var downloadAttr = undefined; + if (this.state.decryptedUrl) { + // If the file is encrypted then we MUST download it rather than displaying it + // because Firefox is vunerable to XSS attacks in data:// URLs + // and all browsers are vunerable to XSS attacks in blob: URLs + // created with window.URL.createObjectURL + // See https://bugzilla.mozilla.org/show_bug.cgi?id=255107 + // See https://w3c.github.io/FileAPI/#originOfBlobURL + // + // This is not a problem for unencrypted links because they are + // either fetched from a different domain so are safe because of + // the same-origin policy or they are fetch from the same domain, + // in which case we trust that the homeserver will set a + // Content-Security-Policy that disables script execution. + // It is reasonable to trust the homeserver in that case since + // it is the same domain that controls this javascript. + // + // We can't apply the same workaround for encrypted files because + // we can't supply HTTP headers when the user clicks on a blob: + // or data:// uri. + // + // We should probably provide a download attribute anyway so that + // the file will have the correct name when the user tries to + // download it. We can't provide a Content-Disposition header + // like we would for HTTP. + downloadAttr = fileName; + } + if (contentUrl) { if (this.props.tileShape === "file_grid") { return (
- - { content.body && content.body.length > 0 ? content.body : "Attachment" } + + { fileName }
{ content.info && content.info.size ? filesize(content.info.size) : "" } @@ -119,7 +149,7 @@ module.exports = React.createClass({ return (
- + Download {text} diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index e6ad13dc9e..8df3f23bcd 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -19,7 +19,7 @@ limitations under the License. var React = require('react'); var filesize = require('filesize'); - +var MFileBody = require('./MFileBody'); var MatrixClientPeg = require('../../../MatrixClientPeg'); var ImageUtils = require('../../../ImageUtils'); var Modal = require('../../../Modal'); @@ -175,30 +175,6 @@ module.exports = React.createClass({ var contentUrl = this._getContentUrl(); var thumbUrl = this._getThumbUrl(); - var download; - if (this.props.tileShape === "file_grid") { - download = ( -
- - {content.body} - -
- { content.info && content.info.size ? filesize(content.info.size) : "" } -
-
- ); - } - else { - download = ( - - ); - } - if (thumbUrl) { return ( @@ -208,7 +184,7 @@ module.exports = React.createClass({ onMouseEnter={this.onImageEnter} onMouseLeave={this.onImageLeave} /> - { download } + ); } else if (content.body) { diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index 821f10be88..4744a95026 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -19,6 +19,7 @@ limitations under the License. var React = require('react'); var filesize = require('filesize'); +import MFileBody from './MFileBody'; var MatrixClientPeg = require('../../../MatrixClientPeg'); var Modal = require('../../../Modal'); @@ -140,38 +141,13 @@ module.exports = React.createClass({ } } - var download; - if (this.props.tileShape === "file_grid") { - download = ( -
- - {content.body} - -
- { content.info && content.info.size ? filesize(content.info.size) : "" } -
-
- ); - } - else { - var TintableSvg = sdk.getComponent("elements.TintableSvg"); - download = ( - - ); - } - return ( - { download } + ); }, From 4214c67f8f86f3721e7c70462ff4f10df4e9360e Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 4 Nov 2016 18:20:20 +0000 Subject: [PATCH 14/18] Pass the decryptedUrl to the MFileBody so that it doesn't need to decrypt it itself. --- src/components/views/messages/MAudioBody.js | 2 +- src/components/views/messages/MFileBody.js | 2 +- src/components/views/messages/MImageBody.js | 5 +++-- src/components/views/messages/MVideoBody.js | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js index d20c594f6f..34dcb9ba7a 100644 --- a/src/components/views/messages/MAudioBody.js +++ b/src/components/views/messages/MAudioBody.js @@ -81,7 +81,7 @@ export default class MAudioBody extends React.Component { return ( ); } diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index 313577054a..1dd3414917 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -28,7 +28,7 @@ module.exports = React.createClass({ getInitialState: function() { return { - decryptedUrl: null, + decryptedUrl: (this.props.decryptedUrl ? this.props.decryptedUrl : null), }; }, diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 8df3f23bcd..e24a6b1081 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -19,7 +19,8 @@ limitations under the License. var React = require('react'); var filesize = require('filesize'); -var MFileBody = require('./MFileBody'); +import MFileBody from './MFileBody'; + var MatrixClientPeg = require('../../../MatrixClientPeg'); var ImageUtils = require('../../../ImageUtils'); var Modal = require('../../../Modal'); @@ -184,7 +185,7 @@ module.exports = React.createClass({ onMouseEnter={this.onImageEnter} onMouseLeave={this.onImageLeave} /> - + ); } else if (content.body) { diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index 4744a95026..d1797339a4 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -147,7 +147,7 @@ module.exports = React.createClass({ controls preload={preload} autoPlay={false} height={height} width={width} poster={poster}> - + ); }, From dc679a8eca7e650acef0522e3066a1968fb7d6ff Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 4 Nov 2016 18:40:57 +0000 Subject: [PATCH 15/18] Fix gif hoverover --- src/components/views/messages/MImageBody.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index e24a6b1081..bfdce1eaba 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -75,9 +75,7 @@ module.exports = React.createClass({ return; } var imgElement = e.target; - imgElement.src = MatrixClientPeg.get().mxcUrlToHttp( - this.props.mxEvent.getContent().url - ); + imgElement.src = this._getContentUrl(); }, onImageLeave: function(e) { From 911f9e4e63ceda7f90712916f907a2c88882ebfd Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Tue, 8 Nov 2016 11:42:20 +0000 Subject: [PATCH 16/18] Review comments --- src/ContentMessages.js | 4 +-- src/components/views/messages/MAudioBody.js | 10 +++--- src/components/views/messages/MFileBody.js | 21 ++++++------ src/components/views/messages/MImageBody.js | 32 ++++++++---------- src/components/views/messages/MVideoBody.js | 37 +++++++++------------ src/utils/DecryptFile.js | 27 +++++++++------ 6 files changed, 63 insertions(+), 68 deletions(-) diff --git a/src/ContentMessages.js b/src/ContentMessages.js index 9059874f81..28c28e875e 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -89,8 +89,8 @@ function infoForVideoFile(videoFile) { * is read. */ function readFileAsArrayBuffer(file) { - var deferred = q.defer(); - var reader = new FileReader(); + const deferred = q.defer(); + const reader = new FileReader(); reader.onload = function(e) { deferred.resolve(e.target.result); }; diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js index 34dcb9ba7a..ff753621c7 100644 --- a/src/components/views/messages/MAudioBody.js +++ b/src/components/views/messages/MAudioBody.js @@ -38,7 +38,7 @@ export default class MAudioBody extends React.Component { } _getContentUrl() { - var content = this.props.mxEvent.getContent(); + const content = this.props.mxEvent.getContent(); if (content.file !== undefined) { return this.state.decryptedUrl; } else { @@ -49,11 +49,11 @@ export default class MAudioBody extends React.Component { componentDidMount() { var content = this.props.mxEvent.getContent(); if (content.file !== undefined && this.state.decryptedUrl === null) { - decryptFile(content.file).then((url) => { + decryptFile(content.file).done((url) => { this.setState({ decryptedUrl: url }); - }).catch((err) => { + }, (err) => { console.warn("Unable to decrypt attachment: ", err) // Set a placeholder image when we can't decrypt the image. this.refs.image.src = "img/warning.svg"; @@ -62,7 +62,7 @@ export default class MAudioBody extends React.Component { } render() { - var content = this.props.mxEvent.getContent(); + const content = this.props.mxEvent.getContent(); if (content.file !== undefined && this.state.decryptedUrl === null) { // Need to decrypt the attachment @@ -76,7 +76,7 @@ export default class MAudioBody extends React.Component { ); } - var contentUrl = this._getContentUrl(); + const contentUrl = this._getContentUrl(); return ( diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index 1dd3414917..a1512738fd 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -55,7 +55,7 @@ module.exports = React.createClass({ }, _getContentUrl: function() { - var content = this.props.mxEvent.getContent(); + const content = this.props.mxEvent.getContent(); if (content.file !== undefined) { return this.state.decryptedUrl; } else { @@ -64,25 +64,24 @@ module.exports = React.createClass({ }, componentDidMount: function() { - var content = this.props.mxEvent.getContent(); - var self = this; + const content = this.props.mxEvent.getContent(); if (content.file !== undefined && this.state.decryptedUrl === null) { - DecryptFile.decryptFile(content.file).then(function(url) { - self.setState({ + DecryptFile.decryptFile(content.file).done((url) => { + this.setState({ decryptedUrl: url, }); - }).catch(function (err) { + }, (err) => { console.warn("Unable to decrypt attachment: ", err) // Set a placeholder image when we can't decrypt the image. - self.refs.image.src = "img/warning.svg"; + this.refs.image.src = "img/warning.svg"; }); } }, render: function() { - var content = this.props.mxEvent.getContent(); + const content = this.props.mxEvent.getContent(); - var text = this.presentableTextForFile(content); + const text = this.presentableTextForFile(content); var TintableSvg = sdk.getComponent("elements.TintableSvg"); if (content.file !== undefined && this.state.decryptedUrl === null) { @@ -98,9 +97,9 @@ module.exports = React.createClass({ ); } - var contentUrl = this._getContentUrl(); + const contentUrl = this._getContentUrl(); - var fileName = content.body && content.body.length > 0 ? content.body : "Attachment"; + const fileName = content.body && content.body.length > 0 ? content.body : "Attachment"; var downloadAttr = undefined; if (this.state.decryptedUrl) { diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index bfdce1eaba..6784cede7a 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -16,17 +16,14 @@ limitations under the License. 'use strict'; -var React = require('react'); -var filesize = require('filesize'); - +import React from 'react'; import MFileBody from './MFileBody'; - -var MatrixClientPeg = require('../../../MatrixClientPeg'); -var ImageUtils = require('../../../ImageUtils'); -var Modal = require('../../../Modal'); -var sdk = require('../../../index'); -var dis = require("../../../dispatcher"); -var DecryptFile = require('../../../utils/DecryptFile'); +import MatrixClientPeg from '../../../MatrixClientPeg'; +import ImageUtils from '../../../ImageUtils'; +import Modal from '../../../Modal'; +import sdk from '../../../index'; +import dis from '../../../dispatcher'; +import DecryptFile from '../../../utils/DecryptFile'; module.exports = React.createClass({ @@ -87,7 +84,7 @@ module.exports = React.createClass({ }, _getContentUrl: function() { - var content = this.props.mxEvent.getContent(); + const content = this.props.mxEvent.getContent(); if (content.file !== undefined) { return this.state.decryptedUrl; } else { @@ -96,7 +93,7 @@ module.exports = React.createClass({ }, _getThumbUrl: function() { - var content = this.props.mxEvent.getContent(); + const content = this.props.mxEvent.getContent(); if (content.file !== undefined) { // TODO: Decrypt and use the thumbnail file if one is present. return this.state.decryptedUrl; @@ -108,17 +105,16 @@ module.exports = React.createClass({ componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); this.fixupHeight(); - var content = this.props.mxEvent.getContent(); - var self = this; + const content = this.props.mxEvent.getContent(); if (content.file !== undefined && this.state.decryptedUrl === null) { - DecryptFile.decryptFile(content.file).then(function(url) { - self.setState({ + DecryptFile.decryptFile(content.file).done((url) => { + this.setState({ decryptedUrl: url, }); - }).catch(function (err) { + }, (err) => { console.warn("Unable to decrypt attachment: ", err) // Set a placeholder image when we can't decrypt the image. - self.refs.image.src = "img/warning.svg"; + this.refs.image.src = "img/warning.svg"; }); } }, diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index d1797339a4..4e60e17e40 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -16,15 +16,12 @@ limitations under the License. 'use strict'; -var React = require('react'); -var filesize = require('filesize'); - +import React from require('react'); import MFileBody from './MFileBody'; - -var MatrixClientPeg = require('../../../MatrixClientPeg'); -var Modal = require('../../../Modal'); -var sdk = require('../../../index'); -var DecryptFile = require("../../../utils/DecryptFile") +import MatrixClientPeg from '../../../MatrixClientPeg'; +import Model from '../../../Modal'; +import sdk from '../../../index'; +import DecryptFile from '../../../utils/DecryptFile'; module.exports = React.createClass({ displayName: 'MVideoBody', @@ -59,7 +56,7 @@ module.exports = React.createClass({ }, _getContentUrl: function() { - var content = this.props.mxEvent.getContent(); + const content = this.props.mxEvent.getContent(); if (content.file !== undefined) { return this.state.decryptedUrl; } else { @@ -68,7 +65,7 @@ module.exports = React.createClass({ }, _getThumbUrl: function() { - var content = this.props.mxEvent.getContent(); + const content = this.props.mxEvent.getContent(); if (content.file !== undefined) { return this.state.decryptedThumbnailUrl; } else if (content.info.thumbnail_url) { @@ -79,9 +76,7 @@ module.exports = React.createClass({ }, componentDidMount: function() { - var content = this.props.mxEvent.getContent(); - var self = this; - + const content = this.props.mxEvent.getContent(); if (content.file !== undefined && this.state.decryptedUrl === null) { var thumbnailPromise = Promise.resolve(null); if (content.info.thumbnail_file) { @@ -89,25 +84,25 @@ module.exports = React.createClass({ content.info.thumbnail_file ); } - thumbnailPromise.then(function(thumbnailUrl) { + thumbnailPromise.done((thumbnailUrl) => { DecryptFile.decryptFile( content.file ).then(function(contentUrl) { - self.setState({ + this.setState({ decryptedUrl: contentUrl, decryptedThumbnailUrl: thumbnailUrl, }); }); - }).catch(function (err) { + }, (err) => { console.warn("Unable to decrypt attachment: ", err) // Set a placeholder image when we can't decrypt the image. - self.refs.image.src = "img/warning.svg"; + this.refs.image.src = "img/warning.svg"; }); } }, render: function() { - var content = this.props.mxEvent.getContent(); + const content = this.props.mxEvent.getContent(); if (content.file !== undefined && this.state.decryptedUrl === null) { // Need to decrypt the attachment @@ -121,15 +116,15 @@ module.exports = React.createClass({ ); } - var contentUrl = this._getContentUrl(); - var thumbUrl = this._getThumbUrl(); + const contentUrl = this._getContentUrl(); + const thumbUrl = this._getThumbUrl(); var height = null; var width = null; var poster = null; var preload = "metadata"; if (content.info) { - var scale = this.thumbScale(content.info.w, content.info.h, 480, 360); + const scale = this.thumbScale(content.info.w, content.info.h, 480, 360); if (scale) { width = Math.floor(content.info.w * scale); height = Math.floor(content.info.h * scale); diff --git a/src/utils/DecryptFile.js b/src/utils/DecryptFile.js index 06a098c5fd..ca7cf33584 100644 --- a/src/utils/DecryptFile.js +++ b/src/utils/DecryptFile.js @@ -14,22 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use struct'; - // Pull in the encryption lib so that we can decrypt attachments. -var encrypt = require("browser-encrypt-attachment"); +import encrypt from 'browser-encrypt-attachment'; // Pull in a fetch polyfill so we can download encrypted attachments. -require("isomorphic-fetch"); +import 'isomorphic-fetch'; // Grab the client so that we can turn mxc:// URLs into https:// URLS. -var MatrixClientPeg = require('../MatrixClientPeg'); -var q = require('q'); +import MatrixClientPeg from '../MatrixClientPeg'; +import q from 'q'; /** * Read blob as a data:// URI. * @return {Promise} A promise that resolves with the data:// URI. */ - function readBlobAsDataUri(file) { var deferred = q.defer(); var reader = new FileReader(); @@ -44,13 +41,21 @@ function readBlobAsDataUri(file) { } +/** + * Decrypt a file attached to a matrix event. + * @param file {Object} The json taken from the matrix event. + * This passed to [link]{@link https://github.com/matrix-org/browser-encrypt-attachments} + * as the encryption info object, so will also have the those keys in addition to + * the keys below. + * @param file.url {string} An mxc:// URL for the encrypted file. + * @param file.mimetype {string} The MIME-type of the plaintext file. + */ export function decryptFile(file) { - var url = MatrixClientPeg.get().mxcUrlToHttp(file.url); - var self = this; + const url = MatrixClientPeg.get().mxcUrlToHttp(file.url); // Download the encrypted file as an array buffer. - return fetch(url).then(function (response) { + return fetch(url).then(function(response) { return response.arrayBuffer(); - }).then(function (responseData) { + }).then(function(responseData) { // Decrypt the array buffer using the information taken from // the event content. return encrypt.decryptAttachment(responseData, file); From 93ddb5539eca91431ac8135e554c1f984e6ecd98 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Tue, 8 Nov 2016 12:57:24 +0000 Subject: [PATCH 17/18] Make imports more consistent, fix m.video --- src/components/views/messages/MFileBody.js | 12 ++++---- src/components/views/messages/MImageBody.js | 31 ++++++++++----------- src/components/views/messages/MVideoBody.js | 16 +++++------ 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index a1512738fd..e8c97e5f44 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -16,11 +16,11 @@ limitations under the License. 'use strict'; -var React = require('react'); -var filesize = require('filesize'); -var MatrixClientPeg = require('../../../MatrixClientPeg'); -var sdk = require('../../../index'); -var DecryptFile = require('../../../utils/DecryptFile'); +import React from 'react'; +import filesize from 'filesize'; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import sdk from '../../../index'; +import {decryptFile} from '../../../utils/DecryptFile'; module.exports = React.createClass({ @@ -66,7 +66,7 @@ module.exports = React.createClass({ componentDidMount: function() { const content = this.props.mxEvent.getContent(); if (content.file !== undefined && this.state.decryptedUrl === null) { - DecryptFile.decryptFile(content.file).done((url) => { + decryptFile(content.file).done((url) => { this.setState({ decryptedUrl: url, }); diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 6784cede7a..4a3cfce591 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -23,7 +23,7 @@ import ImageUtils from '../../../ImageUtils'; import Modal from '../../../Modal'; import sdk from '../../../index'; import dis from '../../../dispatcher'; -import DecryptFile from '../../../utils/DecryptFile'; +import {decryptFile} from '../../../utils/DecryptFile'; module.exports = React.createClass({ @@ -44,10 +44,10 @@ module.exports = React.createClass({ onClick: function onClick(ev) { if (ev.button == 0 && !ev.metaKey) { ev.preventDefault(); - var content = this.props.mxEvent.getContent(); - var httpUrl = this._getContentUrl(); - var ImageView = sdk.getComponent("elements.ImageView"); - var params = { + const content = this.props.mxEvent.getContent(); + const httpUrl = this._getContentUrl(); + const ImageView = sdk.getComponent("elements.ImageView"); + const params = { src: httpUrl, mxEvent: this.props.mxEvent }; @@ -63,7 +63,7 @@ module.exports = React.createClass({ }, _isGif: function() { - var content = this.props.mxEvent.getContent(); + const content = this.props.mxEvent.getContent(); return (content && content.info && content.info.mimetype === "image/gif"); }, @@ -107,7 +107,7 @@ module.exports = React.createClass({ this.fixupHeight(); const content = this.props.mxEvent.getContent(); if (content.file !== undefined && this.state.decryptedUrl === null) { - DecryptFile.decryptFile(content.file).done((url) => { + decryptFile(content.file).done((url) => { this.setState({ decryptedUrl: url, }); @@ -135,14 +135,13 @@ module.exports = React.createClass({ return; } - var content = this.props.mxEvent.getContent(); - - var thumbHeight = null; - var timelineWidth = this.refs.body.offsetWidth; - var maxHeight = 600; // let images take up as much width as they can so long as the height doesn't exceed 600px. + const content = this.props.mxEvent.getContent(); + const timelineWidth = this.refs.body.offsetWidth; + const maxHeight = 600; // let images take up as much width as they can so long as the height doesn't exceed 600px. // the alternative here would be 600*timelineWidth/800; to scale them down to fit inside a 4:3 bounding box //console.log("trying to fit image into timelineWidth of " + this.refs.body.offsetWidth + " or " + this.refs.body.clientWidth); + var thumbHeight = null; if (content.info) { thumbHeight = ImageUtils.thumbHeight(content.info.w, content.info.h, timelineWidth, maxHeight); } @@ -151,8 +150,8 @@ module.exports = React.createClass({ }, render: function() { - var TintableSvg = sdk.getComponent("elements.TintableSvg"); - var content = this.props.mxEvent.getContent(); + const TintableSvg = sdk.getComponent("elements.TintableSvg"); + const content = this.props.mxEvent.getContent(); if (content.file !== undefined && this.state.decryptedUrl === null) { @@ -167,8 +166,8 @@ module.exports = React.createClass({ ); } - var contentUrl = this._getContentUrl(); - var thumbUrl = this._getThumbUrl(); + const contentUrl = this._getContentUrl(); + const thumbUrl = this._getThumbUrl(); if (thumbUrl) { return ( diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index 4e60e17e40..c5fc0f9802 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -21,7 +21,7 @@ import MFileBody from './MFileBody'; import MatrixClientPeg from '../../../MatrixClientPeg'; import Model from '../../../Modal'; import sdk from '../../../index'; -import DecryptFile from '../../../utils/DecryptFile'; +import {decryptFile} from '../../../utils/DecryptFile'; module.exports = React.createClass({ displayName: 'MVideoBody', @@ -80,19 +80,19 @@ module.exports = React.createClass({ if (content.file !== undefined && this.state.decryptedUrl === null) { var thumbnailPromise = Promise.resolve(null); if (content.info.thumbnail_file) { - thumbnailPromise = DecryptFile.decryptFile( + thumbnailPromise = decryptFile( content.info.thumbnail_file ); } - thumbnailPromise.done((thumbnailUrl) => { - DecryptFile.decryptFile( - content.file - ).then(function(contentUrl) { - this.setState({ + thumbnailPromise.then(function (thumbnailUrl) { + decryptFile(content.file).then(function(contentUrl) { + return { decryptedUrl: contentUrl, decryptedThumbnailUrl: thumbnailUrl, - }); + }; }); + }).done((state) => { + this.setState(result); }, (err) => { console.warn("Unable to decrypt attachment: ", err) // Set a placeholder image when we can't decrypt the image. From c838164f4022e5cbb829390537b8206fe6e9240a Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Tue, 8 Nov 2016 16:26:25 +0000 Subject: [PATCH 18/18] Make the promises be q promises --- src/components/views/messages/MVideoBody.js | 21 ++++++++++----------- src/utils/DecryptFile.js | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index c5fc0f9802..a32348ea1a 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -16,12 +16,13 @@ limitations under the License. 'use strict'; -import React from require('react'); +import React from 'react'; import MFileBody from './MFileBody'; import MatrixClientPeg from '../../../MatrixClientPeg'; import Model from '../../../Modal'; import sdk from '../../../index'; -import {decryptFile} from '../../../utils/DecryptFile'; +import { decryptFile } from '../../../utils/DecryptFile'; +import q from 'q'; module.exports = React.createClass({ displayName: 'MVideoBody', @@ -78,26 +79,24 @@ module.exports = React.createClass({ componentDidMount: function() { const content = this.props.mxEvent.getContent(); if (content.file !== undefined && this.state.decryptedUrl === null) { - var thumbnailPromise = Promise.resolve(null); + var thumbnailPromise = q(null); if (content.info.thumbnail_file) { thumbnailPromise = decryptFile( content.info.thumbnail_file ); } - thumbnailPromise.then(function (thumbnailUrl) { - decryptFile(content.file).then(function(contentUrl) { - return { + thumbnailPromise.then((thumbnailUrl) => { + decryptFile(content.file).then((contentUrl) => { + this.setState({ decryptedUrl: contentUrl, decryptedThumbnailUrl: thumbnailUrl, - }; + }); }); - }).done((state) => { - this.setState(result); - }, (err) => { + }).catch((err) => { console.warn("Unable to decrypt attachment: ", err) // Set a placeholder image when we can't decrypt the image. this.refs.image.src = "img/warning.svg"; - }); + }).done(); } }, diff --git a/src/utils/DecryptFile.js b/src/utils/DecryptFile.js index ca7cf33584..38eab1d073 100644 --- a/src/utils/DecryptFile.js +++ b/src/utils/DecryptFile.js @@ -53,7 +53,7 @@ function readBlobAsDataUri(file) { export function decryptFile(file) { const url = MatrixClientPeg.get().mxcUrlToHttp(file.url); // Download the encrypted file as an array buffer. - return fetch(url).then(function(response) { + return q(fetch(url)).then(function(response) { return response.arrayBuffer(); }).then(function(responseData) { // Decrypt the array buffer using the information taken from