@@ -14,7 +14,8 @@ import { Xss } from '../../../js/common/platform/xss.js';
14
14
import { MsgBlockParser } from '../../../js/common/core/msg-block-parser.js' ;
15
15
import { AcctStore } from '../../../js/common/platform/store/acct-store.js' ;
16
16
import { GmailParser } from '../../../js/common/api/email-provider/gmail/gmail-parser.js' ;
17
- import { Str } from '../../../js/common/core/common.js' ;
17
+ import { CID_PATTERN , Str } from '../../../js/common/core/common.js' ;
18
+ import DOMPurify from 'dompurify' ;
18
19
19
20
export class PgpBlockViewRenderModule {
20
21
public doNotSetStateAsReadyYet = false ;
@@ -179,16 +180,12 @@ export class PgpBlockViewRenderModule {
179
180
if ( ! isErr ) {
180
181
// rendering message content
181
182
$ ( '.pgp_print_button' ) . show ( ) ;
182
- const pgpBlock = $ ( '#pgp_block' ) . html ( Xss . htmlSanitizeKeepBasicTags ( htmlContent , 'IMG-TO-LINK' ) ) ; // xss-sanitized
183
+ $ ( '#pgp_block' ) . html ( Xss . htmlSanitizeKeepBasicTags ( htmlContent ) ) ; // xss-sanitized
183
184
Xss . appendRemoteImagesToContainer ( ) ;
184
185
$ ( '#pgp_block .remote_image_container img' ) . on (
185
186
'load' ,
186
187
this . view . setHandler ( ( ) => this . resizePgpBlockFrame ( ) )
187
188
) ;
188
- pgpBlock . find ( 'a.image_src_link' ) . one (
189
- 'click' ,
190
- this . view . setHandler ( ( el , ev ) => this . displayImageSrcLinkAsImg ( el as HTMLAnchorElement , ev as JQuery . Event < HTMLAnchorElement , null > ) )
191
- ) ;
192
189
} else {
193
190
// rendering our own ui
194
191
Xss . sanitizeRender ( '#pgp_block' , htmlContent ) ;
@@ -280,8 +277,9 @@ export class PgpBlockViewRenderModule {
280
277
} else {
281
278
this . renderText ( 'Formatting...' ) ;
282
279
const decoded = await Mime . decode ( decryptedBytes ) ;
280
+ let inlineCIDAttachments : Attachment [ ] = [ ] ;
283
281
if ( typeof decoded . html !== 'undefined' ) {
284
- decryptedContent = decoded . html ;
282
+ ( { sanitizedHtml : decryptedContent , inlineCIDAttachments } = this . replaceInlineImageCIDs ( decoded . html , decoded . attachments ) ) ;
285
283
isHtml = true ;
286
284
} else if ( typeof decoded . text !== 'undefined' ) {
287
285
decryptedContent = decoded . text ;
@@ -299,7 +297,7 @@ export class PgpBlockViewRenderModule {
299
297
for ( const attachment of decoded . attachments ) {
300
298
if ( attachment . isPublicKey ( ) ) {
301
299
publicKeys . push ( attachment . getData ( ) . toUtfStr ( ) ) ;
302
- } else {
300
+ } else if ( ! inlineCIDAttachments . some ( inlineAttachment => inlineAttachment . cid === attachment . cid ) ) {
303
301
renderableAttachments . push ( attachment ) ;
304
302
}
305
303
}
@@ -319,36 +317,46 @@ export class PgpBlockViewRenderModule {
319
317
}
320
318
} ;
321
319
322
- private displayImageSrcLinkAsImg = ( a : HTMLAnchorElement , event : JQuery . Event < HTMLAnchorElement , null > ) => {
323
- const img = document . createElement ( 'img' ) ;
324
- img . setAttribute ( 'style' , a . getAttribute ( 'style' ) || '' ) ;
325
- img . style . background = 'none' ;
326
- img . style . border = 'none' ;
327
- img . addEventListener ( 'load' , ( ) => this . resizePgpBlockFrame ( ) ) ;
328
- if ( a . href . startsWith ( 'cid:' ) ) {
329
- // image included in the email
330
- const contentId = a . href . replace ( / ^ c i d : / g, '' ) ;
331
- const content = this . view . attachmentsModule . includedAttachments . filter ( a => a . type . indexOf ( 'image/' ) === 0 && a . cid === `<${ contentId } >` ) [ 0 ] ;
332
- if ( content ) {
333
- img . src = `data:${ a . type } ;base64,${ content . getData ( ) . toBase64Str ( ) } ` ;
334
- Xss . replaceElementDANGEROUSLY ( a , img . outerHTML ) ; // xss-safe-value - img.outerHTML was built using dom node api
335
- } else {
336
- Xss . replaceElementDANGEROUSLY ( a , Xss . escape ( `[broken link: ${ a . href } ]` ) ) ; // xss-escaped
320
+ /**
321
+ * Replaces inline image CID references with base64 encoded data in sanitized HTML
322
+ * and returns the sanitized HTML along with the inline CID attachments.
323
+ *
324
+ * @param html - The original HTML content.
325
+ * @param attachments - An array of email attachments.
326
+ * @returns An object containing sanitized HTML and an array of inline CID attachments.
327
+ */
328
+ private replaceInlineImageCIDs = ( html : string , attachments : Attachment [ ] ) : { sanitizedHtml : string ; inlineCIDAttachments : Attachment [ ] } => {
329
+ // Array to store inline CID attachments
330
+ const inlineCIDAttachments : Attachment [ ] = [ ] ;
331
+
332
+ // Define the hook function for DOMPurify to process image elements after sanitizing attributes
333
+ const processImageElements = ( node : Element | null ) => {
334
+ // Ensure the node exists and has a 'src' attribute
335
+ if ( ! node || ! ( 'src' in node ) ) return ;
336
+ const imageSrc = node . getAttribute ( 'src' ) as string ;
337
+ const matches = imageSrc . match ( CID_PATTERN ) ;
338
+
339
+ // Check if the src attribute contains a CID
340
+ if ( matches && matches [ 1 ] ) {
341
+ const contentId = matches [ 1 ] ;
342
+ const contentIdAttachment = attachments . find ( attachment => attachment . cid === `<${ contentId } >` ) ;
343
+
344
+ // Replace the src attribute with a base64 encoded string
345
+ if ( contentIdAttachment ) {
346
+ inlineCIDAttachments . push ( contentIdAttachment ) ;
347
+ node . setAttribute ( 'src' , `data:${ contentIdAttachment . type } ;base64,${ contentIdAttachment . getData ( ) . toBase64Str ( ) } ` ) ;
348
+ }
337
349
}
338
- } else if ( a . href . startsWith ( 'https://' ) || a . href . startsWith ( 'http://' ) ) {
339
- // image referenced as url
340
- img . src = a . href ;
341
- Xss . replaceElementDANGEROUSLY ( a , img . outerHTML ) ; // xss-safe-value - img.outerHTML was built using dom node api
342
- } else if ( a . href . startsWith ( 'data:image/' ) ) {
343
- // image directly inlined
344
- img . src = a . href ;
345
- Xss . replaceElementDANGEROUSLY ( a , img . outerHTML ) ; // xss-safe-value - img.outerHTML was built using dom node api
346
- } else {
347
- Xss . replaceElementDANGEROUSLY ( a , Xss . escape ( `[broken link: ${ a . href } ]` ) ) ; // xss-escaped
348
- }
349
- event . preventDefault ( ) ;
350
- event . stopPropagation ( ) ;
351
- event . stopImmediatePropagation ( ) ;
350
+ } ;
351
+
352
+ // Add the DOMPurify hook
353
+ DOMPurify . addHook ( 'afterSanitizeAttributes' , processImageElements ) ;
354
+
355
+ // Sanitize the HTML and remove the DOMPurify hooks
356
+ const sanitizedHtml = DOMPurify . sanitize ( html ) ;
357
+ DOMPurify . removeAllHooks ( ) ;
358
+
359
+ return { sanitizedHtml, inlineCIDAttachments } ;
352
360
} ;
353
361
354
362
private getEncryptedSubjectText = ( subject : string , isHtml : boolean ) => {
0 commit comments