diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000..539b4cdd5c --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +submodules/ diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000000..3b909f8fd4 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,25 @@ +{ + "env": { + "browser": true, + "es2017": true + }, + "extends": [ + "standard" + ], + "globals": { + "__firefox__": "readonly", + "SECURITY_TOKEN": "readonly", + "$FEATURE_SETTINGS$": "readonly", + "$GPC_ENABLED$": "readonly", + "$BLOCKING_ENABLED$": "readonly", + "$TRACKER_DATA$": "readonly", + "$IS_DEBUG$": "readonly", + "webkit": "readonly" + }, + "parserOptions": { + "ecmaVersion": 7 + }, + "rules": { + "indent": ["error", 4] + } +} diff --git a/.gitignore b/.gitignore index 7b50623df9..665ba90fb1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ build/ DerivedData/ Carthage/Checkouts +node_modules/ ## Various settings *.pbxuser diff --git a/Core/ContentBlockerRulesUserScript.swift b/Core/ContentBlockerRulesUserScript.swift index d75522bde9..1c32ba9061 100644 --- a/Core/ContentBlockerRulesUserScript.swift +++ b/Core/ContentBlockerRulesUserScript.swift @@ -76,8 +76,8 @@ public class ContentBlockerRulesUserScript: NSObject, UserScript { + (privacyConfiguration.exceptionsList(forFeature: .contentBlocking).joined(separator: "\n")) return Self.loadJS("contentblockerrules", from: Bundle.core, withReplacements: [ - "${tempUnprotectedDomains}": remoteUnprotectedDomains, - "${userUnprotectedDomains}": privacyConfiguration.userUnprotectedDomains.joined(separator: "\n") + "$TEMP_UNPROTECTED_DOMAINS$": remoteUnprotectedDomains, + "$USER_UNPROTECTED_DOMAINS$": privacyConfiguration.userUnprotectedDomains.joined(separator: "\n") ]) } diff --git a/Core/DoNotSellUserScript.swift b/Core/DoNotSellUserScript.swift index 4af50c2891..e43c464da8 100644 --- a/Core/DoNotSellUserScript.swift +++ b/Core/DoNotSellUserScript.swift @@ -24,7 +24,7 @@ import BrowserServicesKit public class DoNotSellUserScript: NSObject, UserScript { public var source: String { return Self.loadJS("donotsell", from: Bundle.core, withReplacements: [ - "${gpcEnabled}": PrivacyConfigurationManager.shared.privacyConfig + "$GPC_ENABLED$": PrivacyConfigurationManager.shared.privacyConfig .isEnabled(featureKey: .gpc) ? "true" : "false" ]) } diff --git a/Core/FingerprintUserScript.swift b/Core/FingerprintUserScript.swift index 3d54f3c3c3..b0152d787e 100644 --- a/Core/FingerprintUserScript.swift +++ b/Core/FingerprintUserScript.swift @@ -43,10 +43,10 @@ public class FingerprintUserScript: NSObject, UserScript { .exceptionsList(forFeature: .fingerprintingScreenSize).joined(separator: "\n") return Self.loadJS("fingerprint", from: Bundle.core, withReplacements: [ - "${featureSettings}": featureSettings, - "${tempStorageExceptions}": tempStorageExceptions, - "${batteryExceptions}": batteryExceptions, - "${screenSizeExceptions}": screenSizeExceptions + "$FEATURE_SETTINGS$": featureSettings, + "$TEMP_STORAGE_EXCEPTIONS$": tempStorageExceptions, + "$BATTERY_EXCEPTIONS$": batteryExceptions, + "$SCREEN_SIZE_EXCEPTIONS$": screenSizeExceptions ]) } diff --git a/Core/LoginFormDetectionUserScript.swift b/Core/LoginFormDetectionUserScript.swift index 368a4a1dcf..02f83c682c 100644 --- a/Core/LoginFormDetectionUserScript.swift +++ b/Core/LoginFormDetectionUserScript.swift @@ -30,7 +30,7 @@ public class LoginFormDetectionUserScript: NSObject, UserScript { public lazy var source: String = { return Self.loadJS("login-form-detection", from: Bundle.core, withReplacements: [ - "${isDebug}": isDebugBuild ? "true" : "false" + "$IS_DEBUG$": isDebugBuild ? "true" : "false" ]) }() diff --git a/Core/SurrogatesUserScript.swift b/Core/SurrogatesUserScript.swift index d148a4bd63..24502ed285 100644 --- a/Core/SurrogatesUserScript.swift +++ b/Core/SurrogatesUserScript.swift @@ -104,12 +104,12 @@ public class SurrogatesUserScript: NSObject, UserScript { } return Self.loadJS("contentblocker", from: Bundle.core, withReplacements: [ - "${isDebug}": isDebugBuild ? "true" : "false", - "${tempUnprotectedDomains}": remoteUnprotectedDomains, - "${userUnprotectedDomains}": privacyConfiguration.userUnprotectedDomains.joined(separator: "\n"), - "${trackerData}": trackerData, - "${surrogates}": configurationSource.surrogates, - "${blockingEnabled}": privacyConfiguration.isEnabled(featureKey: .contentBlocking) ? "true" : "false" + "$IS_DEBUG$": isDebugBuild ? "true" : "false", + "$TEMP_UNPROTECTED_DOMAINS$": remoteUnprotectedDomains, + "$USER_UNPROTECTED_DOMAINS$": privacyConfiguration.userUnprotectedDomains.joined(separator: "\n"), + "$TRACKER_DATA$": trackerData, + "$SURROGATES$": configurationSource.surrogates, + "$BLOCKING_ENABLED$": privacyConfiguration.isEnabled(featureKey: .contentBlocking) ? "true" : "false" ]) } diff --git a/Core/contentblocker.js b/Core/contentblocker.js index 59dd5098a7..a59363640a 100644 --- a/Core/contentblocker.js +++ b/Core/contentblocker.js @@ -17,83 +17,71 @@ // limitations under the License. // -(function() { - - if (${isDebug}) { - var duckduckgoDebugMessaging = function() { - - function signpostEvent(data) { +(function () { + const duckduckgoDebugMessaging = (function () { + let log = () => {} + let signpostEvent = () => {} + + if ($IS_DEBUG$) { + signpostEvent = function signpostEvent (data) { try { - webkit.messageHandlers.signpostMessage.postMessage(data); - } catch(error) {} + webkit.messageHandlers.signpostMessage.postMessage(data) + } catch (error) {} } - - function log() { + + log = function log () { try { - webkit.messageHandlers.log.postMessage(JSON.stringify(arguments)); - } catch(error) {} - } - - return { - signpostEvent: signpostEvent, - log: log + webkit.messageHandlers.log.postMessage(JSON.stringify(arguments)) + } catch (error) {} } - }() - } else { - var duckduckgoDebugMessaging = function() { - - function signpostEvent(data) {} - - function log() {} - - return { - signpostEvent: signpostEvent, - log: log - } - }() - } + } - function surrogateInjected(data) { - try { - webkit.messageHandlers.trackerDetectedMessage.postMessage(data); - } catch(error) { - // webkit might not be defined - } - } + return { + signpostEvent, + log + } + }()) - // tld.js - var tldjs = { + function surrogateInjected (data) { + try { + webkit.messageHandlers.trackerDetectedMessage.postMessage(data) + } catch (error) { + // webkit might not be defined + } + } - parse: function(url) { + // tld.js + const tldjs = { - if (url.startsWith("//")) { - url = "http:" + url; + parse: function (url) { + if (url.startsWith('//')) { + url = 'http:' + url } try { - var parsed = new URL(url); + const parsed = new URL(url) return { domain: parsed.hostname, hostname: parsed.hostname } - } catch(error) { + } catch (error) { return { - domain: "", - hostname: "" + domain: '', + hostname: '' } } } - }; + } // tld.js // util.js - var utils = { + const utils = { - extractHostFromURL: function(url, shouldKeepWWW) { + extractHostFromURL: function (url, shouldKeepWWW) { if (!url) return '' - let urlObj = tldjs.parse(url) + const urlObj = tldjs.parse(url) let hostname = urlObj.hostname || '' if (!shouldKeepWWW) { @@ -103,97 +91,90 @@ return hostname } - }; + } // util.js // Base64 -/** + /** * * Base64 encode / decode * http://www.webtoolkit.info/ * **/ -var Base64 = { - -// private property -_keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", + const Base64 = { -// public method for encoding -encode : function (input) { - var output = ""; - var chr1, chr2, chr3, enc1, enc2, enc3, enc4; - var i = 0; + // private property + _keyStr: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=', - input = Base64._utf8_encode(input); + // public method for encoding + encode: function (input) { + let output = '' + let chr1, chr2, chr3, enc1, enc2, enc3, enc4 + let i = 0 - while (i < input.length) { + input = Base64._utf8_encode(input) - chr1 = input.charCodeAt(i++); - chr2 = input.charCodeAt(i++); - chr3 = input.charCodeAt(i++); + while (i < input.length) { + chr1 = input.charCodeAt(i++) + chr2 = input.charCodeAt(i++) + chr3 = input.charCodeAt(i++) - enc1 = chr1 >> 2; - enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); - enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); - enc4 = chr3 & 63; + enc1 = chr1 >> 2 + enc2 = ((chr1 & 3) << 4) | (chr2 >> 4) + enc3 = ((chr2 & 15) << 2) | (chr3 >> 6) + enc4 = chr3 & 63 - if (isNaN(chr2)) { - enc3 = enc4 = 64; - } else if (isNaN(chr3)) { - enc4 = 64; - } + if (isNaN(chr2)) { + enc3 = enc4 = 64 + } else if (isNaN(chr3)) { + enc4 = 64 + } - output = output + + output = output + this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) + - this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4); - - } + this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4) + } - return output; -}, + return output + }, -// private method for UTF-8 encoding -_utf8_encode : function (string) { - string = string.replace(/\r\n/g,"\n"); - var utftext = ""; + // private method for UTF-8 encoding + _utf8_encode: function (string) { + string = string.replace(/\r\n/g, '\n') + let utftext = '' - for (var n = 0; n < string.length; n++) { + for (let n = 0; n < string.length; n++) { + const c = string.charCodeAt(n) - var c = string.charCodeAt(n); + if (c < 128) { + utftext += String.fromCharCode(c) + } else if ((c > 127) && (c < 2048)) { + utftext += String.fromCharCode((c >> 6) | 192) + utftext += String.fromCharCode((c & 63) | 128) + } else { + utftext += String.fromCharCode((c >> 12) | 224) + utftext += String.fromCharCode(((c >> 6) & 63) | 128) + utftext += String.fromCharCode((c & 63) | 128) + } + } - if (c < 128) { - utftext += String.fromCharCode(c); - } - else if((c > 127) && (c < 2048)) { - utftext += String.fromCharCode((c >> 6) | 192); - utftext += String.fromCharCode((c & 63) | 128); - } - else { - utftext += String.fromCharCode((c >> 12) | 224); - utftext += String.fromCharCode(((c >> 6) & 63) | 128); - utftext += String.fromCharCode((c & 63) | 128); + return utftext } } - - return utftext; -}, - -} -// Base64 + // Base64 // Buffer class Buffer { - - static from(string, type) { - return new Buffer(string); + static from (string, type) { + return new Buffer(string) } - constructor(string) { - this.string = string; + constructor (string) { + this.string = string } - toString(type) { + toString (type) { return Base64.encode(this.string) } } @@ -219,9 +200,9 @@ _utf8_encode : function (string) { } processTrackerList (data) { - for (let name in data) { + for (const name in data) { if (data[name].rules) { - for (let i in data[name].rules) { + for (const i in data[name].rules) { data[name].rules[i].rule = new RegExp(data[name].rules[i].rule, 'ig') } } @@ -231,7 +212,7 @@ _utf8_encode : function (string) { processEntityList (data) { const processed = {} - for (let entity in data) { + for (const entity in data) { data[entity].domains.forEach(domain => { processed[domain] = entity }) @@ -305,7 +286,7 @@ _utf8_encode : function (string) { const fullTrackerDomain = requestData.urlToCheckSplit.join('.') - const {action, reason} = this.getAction({ + const { action, reason } = this.getAction({ firstParty, matchedRule, matchedRuleException, @@ -329,10 +310,10 @@ _utf8_encode : function (string) { * Pull subdomains off of the reqeust rule and look for a matching tracker object in our data */ findTracker (requestData) { - let urlList = Array.from(requestData.urlToCheckSplit) + const urlList = Array.from(requestData.urlToCheckSplit) while (urlList.length > 1) { - let trackerDomain = urlList.join('.') + const trackerDomain = urlList.join('.') urlList.shift() const matchedTracker = this.trackerList[trackerDomain] @@ -351,10 +332,10 @@ _utf8_encode : function (string) { */ findWebsiteOwner (requestData) { // find the site owner - let siteUrlList = Array.from(requestData.siteUrlSplit) + const siteUrlList = Array.from(requestData.siteUrlSplit) while (siteUrlList.length > 1) { - let siteToCheck = siteUrlList.join('.') + const siteToCheck = siteUrlList.join('.') siteUrlList.shift() if (this.entityList[siteToCheck]) { @@ -370,11 +351,11 @@ _utf8_encode : function (string) { let matchedRule = null // Find a matching rule from this tracker if (tracker.rules && tracker.rules.length) { - tracker.rules.some(ruleObj => { + matchedRule = tracker.rules.find(ruleObj => { if (this.requestMatchesRule(requestData, ruleObj)) { - matchedRule = ruleObj return true } + return false }) } return matchedRule @@ -403,10 +384,12 @@ _utf8_encode : function (string) { const ruleDefinition = rule[type] const matchTypes = (ruleDefinition.types && ruleDefinition.types.length) - ? ruleDefinition.types.includes(requestData.request.type) : true + ? ruleDefinition.types.includes(requestData.request.type) + : true const matchDomains = (ruleDefinition.domains && ruleDefinition.domains.length) - ? ruleDefinition.domains.some(domain => domain.match(requestData.siteDomain)) : true + ? ruleDefinition.domains.some(domain => domain.match(requestData.siteDomain)) + : true return (matchTypes && matchDomains) } @@ -439,129 +422,129 @@ _utf8_encode : function (string) { } } - return {action, reason} + return { action, reason } } } // trackers.js // surrogates - let surrogates = ` - ${surrogates} + const surrogates = ` + $SURROGATES$ ` // surrogates // tracker data set - let trackerData = ${trackerData} + const trackerData = $TRACKER_DATA$ // tracker data set - let blockingEnabled = ${blockingEnabled} - + const blockingEnabled = $BLOCKING_ENABLED$ + // overrides - Trackers.prototype.findTrackerOwner = function(domain) { - var parts = domain.split(".") + Trackers.prototype.findTrackerOwner = function (domain) { + let parts = domain.split('.') while (parts.length > 1) { - let entityName = trackerData.domains[parts.join(".")] + const entityName = trackerData.domains[parts.join('.')] if (entityName) { return entityName } parts = parts.slice(1) } - return null; + return null } Object.freeze(Trackers.prototype) // create an instance to use - let trackers = new Trackers({ + const trackers = new Trackers({ tldjs: tldjs, utils: utils - }); + }) // update algorithm with the data it needs trackers.setLists([{ - name: "tds", - data: trackerData - }, - { - name: "surrogates", - data: surrogates - } - ]); + name: 'tds', + data: trackerData + }, + { + name: 'surrogates', + data: surrogates + } + ]) - let topLevelUrl = getTopLevelURL(); + const topLevelUrl = getTopLevelURL() - var unprotectedDomain = false; - var domainParts = topLevelUrl && topLevelUrl.host ? topLevelUrl.host.split(".") : []; + let unprotectedDomain = false + const domainParts = topLevelUrl && topLevelUrl.host ? topLevelUrl.host.split('.') : [] // walk up the domain to see if it's unprotected - while (domainParts && domainParts.length > 1 && !unprotectedDomain) { - let partialDomain = domainParts.join(".") + while (domainParts.length > 1 && !unprotectedDomain) { + const partialDomain = domainParts.join('.') - unprotectedDomain = ` - ${tempUnprotectedDomains} - `.split("\n").filter(domain => domain.trim() == partialDomain).length > 0; + unprotectedDomain = ` + $TEMP_UNPROTECTED_DOMAINS$ + `.split('\n').filter(domain => domain.trim() === partialDomain).length > 0 - domainParts.shift() + domainParts.shift() } if (!unprotectedDomain && topLevelUrl.host != null) { - unprotectedDomain = ` - ${userUnprotectedDomains} - `.split("\n").filter(domain => domain.trim() == topLevelUrl.host).length > 0; + unprotectedDomain = ` + $USER_UNPROTECTED_DOMAINS$ + `.split('\n').filter(domain => domain.trim() === topLevelUrl.host).length > 0 } // private - function getTopLevelURL() { + function getTopLevelURL () { try { // FROM: https://stackoverflow.com/a/7739035/73479 // FIX: Better capturing of top level URL so that trackers in embedded documents are not considered first party - return new URL(window.location != window.parent.location ? document.referrer : document.location.href) - } catch(error) { + return new URL(window.location !== window.parent.location ? document.referrer : document.location.href) + } catch (error) { return new URL(location.href) } } - - var loadedSurrogates = {} + + const loadedSurrogates = {} // private - function loadSurrogate(surrogatePattern) { - var s = document.createElement("script") - s.type = "application/javascript" + function loadSurrogate (surrogatePattern) { + const s = document.createElement('script') + s.type = 'application/javascript' s.async = true s.src = trackers.surrogateList[surrogatePattern] - var scripts = document.getElementsByTagName("script") + const scripts = document.getElementsByTagName('script') if (scripts && scripts.length > 0) { scripts[0].parentNode.insertBefore(s, scripts[0]) } } // public - function shouldBlock(trackerUrl, type) { + function shouldBlock (trackerUrl, type) { seenUrls.add(trackerUrl) - let startTime = performance.now() - + const startTime = performance.now() + if (!blockingEnabled) { - return false; + return false } - let result = trackers.getTrackerData(trackerUrl.toString(), topLevelUrl.toString(), { + const result = trackers.getTrackerData(trackerUrl.toString(), topLevelUrl.toString(), { type: type - }, null); + }, null) if (result == null) { - return false; + return false } - var blocked = false; + let blocked = false if (unprotectedDomain) { - result.reason = "unprotectedDomain"; + result.reason = 'unprotectedDomain' } else if (result.action !== 'ignore') { // other actions are "block" or "redirect" - anything that is not ignored should be blocked. Surrogates are handled below since // we can't do a redirect. - blocked = true; + blocked = true } - - var isSurrogate = !!(result.matchedRule && result.matchedRule.surrogate) + + const isSurrogate = !!(result.matchedRule && result.matchedRule.surrogate) // Tracker blocking is dealt with by content rules // Only handle surrogates here @@ -570,7 +553,7 @@ _utf8_encode : function (string) { loadSurrogate(result.matchedRule.surrogate) loadedSurrogates[result.matchedRule.surrogate] = true } - + const pageUrl = window.location.href surrogateInjected({ url: trackerUrl, @@ -580,49 +563,51 @@ _utf8_encode : function (string) { pageUrl: pageUrl }) - duckduckgoDebugMessaging.signpostEvent({event: "Surrogate Injected", - url: trackerUrl, - time: performance.now() - startTime}) + duckduckgoDebugMessaging.signpostEvent({ + event: 'Surrogate Injected', + url: trackerUrl, + time: performance.now() - startTime + }) return true } - + return false } - + const seenUrls = new Set() - function hasNotSeen(url) { + function hasNotSeen (url) { return !seenUrls.has(url) } - function processPage() { + function processPage () { [...document.scripts].filter(hasNotSeen).forEach((el) => { if (shouldBlock(el.src, 'SCRIPT')) { - duckduckgoDebugMessaging.log("blocking load") + duckduckgoDebugMessaging.log('blocking load') } }); [...document.images].filter(hasNotSeen).forEach((el) => { - // If the image's natural width is zero, then it has not loaded so we - // can assume that it may have been blocked. - if (el.naturalWidth === 0) { - if (shouldBlock(el.src, 'IMG')) { - duckduckgoDebugMessaging.log("blocking load") - } - } + // If the image's natural width is zero, then it has not loaded so we + // can assume that it may have been blocked. + if (el.naturalWidth === 0) { + if (shouldBlock(el.src, 'IMG')) { + duckduckgoDebugMessaging.log('blocking load') + } + } }); [...document.querySelectorAll('link')].filter(hasNotSeen).forEach((el) => { if (shouldBlock(el.href, 'LINK')) { - duckduckgoDebugMessaging.log("blocking load") + duckduckgoDebugMessaging.log('blocking load') } }); [...document.querySelectorAll('iframe')].filter(hasNotSeen).forEach((el) => { if (shouldBlock(el.src, 'IFRAME')) { - duckduckgoDebugMessaging.log("blocking load") + duckduckgoDebugMessaging.log('blocking load') } - }); + }) } - function debounce(func, wait) { + function debounce (func, wait) { let timeout return function () { clearTimeout(timeout) @@ -639,60 +624,54 @@ _utf8_encode : function (string) { observer.observe(rootElement, { childList: true, subtree: true }); // Init - (function() { - - duckduckgoDebugMessaging.log("installing load detection") - window.addEventListener("load", function(event) { + (function () { + duckduckgoDebugMessaging.log('installing load detection') + window.addEventListener('load', function (event) { processPage() }, false) - try { - duckduckgoDebugMessaging.log("installing image src detection") + duckduckgoDebugMessaging.log('installing image src detection') - var originalImageSrc = Object.getOwnPropertyDescriptor(Image.prototype, 'src') + const originalImageSrc = Object.getOwnPropertyDescriptor(Image.prototype, 'src') Object.defineProperty(Image.prototype, 'src', { writable: true, // Needs to be writable for the content blocking rules script. Will be locked down in that script - get: function() { + get: function () { return originalImageSrc.get.call(this) }, - set: function(value) { - - var instance = this - if (shouldBlock(value, "image")) { - duckduckgoDebugMessaging.log("blocking image src: " + value) + set: function (value) { + const instance = this + if (shouldBlock(value, 'image')) { + duckduckgoDebugMessaging.log('blocking image src: ' + value) } else { - originalImageSrc.set.call(instance, value); + originalImageSrc.set.call(instance, value) } - } }) - - } catch(error) { - duckduckgoDebugMessaging.log("failed to install image src detection") + } catch (error) { + duckduckgoDebugMessaging.log('failed to install image src detection') } try { - duckduckgoDebugMessaging.log("installing xhr detection") + duckduckgoDebugMessaging.log('installing xhr detection') - var xhr = XMLHttpRequest.prototype - var originalOpen = xhr.open + const xhr = XMLHttpRequest.prototype + const originalOpen = xhr.open - xhr.open = function() { - var args = arguments - var url = arguments[1] - if (shouldBlock(url, "xmlhttprequest")) { - args[1] = "about:blank" + xhr.open = function () { + const args = arguments + const url = arguments[1] + if (shouldBlock(url, 'xmlhttprequest')) { + args[1] = 'about:blank' } - duckduckgoDebugMessaging.log("sending xhr " + url + " to " + args[1]) - return originalOpen.apply(this, args); + duckduckgoDebugMessaging.log('sending xhr ' + url + ' to ' + args[1]) + return originalOpen.apply(this, args) } - - } catch(error) { - duckduckgoDebugMessaging.log("failed to install xhr detection") + } catch (error) { + duckduckgoDebugMessaging.log('failed to install xhr detection') } - - duckduckgoDebugMessaging.log("content blocking initialised") + + duckduckgoDebugMessaging.log('content blocking initialised') })() return { diff --git a/Core/contentblockerrules.js b/Core/contentblockerrules.js index 147d8218a4..8aec510c2f 100644 --- a/Core/contentblockerrules.js +++ b/Core/contentblockerrules.js @@ -19,256 +19,254 @@ // "use strict"; -(function() { - - let topLevelUrl = getTopLevelURL(); - - var unprotectedDomain = false; - var domainParts = topLevelUrl && topLevelUrl.host ? topLevelUrl.host.split(".") : []; - - // walk up the domain to see if it's unprotected - while (domainParts && domainParts.length > 1 && !unprotectedDomain) { - let partialDomain = domainParts.join(".") - - unprotectedDomain = ` - ${tempUnprotectedDomains} - `.split("\n").filter(domain => domain.trim() == partialDomain).length > 0; - - domainParts.shift() - } - - if (!unprotectedDomain && topLevelUrl.host != null) { - unprotectedDomain = ` - ${userUnprotectedDomains} - `.split("\n").filter(domain => domain.trim() == topLevelUrl.host).length > 0; - } - - // private - function getTopLevelURL() { - try { - // FROM: https://stackoverflow.com/a/7739035/73479 - // FIX: Better capturing of top level URL so that trackers in embedded documents are not considered first party - if (window.location != window.parent.location) { - return new URL(window.location.href !== 'about:blank' ? document.referrer : window.parent.location.href) - } else { - return new URL(document.location.href) - } - } catch(error) { - return new URL(location.href) - } - } - - if (!window.__firefox__) { - Object.defineProperty(window, "__firefox__", { - enumerable: false, - configurable: false, - writable: false, - value: { - userScripts: {}, - includeOnce: function(userScript, initializer) { - if (!__firefox__.userScripts[userScript]) { - __firefox__.userScripts[userScript] = true; - if (typeof initializer === 'function') { - initializer(); - } - return false; - } - return true; - } - } - }); - } - - if (webkit.messageHandlers.processRule) { - install(); - } - - function install() { - Object.defineProperty(window.__firefox__, "TrackingProtectionStats", { - enumerable: false, - configurable: false, - writable: false, - value: { enabled: false } - }); - - Object.defineProperty(window.__firefox__.TrackingProtectionStats, "setEnabled", { - enumerable: false, - configurable: false, - writable: false, - value: function(enabled, securityToken) { - if (securityToken !== SECURITY_TOKEN) { - return; - } +(function () { + const topLevelUrl = getTopLevelURL() - if (enabled === window.__firefox__.TrackingProtectionStats.enabled) { - return; - } + let unprotectedDomain = false + const domainParts = topLevelUrl && topLevelUrl.host ? topLevelUrl.host.split('.') : [] + + // walk up the domain to see if it's unprotected + while (domainParts.length > 1 && !unprotectedDomain) { + const partialDomain = domainParts.join('.') - window.__firefox__.TrackingProtectionStats.enabled = enabled; - - injectStatsTracking(enabled); - } - }) - - function sendMessage(url, resourceType) { - if (url) { - const pageUrl = window.location.href - webkit.messageHandlers.processRule.postMessage({ - url: url, - resourceType: resourceType === undefined ? null : resourceType, - blocked: !unprotectedDomain, - pageUrl: pageUrl - }); - } + unprotectedDomain = ` + $TEMP_UNPROTECTED_DOMAINS$ + `.split('\n').filter(domain => domain.trim() === partialDomain).length > 0 + + domainParts.shift() } - function onLoadNativeCallback() { - // Send back the sources of every script and image in the DOM back to the host application. - [].slice.apply(document.scripts).forEach(function(el) { sendMessage(el.src, "script"); }); - [].slice.apply(document.querySelectorAll('link')).forEach(function(el) { sendMessage(el.href, "link"); }); - [].slice.apply(document.images).forEach(function(el) { - // If the image's natural width is zero, then it has not loaded so we - // can assume that it may have been blocked. - if (el.naturalWidth === 0) { - sendMessage(el.src, "image"); - } - }); - [].slice.apply(document.querySelectorAll('iframe')).forEach(function(el) { sendMessage(el.src, "iframe" )}) + if (!unprotectedDomain && topLevelUrl.host != null) { + unprotectedDomain = ` + $USER_UNPROTECTED_DOMAINS$ + `.split('\n').filter(domain => domain.trim() === topLevelUrl.host).length > 0 } - let originalOpen = null; - let originalSend = null; - let originalImageSrc = null; - let originalFetch = null; - let mutationObserver = null; - - function injectStatsTracking(enabled) { - // This enable/disable section is a change from the original Focus iOS version. - if (enabled) { - if (originalOpen) { - return; + // private + function getTopLevelURL () { + try { + // FROM: https://stackoverflow.com/a/7739035/73479 + // FIX: Better capturing of top level URL so that trackers in embedded documents are not considered first party + if (window.location !== window.parent.location) { + return new URL(window.location.href !== 'about:blank' ? document.referrer : window.parent.location.href) + } else { + return new URL(document.location.href) + } + } catch (error) { + return new URL(location.href) } - window.addEventListener("load", onLoadNativeCallback, false); - } else { - window.removeEventListener("load", onLoadNativeCallback, false); + } + + if (!window.__firefox__) { + Object.defineProperty(window, '__firefox__', { + enumerable: false, + configurable: false, + writable: false, + value: { + userScripts: {}, + includeOnce: function (userScript, initializer) { + if (!__firefox__.userScripts[userScript]) { + __firefox__.userScripts[userScript] = true + if (typeof initializer === 'function') { + initializer() + } + return false + } + return true + } + } + }) + } - if (originalOpen) { // if one is set, then all the enable code has run - XMLHttpRequest.prototype.open = originalOpen; - XMLHttpRequest.prototype.send = originalSend; - Image.prototype.src = originalImageSrc; - mutationObserver.disconnect(); + if (webkit.messageHandlers.processRule) { + install() + } - originalOpen = originalSend = originalImageSrc = mutationObserver = null; - } - return; - } - - // ------------------------------------------------- - // Send ajax requests URLs to the host application - // ------------------------------------------------- - var xhrProto = XMLHttpRequest.prototype; - if (!originalOpen) { - originalOpen = xhrProto.open; - originalSend = xhrProto.send; - } - - xhrProto.open = function(method, url) { - this._url = url; - return originalOpen.apply(this, arguments); - }; - - xhrProto.send = function(body) { - // Only attach the `error` event listener once for this - // `XMLHttpRequest` instance. - if (!this._tpErrorHandler) { - // If this `XMLHttpRequest` instance fails to load, we - // can assume it has been blocked. - this._tpErrorHandler = function() { - sendMessage(this._url, "xmlhttprequest"); - }; - this.addEventListener("error", this._tpErrorHandler); - } - return originalSend.apply(this, arguments); - }; - - // ------------------------------------------------- - // Detect when new sources get set on Image and send them to the host application - // ------------------------------------------------- - if (!originalImageSrc) { - originalImageSrc = Object.getOwnPropertyDescriptor(Image.prototype, "src"); - } - - delete Image.prototype.src; - Object.defineProperty(Image.prototype, "src", { - configurable: true, - get: function() { - return originalImageSrc.get.call(this); - }, - set: function(value) { - // Only attach the `error` event listener once for this - // Image instance. - if (!this._tpErrorHandler) { - // If this `Image` instance fails to load, we can assume - // it has been blocked. - this._tpErrorHandler = function() { - sendMessage(this.src, "image"); - }; - this.addEventListener("error", this._tpErrorHandler); - } - - originalImageSrc.set.call(this, value); - } - }); - - // ------------------------------------------------- - // Detect when fetch is called and pass the resource to the host application - // ------------------------------------------------- - if (!originalFetch) { - originalFetch = window.fetch; - } - window.fetch = function() { - if (arguments.length === 0) { - return originalFetch.apply(window, arguments); + function install () { + Object.defineProperty(window.__firefox__, 'TrackingProtectionStats', { + enumerable: false, + configurable: false, + writable: false, + value: { enabled: false } + }) + + Object.defineProperty(window.__firefox__.TrackingProtectionStats, 'setEnabled', { + enumerable: false, + configurable: false, + writable: false, + value: function (enabled, securityToken) { + if (securityToken !== SECURITY_TOKEN) { + return + } + + if (enabled === window.__firefox__.TrackingProtectionStats.enabled) { + return + } + + window.__firefox__.TrackingProtectionStats.enabled = enabled + + injectStatsTracking(enabled) + } + }) + + function sendMessage (url, resourceType) { + if (url) { + const pageUrl = window.location.href + webkit.messageHandlers.processRule.postMessage({ + url: url, + resourceType: resourceType === undefined ? null : resourceType, + blocked: !unprotectedDomain, + pageUrl: pageUrl + }) + } } - if (typeof arguments[0] === 'string') { - sendMessage(arguments[0], 'fetch'); - } else if (arguments[0].url) { - // Argument is a Request object - sendMessage(arguments[0].url, 'fetch'); + function onLoadNativeCallback () { + // Send back the sources of every script and image in the DOM back to the host application. + [].slice.apply(document.scripts).forEach(function (el) { sendMessage(el.src, 'script') }); + [].slice.apply(document.querySelectorAll('link')).forEach(function (el) { sendMessage(el.href, 'link') }); + [].slice.apply(document.images).forEach(function (el) { + // If the image's natural width is zero, then it has not loaded so we + // can assume that it may have been blocked. + if (el.naturalWidth === 0) { + sendMessage(el.src, 'image') + } + }); + [].slice.apply(document.querySelectorAll('iframe')).forEach(function (el) { sendMessage(el.src, 'iframe') }) } - return originalFetch.apply(window, arguments); - } - - // ------------------------------------------------- - // Listen to when new