import { Browser , Runtime , WebRequest } from 'webextension-polyfill-ts' ; import { parse } from 'tldts-experimental' ; import { FiltersEngine , HTMLSelector , isUTF8 , Request , StreamingHtmlFilter , } from '@cliqz/adblocker' ; import { IBackgroundCallback , IMessageFromBackground } from '@cliqz/adblocker-content' ; export type OnBeforeRequestDetailsType = Pick < WebRequest . OnBeforeRequestDetailsType , 'url' | 'type' | 'requestId' | 'tabId' | 'originUrl' | 'documentUrl' > & { initiator ? : string ; } ; type OnHeadersReceivedDetailsType = Pick < WebRequest . OnHeadersReceivedDetailsType , 'responseHeaders' | 'url' | 'type' | 'tabId' | 'requestId' > ; type StreamFilter = WebRequest . StreamFilter & { onstart : ( event : any ) => void ; ondata : ( event : { data : ArrayBuffer ; } ) => void ; onstop : ( event : any ) => void ; onerror : ( event : any ) => void ; } ; /**
 * Create an instance of `Request` from WebRequest details.
 */ export function fromWebRequestDetails ( details ) { const sourceUrl = details . initiator || details . originUrl || details . documentUrl ; return Request . fromRawDetails ( sourceUrl ? { _originalRequestDetails : details , requestId : details . requestId , sourceUrl , tabId : details . tabId , type : details . type , url : details . url , } : { _originalRequestDetails : details , requestId : details . requestId , tabId : details . tabId , type : details . type , url : details . url , } ) ; } /**
 * Helper used when injecting custom CSP headers to update `responseHeaders`.
 */ export function updateResponseHeadersWithCSP ( details , policies ) { if ( policies === undefined ) { return { } ; } let responseHeaders = details . responseHeaders || [ ] ; const CSP_HEADER_NAME = 'content-security-policy' ; responseHeaders . forEach ( ( { name , value } ) => { if ( name . toLowerCase ( ) === CSP_HEADER_NAME ) { policies += `; ${ value }` ; } } ) ; responseHeaders = responseHeaders . filter ( ( { name } ) => name . toLowerCase ( ) !== CSP_HEADER_NAME ) ; responseHeaders . push ( { name : CSP_HEADER_NAME , value : policies } ) ; return { responseHeaders } ; } /**
 * Enable stream HTML filter on request `id` using `rules`.
 */ export function filterRequestHTML ( filterResponseData , { id } , rules ) { const filter = filterResponseData ( id ) as StreamFilter ; const decoder = new TextDecoder ( ) ; const encoder = new TextEncoder ( ) ; const htmlFilter = new StreamingHtmlFilter ( rules ) ; const teardown = ( event ) => { try { const remaining = htmlFilter . write ( decoder . decode ( ) ) + htmlFilter . flush ( ) ; if ( remaining . length !== 0 ) { filter . write ( encoder . encode ( remaining ) ) ; } } catch ( ex ) { console . error ( 'Failed to flush HTML filterer' , ex ) ; } if ( event . data !== undefined ) { filter . write ( event . data ) ; } filter . disconnect ( ) ; } ; filter . ondata = ( event ) => { if ( isUTF8 ( new Uint8Array ( event . data ) ) === false ) { return teardown ( event ) ; } try { filter . write ( encoder . encode ( htmlFilter . write ( decoder . decode ( event . data , { stream : true } ) ) ) ) ; } catch ( ex ) { return teardown ( event ) ; } } ; filter . onstop = ( ) => { teardown ( { } ) ; } ; } /**
 * This abstraction takes care of blocking in one instance of `browser` (in
 * practice this would be `chrome` or `browser` global in the WebExtension
 * context).
 */ export class BlockingContext { private readonly onBeforeRequest : ( details : OnBeforeRequestDetailsType ) => WebRequest . BlockingResponse ; private readonly onHeadersReceived : ( details : OnHeadersReceivedDetailsType ) => WebRequest . BlockingResponse ; private readonly onRuntimeMessage : ( msg : IBackgroundCallback & { action ? : string ; } , sender : Runtime . MessageSender ) => Promise < IMessageFromBackground | undefined > ; constructor ( private readonly browser , private readonly blocker ) { this . onBeforeRequest = ( details ) => blocker . onBeforeRequest ( browser , details ) ; this . onHeadersReceived = ( details ) => blocker . onHeadersReceived ( browser , details ) ; this . onRuntimeMessage = ( msg , sender ) => blocker . onRuntimeMessage ( browser , msg , sender ) ; } public enable ( ) { if ( this . blocker . config . loadNetworkFilters === true && this . browser . webRequest !== undefined ) { this . browser . webRequest . onBeforeRequest . addListener ( this . onBeforeRequest , { urls : [ '<all_urls>' ] } , [ 'blocking' ] ) ; this . browser . webRequest . onHeadersReceived . addListener ( this . onHeadersReceived , { urls : [ '<all_urls>' ] , types : [ 'main_frame' ] } , [ 'blocking' , 'responseHeaders' ] ) ; } if ( this . blocker . config . loadCosmeticFilters === true && this . browser . runtime !== undefined && this . browser . runtime . onMessage !== undefined ) { this . browser . runtime . onMessage . addListener ( this . onRuntimeMessage ) ; } } public disable ( ) { if ( this . browser . webRequest !== undefined ) { this . browser . webRequest . onBeforeRequest . removeListener ( this . onBeforeRequest ) ; this . browser . webRequest . onHeadersReceived . removeListener ( this . onHeadersReceived ) ; } if ( this . browser . runtime !== undefined && this . browser . runtime . onMessage !== undefined ) { this . browser . runtime . onMessage . removeListener ( this . onRuntimeMessage ) ; } } } /**
 * Wrap `FiltersEngine` into a WebExtension-friendly helper class. It exposes
 * methods to interface with WebExtension APIs needed to block ads.
 */ export class WebExtensionBlocker extends FiltersEngine { private readonly contexts : WeakMap < Browser , BlockingContext > = new WeakMap ( ) ; public enableBlockingInBrowser ( browser ) { let context = this . contexts . get ( browser ) ; if ( context !== undefined ) { return context ; } context = new BlockingContext ( browser , this ) ; this . contexts . set ( browser , context ) ; context . enable ( ) ; return context ; } public disableBlockingInBrowser ( browser ) { const context = this . contexts . get ( browser ) ; if ( context === undefined ) { throw new Error ( 'Trying to disable blocking which was not enabled' ) ; } this . contexts . delete ( browser ) ; context . disable ( ) ; } public isBlockingEnabled ( browser ) { return this . contexts . has ( browser ) ; } /**
     * This methods takes care of optionally performing HTML filtering.
     *
     * This can only be done if:
     * 1. Request is 'main_frame'
     * 2. `enableHtmlFiltering` is set to `true`.
     * 3. `browser.webRequest.filterResponseData` (Firefox only!).
     * 4. `TextEncoder` and `TextDecoder` are available.
     */ public performHTMLFiltering ( browser , request ) { if ( this . config . enableHtmlFiltering === true && browser . webRequest !== undefined && browser . webRequest . filterResponseData !== undefined && request . isMainFrame ( ) === true && typeof TextDecoder !== 'undefined' && typeof TextEncoder !== 'undefined' ) { const htmlFilters = this . getHtmlFilters ( request ) ; if ( htmlFilters . length !== 0 ) { filterRequestHTML ( browser . webRequest . filterResponseData , request , htmlFilters ) ; } } } private handleRuntimeMessage = async ( browser , msg , sender , sendResponse ) : Promise < void > => { const promises = [ ] ; if ( sender . tab === undefined ) { throw new Error ( 'required "sender.tab" information is not available' ) ; } if ( sender . tab . id === undefined ) { throw new Error ( 'required "sender.tab.id" information is not available' ) ; } if ( sender . frameId === undefined ) { throw new Error ( 'required "sender.frameId" information is not available' ) ; } if ( msg . action === 'getCosmeticsFilters' ) { const { url = '' , frameId } = sender ; const parsed = parse ( url ) ; const hostname = parsed . hostname || '' ; const domain = parsed . domain || '' ; if ( frameId === 0 && msg . lifecycle === 'start' ) { const { active , styles } = this . getCosmeticsFilters ( { domain , hostname , url , classes : msg . classes , hrefs : msg . hrefs , ids : msg . ids , getBaseRules : true , getInjectionRules : false , getExtendedRules : false , getRulesFromDOM : false , getRulesFromHostname : false , } ) ; if ( active === false ) { return ; } promises . push ( this . injectStylesWebExtension ( browser , styles , { tabId : sender . tab . id , allFrames : true , } ) ) ; } { const { active , styles , scripts , extended } = this . getCosmeticsFilters ( { domain , hostname , url , classes : msg . classes , hrefs : msg . hrefs , ids : msg . ids , getBaseRules : false , getInjectionRules : msg . lifecycle === 'start' , getExtendedRules : msg . lifecycle === 'start' , getRulesFromHostname : msg . lifecycle === 'start' , getRulesFromDOM : msg . lifecycle === 'dom-update' , } ) ; if ( active === false ) { return ; } promises . push ( this . injectStylesWebExtension ( browser , styles , { tabId : sender . tab . id , frameId } ) ) ; if ( scripts . length !== 0 ) { sendResponse ( { active , extended , scripts , styles : '' , } ) ; } } } await Promise . all ( promises ) ; } ; /**
     * Deal with request cancellation (`{ cancel: true }`) and redirection (`{ redirectUrl: '...' }`).
     */ public onBeforeRequest = ( browser , details ) : WebRequest . BlockingResponse => { const request = fromWebRequestDetails ( details ) ; if ( this . config . guessRequestTypeFromUrl === true && request . type === 'other' ) { request . guessTypeOfRequest ( ) ; } if ( request . isMainFrame ( ) ) { this . performHTMLFiltering ( browser , request ) ; return { } ; } const { redirect , match } = this . match ( request ) ; if ( redirect !== undefined ) { return { redirectUrl : redirect . dataUrl } ; } else if ( match === true ) { return { cancel : true } ; } return { } ; } ; public onHeadersReceived = ( _ , details ) : WebRequest . BlockingResponse => { return updateResponseHeadersWithCSP ( details , this . getCSPDirectives ( fromWebRequestDetails ( details ) ) ) ; } ; public onRuntimeMessage = ( browser , msg , sender ) : Promise < IMessageFromBackground | undefined > => { return new Promise ( ( resolve , reject ) => { this . handleRuntimeMessage ( browser , msg , sender , resolve ) . catch ( reject ) ; } ) ; } ; private async injectStylesWebExtension ( browser , styles , { tabId , frameId , allFrames = false , } ) { if ( styles . length === 0 ) { return ; } if ( browser . tabs === undefined ) { throw new Error ( 'required "tabs" API is not defined' ) ; } if ( browser . tabs . insertCSS === undefined ) { throw new Error ( 'required "tabs.insertCSS" API is not defined' ) ; } return browser . tabs . insertCSS ( tabId , frameId ? { allFrames , code : styles , cssOrigin : 'user' , frameId , matchAboutBlank : true , runAt : 'document_start' , } : { allFrames , code : styles , cssOrigin : 'user' , matchAboutBlank : true , runAt : 'document_start' , } ) ; } } export * from '@cliqz/adblocker' ;