From 49b3aec1e2b369e78b6b63964d2d31575291c408 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 4 Nov 2016 11:52:47 +0000 Subject: [PATCH 01/15] Include the mimetype with the file info. Store the objectURL in state so that it can be used normally by the exising templates Conflicts: src/ContentMessages.js src/components/views/messages/MImageBody.js --- src/ContentMessages.js | 14 +++- src/components/views/messages/MImageBody.js | 84 +++++++++++++++++++-- 2 files changed, 91 insertions(+), 7 deletions(-) diff --git a/src/ContentMessages.js b/src/ContentMessages.js index fd18b22d30..d20266de50 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -149,7 +149,19 @@ 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; + if (file.type) { + encryptInfo.mimetype = file.type; + } + 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..0e4be9a83f 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -33,11 +33,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, @@ -77,18 +84,68 @@ 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() { this.dispatcherRef = dis.register(this.onAction); this.fixupHeight(); + 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. + 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"; + }); + } + }, + + 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; + if (this.state.decryptedUrl) { + window.URL.revokeObjectURL(this.state.decryptedUrl); + } }, onAction: function(payload) { @@ -123,11 +180,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}
@@ -139,7 +212,7 @@ module.exports = React.createClass({ else { download = (
- + Download {content.body} ({ content.info && content.info.size ? filesize(content.info.size) : "Unknown size" }) @@ -147,11 +220,10 @@ module.exports = React.createClass({ ); } - var thumbUrl = this._getThumbUrl(); if (thumbUrl) { return ( - + {content.body} Date: Fri, 4 Nov 2016 12:46:45 +0000 Subject: [PATCH 02/15] Move decryptFile into a utility function so that it can be shared between different components Conflicts: src/components/views/messages/MImageBody.js --- src/components/views/messages/MImageBody.js | 32 ++++------------ src/utils/DecryptFile.js | 42 +++++++++++++++++++++ 2 files changed, 50 insertions(+), 24 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 0e4be9a83f..54fea699d2 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -18,12 +18,12 @@ limitations under the License. var React = require('react'); var filesize = require('filesize'); - 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({ displayName: 'MImageBody', @@ -110,7 +110,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"; @@ -118,28 +124,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 95e8889857d0be1ecf062ee6c2159a164f60e7b7 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 4 Nov 2016 13:05:34 +0000 Subject: [PATCH 03/15] 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 54fea699d2..8d5f3ed756 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -162,7 +162,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 b56417f46e8a8533d182148eb71d2dc721d8e78d Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 4 Nov 2016 13:08:17 +0000 Subject: [PATCH 04/15] 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 8d5f3ed756..56da243950 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -109,7 +109,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 48340a28176c9856c555b0dacb805c3d7ea23dc3 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 4 Nov 2016 13:13:25 +0000 Subject: [PATCH 05/15] 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 4e01a4f6925c21149c53953eb6016d7c566fb2e5 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 4 Nov 2016 13:45:00 +0000 Subject: [PATCH 06/15] 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 b69e88d4e3247e340ddd2540df5e9ac5a4baf908 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 4 Nov 2016 14:00:26 +0000 Subject: [PATCH 07/15] 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 ee1768f6443f7c6deb84e3ea365dc7bd4f20ca53 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 4 Nov 2016 15:39:39 +0000 Subject: [PATCH 08/15] 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 56da243950..c804b44d3e 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -109,12 +109,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. @@ -125,10 +123,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 736ab743ee96f99d1ec7506349e4429eda5dbf50 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 4 Nov 2016 18:09:12 +0000 Subject: [PATCH 09/15] Make everything use MFileBody for downloads, ensure that encrypted attachments are actually downloaded Conflicts: src/components/views/messages/MImageBody.js --- src/components/views/messages/MFileBody.js | 36 +++++++++++++++++++-- src/components/views/messages/MImageBody.js | 27 ++-------------- src/components/views/messages/MVideoBody.js | 28 ++-------------- 3 files changed, 37 insertions(+), 54 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 c804b44d3e..afafbedcb9 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -18,6 +18,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'); @@ -172,30 +173,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 ( @@ -205,7 +182,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 de384480c689b71dc6522a802d997f3258eda123 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 4 Nov 2016 18:20:20 +0000 Subject: [PATCH 10/15] Pass the decryptedUrl to the MFileBody so that it doesn't need to decrypt it itself. Conflicts: src/components/views/messages/MImageBody.js --- src/components/views/messages/MAudioBody.js | 2 +- src/components/views/messages/MFileBody.js | 2 +- src/components/views/messages/MImageBody.js | 6 ++++-- src/components/views/messages/MVideoBody.js | 2 +- 4 files changed, 7 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 afafbedcb9..bdb0ec6ee8 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -18,7 +18,9 @@ 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'); @@ -182,7 +184,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 734c4eb6387d825bd7579fc3d67e75dff00fde65 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 4 Nov 2016 18:40:57 +0000 Subject: [PATCH 11/15] 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 bdb0ec6ee8..a75d27d57d 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -74,9 +74,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 bf5ecbd016a8c5a575ed21b7181e3058f865c99b Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Tue, 8 Nov 2016 11:42:20 +0000 Subject: [PATCH 12/15] Review comments Conflicts: src/ContentMessages.js --- src/ContentMessages.js | 18 ++++++++++ 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, 79 insertions(+), 66 deletions(-) diff --git a/src/ContentMessages.js b/src/ContentMessages.js index d20266de50..8665c40ae0 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -81,6 +81,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) { + const deferred = q.defer(); + const 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() { 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 a75d27d57d..63928e9518 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({ displayName: 'MImageBody', @@ -86,7 +83,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 { @@ -95,7 +92,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; @@ -107,17 +104,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 9edfea3b321185e938ee9733ebde8ecf2569ef0f Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Tue, 8 Nov 2016 12:57:24 +0000 Subject: [PATCH 13/15] 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 63928e9518..4a5dbab51e 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({ displayName: 'MImageBody', @@ -43,10 +43,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 }; @@ -62,7 +62,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"); }, @@ -106,7 +106,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, }); @@ -134,14 +134,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); } @@ -150,8 +149,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) { @@ -166,8 +165,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 72d24f58d0e074d2cf74f646c64c5ce80bf1ad8b Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Tue, 8 Nov 2016 16:26:25 +0000 Subject: [PATCH 14/15] 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 From 0bfcb38879fbd9dab7e5a521858fb736b5b3d607 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 11 Nov 2016 14:24:39 +0000 Subject: [PATCH 15/15] Add dependency on browser-encrypted-attachment --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index a6fce492e4..e128d09911 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ }, "dependencies": { "babel-runtime": "^6.11.6", + "browser-encrypt-attachment": "^0.1.0", "browser-request": "^0.3.3", "classnames": "^2.1.2", "draft-js": "^0.8.1",