@@ -9,6 +9,11 @@ import { getUserPermissionsInChannel } from '../../models/usersChannels';
99import { isAuthedResolver as requireAuth } from '../../utils/permissions' ;
1010import { events } from 'shared/analytics' ;
1111import { trackQueue } from 'shared/bull/queues' ;
12+ import {
13+ LEGACY_PREFIX ,
14+ hasLegacyPrefix ,
15+ stripLegacyPrefix ,
16+ } from 'shared/imgix' ;
1217
1318type 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 ;
0 commit comments