Back

100% Statements 34/34
86.36% Branches 19/22
100% Functions 7/7
100% Lines 31/31

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119                69x 69x 69x 69x 69x                         137x 128x 127x       10x                       125x 125x 125x 125x 125x 125x   125x                               140x 1x     139x                           4x                           145x 9x     136x   136x 269x 133x   136x 133x 133x 125x         11x    
export interface DashboardFrame {
	url: string;
	path: string;
	widget?: string;
	line?: number;
	column?: number;
}
 
const POSITION = /:(\d+):(\d+)$/;
const FRAME_LINE = /^\s*at\s|^[^:]*@/;
const WIDGET_PATH = /^widgets\/([^/]+)\//;
const QUERY_OR_HASH = /[?#]/;
const URL_START = /[\s(@]/;
 
/**
 * Where an allowlisted origin starts a frame URL in one stack line.
 *
 * An origin can also appear inside another script's URL, as the value of a query parameter. The
 * frame is that other script's, so the origin counts only where a URL itself begins.
 *
 * @param line   - The stack line.
 * @param origin - The allowlisted origin to look for.
 * @return The index of the frame URL, or -1.
 */
function originStart( line: string, origin: string ): number {
	for ( let at = line.indexOf( origin ); at !== -1; at = line.indexOf( origin, at + 1 ) ) {
		if ( at === 0 || URL_START.test( line[ at - 1 ] ) ) {
			return at;
		}
	}
 
	return -1;
}
 
/**
 * Reads the script URL, path, widget and position out of one stack line.
 *
 * @param line   - The stack line containing the origin.
 * @param origin - The allowlisted origin found in the line.
 * @param start  - Where that origin starts the frame URL.
 * @return The parsed frame.
 */
function parseFrame( line: string, origin: string, start: number ): DashboardFrame {
	const url = line.slice( start ).split( /[\s)]/ )[ 0 ];
	const position = POSITION.exec( url );
	const withoutPosition = position ? url.slice( 0, position.index ) : url;
	const relative = withoutPosition.slice( origin.length );
	const cut = relative.search( QUERY_OR_HASH );
	const path = cut === -1 ? relative : relative.slice( 0, cut );
 
	return {
		url,
		path,
		widget: WIDGET_PATH.exec( path )?.[ 1 ],
		line: position ? Number( position[ 1 ] ) : undefined,
		column: position ? Number( position[ 2 ] ) : undefined,
	};
}
 
/**
 * The origins that can be matched, of those the page published.
 *
 * @param origins - Absolute URL prefixes that count as dashboard code, if any.
 * @return The usable ones.
 */
function allowlisted( origins: string[] | undefined ): string[] {
	if ( ! Array.isArray( origins ) ) {
		return [];
	}
 
	return origins.filter( origin => typeof origin === 'string' && origin !== '' );
}
 
/**
 * Whether free text names a script of an allowlisted origin.
 *
 * This is weaker evidence than a stack frame, since any text can quote a URL. It is for the
 * failure whose own message names the module, where there is no frame to read.
 *
 * @param text    - A message or other free text.
 * @param origins - Absolute URL prefixes that count as dashboard code, if any.
 * @return Whether an allowlisted origin starts a URL in it.
 */
export function namesDashboardOrigin( text: string, origins: string[] | undefined ): boolean {
	return allowlisted( origins ).some( origin => originStart( text, origin ) !== -1 );
}
 
/**
 * Finds the first stack frame served from an allowlisted script origin.
 *
 * @param stack   - The error stack, if any.
 * @param origins - Absolute URL prefixes that count as dashboard code, if any.
 * @return The frame, or null when none matches.
 */
export function findDashboardFrame(
	stack: string | undefined,
	origins: string[] | undefined
): DashboardFrame | null {
	if ( typeof stack !== 'string' ) {
		return null;
	}
 
	const candidates = allowlisted( origins );
 
	for ( const line of stack.split( '\n' ) ) {
		if ( ! FRAME_LINE.test( line ) ) {
			continue;
		}
		for ( const origin of candidates ) {
			const start = originStart( line, origin );
			if ( start !== -1 ) {
				return parseFrame( line, origin, start );
			}
		}
	}
 
	return null;
}