author Santo Pfingsten
 *  see https import { messageUtil } from "../lib/messageUtil" ; import { isFirefox , browserInfo , isNodeTest } from "./browserInfo" ; import { SettingsTypeMap , SettingsSignature , RuleDefinition , CleanupType } from "./settingsSignature" ; import { browser , Storage } from "webextension-polyfill-ts" ; import { getRegExForRule } from "./regexp" ; import migrateSettings , { manifestVersion } from "./settingsMigrations" ; type Callback = ( ) => void ; type SettingsValue = string | boolean | number | ( RuleDefinition [ ] ) | { [ s ] : boolean ; } ; export type SettingsMap = { [ s ] : SettingsValue ; } ; export const localStorageDefault = isNodeTest || ( isFirefox && browserInfo . versionAsNumber >= 58 ) ; export const defaultSettings = { "version" : manifestVersion , "showUpdateNotification" : true , "showCookieRemovalNotification" : false , "rules" : [ ] , "whitelistNoTLD" : false , "whitelistFileSystem" : true , "fallbackRule" : CleanupType . LEAVE , "domainsToClean" : { } , "downloadsToClean" : { } , "showBadge" : true , "initialTab" : "this_tab" , "lastTab" : "this_tab" , "cleanAll.cookies" : true , "cleanAll.cookies.applyRules" : true , "cleanAll.localStorage" : localStorageDefault , "cleanAll.localStorage.applyRules" : localStorageDefault , "cleanAll.protectOpenDomains" : true , "cleanAll.history" : false , "cleanAll.history.applyRules" : true , "cleanAll.downloads" : true , "cleanAll.downloads.applyRules" : true , "cleanAll.formData" : false , "cleanAll.passwords" : false , "cleanAll.indexedDB" : true , "cleanAll.pluginData" : true , "cleanAll.serviceWorkers" : true , "cleanAll.serverBoundCertificates" : false , "cleanAll.cache" : false , "cleanThirdPartyCookies.enabled" : false , "cleanThirdPartyCookies.delay" : 60 , "cleanThirdPartyCookies.beforeCreation" : false , "domainLeave.enabled" : false , "domainLeave.delay" : 120 , "domainLeave.cookies" : true , "domainLeave.localStorage" : localStorageDefault , "domainLeave.history" : false , "domainLeave.downloads" : false , "instantly.enabled" : true , "instantly.cookies" : true , "instantly.history" : false , "instantly.history.applyRules" : true , "instantly.downloads" : false , "instantly.downloads.applyRules" : true , "startup.enabled" : false , "startup.cookies" : true , "startup.cookies.applyRules" : true , "startup.localStorage" : localStorageDefault , "startup.localStorage.applyRules" : localStorageDefault , "startup.history" : false , "startup.history.applyRules" : true , "startup.downloads" : true , "startup.downloads.applyRules" : false , "startup.formData" : false , "startup.passwords" : false , "startup.indexedDB" : true , "startup.pluginData" : true , "startup.serviceWorkers" : true , "startup.serverBoundCertificates" : false , "startup.cache" : false , "purgeExpiredCookies" : false , "logRAD.enabled" : true , "logRAD.limit" : 20 } ; const isAlNum = /^[a-z0-9]+$/ ; const isAlNumDash = /^[a-z0-9\-]+$/ ; const validCookieName = /^[!,#,\$,%,&,',\*,\+,\-,\.,0-9,:,;,A-Z,\\,\^,_,`,a-z,\|,~]+$/i ; function isValidExpressionPart ( part ) { if ( part . length === 0 ) return false ; if ( part === "*" ) return true ; return isAlNum . test ( part [ 0 ] ) && isAlNum . test ( part [ part . length - 1 ] ) && isAlNumDash . test ( part ) ; } function isValidDomainExpression ( exp ) { const parts = exp . split ( "." ) ; return parts . length > 0 && parts . findIndex ( ( p ) => ! isValidExpressionPart ( p ) ) === - 1 ; } export function isValidExpression ( exp ) { const parts = exp . split ( "@" ) ; if ( parts . length === 1 ) return isValidDomainExpression ( exp ) ; return parts . length === 2 && validCookieName . test ( parts [ 0 ] ) && isValidDomainExpression ( parts [ 1 ] ) ; } export function classNameForCleanupType ( type ) { if ( type === CleanupType . NEVER ) return "cleanup_type_never" ; if ( type === CleanupType . STARTUP ) return "cleanup_type_startup" ; if ( type === CleanupType . INSTANTLY ) return "cleanup_type_instantly" ; return "cleanup_type_leave" ; } export function cleanupTypeForElement ( element ) { if ( element . classList . contains ( "cleanup_type_never" ) ) return CleanupType . NEVER ; if ( element . classList . contains ( "cleanup_type_startup" ) ) return CleanupType . STARTUP ; if ( element . classList . contains ( "cleanup_type_leave" ) ) return CleanupType . LEAVE ; if ( element . classList . contains ( "cleanup_type_instantly" ) ) return CleanupType . INSTANTLY ; return null ; } function isValidCleanupType ( type ) { return type === CleanupType . NEVER || type === CleanupType . STARTUP || type === CleanupType . LEAVE || type === CleanupType . INSTANTLY ; } function sanitizeRules ( rules , expressionValidator ) { const validRules = [ ] ; for ( const ruleDef of rules ) { if ( typeof ( ruleDef . rule ) === "string" && expressionValidator ( ruleDef . rule ) && isValidCleanupType ( ruleDef . type ) ) { validRules . push ( { rule : ruleDef . rule , type : ruleDef . type } ) ; } } return validRules ; } interface CompiledRuleDefinition { definition : RuleDefinition ; regex : RegExp ; cookieName ? : string ; } export class Settings { private rules : CompiledRuleDefinition [ ] = [ ] ; private cookieRules : CompiledRuleDefinition [ ] = [ ] ; private readonly storage : Storage . StorageArea ; private map : SettingsMap = { } ; private readyCallbacks : Callback [ ] | null = [ ] ; public constructor ( ) { this . storage = browser . storage . local ; this . load ( ) ; this . load = this . load . bind ( this ) ; browser . storage . onChanged . addListener ( this . load ) ; } public load ( changes ? ) { this . storage . get ( null ) . then ( ( map ) => { this . map = map ; const changedKeys = Object . getOwnPropertyNames ( changes || map ) ; if ( changedKeys . indexOf ( "rules" ) >= 0 ) this . rebuildRules ( ) ; if ( this . readyCallbacks ) { for ( const callback of this . readyCallbacks ) callback ( ) ; this . readyCallbacks = null ; } if ( typeof ( messageUtil ) !== "undefined" ) { messageUtil . send ( "settingsChanged" , changedKeys ) ; messageUtil . sendSelf ( "settingsChanged" , changedKeys ) ; } } ) ; } public rebuildRules ( ) { this . rules = [ ] ; this . cookieRules = [ ] ; const rules = this . get ( "rules" ) ; for ( const rule of rules ) { const parts = rule . rule . split ( "@" ) ; const isCookieRule = parts . length === 2 ; if ( isCookieRule ) { this . cookieRules . push ( { definition : rule , regex : getRegExForRule ( parts [ 1 ] ) , cookieName : parts [ 0 ] . toLowerCase ( ) } ) ; } else { this . rules . push ( { definition : rule , regex : getRegExForRule ( rule . rule ) } ) ; } } } public save ( ) { return this . storage . set ( this . map ) ; } public onReady ( callback ) { if ( this . readyCallbacks ) this . readyCallbacks . push ( callback ) ; else callback ( ) ; } public restoreDefaults ( ) { this . map = { } ; this . storage . clear ( ) ; this . rebuildRules ( ) ; this . save ( ) ; } public performUpgrade ( previousVersion ) { migrateSettings ( previousVersion , this . map ) ; } public setAll ( json ) { if ( typeof ( json ) !== "object" ) return false ; if ( json . rules ) { if ( ! Array . isArray ( json . rules ) ) delete json . rules ; else ( json as any ) . rules = sanitizeRules ( ( json as any ) . rules as RuleDefinition [ ] , isValidExpression ) ; } for ( const key in json ) { if ( ! json . hasOwnProperty ( key ) ) continue ; if ( ! defaultSettings . hasOwnProperty ( key ) ) { if ( ! isNodeTest ) console . warn ( "Unknown setting: " , key ) ; delete json [ key ] ; } if ( typeof ( defaultSettings [ key ] ) !== typeof ( json [ key ] ) ) { if ( ! isNodeTest ) console . warn ( "Types do not match while importing setting: " , key , typeof ( defaultSettings [ key ] ) , typeof ( json [ key ] ) ) ; delete json [ key ] ; } } const keysToRemove = Object . getOwnPropertyNames ( this . getAll ( ) ) . filter ( ( key ) => ! json . hasOwnProperty ( key ) ) ; this . storage . remove ( keysToRemove ) ; this . map = json ; this . performUpgrade ( this . get ( "version" ) ) ; this . set ( "version" , browser . runtime . getManifest ( ) . version ) ; this . rebuildRules ( ) ; this . save ( ) ; return true ; } public getAll ( ) { const result = { } ; for ( const key in defaultSettings ) { if ( this . map . hasOwnProperty ( key ) ) result [ key ] = this . map [ key ] ; else result [ key ] = defaultSettings [ key ] ; } return result as SettingsSignature ; } public get < K extends keyof SettingsTypeMap > ( key ) { if ( this . map . hasOwnProperty ( key ) ) return this . map [ key ] as SettingsTypeMap [ K ] ; return defaultSettings [ key ] as SettingsTypeMap [ K ] ; } public set < K extends keyof SettingsTypeMap > ( key , value ) { this . map [ key ] = value ; if ( key === "rules" ) this . rebuildRules ( ) ; } public getExactRuleDefinition ( expression ) { for ( const crd of this . rules ) { if ( crd . definition . rule === expression ) return crd . definition ; } return null ; } public getExactCleanupType ( expression ) { const definition = this . getExactRuleDefinition ( expression ) ; return definition && definition . type ; } public getMatchingRules ( domain , cookieName = false ) { const rules = cookieName !== false ? this . cookieRules : this . rules ; const lowerCookieName = cookieName && cookieName . toLowerCase ( ) ; const matchingRules = [ ] ; for ( const rule of rules ) { if ( rule . regex . test ( domain ) && ( ! rule . cookieName || rule . cookieName === lowerCookieName ) ) matchingRules . push ( rule . definition ) ; } return matchingRules ; } public hasBlockingRule ( ) { return this . get ( "fallbackRule" ) === CleanupType . INSTANTLY || ! ! this . get ( "rules" ) . find ( ( r ) => r . type === CleanupType . INSTANTLY ) ; } private getCleanupTypeFromMatchingRules ( matchingRules ) { if ( matchingRules . find ( ( r ) => r . type === CleanupType . INSTANTLY ) ) return CleanupType . INSTANTLY ; if ( matchingRules . find ( ( r ) => r . type === CleanupType . LEAVE ) ) return CleanupType . LEAVE ; if ( matchingRules . find ( ( r ) => r . type === CleanupType . NEVER ) ) return CleanupType . NEVER ; return CleanupType . STARTUP ; } public getCleanupTypeForCookie ( domain , name ) { if ( this . get ( "whitelistFileSystem" ) && domain . length === 0 ) return CleanupType . NEVER ; if ( this . get ( "whitelistNoTLD" ) && domain . length > 0 && domain . indexOf ( "." ) === - 1 ) return CleanupType . NEVER ; const matchingRules = this . getMatchingRules ( domain , name ) ; if ( matchingRules . length ) return this . getCleanupTypeFromMatchingRules ( matchingRules ) ; return this . getCleanupTypeForDomain ( domain ) ; } public getCleanupTypeForDomain ( domain ) { if ( this . get ( "whitelistFileSystem" ) && domain . length === 0 ) return CleanupType . NEVER ; if ( this . get ( "whitelistNoTLD" ) && domain . length > 0 && domain . indexOf ( "." ) === - 1 ) return CleanupType . NEVER ; const matchingRules = this . getMatchingRules ( domain ) ; if ( matchingRules . length ) return this . getCleanupTypeFromMatchingRules ( matchingRules ) ; return this . get ( "fallbackRule" ) ; } public isDomainProtected ( domain , ignoreStartupType ) { const type = this . getCleanupTypeForDomain ( domain ) ; return type === CleanupType . NEVER || ( type === CleanupType . STARTUP && ! ignoreStartupType ) ; } public isDomainBlocked ( domain ) { return this . getCleanupTypeForDomain ( domain ) === CleanupType . INSTANTLY ; } public getRulesForDomain ( domain ) { return this . rules . concat ( this . cookieRules ) . filter ( ( rule ) => rule . regex . test ( domain ) ) . map ( ( rule ) => rule . definition ) ; } public getChosenRulesForDomain ( domain ) { if ( this . get ( "whitelistFileSystem" ) && domain . length === 0 ) return [ ] ; if ( this . get ( "whitelistNoTLD" ) && domain . length > 0 && domain . indexOf ( "." ) === - 1 ) return [ ] ; const matchingRules = this . getMatchingRules ( domain ) ; if ( matchingRules . length ) { const types = [ CleanupType . INSTANTLY , CleanupType . LEAVE , CleanupType . NEVER , CleanupType . STARTUP ] ; for ( const type of types ) { const rules = matchingRules . filter ( ( r ) => r . type === type ) ; if ( rules . length ) return rules ; } } return [ ] ; } public setRule ( expression , type , temporary ) { const rules = this . get ( "rules" ) . slice ( ) ; let ruleDef = rules . find ( ( r ) => r . rule === expression ) ; if ( ruleDef ) ruleDef . type = type ; else { ruleDef = { rule : expression , type } ; rules . push ( ruleDef ) ; } if ( temporary ) ruleDef . temporary = true ; else delete ruleDef . temporary ; this . set ( "rules" , rules ) ; this . save ( ) ; } public removeRule ( expression ) { const rules = this . get ( "rules" ) . filter ( ( r ) => r . rule !== expression ) ; this . set ( "rules" , rules ) ; this . save ( ) ; } public removeRules ( rules ) { const remainingRules = this . get ( "rules" ) . filter ( ( r ) => rules . indexOf ( r . rule ) === - 1 ) ; this . set ( "rules" , remainingRules ) ; this . save ( ) ; this . rebuildRules ( ) ; } public getTemporaryRules ( ) { return this . rules . filter ( ( r ) => ! ! r . definition . temporary ) ; } public removeTemporaryRules ( ) { const rules = this . get ( "rules" ) . filter ( ( r ) => ! r . temporary ) ; this . set ( "rules" , rules ) ; this . save ( ) ; } } export const settings = new Settings ( ) ;