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

Commit 95f9014

Browse files
authored
Merge pull request #4160 from withspectrum/2.4.54
2.4.54
2 parents f54283f + 59be537 commit 95f9014

File tree

5 files changed

+67
-21
lines changed

5 files changed

+67
-21
lines changed

api/mutations/thread/editThread.js

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ import { getUserPermissionsInChannel } from '../../models/usersChannels';
99
import { isAuthedResolver as requireAuth } from '../../utils/permissions';
1010
import { events } from 'shared/analytics';
1111
import { trackQueue } from 'shared/bull/queues';
12+
import {
13+
LEGACY_PREFIX,
14+
hasLegacyPrefix,
15+
stripLegacyPrefix,
16+
} from 'shared/imgix';
1217

1318
type Input = {
1419
input: EditThreadInput,
@@ -78,10 +83,57 @@ export default requireAuth(async (_: any, args: Input, ctx: GraphQLContext) => {
7883
);
7984
}
8085

86+
/*
87+
When threads are sent to the client, all image urls are signed and proxied
88+
via imgix. If a user edits the thread, we have to restore all image upload
89+
urls back to their previous state so that we don't accidentally store
90+
an encoded, signed, and expired image url back into the db
91+
*/
92+
const initialBody = input.content.body && JSON.parse(input.content.body);
93+
94+
if (initialBody) {
95+
const imageKeys = Object.keys(initialBody.entityMap).filter(
96+
key => initialBody.entityMap[key].type.toLowerCase() === 'image'
97+
);
98+
99+
const stripQueryParams = (str: string): string => {
100+
if (
101+
str.indexOf('https://spectrum.imgix.net') < 0 &&
102+
str.indexOf('https://spectrum-proxy.imgix.net') < 0
103+
) {
104+
return str;
105+
}
106+
107+
const split = str.split('?');
108+
// if no query params existed, we can just return the original image
109+
if (split.length < 2) return str;
110+
111+
// otherwise the image path is everything before the first ? in the url
112+
const imagePath = split[0];
113+
// images are encoded during the signing process (shared/imgix/index.js)
114+
// so they must be decoded here for accurate storage in the db
115+
const decoded = decodeURIComponent(imagePath);
116+
// we remove https://spectrum.imgix.net from the path as well so that the
117+
// path represents the generic location of the file in s3 and decouples
118+
// usage with imgix
119+
const processed = hasLegacyPrefix(decoded)
120+
? stripLegacyPrefix(decoded)
121+
: decoded;
122+
return processed;
123+
};
124+
125+
imageKeys.forEach((key, index) => {
126+
if (!initialBody.entityMap[key[index]]) return;
127+
128+
const { src } = initialBody.entityMap[imageKeys[index]].data;
129+
initialBody.entityMap[imageKeys[index]].data.src = stripQueryParams(src);
130+
});
131+
}
132+
81133
const newInput = Object.assign({}, input, {
82134
...input,
83135
content: {
84-
...input.content,
136+
body: JSON.stringify(initialBody),
85137
title: input.content.title.trim(),
86138
},
87139
});
@@ -116,9 +168,13 @@ export default requireAuth(async (_: any, args: Input, ctx: GraphQLContext) => {
116168
// Replace the local image srcs with the remote image src
117169
const body =
118170
editedThread.content.body && JSON.parse(editedThread.content.body);
171+
119172
const imageKeys = Object.keys(body.entityMap).filter(
120-
key => body.entityMap[key].type.toLowerCase() === 'image'
173+
key =>
174+
body.entityMap[key].type.toLowerCase() === 'image' &&
175+
body.entityMap[key].data.src.startsWith('blob:')
121176
);
177+
122178
urls.forEach((url, index) => {
123179
if (!body.entityMap[imageKeys[index]]) return;
124180
body.entityMap[imageKeys[index]].data.src = url;

api/queries/thread/content/body.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ export default (thread: DBThread, imageSignatureExpiration: number) => {
3030
);
3131

3232
imageKeys.forEach((key, index) => {
33-
if (!body.entityMap[key[index]]) return;
33+
if (!body.entityMap[key]) return;
3434

35-
const { src } = body.entityMap[imageKeys[index]].data;
35+
const { src } = body.entityMap[key].data;
3636

3737
// transform the body inline with signed image urls
38-
body.entityMap[imageKeys[index]].data.src = signImageUrl(src, {
38+
body.entityMap[key].data.src = signImageUrl(src, {
3939
expires: imageSignatureExpiration,
4040
});
4141
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "Spectrum",
3-
"version": "2.4.53",
3+
"version": "2.4.54",
44
"license": "BSD-3-Clause",
55
"devDependencies": {
66
"babel-cli": "^6.24.1",

shared/imgix/index.js

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,15 @@ import ImgixClient from 'imgix-core-js';
44
import decodeUriComponent from 'decode-uri-component';
55

66
const IS_PROD = process.env.NODE_ENV === 'production';
7-
const LEGACY_PREFIX = 'https://spectrum.imgix.net/';
7+
export const LEGACY_PREFIX = 'https://spectrum.imgix.net/';
88
const EXPIRATION_TIME = 60 * 60 * 10;
99

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

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

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

5756
// we never have to worry about escaping or unescaping proxied urls e.g. twitter images
5857
if (useProxy(url)) return signProxy(processedUrl, opts);
59-
60-
let decoded = processedUrl;
61-
if (isEncoded(processedUrl)) {
62-
const pathParts = decoded.split('/');
63-
const filename = pathParts.pop();
64-
const bucketPath = pathParts.join('/');
65-
decoded = bucketPath + '/' + encodeURIComponent(filename);
66-
decoded = decodeUriComponent(decoded);
67-
}
68-
69-
return signPrimary(decoded, opts);
58+
return signPrimary(processedUrl, opts);
7059
};

src/views/thread/components/threadDetail.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ class ThreadDetailPure extends React.Component<Props, State> {
388388
{timestamp}
389389
{thread.modifiedAt && (
390390
<React.Fragment>
391+
{' '}
391392
(Edited{' '}
392393
{timeDifference(Date.now(), editedTimestamp).toLowerCase()})
393394
</React.Fragment>

0 commit comments

Comments
 (0)