Skip to content
This repository was archived by the owner on Oct 11, 2022. It is now read-only.
Merged

2.4.54 #4160

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 58 additions & 2 deletions api/mutations/thread/editThread.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import { getUserPermissionsInChannel } from '../../models/usersChannels';
import { isAuthedResolver as requireAuth } from '../../utils/permissions';
import { events } from 'shared/analytics';
import { trackQueue } from 'shared/bull/queues';
import {
LEGACY_PREFIX,
hasLegacyPrefix,
stripLegacyPrefix,
} from 'shared/imgix';

type Input = {
input: EditThreadInput,
Expand Down Expand Up @@ -78,10 +83,57 @@ export default requireAuth(async (_: any, args: Input, ctx: GraphQLContext) => {
);
}

/*
When threads are sent to the client, all image urls are signed and proxied
via imgix. If a user edits the thread, we have to restore all image upload
urls back to their previous state so that we don't accidentally store
an encoded, signed, and expired image url back into the db
*/
const initialBody = input.content.body && JSON.parse(input.content.body);

if (initialBody) {
const imageKeys = Object.keys(initialBody.entityMap).filter(
key => initialBody.entityMap[key].type.toLowerCase() === 'image'
);

const stripQueryParams = (str: string): string => {
if (
str.indexOf('https://spectrum.imgix.net') < 0 &&
str.indexOf('https://spectrum-proxy.imgix.net') < 0
) {
return str;
}

const split = str.split('?');
// if no query params existed, we can just return the original image
if (split.length < 2) return str;

// otherwise the image path is everything before the first ? in the url
const imagePath = split[0];
// images are encoded during the signing process (shared/imgix/index.js)
// so they must be decoded here for accurate storage in the db
const decoded = decodeURIComponent(imagePath);
// we remove https://spectrum.imgix.net from the path as well so that the
// path represents the generic location of the file in s3 and decouples
// usage with imgix
const processed = hasLegacyPrefix(decoded)
? stripLegacyPrefix(decoded)
: decoded;
return processed;
};

imageKeys.forEach((key, index) => {
if (!initialBody.entityMap[key[index]]) return;

const { src } = initialBody.entityMap[imageKeys[index]].data;
initialBody.entityMap[imageKeys[index]].data.src = stripQueryParams(src);
});
}

const newInput = Object.assign({}, input, {
...input,
content: {
...input.content,
body: JSON.stringify(initialBody),
title: input.content.title.trim(),
},
});
Expand Down Expand Up @@ -116,9 +168,13 @@ export default requireAuth(async (_: any, args: Input, ctx: GraphQLContext) => {
// Replace the local image srcs with the remote image src
const body =
editedThread.content.body && JSON.parse(editedThread.content.body);

const imageKeys = Object.keys(body.entityMap).filter(
key => body.entityMap[key].type.toLowerCase() === 'image'
key =>
body.entityMap[key].type.toLowerCase() === 'image' &&
body.entityMap[key].data.src.startsWith('blob:')
);

urls.forEach((url, index) => {
if (!body.entityMap[imageKeys[index]]) return;
body.entityMap[imageKeys[index]].data.src = url;
Expand Down
6 changes: 3 additions & 3 deletions api/queries/thread/content/body.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ export default (thread: DBThread, imageSignatureExpiration: number) => {
);

imageKeys.forEach((key, index) => {
if (!body.entityMap[key[index]]) return;
if (!body.entityMap[key]) return;

const { src } = body.entityMap[imageKeys[index]].data;
const { src } = body.entityMap[key].data;

// transform the body inline with signed image urls
body.entityMap[imageKeys[index]].data.src = signImageUrl(src, {
body.entityMap[key].data.src = signImageUrl(src, {
expires: imageSignatureExpiration,
});
});
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Spectrum",
"version": "2.4.53",
"version": "2.4.54",
"license": "BSD-3-Clause",
"devDependencies": {
"babel-cli": "^6.24.1",
Expand Down
19 changes: 4 additions & 15 deletions shared/imgix/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@ import ImgixClient from 'imgix-core-js';
import decodeUriComponent from 'decode-uri-component';

const IS_PROD = process.env.NODE_ENV === 'production';
const LEGACY_PREFIX = 'https://spectrum.imgix.net/';
export const LEGACY_PREFIX = 'https://spectrum.imgix.net/';
const EXPIRATION_TIME = 60 * 60 * 10;

// prettier-ignore
const isLocalUpload = (url: string): boolean => url.startsWith('/uploads/', 0) && !IS_PROD
// prettier-ignore
const hasLegacyPrefix = (url: string): boolean => url.startsWith(LEGACY_PREFIX, 0)
export const hasLegacyPrefix = (url: string): boolean => url.startsWith(LEGACY_PREFIX, 0)
// prettier-ignore
const useProxy = (url: string): boolean => url.indexOf('spectrum.imgix.net') < 0 && url.startsWith('http', 0)
const isEncoded = (url: string): boolean => url.indexOf('%') >= 0;

/*
When an image is uploaded to s3, we generate a url to be stored in our db
Expand All @@ -25,7 +24,7 @@ const isEncoded = (url: string): boolean => url.indexOf('%') >= 0;
url in this utility
*/
// prettier-ignore
const stripLegacyPrefix = (url: string): string => url.replace(LEGACY_PREFIX, '')
export const stripLegacyPrefix = (url: string): string => url.replace(LEGACY_PREFIX, '')

const signPrimary = (url: string, opts: Object = {}): string => {
const client = new ImgixClient({
Expand Down Expand Up @@ -56,15 +55,5 @@ export const signImageUrl = (url: string, opts: Opts) => {

// we never have to worry about escaping or unescaping proxied urls e.g. twitter images
if (useProxy(url)) return signProxy(processedUrl, opts);

let decoded = processedUrl;
if (isEncoded(processedUrl)) {
const pathParts = decoded.split('/');
const filename = pathParts.pop();
const bucketPath = pathParts.join('/');
decoded = bucketPath + '/' + encodeURIComponent(filename);
decoded = decodeUriComponent(decoded);
}

return signPrimary(decoded, opts);
return signPrimary(processedUrl, opts);
};
1 change: 1 addition & 0 deletions src/views/thread/components/threadDetail.js
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ class ThreadDetailPure extends React.Component<Props, State> {
{timestamp}
{thread.modifiedAt && (
<React.Fragment>
{' '}
(Edited{' '}
{timeDifference(Date.now(), editedTimestamp).toLowerCase()})
</React.Fragment>
Expand Down