author Santo Pfingsten
 *  see https import { getValidHostname , DEFAULT_COOKIE_STORE_ID } from "../shared" ; import { browser , Tabs } from "webextension-polyfill-ts" ; import { getFirstPartyCookieDomain } from "./backgroundHelpers" ; import { RequestWatcher , RequestWatcherListener } from "./requestWatcher" ; import { TabInfo } from "./tabInfo" ; export interface TabWatcherListener { onDomainEnter ( cookieStoreId , hostname ) : void ; onDomainLeave ( cookieStoreId , hostname ) : void ; } export class TabWatcher implements RequestWatcherListener { public destroy : ( ) => void = ( ) => undefined ; private readonly listener : TabWatcherListener ; private readonly tabInfos : { [ s ] : TabInfo ; } = { } ; private readonly tabInfosByCookieStore : { [ s ] : TabInfo [ ] ; } = { } ; public constructor ( listener ) { this . listener = listener ; this . checkDomainLeaveSet = this . checkDomainLeaveSet . bind ( this ) ; browser . tabs . query ( { } ) . then ( ( tabs ) => { for ( const tab of tabs ) this . onTabCreated ( tab ) ; browser . tabs . onRemoved . addListener ( this . onTabRemoved = this . onTabRemoved . bind ( this ) ) ; browser . tabs . onCreated . addListener ( this . onTabCreated = this . onTabCreated . bind ( this ) ) ; new RequestWatcher ( this ) ; } ) ; } public prepareNavigation ( tabId , frameId , hostname ) { const tabInfo = this . tabInfos [ tabId ] ; tabInfo && this . checkDomainLeave ( tabInfo . cookieStoreId , tabInfo . prepareNavigation ( frameId , hostname ) ) ; } public commitNavigation ( tabId , frameId , hostname ) { const tabInfo = this . tabInfos [ tabId ] ; if ( tabInfo ) { this . checkDomainEnter ( tabInfo . cookieStoreId , hostname ) ; this . checkDomainLeaveSet ( tabInfo . cookieStoreId , tabInfo . commitNavigation ( frameId , hostname ) ) ; } } public completeNavigation ( tabId , frameId ) { const tabInfo = this . tabInfos [ tabId ] ; tabInfo && tabInfo . scheduleDeadFramesCheck ( ) ; } private checkDomainEnter ( cookieStoreId , hostname ) { if ( hostname && ! this . cookieStoreContainsDomain ( cookieStoreId , hostname , false ) ) this . listener . onDomainEnter ( cookieStoreId , hostname ) ; } private checkDomainLeaveSet ( cookieStoreId , hostnames ) { for ( const hostname of hostnames ) hostname && this . checkDomainLeave ( cookieStoreId , hostname ) ; } private checkDomainLeave ( cookieStoreId , hostname ) { if ( hostname && ! this . cookieStoreContainsDomain ( cookieStoreId , hostname , true ) ) this . listener . onDomainLeave ( cookieStoreId , hostname ) ; } public cookieStoreContainsDomain ( cookieStoreId , domain , checkNext ) { const list = this . tabInfosByCookieStore [ cookieStoreId ] ; return list ? list . some ( ( ti ) => ti . contains ( domain , checkNext ) ) : false ; } public containsDomain ( domain ) { for ( const key in this . tabInfos ) { const ti = this . tabInfos [ key ] ; if ( ti . contains ( domain , true ) ) return true ; } return false ; } public containsRuleFP ( rule ) { for ( const key in this . tabInfos ) { const ti = this . tabInfos [ key ] ; if ( ti . containsRuleFP ( rule ) ) return true ; } return false ; } private onTabRemoved ( tabId ) { const tabInfo = this . tabInfos [ tabId ] ; if ( tabInfo ) { delete this . tabInfos [ tabId ] ; const list = this . tabInfosByCookieStore [ tabInfo . cookieStoreId ] ; if ( list ) { const index = list . findIndex ( ( ti ) => ti . tabId === tabId ) ; if ( index !== - 1 ) list . splice ( index , 1 ) ; } this . checkDomainLeaveSet ( tabInfo . cookieStoreId , tabInfo . commitNavigation ( 0 , "" ) ) ; } } private onTabCreated ( tab ) { if ( tab . id && ! tab . incognito ) { const cookieStoreId = tab . cookieStoreId || DEFAULT_COOKIE_STORE_ID ; const hostname = tab . url ? getValidHostname ( tab . url ) : "" ; this . checkDomainEnter ( cookieStoreId , hostname ) ; const tabInfo = this . tabInfos [ tab . id ] = new TabInfo ( tab . id , hostname , cookieStoreId , this . checkDomainLeaveSet ) ; const list = this . tabInfosByCookieStore [ cookieStoreId ] ; if ( ! list ) this . tabInfosByCookieStore [ cookieStoreId ] = [ tabInfo ] ; else { const index = list . findIndex ( ( ti ) => ti . tabId === tabInfo . tabId ) ; if ( index === - 1 ) list . push ( tabInfo ) ; else list [ index ] = tabInfo ; } } } public isThirdPartyCookieOnTab ( tabId , domain ) { const tabInfo = this . tabInfos [ tabId ] ; if ( ! tabInfo ) return false ; return ! tabInfo . matchHostnameFP ( getFirstPartyCookieDomain ( domain ) ) ; } public cookieStoreContainsDomainFP ( storeId , domainFP , deep ) { const tabInfos = this . tabInfosByCookieStore [ storeId ] ; if ( ! tabInfos || ! tabInfos . length ) return false ; if ( deep ) return tabInfos . some ( ( ti ) => ti . containsHostnameFP ( domainFP ) ) ; return tabInfos . some ( ( ti ) => ti . matchHostnameFP ( domainFP ) ) ; } }