import * as pw from 'playwright' ; import { parse } from 'tldts-experimental' ; import { FiltersEngine , PlaywrightRequestType , Request , RequestType } from '@cliqz/adblocker' ; import { autoRemoveScript , extractFeaturesFromDOM , DOMMonitor } from '@cliqz/adblocker-content' ; function sleep ( milliseconds ) { return new Promise ( ( resolve ) => { setTimeout ( resolve , milliseconds ) ; } ) ; } function getTopLevelUrl ( frame ) { let sourceUrl = '' ; while ( frame !== null ) { sourceUrl = frame . url ( ) ; if ( sourceUrl . length !== 0 ) { break ; } frame = frame . parentFrame ( ) ; } return sourceUrl ; } /**
 * Create an instance of `Request` from `pw.Request`.
 */ export function fromPlaywrightDetails ( details ) { const sourceUrl = getTopLevelUrl ( details . frame ( ) ) ; const url = details . url ( ) ; const type = details . resourceType ( ) as PlaywrightRequestType ; return Request . fromRawDetails ( { _originalRequestDetails : details , requestId : `${ type }-${ url }-${ sourceUrl }` , sourceUrl , type , url , } ) ; } /**
 * Wrap `FiltersEngine` into a Playwright-friendly helper class.
 */ export class BlockingContext { private readonly onFrameNavigated : ( frame : pw . Frame ) => Promise < void > ; private readonly onRequest : ( route : pw . Route ) => void ; constructor ( private readonly page , private readonly blocker ) { this . onFrameNavigated = ( frame ) => blocker . onFrameNavigated ( frame ) ; this . onRequest = ( route ) => blocker . onRequest ( route ) ; } public async enable ( ) { if ( this . blocker . config . loadCosmeticFilters === true ) { this . page . on ( 'framenavigated' , this . onFrameNavigated ) ; this . page . once ( 'domcontentloaded' , ( ) => { this . onFrameNavigated ( this . page . mainFrame ( ) ) ; } ) ; } if ( this . blocker . config . loadNetworkFilters === true ) { this . page . route ( '**/*' , this . onRequest ) ; } } public async disable ( ) { if ( this . blocker . config . loadNetworkFilters === true ) { await this . page . unroute ( '**/*' , this . onRequest ) ; } if ( this . blocker . config . loadCosmeticFilters === true ) { this . page . off ( 'framenavigated' , this . onFrameNavigated ) ; } } } /**
 * Wrap `FiltersEngine` into a Playwright-friendly helper class. It exposes
 * methods to interface with Playwright APIs needed to block ads.
 */ export class PlaywrightBlocker extends FiltersEngine { private readonly contexts : WeakMap < pw . Page , BlockingContext > = new WeakMap ( ) ; public async enableBlockingInPage ( page ) { let context = this . contexts . get ( page ) ; if ( context !== undefined ) { return context ; } context = new BlockingContext ( page , this ) ; this . contexts . set ( page , context ) ; await context . enable ( ) ; return context ; } public async disableBlockingInPage ( page ) { const context = this . contexts . get ( page ) ; if ( context === undefined ) { throw new Error ( 'Trying to disable blocking which was not enabled' ) ; } this . contexts . delete ( page ) ; await context . disable ( ) ; } public isBlockingEnabled ( page ) { return this . contexts . has ( page ) ; } public onFrameNavigated = async ( frame ) => { try { await this . onFrame ( frame ) ; } catch ( ex ) { } } ; private onFrame = async ( frame ) : Promise < void > => { const url = frame . url ( ) ; if ( url === 'chrome-error://chromewebdata/' ) { return ; } this . removeBlockedFrames ( frame ) . catch ( ( ) => { } ) ; const parsed = parse ( url ) ; const hostname = parsed . hostname || '' ; const domain = parsed . domain || '' ; { const { active , styles , scripts } = this . getCosmeticsFilters ( { domain , hostname , url , getBaseRules : true , getInjectionRules : true , getExtendedRules : true , getRulesFromHostname : true , getRulesFromDOM : false , } ) ; if ( active === false ) { return ; } Promise . all ( [ this . injectScriptletsIntoFrame ( frame , scripts ) , this . injectStylesIntoFrame ( frame , styles ) , ] ) . catch ( ( ) => { } ) ; } const observer = new DOMMonitor ( ( update ) => { if ( update . type === 'features' ) { const { active , styles } = this . getCosmeticsFilters ( { domain , hostname , url , ... update , getBaseRules : false , getInjectionRules : false , getExtendedRules : false , getRulesFromHostname : false , getRulesFromDOM : true , } ) ; if ( active === false ) { return ; } this . injectStylesIntoFrame ( frame , styles ) . catch ( ( ) => { } ) ; } } ) ; let numberOfIterations = 0 ; do { if ( frame . isDetached ( ) ) { break ; } try { const foundNewFeatures = observer . handleNewFeatures ( await frame . $$eval ( ':root' , extractFeaturesFromDOM ) ) ; numberOfIterations += 1 ; if ( numberOfIterations === 10 ) { break ; } if ( foundNewFeatures === false ) { break ; } } catch ( ex ) { break ; } if ( this . config . enableMutationObserver === false ) { break ; } await sleep ( 500 ) ; } while ( true ) ; } ; public onRequest = async ( route ) : Promise < void > => { const details = route . request ( ) ; const request = fromPlaywrightDetails ( details ) ; if ( this . config . guessRequestTypeFromUrl === true && request . type === 'other' ) { request . guessTypeOfRequest ( ) ; } const frame = details . frame ( ) ; if ( request . isMainFrame ( ) || ( request . type === 'document' && frame !== null && frame . parentFrame ( ) === null ) ) { route . continue ( ) ; return ; } const { redirect , match } = this . match ( request ) ; if ( redirect !== undefined ) { if ( redirect . contentType . endsWith ( ';base64' ) ) { route . fulfill ( { body : Buffer . from ( redirect . body , 'base64' ) , contentType : redirect . contentType . slice ( 0 , - 7 ) , } ) ; } else { route . fulfill ( { body : redirect . body , contentType : redirect . contentType , } ) ; } } else if ( match === true ) { route . abort ( 'blockedbyclient' ) ; } else { route . continue ( ) ; } } ; private async injectStylesIntoFrame ( frame , styles ) { if ( styles . length !== 0 ) { await frame . addStyleTag ( { content : styles , } ) ; } } private async injectScriptletsIntoFrame ( frame , scripts ) { const promises = [ ] ; if ( scripts . length !== 0 ) { for ( const script of scripts ) { promises . push ( frame . addScriptTag ( { content : autoRemoveScript ( script ) , } ) ) ; } } await Promise . all ( promises ) ; } /**
     * Look for sub-frames in `frame`, check if their `src` or `href` would be
     * blocked, and then proceed to removing them from the DOM completely.
     */ private async removeBlockedFrames ( frame ) { const promises = [ ] ; const sourceUrl = getTopLevelUrl ( frame ) ; for ( const url of await frame . $$eval ( 'iframe[src],iframe[href]' , ( elements ) => elements . map ( ( { src , href } ) => src || href ) ) ) { const { match } = this . match ( Request . fromRawDetails ( { url , sourceUrl , type : 'sub_frame' , } ) ) ; if ( match ) { promises . push ( frame . $$eval ( `iframe[src="${ url }"],iframe[href="${ url }"]` , ( iframes ) => { for ( const iframe of iframes ) { iframe ?. parentNode ?. removeChild ( iframe ) ; } } ) . catch ( ( ) => { } ) ) ; } } await Promise . all ( promises ) ; } } export * from '@cliqz/adblocker' ;