import { URL } from 'url' ; import { Engine } from 'hint' ; import { createHelpers , DocumentData , getElementByUrl , HTMLDocument , HTMLElement , restoreReferences , traverse } from '@hint/utils-dom' ; import { IConnector , FetchEnd , NetworkData } from 'hint/dist/src/lib/types' ; import { ConnectorOptionsConfig } from '@hint/utils' ; import { browser , document , eval , location , window } from '../shared/globals' ; import { Events } from '../shared/types' ; import { Evaluator } from './evaluator' ; import { Fetcher } from './fetcher' ; import { setFetchType } from './set-fetch-type' ; export default class WebExtensionConnector implements IConnector { private _document : HTMLDocument | undefined ; private _originalDocument : HTMLDocument | undefined ; private _engine : Engine ; private _evaluator = new Evaluator ( ) ; private _fetcher = new Fetcher ( ) ; private _fetchEndQueue : FetchEnd [ ] = [ ] ; private _onComplete : ( err : Error | null , resource ? : string ) => void = ( ) => { } ; private _options : ConnectorOptionsConfig ; public static schema = { additionalProperties : false , properties : { waitFor : { minimum : 0 , type : 'number' } } } ; public constructor ( engine , options ? ) { this . _engine = engine ; this . _options = options || { } ; if ( ! this . _options . waitFor ) { this . _options . waitFor = 1000 ; } ( engine as Engine < import ( '@hint/parser-html' ) . HTMLEvents > ) . on ( 'parse::end::html' , ( event ) => { if ( event . resource === location . href ) { this . _originalDocument = event . document ; } } ) ; browser . runtime . onMessage . addListener ( async ( events ) => { try { /** Extension resources cause overhead and noise to the user so they are ignored. */ if ( this . isExtensionResource ( events ) ) { return ; } if ( this . _fetcher . handle ( events ) ) { return ; } if ( events . fetchEnd ) { await this . notifyFetch ( events . fetchEnd ) ; } if ( events . fetchStart ) { await this . _engine . emitAsync ( 'fetch::start' , events . fetchStart ) ; } } catch ( err ) { this . _onComplete ( err ) ; } } ) ; const onLoad = ( ) => { const resource = location . href ; setTimeout ( async ( ) => { try { await this . evaluateInPage ( `(${ createHelpers })()` ) ; const snapshot = await this . evaluateInPage ( '__webhint.snapshotDocument(document)' ) ; restoreReferences ( snapshot ) ; this . _document = new HTMLDocument ( snapshot , location . href , this . _originalDocument ) ; await this . sendFetchEndEvents ( ) ; await traverse ( this . _document , this . _engine , resource ) ; const event = { document : this . _document , resource } ; await this . _engine . emitAsync ( 'can-evaluate::script' , event ) ; this . _onComplete ( null , resource ) ; } catch ( err ) { this . _onComplete ( err ) ; } } , this . _options . waitFor ) ; } ; if ( document . readyState === 'complete' ) { setTimeout ( onLoad , 0 ) ; } else { window . addEventListener ( 'load' , onLoad ) ; } } private isExtensionResource ( events ) { /** Only chromium resources seem to get tracked but just in case we add the Firefox protocol as well. */ const event = events . fetchStart || events . fetchEnd ; if ( ! event ) { return false ; } const resource = event . resource ; return resource . startsWith ( 'chrome-extension:' ) || resource . startsWith ( 'moz-extension:' ) ; } private sendMessage ( message ) { browser . runtime . sendMessage ( message ) ; } private async sendFetchEndEvents ( ) { for ( const event of this . _fetchEndQueue ) { await this . notifyFetch ( event ) ; } } private setFetchElement ( event ) { const url = event . request . url ; if ( this . _document ) { event . element = getElementByUrl ( this . _document , url ) ; } } private async notifyFetch ( event ) { if ( ! this . _document && event . response . url !== location . href ) { this . _fetchEndQueue . push ( event ) ; return ; } this . setFetchElement ( event ) ; const type = await setFetchType ( event ) ; await this . _engine . emitAsync ( `fetch::end::${ type }` as 'fetch::end::*' , event ) ; } public fetchContent ( target , headers ? ) { return this . _fetcher . fetch ( target , headers ) ; } public async collect ( target ) { const resource = target . href ; await this . _engine . emitAsync ( 'scan::start' , { resource } ) ; this . sendMessage ( { ready : true } ) ; return new Promise < void > ( ( resolve , reject ) => { this . _onComplete = async ( err , resource = '' ) => { if ( err ) { reject ( err ) ; return ; } try { await this . _engine . emitAsync ( 'scan::end' , { resource } ) ; resolve ( ) ; this . sendMessage ( { done : true } ) ; } catch ( e ) { reject ( e ) ; } } ; } ) ; } private needsToRunInPage ( source ) { return source . includes ( '/*RunInPageContext*/' ) ; } /**
     * Runs a script in the website context.
     *
     * By default, `eval` runs the scripts in a different context
     * but, some scripts, needs to run in the same context
     * of the website.
     */ private evaluateInPage ( source ) { return this . _evaluator . evaluateInPage ( source ) ; } public evaluate ( source ) { if ( this . needsToRunInPage ( source ) ) { return this . evaluateInPage ( source ) ; } return Promise . resolve ( eval ( source ) ) ; } public querySelectorAll ( selector ) { return this . _document ? this . _document . querySelectorAll ( selector ) : [ ] ; } public close ( ) { return Promise . resolve ( ) ; } public get dom ( ) : HTMLDocument | undefined { return this . _document ; } public get html ( ) : string { return this . _document ? this . _document . pageHTML ( ) : '' ; } }