'use strict' ; import { MsgBlock , ReplaceableMsgBlockType } from './msg-block.js' ; import { SanitizeImgHandling , Xss } from '../platform/xss.js' ; import { Buf } from './buf.js' ; import { Catch } from '../platform/catch.js' ; import { Mime } from './mime.js' ; import { PgpArmor } from './crypto/pgp/pgp-armor.js' ; import { Str } from './common.js' ; import { FcAttachmentLinkData } from './attachment.js' ; import { KeyUtil } from './crypto/key.js' ; type SanitizedBlocks = { blocks : MsgBlock [ ] ; subject : string | undefined ; isRichText : boolean ; webReplyToken : any | undefined ; } ; export class MsgBlockParser { private static ARMOR_HEADER_MAX_LENGTH = 50 ; public static detectBlocks = ( origText ) => { const blocks = [ ] ; const normalized = Str . normalize ( origText ) ; let startAt = 0 ; while ( true ) { const { found , continueAt } = MsgBlockParser . detectBlockNext ( normalized , startAt ) ; if ( found ) { blocks . push ( ... found ) ; } if ( typeof continueAt === 'undefined' ) { return { blocks , normalized } ; } else { if ( continueAt <= startAt ) { Catch . report ( `MsgBlockParser.detectBlocks likely infinite loop: r.continueAt(${ continueAt }) <= startAt(${ startAt })` ) ; return { blocks , normalized } ; } startAt = continueAt ; } } } ; public static fmtDecryptedAsSanitizedHtmlBlocks = async ( decryptedContent , imgHandling = 'IMG-TO-LINK' ) : Promise < SanitizedBlocks > => { const blocks = [ ] ; let isRichText = false ; let webReplyToken ; if ( ! Mime . resemblesMsg ( decryptedContent ) ) { let plain = Buf . fromUint8 ( decryptedContent ) . toUtfStr ( ) ; plain = MsgBlockParser . extractFcAttachments ( plain , blocks ) ; webReplyToken = MsgBlockParser . extractFcReplyToken ( plain ) ; if ( webReplyToken ) { plain = MsgBlockParser . stripFcTeplyToken ( plain ) ; } const armoredPubKeys = [ ] ; plain = MsgBlockParser . stripPublicKeys ( plain , armoredPubKeys ) ; blocks . push ( MsgBlock . fromContent ( 'decryptedHtml' , Str . escapeTextAsRenderableHtml ( plain ) ) ) ; await MsgBlockParser . pushArmoredPubkeysToBlocks ( armoredPubKeys , blocks ) ; return { blocks , subject : undefined , isRichText , webReplyToken } ; } const decoded = await Mime . decode ( decryptedContent ) ; if ( typeof decoded . html !== 'undefined' ) { webReplyToken = MsgBlockParser . extractFcReplyToken ( decoded . html ) ; if ( webReplyToken ) { decoded . html = MsgBlockParser . stripFcTeplyToken ( decoded . html ) ; } blocks . push ( MsgBlock . fromContent ( 'decryptedHtml' , Xss . htmlSanitizeKeepBasicTags ( decoded . html , imgHandling ) ) ) ; isRichText = true ; } else if ( typeof decoded . text !== 'undefined' ) { webReplyToken = MsgBlockParser . extractFcReplyToken ( decoded . text ) ; if ( webReplyToken ) { decoded . text = MsgBlockParser . stripFcTeplyToken ( decoded . text ) ; } blocks . push ( MsgBlock . fromContent ( 'decryptedHtml' , Str . escapeTextAsRenderableHtml ( decoded . text ) ) ) ; } else { blocks . push ( MsgBlock . fromContent ( 'decryptedHtml' , Str . escapeTextAsRenderableHtml ( Buf . with ( decryptedContent ) . toUtfStr ( ) ) ) ) ; } for ( const attachment of decoded . attachments ) { if ( attachment . treatAs ( ) === 'publicKey' ) { await MsgBlockParser . pushArmoredPubkeysToBlocks ( [ attachment . getData ( ) . toUtfStr ( ) ] , blocks ) ; } else { blocks . push ( MsgBlock . fromAttachment ( 'decryptedAttachment' , '' , { name : attachment . name , data : attachment . getData ( ) , length : attachment . length , type : attachment . type } ) ) ; } } return { blocks , subject : decoded . subject , isRichText , webReplyToken } ; } ; public static extractFcAttachments = ( decryptedContent , blocks ) => { if ( decryptedContent . includes ( 'class="cryptup_file"' ) ) { decryptedContent = decryptedContent . replace ( /<a\s+href="([^"]+)"\s+class="cryptup_file"\s+cryptup-data="([^"]+)"\s*>[^<]+<\/a>\n?/gm , ( _ , url , fcData ) => { const a = Str . htmlAttrDecode ( String ( fcData ) ) ; if ( MsgBlockParser . isFcAttachmentLinkData ( a ) ) { blocks . push ( MsgBlock . fromAttachment ( 'encryptedAttachmentLink' , '' , { type : a . type , name : a . name , length : a . size , url : String ( url ) } ) ) ; } return '' ; } ) ; } return decryptedContent ; } ; public static stripPublicKeys = ( decryptedContent , foundPublicKeys ) => { let { blocks , normalized } = MsgBlockParser . detectBlocks ( decryptedContent ) ; for ( const block of blocks ) { if ( block . type === 'publicKey' ) { const armored = block . content . toString ( ) ; foundPublicKeys . push ( armored ) ; normalized = normalized . replace ( armored , '' ) ; } } return normalized ; } ; public static extractFcReplyToken = ( decryptedContent ) : undefined | any => { const fcTokenElement = $ ( `<div>${ decryptedContent }</div>` ) . find ( '.cryptup_reply' ) ; if ( fcTokenElement . length ) { const fcData = fcTokenElement . attr ( 'cryptup-data' ) ; if ( fcData ) { return Str . htmlAttrDecode ( fcData ) ; } } } ; public static stripFcTeplyToken = ( decryptedContent ) => { return decryptedContent . replace ( /<div[^>]+class="cryptup_reply"[^>]+><\/div>/ , '' ) ; } ; private static isFcAttachmentLinkData = ( o ) : o is FcAttachmentLinkData => { return o && typeof o === 'object' && typeof ( o as FcAttachmentLinkData ) . name !== 'undefined' && typeof ( o as FcAttachmentLinkData ) . size !== 'undefined' && typeof ( o as FcAttachmentLinkData ) . type !== 'undefined' ; } ; private static detectBlockNext = ( origText , startAt ) => { const armorHdrTypes = Object . keys ( PgpArmor . ARMOR_HEADER_DICT ) as ReplaceableMsgBlockType [ ] ; const result = { found : [ ] as MsgBlock [ ] } ; const begin = origText . indexOf ( PgpArmor . headers ( 'null' ) . begin , startAt ) ; if ( begin !== - 1 ) { const potentialBeginHeader = origText . substr ( begin , MsgBlockParser . ARMOR_HEADER_MAX_LENGTH ) ; for ( const armorHdrType of armorHdrTypes ) { const blockHeaderDef = PgpArmor . ARMOR_HEADER_DICT [ armorHdrType ] ; if ( blockHeaderDef . replace ) { const indexOfConfirmedBegin = potentialBeginHeader . indexOf ( blockHeaderDef . begin ) ; if ( indexOfConfirmedBegin === 0 ) { if ( begin > startAt ) { let potentialTextBeforeBlockBegun = origText . substring ( startAt , begin ) ; if ( ! potentialTextBeforeBlockBegun . endsWith ( '\n' ) ) { continue ; } potentialTextBeforeBlockBegun = potentialTextBeforeBlockBegun . trim ( ) ; if ( potentialTextBeforeBlockBegun ) { result . found . push ( MsgBlock . fromContent ( 'plainText' , potentialTextBeforeBlockBegun ) ) ; } } let endIndex = - 1 ; let foundBlockEndHeaderLength = 0 ; if ( typeof blockHeaderDef . end === 'string' ) { endIndex = origText . indexOf ( blockHeaderDef . end , begin + blockHeaderDef . begin . length ) ; foundBlockEndHeaderLength = blockHeaderDef . end . length ; } else { const origTextAfterBeginIndex = origText . substring ( begin ) ; const matchEnd = origTextAfterBeginIndex . match ( blockHeaderDef . end ) ; if ( matchEnd ) { endIndex = matchEnd . index ? begin + matchEnd . index : - 1 ; foundBlockEndHeaderLength = matchEnd [ 0 ] . length ; } } if ( endIndex !== - 1 ) { result . found . push ( MsgBlock . fromContent ( armorHdrType , origText . substring ( begin , endIndex + foundBlockEndHeaderLength ) . trim ( ) ) ) ; result . continueAt = endIndex + foundBlockEndHeaderLength ; } else { result . found . push ( MsgBlock . fromContent ( armorHdrType , origText . substr ( begin ) , true ) ) ; } break ; } } } } if ( origText && ! result . found . length ) { const potentialText = origText . substr ( startAt ) . trim ( ) ; if ( potentialText ) { result . found . push ( MsgBlock . fromContent ( 'plainText' , potentialText ) ) ; } } return result ; } ; private static pushArmoredPubkeysToBlocks = async ( armoredPubkeys , blocks ) : Promise < void > => { for ( const armoredPubkey of armoredPubkeys ) { const keys = await KeyUtil . parseMany ( armoredPubkey ) ; for ( const key of keys ) { const pub = await KeyUtil . asPublicKey ( key ) ; blocks . push ( MsgBlock . fromContent ( 'publicKey' , KeyUtil . armor ( pub ) ) ) ; } } } ; }