'use strict' ; import { FLAVOR , VERSION } from '../core/const.js' ; export class UnreportableError extends Error { } type ObjWithStack = { stack : string ; } ; export type ErrorReport = { name : string ; message : string ; url : string ; line : number ; col : number ; trace : string ; version : string ; environment : string ; } ; export class Catch { public static RUNTIME_ENVIRONMENT = 'undetermined' ; private static ORIG_ONERROR = window . onerror ; private static CONSOLE_MSG = ' Please report errors above to human@flowcrypt.com. We fix errors VERY promptly.' ; private static IGNORE_ERR_MSG = [ 'Invocation of form get(, function) doesn\'t match definition get(optional string or array or object keys, function callback)' , 'Invocation of form set(, function) doesn\'t match definition set(object items, optional function callback)' , 'Invocation of form runtime.connect(null, ) doesn\'t match definition runtime.connect(optional string extensionId, optional object connectInfo)' , 'TypeError: a is null' , 'TypeError: d is null' , 'TypeError: G is null' , 'TypeError: window.opener is null' , 'Script error.' , 'ResizeObserver loop limit exceeded' , ] ; public static rewrapErr = ( e , message ) : Error => { const newErr = new Error ( `${ message }::${ e instanceof Error ? `${ e . name }: ${ e . message }` : String ( e ) }` ) ; newErr . stack += `\n\n${ Catch . stringify ( e ) }` ; return newErr ; } ; public static stringify = ( e ) : string => { if ( e instanceof Error ) { return `[typeof:Error:${ e . name }] ${ e . message }\n\n${ e . stack }` ; } if ( typeof e === 'string' ) { return `[typeof:string] ${ e }` ; } try { return `[typeof:${ ( typeof e ) }:${ String ( e ) }] ${ JSON . stringify ( e ) }` ; } catch ( cannotStringify ) { return `[unstringifiable typeof:${ ( typeof e ) }:${ String ( e ) }]` ; } } ; public static hasStack = ( e ) : e is ObjWithStack => { return e && typeof e === 'object' && typeof ( e as ObjWithStack ) . stack === 'string' && Boolean ( ( e as ObjWithStack ) . stack ) ; } ; returns public static onErrorInternalHandler = ( errMsg , url , line , col , originalErr , isManuallyCalled ) : boolean => { const exception = Catch . formExceptionFromThrown ( originalErr , errMsg , url , line , col , isManuallyCalled ) ; if ( ( Catch . IGNORE_ERR_MSG . indexOf ( exception . message ) !== - 1 ) || ( errMsg && Catch . IGNORE_ERR_MSG . indexOf ( errMsg ) !== - 1 ) ) { return false ; } console . error ( originalErr ) ; if ( exception !== originalErr ) { console . error ( exception ) ; } console . error ( exception . message + '\n' + exception . stack ) ; if ( isManuallyCalled !== true && Catch . ORIG_ONERROR && Catch . ORIG_ONERROR !== ( Catch . onErrorInternalHandler as OnErrorEventHandler ) ) { Catch . ORIG_ONERROR . apply ( undefined , arguments ) ; } if ( exception instanceof UnreportableError ) { console . error ( 'Not reporting UnreportableError above' ) ; return false ; } if ( ( exception . stack || '' ) . indexOf ( 'PRIVATE' ) !== - 1 ) { exception . stack = '~censored:PRIVATE' ; } const formatted = Catch . formatExceptionForReport ( exception , line , col ) ; if ( FLAVOR === 'enterprise' ) { console . log ( 'enterprise flavor - not reporting this remotely' ) ; return false ; } Catch . doSendErrorToFlowCryptComBackend ( formatted ) ; return true ; } ; returns public static reportErr = ( e ) : boolean => { const { line , col } = Catch . getErrorLineAndCol ( e ) ; return Catch . onErrorInternalHandler ( e instanceof Error ? e . message : String ( e ) , window . location . href , line , col , e , true ) ; } ; returns public static report = ( name , details ? ) : boolean => { return Catch . reportErr ( Catch . nameAndDetailsAsException ( name , details ) ) ; } ; public static isPromise = ( v ) : v is Promise < any > => { return v && typeof v === 'object' && typeof ( v as Promise < any > ) . then === 'function' && typeof ( v as Promise < any > ) . catch === 'function' ; } ; public static try = ( code ) => { return ( ) => { try { const r = code ( ) ; if ( Catch . isPromise ( r ) ) { r . catch ( Catch . reportErr ) ; } } catch ( codeErr ) { Catch . reportErr ( codeErr ) ; } } ; } ; public static browser = ( ) : { name : 'firefox' | 'ie' | 'chrome' | 'opera' | 'safari' | 'unknown' ; v : number | undefined ; } => { if ( /Firefox[\/\s](\d+\.\d+)/ . test ( navigator . userAgent ) ) { return { name : 'firefox' , v : Number ( RegExp . $1 ) } ; } else if ( /MSIE (\d+\.\d+);/ . test ( navigator . userAgent ) ) { return { name : 'ie' , v : Number ( RegExp . $1 ) } ; } else if ( /Chrome[\/\s](\d+\.\d+)/ . test ( navigator . userAgent ) ) { return { name : 'chrome' , v : Number ( RegExp . $1 ) } ; } else if ( /Opera[\/\s](\d+\.\d+)/ . test ( navigator . userAgent ) ) { return { name : 'opera' , v : Number ( RegExp . $1 ) } ; } else if ( /Safari[\/\s](\d+\.\d+)/ . test ( navigator . userAgent ) ) { return { name : 'safari' , v : Number ( RegExp . $1 ) } ; } else { return { name : 'unknown' , v : undefined } ; } } ; public static environment = ( url = window . location . href ) : string => { const browserName = Catch . browser ( ) . name ; const origin = new URL ( window . location . href ) . origin ; let env = 'unknown' ; if ( url . indexOf ( 'bnjglocicd' ) !== - 1 ) { env = 'ex:prod' ; } else if ( url . indexOf ( 'gjdhkacdgd' ) !== - 1 || url . indexOf ( 'gggocmadhd' ) !== - 1 ) { env = 'ex:dev' ; } else if ( url . indexOf ( 'mefaeofbcc' ) !== - 1 ) { env = 'ex:stable' ; } else if ( /chrome-extension:\/\/[a-z]{32}\/.+/ . test ( url ) ) { env = 'ex:fork' ; } else if ( /moz-extension:\/\/.+/ . test ( url ) ) { env = 'ex' ; } else if ( origin === 'http://l.flowcrypt.com' ) { env = 'web:local' ; } else if ( origin === 'https://flowcrypt.com' ) { env = 'web:prod' ; } else if ( origin === 'https://mail.google.com' ) { env = 'ex:script:gmail' ; } return browserName + ':' + env ; } ; public static test = ( type = 'error' ) => { if ( type === 'error' ) { throw new Error ( 'intentional error for debugging' ) ; } else { throw { what : 'intentional thrown object for debugging' } ; } } ; public static stackTrace = ( ) : string => { try { Catch . test ( ) ; } catch ( e ) { return `${ ( ( e as Error ) . stack || '' ) . split ( '\n' ) . splice ( 3 ) . join ( '\n' ) }\n\nurl: ${ Catch . censoredUrl ( window . location . href ) }\n` ; } return '' ; } ; public static censoredUrl = ( url ) : string => { if ( ! url ) { return '(unknown url)' ; } if ( url . indexOf ( 'refreshToken=' ) !== - 1 ) { return `${ url . split ( '?' ) [ 0 ] }~censored:refreshToken` ; } if ( url . indexOf ( 'token=' ) !== - 1 ) { return `${ url . split ( '?' ) [ 0 ] }~censored:token` ; } if ( url . indexOf ( 'code=' ) !== - 1 ) { return `${ url . split ( '?' ) [ 0 ] }~censored:code` ; } if ( url . indexOf ( 'idToken=' ) !== - 1 ) { return `${ url . split ( '?' ) [ 0 ] }~censored:idToken` ; } return url ; } ; public static onUnhandledRejectionInternalHandler = ( e ) => { if ( Catch . isPromiseRejectionEvent ( e ) ) { Catch . reportErr ( e . reason ) ; } else { const str = Catch . stringify ( e ) ; if ( str . match ( /^\[typeof:object:\[object (PromiseRejectionEvent|CustomEvent|ProgressEvent)\]\] \{"isTrusted":(?:true|false)\}$/ ) ) { return ; } const { line , col } = Catch . getErrorLineAndCol ( e ) ; const msg = e instanceof Error ? e . message : String ( e ) ; Catch . onErrorInternalHandler ( `REJECTION: ${ msg }` , window . location . href , line , col , e , true ) ; } } ; public static setHandledInterval = ( cb , ms ) : number => { return window . setInterval ( Catch . try ( cb ) , ms ) ; } ; public static setHandledTimeout = ( cb , ms ) : number => { return window . setTimeout ( Catch . try ( cb ) , ms ) ; } ; public static doesReject = async ( p , errNeedle ? ) => { try { await p ; return false ; } catch ( e ) { if ( ! errNeedle ) { return true ; } return ! ! errNeedle . find ( needle => String ( e ) . includes ( needle ) ) ; } } ; public static undefinedOnException = async < T > ( p ) : Promise < T | undefined > => { try { return await p ; } catch ( e ) { return undefined ; } } ; private static formatExceptionForReport = ( thrown , line ? , col ? ) : ErrorReport => { if ( ! line || ! col ) { const { line : parsedLine , col : parsedCol } = Catch . getErrorLineAndCol ( thrown ) ; line = parsedLine ; col = parsedCol ; } if ( thrown instanceof Error ) { thrown . stack += Catch . formattedStackBlock ( 'Catch.reportErr calling stack' , Catch . stackTrace ( ) ) ; if ( thrown . hasOwnProperty ( 'workerStack' ) ) { thrown . stack += Catch . formattedStackBlock ( 'openpgp.js worker stack' , String ( ( thrown as any ) . workerStack ) ) ; } } const exception = Catch . formExceptionFromThrown ( thrown ) ; return { name : exception . name . substring ( 0 , 50 ) , message : exception . message . substring ( 0 , 200 ) , url : window . location . href . substring ( 0 , 100 ) , line : line || 0 , col : col || 0 , trace : exception . stack || '' , version : VERSION , environment : Catch . RUNTIME_ENVIRONMENT , } ; } ; private static doSendErrorToFlowCryptComBackend = ( errorReport ) => { try { $ . ajax ( { url : 'https://flowcrypt.com/api/help/error' , method : 'POST' , data : JSON . stringify ( errorReport ) , dataType : 'json' , crossDomain : true , contentType : 'application/json; charset=UTF-8' , async : true , success : ( response ) => { if ( response && typeof response === 'object' && response . saved === true ) { console . log ( '%cFlowCrypt ERROR:' + Catch . CONSOLE_MSG , 'font-weight: bold;' ) ; } else { console . error ( '%cFlowCrypt EXCEPTION:' + Catch . CONSOLE_MSG , 'font-weight: bold;' ) ; } } , error : ( ) => { console . error ( '%cFlowCrypt FAILED:' + Catch . CONSOLE_MSG , 'font-weight: bold;' ) ; } , } ) ; } catch ( ajaxErr ) { console . error ( ajaxErr ) ; console . error ( '%cFlowCrypt ISSUE:' + Catch . CONSOLE_MSG , 'font-weight: bold;' ) ; } } ; private static formExceptionFromThrown = ( thrown , errMsg ? , url ? , line ? , col ? , isManuallyCalled ? ) : Error => { let exception ; if ( typeof thrown !== 'object' ) { exception = new Error ( `THROWN_NON_OBJECT[${ typeof thrown }]: ${ String ( thrown ) }` ) ; } else if ( errMsg && url && typeof line !== 'undefined' && ! col && ! thrown && ! isManuallyCalled ) { exception = new Error ( `LIMITED_ERROR: ${ errMsg }` ) ; } else if ( thrown instanceof Error ) { exception = thrown ; if ( thrown . hasOwnProperty ( 'thrown' ) ) { exception . stack += `\n\ne.thrown:\n${ Catch . stringify ( ( thrown as any ) . thrown ) }` ; } } else { exception = new Error ( `THROWN_OBJECT: ${ errMsg }` ) ; if ( Catch . hasStack ( thrown ) ) { exception . stack += `\n\nORIGINAL_THROWN_OBJECT_STACK:\n${ thrown . stack }\n\n` ; } exception . stack += `\n\nORIGINAL_ERR:\n${ Catch . stringify ( thrown ) }` ; } return exception ; } ; private static getErrorLineAndCol = ( e ) => { try { const callerLine = e . stack ! . split ( '\n' ) [ 1 ] ; const matched = callerLine . match ( /\.js:([0-9]+):([0-9]+)\)?/ ) ; return { line : Number ( matched ! [ 1 ] ) , col : Number ( matched ! [ 2 ] ) } ; } catch ( lineErr ) { return { line : 0 , col : 0 } ; } } ; private static formattedStackBlock = ( name , text ) => { return `\n\n### ${ name } ###\n# ${ text . split ( '\n' ) . join ( '\n# ' ) }\n######################\n` ; } ; private static nameAndDetailsAsException = ( name , details ) : Error => { try { throw new Error ( name ) ; } catch ( e ) { ( e as Error ) . stack += `\n\n\ndetails:\n${ Catch . stringify ( details ) }` ; return e as Error ; } } ; private static isPromiseRejectionEvent = ( ev ) : ev is PromiseRejectionEvent => { if ( ev && typeof ev === 'object' ) { const eHasReason = ( ev as { } ) . hasOwnProperty ( 'reason' ) && typeof ( ev as PromiseRejectionEvent ) . reason === 'object' ; const eHasPromise = ( ev as { } ) . hasOwnProperty ( 'promise' ) && Catch . isPromise ( ( ev as PromiseRejectionEvent ) . promise ) ; return eHasReason && eHasPromise ; } return false ; } ; } Catch . RUNTIME_ENVIRONMENT = Catch . environment ( ) ; window . onerror = ( Catch . onErrorInternalHandler as OnErrorEventHandler ) ; window . onunhandledrejection = Catch . onUnhandledRejectionInternalHandler ;