import { querySelectorAll } from '@cliqz/adblocker-extended-selectors' ; import { IBackgroundCallback , IMessageFromBackground , DOMMonitor , injectScript , } from '@cliqz/adblocker-content' ; type ExtendedSelector = IMessageFromBackground [ 'extended' ] [ number ] ; let ACTIVE = true ; let DOM_MONITOR = null ; let UPDATE_EXTENDED_TIMEOUT = null ; const PENDING = new Set ( ) ; const EXTENDED = [ ] ; const HIDDEN = new Map ( ) ; function unload ( ) { if ( DOM_MONITOR !== null ) { DOM_MONITOR . stop ( ) ; DOM_MONITOR = null ; } } /**
 * Because all the filters and matching logic lives in the background of the
 * extension, the content script needs a way to request relevant cosmetic
 * filters for each frame. This channel of communication can be handled in
 * several ways (`connect`, `sendMessage`). Here we will make use of
 * `sendMessage` for one-off communications.
 *
 * `getCosmeticsFiltersWithSendMessage` wraps the logic of communicating with
 * the background and will be used to request cosmetics filters for the current
 * frame.
 *
 * The background should listen to these messages and answer back with lists of
 * filters to be injected in the page.
 */ function getCosmeticsFiltersWithSendMessage ( arg ) { return new Promise ( ( resolve ) => { chrome . runtime . sendMessage ( { action : 'getCosmeticsFilters' , ... arg , } , ( response ) => { if ( response !== undefined ) { resolve ( response ) ; } } ) ; } ) ; } function cachedQuerySelector ( root , selector , cache ) { const cachedElements = cache . get ( root ) ?. get ( selector ) ; if ( cachedElements !== undefined ) { return cachedElements ; } const selected = new Set ( querySelectorAll ( root , selector . ast ) ) ; if ( selector . attribute !== undefined ) { let cachedSelectors = cache . get ( root ) ; if ( cachedSelectors === undefined ) { cachedSelectors = new Map ( ) ; cache . set ( root , cachedSelectors ) ; } let cachedSelected = cachedSelectors . get ( selector ) ; if ( cachedSelected === undefined ) { cachedSelected = new Set ( ) ; cachedSelectors . set ( selector , cachedSelected ) ; } for ( const element of selected ) { cachedSelected . add ( element ) ; } } return selected ; } function updateExtended ( ) { if ( PENDING . size === 0 || EXTENDED . length === 0 ) { return ; } const cache = new Map ( ) ; const elementsToHide = new Map ( ) ; const roots = [ ... PENDING ] . filter ( ( e ) => e . isConnected === true ) ; PENDING . clear ( ) ; for ( const root of roots ) { for ( const selector of EXTENDED ) { for ( const element of cachedQuerySelector ( root , selector , cache ) ) { if ( selector . remove === true ) { element . textContent = '' ; element . remove ( ) ; } else if ( selector . attribute !== undefined && HIDDEN . has ( element ) === false ) { elementsToHide . set ( element , { selector , root } ) ; } } } } for ( const [ element , { selector , root } ] of elementsToHide . entries ( ) ) { if ( selector . attribute !== undefined ) { element . setAttribute ( selector . attribute , '' ) ; HIDDEN . set ( element , { selector , root } ) ; } } for ( const [ element , { selector , root } ] of [ ... HIDDEN . entries ( ) ] ) { if ( selector . attribute !== undefined ) { if ( root . isConnected === false || element . isConnected === false || cachedQuerySelector ( root , selector , cache ) . has ( element ) === false ) { HIDDEN . delete ( element ) ; element . removeAttribute ( selector . attribute ) ; } } } } /**
 * Queue `elements` to be processed asynchronously in a batch way (for
 * efficiency).  This is important to not do more work than necessary, for
 * example if the same set of nodes is updated multiple times in a raw on
 * user-interaction (e.g. a dropdown); this allows to only check these nodes
 * once, and to not block the UI.
 */ function delayedUpdateExtended ( elements ) { if ( EXTENDED . length === 0 ) { return ; } if ( PENDING . has ( window . document . documentElement ) ) { return ; } for ( const element of elements ) { if ( element === window . document . documentElement ) { PENDING . clear ( ) ; PENDING . add ( element ) ; break ; } PENDING . add ( element ) ; } if ( UPDATE_EXTENDED_TIMEOUT === null ) { UPDATE_EXTENDED_TIMEOUT = setTimeout ( ( ) => { UPDATE_EXTENDED_TIMEOUT = null ; updateExtended ( ) ; } , 1000 ) ; } } function handleResponseFromBackground ( window , { active , scripts , extended } ) { if ( active === false ) { ACTIVE = false ; unload ( ) ; return ; } else { ACTIVE = true ; } if ( scripts ) { setTimeout ( ( ) => { for ( const script of scripts ) { injectScript ( script , window . document ) ; } } , 0 ) ; } if ( extended && extended . length > 0 ) { EXTENDED . push ( ... extended ) ; delayedUpdateExtended ( [ window . document . documentElement ] ) ; } } /**
 * Takes care of injecting cosmetic filters in a given window. Responsabilities:
 * - Inject scripts.
 * - Block scripts.
 *
 * NOTE: Custom stylesheets are now injected from background.
 *
 * All this happens by communicating with the background through the
 * `backgroundAction` function (to trigger request the sending of new rules
 * based on a domain or node selectors) and the `handleResponseFromBackground`
 * callback to apply new rules.
 */ export function injectCosmetics ( window , enableMutationObserver = true , getCosmeticsFilters = getCosmeticsFiltersWithSendMessage ) { getCosmeticsFilters ( { lifecycle : 'start' , ids : [ ] , classes : [ ] , hrefs : [ ] } ) . then ( ( response ) => handleResponseFromBackground ( window , response ) ) ; window . addEventListener ( 'DOMContentLoaded' , ( ) => { DOM_MONITOR = new DOMMonitor ( ( update ) => { if ( update . type === 'elements' ) { if ( update . elements . length !== 0 ) { delayedUpdateExtended ( update . elements ) ; } } else { getCosmeticsFilters ( { ... update , lifecycle : 'dom-update' } ) . then ( ( response ) => handleResponseFromBackground ( window , response ) ) ; } } ) ; DOM_MONITOR . queryAll ( window ) ; if ( ACTIVE && enableMutationObserver ) { DOM_MONITOR . start ( window ) ; } } , { once : true , passive : true } ) ; window . addEventListener ( 'unload' , unload , { once : true , passive : true } ) ; }