diff --git a/components/post_view/message_attachments/message_attachment/index.js b/components/post_view/message_attachments/message_attachment/index.js index 35be6746aecd..4a9845970fb0 100644 --- a/components/post_view/message_attachments/message_attachment/index.js +++ b/components/post_view/message_attachments/message_attachment/index.js @@ -5,9 +5,16 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; import {doPostAction} from 'mattermost-redux/actions/posts'; +import {getConfig} from 'mattermost-redux/selectors/entities/general'; import MessageAttachment from './message_attachment'; +function mapStateToProps(state) { + return { + hasImageProxy: getConfig(state).HasImageProxy === 'true', + }; +} + function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ @@ -16,4 +23,4 @@ function mapDispatchToProps(dispatch) { }; } -export default connect(null, mapDispatchToProps)(MessageAttachment); +export default connect(mapStateToProps, mapDispatchToProps)(MessageAttachment); diff --git a/components/post_view/message_attachments/message_attachment/message_attachment.jsx b/components/post_view/message_attachments/message_attachment/message_attachment.jsx index 81a4e8b6f87c..52f4310410f3 100644 --- a/components/post_view/message_attachments/message_attachment/message_attachment.jsx +++ b/components/post_view/message_attachments/message_attachment/message_attachment.jsx @@ -6,7 +6,8 @@ import React from 'react'; import {postListScrollChange} from 'actions/global_actions'; -import {isUrlSafe} from 'utils/url.jsx'; +import {getImageSrc} from 'utils/post_utils'; +import {isUrlSafe} from 'utils/url'; import {handleFormattedTextClick} from 'utils/utils'; import Markdown from 'components/markdown'; @@ -36,6 +37,11 @@ export default class MessageAttachment extends React.PureComponent { */ options: PropTypes.object, + /** + * Whether or not the server has an image proxy enabled + */ + hasImageProxy: PropTypes.bool.isRequired, + /** * images object for dimensions */ @@ -254,7 +260,7 @@ export default class MessageAttachment extends React.PureComponent { author.push( @@ -343,12 +349,10 @@ export default class MessageAttachment extends React.PureComponent { let thumb; if (attachment.thumb_url) { thumb = ( -
+
diff --git a/components/post_view/message_attachments/message_attachment/message_attachment.test.jsx b/components/post_view/message_attachments/message_attachment/message_attachment.test.jsx index 4ea3c64ba0bb..6f9889fcc8fa 100644 --- a/components/post_view/message_attachments/message_attachment/message_attachment.test.jsx +++ b/components/post_view/message_attachments/message_attachment/message_attachment.test.jsx @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import React from 'react'; -import {shallow} from 'enzyme'; +import {mount, shallow} from 'enzyme'; import MessageAttachment from 'components/post_view/message_attachments/message_attachment/message_attachment.jsx'; import {postListScrollChange} from 'actions/global_actions'; @@ -29,6 +29,7 @@ describe('components/post_view/MessageAttachment', () => { postId: 'post_id', attachment, actions: {doPostAction: jest.fn()}, + hasImageProxy: false, imagesMetadata: { image_url: { height: 200, @@ -118,4 +119,22 @@ describe('components/post_view/MessageAttachment', () => { wrapper = shallow(); expect(wrapper.instance().getFieldsTable()).toMatchSnapshot(); }); + + test('should proxy external images if image proxy is enabled', () => { + const props = { + ...baseProps, + attachment: { + author_icon: 'http://example.com/author.png', + image_url: 'http://example.com/image.png', + thumb_url: 'http://example.com/thumb.png', + }, + hasImageProxy: true, + }; + + const wrapper = mount(); + + expect(wrapper.find('.attachment__author-icon').prop('src')).toMatch(`/api/v4/image?url=${encodeURIComponent(props.attachment.author_icon)}`); + expect(wrapper.find('.attachment__image-container img').prop('src')).toMatch(`/api/v4/image?url=${encodeURIComponent(props.attachment.image_url)}`); + expect(wrapper.find('.attachment__thumb-container img').prop('src')).toMatch(`/api/v4/image?url=${encodeURIComponent(props.attachment.thumb_url)}`); + }); }); diff --git a/components/post_view/post_attachment_opengraph/index.js b/components/post_view/post_attachment_opengraph/index.js index df46fb454161..a3bbb84ae67d 100644 --- a/components/post_view/post_attachment_opengraph/index.js +++ b/components/post_view/post_attachment_opengraph/index.js @@ -4,6 +4,7 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; import {getOpenGraphMetadata} from 'mattermost-redux/actions/posts'; +import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {getOpenGraphMetadataForUrl} from 'mattermost-redux/selectors/entities/posts'; import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; @@ -14,6 +15,7 @@ import PostAttachmentOpenGraph from './post_attachment_opengraph.jsx'; function mapStateToProps(state, ownProps) { return { currentUser: getCurrentUser(state), + hasImageProxy: getConfig(state).HasImageProxy === 'true', openGraphData: getOpenGraphMetadataForUrl(state, ownProps.link), }; } diff --git a/components/post_view/post_attachment_opengraph/post_attachment_opengraph.jsx b/components/post_view/post_attachment_opengraph/post_attachment_opengraph.jsx index 38c507467f7a..8c0030982988 100644 --- a/components/post_view/post_attachment_opengraph/post_attachment_opengraph.jsx +++ b/components/post_view/post_attachment_opengraph/post_attachment_opengraph.jsx @@ -9,7 +9,7 @@ import * as CommonUtils from 'utils/commons.jsx'; import {PostTypes} from 'utils/constants.jsx'; import {useSafeUrl} from 'utils/url'; import * as Utils from 'utils/utils.jsx'; -import {isSystemMessage} from 'utils/post_utils.jsx'; +import {getImageSrc, isSystemMessage} from 'utils/post_utils.jsx'; import {getFileDimensionsForDisplay} from 'utils/file_utils'; const MAX_DIMENSIONS_LARGE_IMAGE = { @@ -50,6 +50,11 @@ export default class PostAttachmentOpenGraph extends React.PureComponent { */ openGraphData: PropTypes.object, + /** + * Whether or not the server has an image proxy enabled + */ + hasImageProxy: PropTypes.bool.isRequired, + isEmbedVisible: PropTypes.bool, toggleEmbedVisibility: PropTypes.func.isRequired, @@ -67,7 +72,7 @@ export default class PostAttachmentOpenGraph extends React.PureComponent { super(props); const removePreview = this.isRemovePreview(props.post, props.currentUser); - const imageUrl = this.getBestImageUrl(props.openGraphData); + const imageUrl = PostAttachmentOpenGraph.getBestImageUrl(props.openGraphData, props.hasImageProxy); const {metadata} = props.post; const hasLargeImage = metadata && metadata.images && metadata.images[imageUrl] && imageUrl ? this.hasLargeImage(metadata.images[imageUrl]) : false; @@ -98,7 +103,7 @@ export default class PostAttachmentOpenGraph extends React.PureComponent { } if (!Utils.areObjectsEqual(nextProps.openGraphData, this.props.openGraphData)) { - const imageUrl = this.getBestImageUrl(nextProps.openGraphData); + const imageUrl = PostAttachmentOpenGraph.getBestImageUrl(nextProps.openGraphData, nextProps.hasImageProxy); const {metadata} = nextProps.post; const hasLargeImage = metadata && metadata.images && metadata.images[imageUrl] && imageUrl ? this.hasLargeImage(metadata.images[imageUrl]) : false; @@ -129,15 +134,6 @@ export default class PostAttachmentOpenGraph extends React.PureComponent { } } - getBestImageUrl(data) { - if (!data || Utils.isEmptyObject(data.images)) { - return null; - } - - const bestImage = CommonUtils.getNearestPoint(DIMENSIONS_NEAREST_POINT_IMAGE, data.images, 'width', 'height'); - return bestImage.secure_url || bestImage.url; - } - hasLargeImage({height, width}) { let hasLargeImage; const largeImageMinRatio = 16 / 9; @@ -355,4 +351,14 @@ export default class PostAttachmentOpenGraph extends React.PureComponent {
); } + + static getBestImageUrl(data, hasImageProxy) { + if (!data || !data.images || data.images.length === 0) { + return null; + } + + const bestImage = CommonUtils.getNearestPoint(DIMENSIONS_NEAREST_POINT_IMAGE, data.images, 'width', 'height'); + + return getImageSrc(bestImage.secure_url || bestImage.url, hasImageProxy); + } } diff --git a/components/post_view/post_attachment_opengraph/post_attachment_opengraph.test.jsx b/components/post_view/post_attachment_opengraph/post_attachment_opengraph.test.jsx new file mode 100644 index 000000000000..f0692a58dbb8 --- /dev/null +++ b/components/post_view/post_attachment_opengraph/post_attachment_opengraph.test.jsx @@ -0,0 +1,72 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Client4} from 'mattermost-redux/client'; + +import PostAttachmentOpenGraph from './post_attachment_opengraph'; + +describe('PostAttachmentOpenGraph', () => { + describe('getBestImageUrl', () => { + test('should return nothing with no OpenGraph metadata', () => { + const data = null; + + expect(PostAttachmentOpenGraph.getBestImageUrl(data, false)).toEqual(null); + }); + + test('should return nothing with missing OpenGraph images', () => { + const data = {}; + + expect(PostAttachmentOpenGraph.getBestImageUrl(data, false)).toEqual(null); + }); + + test('should return nothing with no OpenGraph images', () => { + const data = { + images: [], + }; + + expect(PostAttachmentOpenGraph.getBestImageUrl(data, false)).toEqual(null); + }); + + test('should return secure_url if specified', () => { + const data = { + images: [{ + secure_url: 'https://example.com/image.png', + url: 'http://example.com/image.png', + }], + }; + + expect(PostAttachmentOpenGraph.getBestImageUrl(data, false)).toEqual(data.images[0].secure_url); + }); + + test('should return url if secure_url is not specified', () => { + const data = { + images: [{ + secure_url: '', + url: 'http://example.com/image.png', + }], + }; + + expect(PostAttachmentOpenGraph.getBestImageUrl(data, false)).toEqual(data.images[0].url); + }); + + test('should return a proxied image URL if the image proxy is enabled', () => { + const data = { + images: [{ + url: 'http://example.com/image.png', + }], + }; + + expect(PostAttachmentOpenGraph.getBestImageUrl(data, true).endsWith(`/api/v4/image?url=${encodeURIComponent(data.images[0].url)}`)).toEqual(true); + }); + + test('should not mangle a URL that is already proxied if the image proxy is enabled', () => { + const data = { + images: [{ + url: `${Client4.getBaseRoute()}/image?url=${encodeURIComponent('http://example.com/image.png')}`, + }], + }; + + expect(PostAttachmentOpenGraph.getBestImageUrl(data, true)).toEqual(data.images[0].url); + }); + }); +}); diff --git a/utils/post_utils.jsx b/utils/post_utils.jsx index b26c62865ec2..91bdb8bca838 100644 --- a/utils/post_utils.jsx +++ b/utils/post_utils.jsx @@ -47,9 +47,16 @@ export function isEdited(post) { } export function getImageSrc(src, hasImageProxy) { - if (hasImageProxy) { - return Client4.getBaseRoute() + '/image?url=' + encodeURIComponent(src); + if (!src) { + return src; } + + const imageAPI = Client4.getBaseRoute() + '/image?url='; + + if (hasImageProxy && !src.startsWith(imageAPI)) { + return imageAPI + encodeURIComponent(src); + } + return src; }