diff --git a/src/drivers/webextension/js/content.js b/src/drivers/webextension/js/content.js index be689bceb..b6b9f0ef2 100644 --- a/src/drivers/webextension/js/content.js +++ b/src/drivers/webextension/js/content.js @@ -3,6 +3,9 @@ /* globals chrome */ const Content = { + /** + * Initialize content detection. + */ async init() { await new Promise((resolve) => setTimeout(resolve, 1000)) @@ -77,6 +80,10 @@ const Content = { } }, + /** + * Callback for fetching technologies. + * @param {Object} technologies + */ onGetTechnologies(technologies) { const script = document.createElement('script') diff --git a/src/drivers/webextension/js/driver.js b/src/drivers/webextension/js/driver.js index 7369600db..1fa388d6a 100644 --- a/src/drivers/webextension/js/driver.js +++ b/src/drivers/webextension/js/driver.js @@ -16,6 +16,9 @@ const expiry = 1000 * 60 * 60 * 24 const Driver = { lastPing: Date.now(), + /** + * Initialize Chrome extension. + */ async init() { chrome.runtime.onConnect.addListener(Driver.onRuntimeConnect) @@ -74,19 +77,38 @@ const Driver = { await setOption('version', version) }, + /** + * Logging utility function. + * @param {String} message + * @param {String} source + * @param {String} type + */ log(message, source = 'driver', type = 'log') { // eslint-disable-next-line no-console console[type](`wappalyzer | ${source} |`, message) }, + /** + * Error utility function. + * @param {String} error + * @param {String} source + */ error(error, source = 'driver') { Driver.log(error, source, 'error') }, + /** + * Open browser tab utility function. + * @param {String} url + * @param {Boolean} active + */ open(url, active = true) { chrome.tabs.create({ url, active }) }, + /** + * Load technologies into memory. + */ async loadTechnologies() { try { const { apps: technologies, categories } = await ( @@ -100,6 +122,11 @@ const Driver = { } }, + /** + * Post-wrapper for fetch. + * @param {String} url + * @param {String} body + */ post(url, body) { try { return fetch(url, { @@ -111,6 +138,11 @@ const Driver = { } }, + /** + * Analyize JavaScript detections. + * @param {String} url + * @param {Array} js + */ async analyzeJs(url, js) { await Driver.onDetect( url, @@ -127,6 +159,10 @@ const Driver = { ) }, + /** + * Initialize PostMessage interface. + * @param {Object} port + */ onRuntimeConnect(port) { Driver.log(`Connected to ${port.name}`) @@ -150,6 +186,10 @@ const Driver = { }) }, + /** + * Callback for WebRequestComplete listener. + * @param {Object} request + */ async onWebRequestComplete(request) { if (request.responseHeaders) { const headers = {} @@ -183,6 +223,12 @@ const Driver = { } }, + /** + * Callback for onContentLoad listener. + * @param {String} url + * @param {Object} items + * @param {String} language + */ async onContentLoad(url, items, language) { try { const { hostname } = new URL(url) @@ -205,10 +251,20 @@ const Driver = { } }, + /** + * Get technology detections. + */ getTechnologies() { return Wappalyzer.technologies }, + /** + * Callback for detections. + * @param {String} url + * @param {Array} detections + * @param {String} language + * @param {Boolean} incrementHits + */ async onDetect(url, detections = [], language, incrementHits = false) { if (!detections.length) { return @@ -293,12 +349,21 @@ const Driver = { await Driver.ping() }, + /** + * Callback for onAd listener. + * @param {Object} ad + */ async onAd(ad) { Driver.cache.ads.push(ad) await setOption('ads', Driver.cache.ads) }, + /** + * Generate linked icon for detections. + * @param {String} url + * @param {Object} technologies + */ async setIcon(url, technologies) { const dynamicIcon = await getOption('dynamicIcon', true) @@ -340,6 +405,9 @@ const Driver = { ) }, + /** + * Get detections from site. + */ async getDetections() { const [{ id }] = await promisify(chrome.tabs, 'query', { active: true, @@ -349,6 +417,11 @@ const Driver = { return Driver.cache.tabs[id] }, + /** + * Check for robot rules on site. + * @param {String} hostname + * @param {Boolean} secure + */ async getRobots(hostname, secure = false) { if (!(await getOption('tracking', true))) { return @@ -416,6 +489,10 @@ const Driver = { } }, + /** + * Check for robots.txt rules. + * @param {String} href + */ async checkRobots(href) { const url = new URL(href) @@ -433,6 +510,9 @@ const Driver = { } }, + /** + * Ping back home. + */ async ping() { const tracking = await getOption('tracking', true) const termsAccepted = diff --git a/src/drivers/webextension/js/lib/iframe.js b/src/drivers/webextension/js/lib/iframe.js index 9fd152494..5fc1ef0ef 100644 --- a/src/drivers/webextension/js/lib/iframe.js +++ b/src/drivers/webextension/js/lib/iframe.js @@ -1,1188 +1,1557 @@ -'use strict'; - -(function(win) { +'use strict' +;(function(win) { + var exports = {} + + ;(function(exports) { + var utils = { + /** + * Normalize URL + * @param {String} url + */ + normalizeUrl: function(url) { + return this.hashUrl(url) || null + }, -var exports = {}; + /** + * Get referrer. + */ + getReferrer: function() { + return this.normalizeUrl(document.referrer) + }, -(function(exports) { + /** + * Get current page URL. + */ + getPageUrl: function() { + return this.normalizeUrl(window.location.href) + }, - var utils = { - normalizeUrl: function(url) { + /** + * Generated hashed URL. + * @param {String} url + */ + hashUrl: function(url) { + var a, result - return this.hashUrl(url) || null; + if (!url || url.indexOf('http') !== 0) { + return null + } - }, + a = document.createElement('a') + a.href = url - getReferrer: function() { - return this.normalizeUrl(document.referrer); - }, + result = a.protocol + '//' + a.hostname + '/' - getPageUrl: function() { - return this.normalizeUrl(window.location.href); - }, - hashUrl: function(url) { - var a, - result; + if (a.pathname && a.pathname !== '/') { + result += this.hashCode(a.pathname) + } - if ( !url || url.indexOf('http') !== 0 ) { - return null; - } + if (a.search) { + result += '?' + this.hashCode(a.search) + } - a = document.createElement('a'); - a.href = url; + if (a.hash) { + result += '#' + this.hashCode(a.hash) + } - result = a.protocol + '//' + a.hostname + '/'; + return result + }, - if ( a.pathname && a.pathname !== '/' ) { - result += this.hashCode(a.pathname); - } + /** + * Generate random hash. + * @param {String} str + */ + hashCode: function(str) { + var hash = 0, + kar, + i + + if (str.length === 0) { + return hash + } - if ( a.search ) { - result += '?' + this.hashCode(a.search); - } + for (i = 0; i < str.length; i++) { + kar = str.charCodeAt(i) + hash = (hash << 5) - hash + kar + hash = hash & hash + } - if ( a.hash ) { - result += '#' + this.hashCode(a.hash); - } + return hash + Math.pow(2, 32) + }, - return result; - }, + /** + * Apply array function to non-array. + * @param {Object} a + */ + realArray: function(a) { + return Array.prototype.slice.apply(a) + }, - hashCode: function(str) { - var hash = 0, - kar, - i; + /** + * Listener callback for onDocLoaded. + * @param {Object} doc + * @param {Function} callback + */ + onDocLoaded: function(doc, callback) { + if (doc.readyState === 'loading') { + doc.addEventListener('DOMContentLoaded', callback) + } else { + callback() + } + }, - if ( str.length === 0 ) { - return hash; - } + SCRIPT_IN_WINDOW_TOP: window === window.top, - for ( i = 0; i < str.length; i++ ) { - kar = str.charCodeAt(i); - hash = ((hash << 5) - hash) + kar; - hash = hash & hash; - } + /** + * Check for href Window object. + * @param {Object} win + */ + isFriendlyWindow: function(win) { + var href + try { + href = win.location.href + } catch (e) { + return false + } + return true + }, - return hash + Math.pow(2, 32); - }, + /** + * Get default view from element. + * @param {Object} el + */ + elementWindow: function(el) { + return el.ownerDocument.defaultView + }, - realArray: function(a) { - return Array.prototype.slice.apply(a); - }, + /** + * Get viewport size. + * @param {Object} win + */ + viewport: function(win) { + return { width: win.innerWidth, height: win.innerHeight } + }, - onDocLoaded: function(doc, callback) { - if ( doc.readyState === 'loading' ) { - doc.addEventListener('DOMContentLoaded', callback); - } else { - callback(); - } - }, + /** + * Parse query string parameters. + * @param {String} qs + */ + parseQS: function(qs) { + if (qs.indexOf('http') === 0) { + qs = qs.split('?')[1] + } + var i, kvs, key, val + var dict = {} + qs = qs.split('&') + for (i = 0; i < qs.length; i++) { + kvs = qs[i].split('=') + key = kvs[0] + val = kvs.slice(1).join('=') + try { + dict[key] = window.decodeURIComponent(val) + } catch (e) { + continue + } + } + return dict + }, - SCRIPT_IN_WINDOW_TOP: window === window.top, + /** + * Send PostMessage response. + * @param {Object} message + * @param {String} event + * @param {String} responseMessage + */ + sendToBackground: function(message, event, responseMessage) { + if (typeof chrome !== 'undefined') { + var port = chrome.runtime.connect({ name: 'adparser' }) + + port.onMessage.addListener((message) => { + if (message && typeof message.tracking_enabled !== 'undefined') { + if (message.tracking_enabled) { + utilCallback() + } else { + utilElseCallback() + } + } + }) - isFriendlyWindow: function(win) { + port.postMessage(message) + } else if (window.self.port) { + window.self.port.on(responseMessage, onResponse) + window.self.port.emit(event, message) + } + }, - var href; - try { - href = win.location.href; - } catch(e) { - return false; + /** + * Check if anonymous tracking is enabled. + * @param {Function} callback + * @param {Function} elseCallback + * @todo validate if utilCallback or utilElseCallback are being used. + */ + askIfTrackingEnabled: function(callback, elseCallback) { + utilCallback = callback + utilElseCallback = elseCallback + + this.sendToBackground( + 'is_tracking_enabled', + '', + 'tracking_enabled_response' + ) } - return true; - }, - - elementWindow: function(el) { - return el.ownerDocument.defaultView; - }, - - viewport: function(win) { - return {width: win.innerWidth, height: win.innerHeight}; - }, + } - parseQS: function(qs) { - if ( qs.indexOf('http') === 0 ) { - qs = qs.split('?')[1]; + utils.SCRIPT_IN_FRIENDLY_IFRAME = + !utils.SCRIPT_IN_WINDOW_TOP && utils.isFriendlyWindow(window.parent) + utils.SCRIPT_IN_HOSTILE_IFRAME = + !utils.SCRIPT_IN_WINDOW_TOP && !utils.SCRIPT_IN_FRIENDLY_IFRAME + + /** + * Generate new Logging object. + */ + function LogGenerator() { + this.msgNum = 0 + this.pageMeta = { + url: utils.getPageUrl(), + isHP: window.location.pathname === '/', + referrer: utils.getReferrer(), + rand: Math.floor(Math.random() * 10e12), + startTime: new Date().getTime() } - var i, kvs, key, val; - var dict = {}; - qs = qs.split('&'); - for ( i = 0; i < qs.length; i++ ) { - kvs = qs[i].split('='); - key = kvs[0]; - val = kvs.slice(1).join('='); - try { - dict[key] = window.decodeURIComponent(val); - } catch (e) { + } - continue; + LogGenerator.prototype = { + /** + * Log data. + * @param {String} event + * @param {Array} opt_assets + * @param {Array} opt_pageTags + */ + log: function(event, opt_assets, opt_pageTags) { + var opt_video_assets + if (event === 'video' || event === 'invalid-video') { + opt_video_assets = opt_assets || [] + opt_assets = [] + } else { + opt_video_assets = [] + opt_assets = opt_assets || [] } + var result = { + doc: this.pageMeta, + event: event, + video_assets: opt_video_assets, + assets: opt_assets, + version: '3', + mrev: '15a9f21-d', + msgNum: this.msgNum, + timestamp: new Date().getTime(), + pageVis: document.visibilityState, + pageFoc: document.hasFocus(), + pageTags: opt_pageTags || [] + } + this.msgNum++ + return result } - return dict; - }, - sendToBackground: function(message, event, responseMessage) { - if ( typeof chrome !== 'undefined' ) { - var port = chrome.runtime.connect({name:"adparser"}); - - port.onMessage.addListener((message) => { - if ( message && typeof message.tracking_enabled !== 'undefined' ) { - if (message.tracking_enabled) { - utilCallback(); - } else { - utilElseCallback(); - } - } - }); - - port.postMessage(message); - } else if ( window.self.port ) { - window.self.port.on(responseMessage, onResponse); - window.self.port.emit(event, message); - } - }, + } - askIfTrackingEnabled: function(callback, elseCallback) { - utilCallback = callback; - utilElseCallback = elseCallback; + utils.LogGenerator = LogGenerator + + let utilCallback, utilElseCallback + + exports.utils = utils + })(exports) + ;(function(exports) { + var SizeMatcher = { + VALID_AD_SIZES: [ + [300, 50], + [320, 50], + [160, 600], + [300, 250], + [300, 600], + [300, 1050], + [336, 280], + [336, 850], + [468, 60], + [728, 90], + [728, 250], + [728, 270], + [970, 66], + [970, 90], + [970, 125], + [970, 250], + [970, 400], + [970, 415], + [1280, 100] + ], + + PX_SIZE_TOL: 10, + + /** + * Get ad size. + * @param {Int} width + * @param {Int} height + */ + getMatchedAdSize: function(width, height) { + if (!this.set) { + this.set = this._makeSizeSet() + } - this.sendToBackground( - 'is_tracking_enabled', - '', - 'tracking_enabled_response' - ); + return this.set[Math.round(width) + 'x' + Math.round(height)] + }, - } - }; - - utils.SCRIPT_IN_FRIENDLY_IFRAME = !utils.SCRIPT_IN_WINDOW_TOP && utils.isFriendlyWindow(window.parent); - utils.SCRIPT_IN_HOSTILE_IFRAME = !utils.SCRIPT_IN_WINDOW_TOP && !utils.SCRIPT_IN_FRIENDLY_IFRAME; - - function LogGenerator() { - this.msgNum = 0; - this.pageMeta = { - 'url': utils.getPageUrl(), - 'isHP': window.location.pathname === '/', - 'referrer': utils.getReferrer(), - 'rand': Math.floor(Math.random() * 10e12), - 'startTime': new Date().getTime() - }; - } + /** + * Check element size. + * @param {HTMLElement} el + */ + elementIsAdShaped: function(el) { + return !!this.getMatchedAdSizeForElement(el) + }, - LogGenerator.prototype = { - log: function(event, opt_assets, opt_pageTags) { - var opt_video_assets; - if ( event === 'video' || event === 'invalid-video' ) { - opt_video_assets = opt_assets || []; - opt_assets = []; - } else { - opt_video_assets = []; - opt_assets = opt_assets || []; - } - var result = { - doc: this.pageMeta, - event: event, - video_assets: opt_video_assets, - assets: opt_assets, - version: '3', - mrev: '15a9f21-d', - msgNum: this.msgNum, - timestamp: new Date().getTime(), - pageVis: document.visibilityState, - pageFoc: document.hasFocus(), - pageTags: opt_pageTags || [] - }; - this.msgNum++; - return result; - } - }; - - utils.LogGenerator = LogGenerator; - - let utilCallback, utilElseCallback; - - exports.utils = utils; -})(exports); - -(function(exports) { - - var SizeMatcher = { - VALID_AD_SIZES: [ - [300, 50], - [320, 50], - [160, 600], - [300, 250], - [300, 600], - [300, 1050], - [336, 280], - [336, 850], - [468, 60], - [728, 90], - [728, 250], - [728, 270], - [970, 66], - [970, 90], - [970, 125], - [970, 250], - [970, 400], - [970, 415], - [1280, 100] - ], - - PX_SIZE_TOL: 10, - - getMatchedAdSize: function(width, height) { - - if ( !this.set ) { - this.set = this._makeSizeSet(); - } + /** + * Get ad size. + * @param {HTMLElement} el + * @todo better description + */ + getMatchedAdSizeForElement: function(el) { + var rect = el.getBoundingClientRect() + return this.getMatchedAdSize(rect.width, rect.height) + }, - return this.set[Math.round(width) + 'x' + Math.round(height)]; - }, - - elementIsAdShaped: function(el) { - return !!this.getMatchedAdSizeForElement(el); - }, - - getMatchedAdSizeForElement: function(el) { - var rect = el.getBoundingClientRect(); - return this.getMatchedAdSize(rect.width, rect.height); - }, - - _makeSizeSet: function() { - var set = {}; - var i; - var xfuz; - var yfuz; - var size; - var width; - var height; - - for ( i = 0; i < this.VALID_AD_SIZES.length; i++ ) { - for ( xfuz = -this.PX_SIZE_TOL; xfuz <= this.PX_SIZE_TOL; xfuz++ ) { - for ( yfuz = -this.PX_SIZE_TOL; yfuz <= this.PX_SIZE_TOL; yfuz++ ) { - size = this.VALID_AD_SIZES[i]; - width = size[0] + xfuz; - height = size[1] + yfuz; - set[width + 'x' + height] = size; + /** + * Generate ad sizes. + */ + _makeSizeSet: function() { + var set = {} + var i + var xfuz + var yfuz + var size + var width + var height + + for (i = 0; i < this.VALID_AD_SIZES.length; i++) { + for (xfuz = -this.PX_SIZE_TOL; xfuz <= this.PX_SIZE_TOL; xfuz++) { + for (yfuz = -this.PX_SIZE_TOL; yfuz <= this.PX_SIZE_TOL; yfuz++) { + size = this.VALID_AD_SIZES[i] + width = size[0] + xfuz + height = size[1] + yfuz + set[width + 'x' + height] = size + } } } + return set } - return set; } - }; - var Throttler = { - MAX_SEARCHES_PER_WINDOW: 10, - MAX_SEARCHES_PER_ELEMENT: 2, + var Throttler = { + MAX_SEARCHES_PER_WINDOW: 10, + MAX_SEARCHES_PER_ELEMENT: 2, + + /** + * Count number of elements. + * @param {HTMLElement} el + */ + countSearch: (el) => { + if (typeof el.searches !== 'number') { + el.searches = 0 + } - countSearch: function(el) { - if ( typeof el.searches !== 'number' ) { - el.searches = 0; - } + el.searches += 1 + }, - el.searches += 1; - }, + /** + * + * @param {*} el + * @param {*} max + * + * @todo add description + */ + throttle: function(el, max) { + if (typeof el.searches === 'number' && el.searches >= max) { + return true + } + return false + }, - throttle: function(el, max) { - if ( typeof el.searches === 'number' && el.searches >= max ) { - return true; - } - return false; - }, + /** + * + * @param {*} el + * + * @todo add description + */ + throttleElement: function(el) { + return this.throttle(el, this.MAX_SEARCHES_PER_ELEMENT) + }, - throttleElement: function(el) { - return this.throttle(el, this.MAX_SEARCHES_PER_ELEMENT); - }, + /** + * + * @param {*} win + * + * @todo add description + */ + throttleWin: function(win) { + return this.throttle(win, this.MAX_SEARCHES_PER_WINDOW) + }, - throttleWin: function(win) { - return this.throttle(win, this.MAX_SEARCHES_PER_WINDOW); - }, + /** + * + * @param {*} el + * + * @todo add description + */ + getCount: function(el) { + return el.searches || 0 + } + } - getCount: function(el) { - return el.searches || 0; + /** + * Initialize window and document elements. + * @param {*} win + */ + function TopSearcher(win) { + this.win = win + this.doc = win.document } - }; - function TopSearcher(win) { - this.win = win; - this.doc = win.document; - } + /** + * Add search function. + */ + TopSearcher.prototype.search = function() { + var candidates = exports.utils.realArray( + this.doc.querySelectorAll('img, object, embed') + ), + html5Ad, + ads = [] + + ads = ads.concat( + candidates.filter(function(el) { + if (!el.mpAdFound && !Throttler.throttleElement(el)) { + Throttler.countSearch(el) + if ( + (el.tagName !== 'IMG' || isStandardImage(el)) && + SizeMatcher.elementIsAdShaped(el) + ) { + el.mpAdFound = true + return true + } + } + return false + }) + ) + + html5Ad = this._mainGetHTMLAd() + if (html5Ad) { + html5Ad.html5 = true + html5Ad.mpAdFound = true + ads.push(html5Ad) + } + + return ads + } - TopSearcher.prototype.search = function() { - var candidates = exports.utils.realArray(this.doc.querySelectorAll('img, object, embed')), - html5Ad, - ads = []; - - ads = ads.concat(candidates.filter(function(el) { - if ( !el.mpAdFound && !Throttler.throttleElement(el) ) { - Throttler.countSearch(el); - if ( (el.tagName !== 'IMG' || isStandardImage(el)) && SizeMatcher.elementIsAdShaped(el) ) { - el.mpAdFound = true; - return true; + /** + * @todo add description + */ + TopSearcher.prototype._mainGetHTMLAd = function() { + var styles = this.doc.querySelectorAll( + 'div > style, div > link[rel="stylesheet"]' + ), + i, + div + for (i = 0; i < styles.length; i++) { + div = styles[i].parentNode + if ( + !div.mpAdFound && + SizeMatcher.elementIsAdShaped(div) && + this._jumpedOut(div) + ) { + return div } } - return false; - })); - - html5Ad = this._mainGetHTMLAd(); - if ( html5Ad ) { - html5Ad.html5 = true; - html5Ad.mpAdFound = true; - ads.push(html5Ad); } - return ads; - }; + /** + * @todo add description + */ + TopSearcher.prototype._jumpedOut = function(el) { + var siblings, ifrs + siblings = exports.utils.realArray(el.parentNode.children) + ifrs = siblings.filter(function(el) { + return ( + el.tagName === 'IFRAME' && + el.offsetWidth === 0 && + el.offsetHeight === 0 + ) + }) + return ifrs.length > 0 + } - TopSearcher.prototype._mainGetHTMLAd = function() { - var styles = this.doc.querySelectorAll('div > style, div > link[rel="stylesheet"]'), - i, div; - for ( i = 0; i < styles.length; i++ ) { - div = styles[i].parentNode; - if ( !div.mpAdFound && SizeMatcher.elementIsAdShaped(div) && this._jumpedOut(div) ) { - return div; + /** + * + * @param {*} win + * + * @todo add description + */ + function IframeSearcher(win) { + this.MIN_AD_AREA = 14000 + this.MIN_WINDOW_PX = 10 + + this.win = win + this.doc = win.document + this.body = win.document.body + this.winClickTag = win.clickTag + this.adSizeMeta = this._getAdSizeMeta() + this.numElementsInBody = + (this.body && this.body.querySelectorAll('*').length) || 0 + + this.shouldSearchWindow = false + if ( + !this.win.mpAdFound && + this.body && + !Throttler.throttleWin(this.win) + ) { + this.winWidth = this.win.innerWidth + this.winHeight = this.win.innerHeight + if ( + this._meetsMinAdSize(this.winWidth, this.winHeight) && + !this._containsLargeIframes() + ) { + this.shouldSearchWindow = true + } } } - }; - - TopSearcher.prototype._jumpedOut = function(el) { - var siblings, ifrs; - siblings = exports.utils.realArray(el.parentNode.children); - ifrs = siblings.filter(function(el) { - return el.tagName === 'IFRAME' && el.offsetWidth === 0 && el.offsetHeight === 0; - }); - return ifrs.length > 0; - }; - - function IframeSearcher(win) { - this.MIN_AD_AREA = 14000; - this.MIN_WINDOW_PX = 10; - - this.win = win; - this.doc = win.document; - this.body = win.document.body; - this.winClickTag = win.clickTag; - this.adSizeMeta = this._getAdSizeMeta(); - this.numElementsInBody = (this.body && this.body.querySelectorAll('*').length) || 0; - - this.shouldSearchWindow = false; - if ( !this.win.mpAdFound && this.body && !Throttler.throttleWin(this.win) ) { - this.winWidth = this.win.innerWidth; - this.winHeight = this.win.innerHeight; - if ( this._meetsMinAdSize(this.winWidth, this.winHeight) && !this._containsLargeIframes() ) { - this.shouldSearchWindow = true; + + /** + * @todo add description + */ + IframeSearcher.prototype.search = function() { + var ad + + if (this.shouldSearchWindow) { + ad = this._search() + if (ad) { + ad.mpAdFound = true + win.mpAdFound = true + return ad + } + Throttler.countSearch(this.win) } + + return null } - } + /** + * @todo add description + */ + IframeSearcher.prototype._search = function() { + var _this = this, + stdCandidates, + html5Candidates, + stdEl, + html5El + + stdCandidates = this.body.querySelectorAll('img, object, embed') + + stdEl = getFirst(stdCandidates, function(el) { + if ( + !el.mpAdFound && + !Throttler.throttleElement(el) && + (el.tagName !== 'IMG' || isStandardImage(el)) && + _this._elementIsAtLeastAsBigAsWindow(el) + ) { + return true + } + Throttler.countSearch(el) + return false + }) - IframeSearcher.prototype.search = function() { - var ad; + if (stdEl) { + return stdEl + } - if ( this.shouldSearchWindow ) { - ad = this._search(); - if ( ad ) { - ad.mpAdFound = true; - win.mpAdFound = true; - return ad; + if (this._isHTML5Iframe()) { + html5Candidates = this.doc.querySelectorAll( + 'body, canvas, button, video, svg, div' + ) + html5El = getFirst(html5Candidates, function(el) { + if (_this._elementIsAtLeastAsBigAsWindow(el)) { + return true + } + Throttler.countSearch(el) + return false + }) } - Throttler.countSearch(this.win); - } - return null; - }; - - IframeSearcher.prototype._search = function() { - var _this = this, - stdCandidates, - html5Candidates, - stdEl, - html5El; - - stdCandidates = this.body.querySelectorAll('img, object, embed'); - - stdEl = getFirst(stdCandidates, function(el) { - if ( !el.mpAdFound && - !Throttler.throttleElement(el) && - (el.tagName !== 'IMG' || isStandardImage(el)) && - _this._elementIsAtLeastAsBigAsWindow(el)) - { - return true; + if (html5El) { + html5El.html5 = true + html5El.winClickTag = this.winClickTag + html5El.adSizeMeta = this.adSizeMeta + return html5El } - Throttler.countSearch(el); - return false; - }); - if ( stdEl ) { - return stdEl; + return null } - if ( this._isHTML5Iframe() ) { - html5Candidates = this.doc.querySelectorAll('body, canvas, button, video, svg, div'); - html5El = getFirst(html5Candidates, function(el) { - - if ( _this._elementIsAtLeastAsBigAsWindow(el) ) { - return true; - } - Throttler.countSearch(el); - return false; - }); - } + /** + * @todo add description + */ + IframeSearcher.prototype._isHTML5Iframe = function() { + if (this.winClickTag || this.adSizeMeta) { + return true + } - if ( html5El ) { - html5El.html5 = true; - html5El.winClickTag = this.winClickTag; - html5El.adSizeMeta = this.adSizeMeta; - return html5El; - } + if ( + this.doc.querySelectorAll('canvas', 'button', 'video', 'svg').length > 0 + ) { + return true + } - return null; - }; + if ( + this.numElementsInBody >= 5 && + Throttler.getCount(this.win) > 0 && + this.doc.querySelectorAll('div').length > 0 + ) { + return true + } - IframeSearcher.prototype._isHTML5Iframe = function() { - if ( this.winClickTag || this.adSizeMeta ) { - return true; + return false } - if ( this.doc.querySelectorAll('canvas', 'button', 'video', 'svg').length > 0 ) { - return true; - } + /** + * @todo add description + */ + IframeSearcher.prototype._elementIsAtLeastAsBigAsWindow = function(el) { + var rect = el.getBoundingClientRect(), + tol = 0.95 - if ( this.numElementsInBody >= 5 && Throttler.getCount(this.win) > 0 && this.doc.querySelectorAll('div').length > 0 ) { - return true; + return ( + rect.width >= tol * this.winWidth && rect.height >= tol * this.winHeight + ) } - return false; - }; - - IframeSearcher.prototype._elementIsAtLeastAsBigAsWindow = function(el) { - var rect = el.getBoundingClientRect(), - tol = 0.95; - - return rect.width >= (tol * this.winWidth) && rect.height >= (tol * this.winHeight); - }; - - IframeSearcher.prototype._meetsMinAdSize = function(width, height) { - return (width * height) >= this.MIN_AD_AREA; - }; + /** + * @todo add description + */ + IframeSearcher.prototype._meetsMinAdSize = function(width, height) { + return width * height >= this.MIN_AD_AREA + } - IframeSearcher.prototype._containsLargeIframes = function() { - var iframes = this.doc.querySelectorAll('iframe'); - var rect; - var i; - for ( i = 0; i < iframes.length; i++ ) { - rect = iframes[i].getBoundingClientRect(); - if ( rect.width > this.MIN_WINDOW_PX || rect.height > this.MIN_WINDOW_PX ) { - return true; + /** + * @todo add description + */ + IframeSearcher.prototype._containsLargeIframes = function() { + var iframes = this.doc.querySelectorAll('iframe') + var rect + var i + for (i = 0; i < iframes.length; i++) { + rect = iframes[i].getBoundingClientRect() + if ( + rect.width > this.MIN_WINDOW_PX || + rect.height > this.MIN_WINDOW_PX + ) { + return true + } } + return false } - return false; - }; - - IframeSearcher.prototype._getAdSizeMeta = function() { - var adSizeMeta = this.doc.querySelectorAll('meta[name="ad.size"]'); - if ( adSizeMeta.length > 0 ) { - return adSizeMeta[0].content; - } else { - return null; - } - }; - - function getFirst(arr, testFn) { - var i, el; - for ( i = 0; i < arr.length; i++ ) { - el = arr[i]; - if ( testFn(el) ) { - return el; + + /** + * @todo add description + */ + IframeSearcher.prototype._getAdSizeMeta = function() { + var adSizeMeta = this.doc.querySelectorAll('meta[name="ad.size"]') + if (adSizeMeta.length > 0) { + return adSizeMeta[0].content + } else { + return null } } - return null; - } - function isStandardImage(img) { - - return img.src && (img.parentNode.tagName === 'A' || img.getAttribute('onclick')); - } - - function getFriendlyIframes(win) { - var iframes = win.document.querySelectorAll('iframe'); - iframes = exports.utils.realArray(iframes); - var friendlyIframes = iframes.filter(function(ifr) { - return exports.utils.isFriendlyWindow(ifr.contentWindow); - }); - return friendlyIframes; - } - - function findAds(win) { - var i, - iframes, - searcher, - ad, - ads = []; - - if ( win === win.top ) { - searcher = new TopSearcher(win); - ads = ads.concat(searcher.search()); - } else { - searcher = new IframeSearcher(win); - ad = searcher.search(); - if ( ad ) { - ads.push(ad); + /** + * + * @param {*} arr + * @param {*} testFn + * + * @todo add description + */ + function getFirst(arr, testFn) { + var i, el + for (i = 0; i < arr.length; i++) { + el = arr[i] + if (testFn(el)) { + return el + } } + return null } - iframes = getFriendlyIframes(win); - for ( i = 0; i < iframes.length; i++ ) { - ads = ads.concat(findAds(iframes[i].contentWindow)); + /** + * Check for image attributes. + * @param {HTMLElement} img + */ + function isStandardImage(img) { + return ( + img.src && + (img.parentNode.tagName === 'A' || img.getAttribute('onclick')) + ) } - return ads; - } - - exports.adfinder = { - getMatchedAdSize: SizeMatcher.getMatchedAdSize.bind(SizeMatcher), - findAds: findAds - }; -})(exports); - -(function(exports) { - - var parser = { - TAGS_WITH_SRC_ATTR: { - 'IMG': true, - 'SCRIPT': true, - 'IFRAME': true, - 'EMBED': true - }, - - MAX_ATTR_LEN: 100, - - getUrl: function(el, params) { - var url; - - if ( this.TAGS_WITH_SRC_ATTR.hasOwnProperty(el.tagName) ) { - url = el.src; - - } else if ( el.tagName === 'OBJECT' ) { - url = el.data || (params && params.movie) || null; - - } else if ( el.tagName === 'A' ) { - url = el.href; - } + /** + * Extract iFrames from page. + * @param {Object} win + */ + function getFriendlyIframes(win) { + var iframes = win.document.querySelectorAll('iframe') + iframes = exports.utils.realArray(iframes) + var friendlyIframes = iframes.filter(function(ifr) { + return exports.utils.isFriendlyWindow(ifr.contentWindow) + }) + return friendlyIframes + } - if ( url && url.indexOf('http') === 0 ) { - return url; + /** + * + * @param {*} win + */ + function findAds(win) { + var i, + iframes, + searcher, + ad, + ads = [] + + if (win === win.top) { + searcher = new TopSearcher(win) + ads = ads.concat(searcher.search()) } else { - return null; + searcher = new IframeSearcher(win) + ad = searcher.search() + if (ad) { + ads.push(ad) + } } - }, - getParams: function(el) { - if ( el.tagName !== 'OBJECT' ) { - return null; + iframes = getFriendlyIframes(win) + for (i = 0; i < iframes.length; i++) { + ads = ads.concat(findAds(iframes[i].contentWindow)) } - var i, child; - var params = {}; - var children = el.children; - for ( i = 0; i < children.length; i++ ) { - child = children[i]; - if ( child.tagName === 'PARAM' && child.name ) { + return ads + } - params[child.name.toLowerCase()] = child.value; + exports.adfinder = { + getMatchedAdSize: SizeMatcher.getMatchedAdSize.bind(SizeMatcher), + findAds: findAds + } + })(exports) + ;(function(exports) { + var parser = { + TAGS_WITH_SRC_ATTR: { + IMG: true, + SCRIPT: true, + IFRAME: true, + EMBED: true + }, + + MAX_ATTR_LEN: 100, + + /** + * + * @param {*} el + * @param {*} params + * + * @todo add description + */ + getUrl: function(el, params) { + var url + + if (this.TAGS_WITH_SRC_ATTR.hasOwnProperty(el.tagName)) { + url = el.src + } else if (el.tagName === 'OBJECT') { + url = el.data || (params && params.movie) || null + } else if (el.tagName === 'A') { + url = el.href } - } - return params; - }, - - getPosition: function(el) { - var rect = el.getBoundingClientRect(); - var win = exports.utils.elementWindow(el); - - return { - width: Math.round(rect.width), - height: Math.round(rect.height), - left: Math.round(rect.left + win.pageXOffset), - top: Math.round(rect.top + win.pageYOffset) - }; - }, - - getFlashvars: function(el, params, url) { - var flashvars; - var urlQS = url && url.split('?')[1]; - - if ( el.tagName === 'EMBED' ) { - flashvars = el.getAttribute('flashvars') || urlQS; - - } else if ( el.tagName === 'OBJECT' ) { - flashvars = params.flashvars || el.getAttribute('flashvars') || urlQS; - } - return (flashvars && exports.utils.parseQS(flashvars)) || null; - }, + if (url && url.indexOf('http') === 0) { + return url + } else { + return null + } + }, - findClickThru: function(el, flashvars) { - var key; - if ( el.tagName === 'IMG' && el.parentElement.tagName === 'A' ) { - return el.parentElement.href; - } else if ( flashvars ) { - for ( key in flashvars ) { - if ( flashvars.hasOwnProperty(key) ) { + /** + * + * @param {*} el + * + * @todo add description + */ + getParams: function(el) { + if (el.tagName !== 'OBJECT') { + return null + } - if ( key.toLowerCase().indexOf('clicktag') === 0 ) { - return flashvars[key]; - } + var i, child + var params = {} + var children = el.children + for (i = 0; i < children.length; i++) { + child = children[i] + if (child.tagName === 'PARAM' && child.name) { + params[child.name.toLowerCase()] = child.value } } - } - return null; - }, - - getAttr: function(el, name) { - var val = el.getAttribute(name); - - if ( val && val.slice && val.toString ) { + return params + }, - return val.slice(0, this.MAX_ATTR_LEN).toString(); - } else { - return null; - } - }, + /** + * Get element position. + * @param {HTMLElement} el + */ + getPosition: function(el) { + var rect = el.getBoundingClientRect() + var win = exports.utils.elementWindow(el) + + return { + width: Math.round(rect.width), + height: Math.round(rect.height), + left: Math.round(rect.left + win.pageXOffset), + top: Math.round(rect.top + win.pageYOffset) + } + }, - putPropIfExists: function(obj, name, val) { - if ( val ) { - obj[name] = val; - } - }, - - putAttrIfExists: function(obj, el, name) { - var val = this.getAttr(el, name); - this.putPropIfExists(obj, name, val); - }, - - elementToJSON: function(el, opt_findClickThru) { - var pos = this.getPosition(el); - var params = this.getParams(el); - var url = this.getUrl(el, params); - var flashvars = this.getFlashvars(el, params, url); - var clickThru = opt_findClickThru && this.findClickThru(el, flashvars); - var json = { - tagName: el.tagName, - width: pos.width, - height: pos.height, - left: pos.left, - top: pos.top, - children: [] - }; - - if ( params ) { - - delete params.flashvars; - } + /** + * + * @param {*} el + * @param {*} params + * @param {*} url + * + * @todo add description + */ + getFlashvars: function(el, params, url) { + var flashvars + var urlQS = url && url.split('?')[1] + + if (el.tagName === 'EMBED') { + flashvars = el.getAttribute('flashvars') || urlQS + } else if (el.tagName === 'OBJECT') { + flashvars = params.flashvars || el.getAttribute('flashvars') || urlQS + } - this.putAttrIfExists(json, el, 'id'); - this.putAttrIfExists(json, el, 'class'); - this.putAttrIfExists(json, el, 'name'); + return (flashvars && exports.utils.parseQS(flashvars)) || null + }, - this.putPropIfExists(json, 'flashvars', flashvars); - this.putPropIfExists(json, 'url', url); - this.putPropIfExists(json, 'params', params); - this.putPropIfExists(json, 'clickThru', clickThru); + /** + * + * @param {*} el + * @param {*} flashvars + * + * @todo add description + */ + findClickThru: function(el, flashvars) { + var key + if (el.tagName === 'IMG' && el.parentElement.tagName === 'A') { + return el.parentElement.href + } else if (flashvars) { + for (key in flashvars) { + if (flashvars.hasOwnProperty(key)) { + if (key.toLowerCase().indexOf('clicktag') === 0) { + return flashvars[key] + } + } + } + } + return null + }, - return json; - } - }; + /** + * Get element attribute. + * @param {HTMLElement} el + * @param {String} name + */ + getAttr: function(el, name) { + var val = el.getAttribute(name) - exports.parser = { elementToJSON: parser.elementToJSON.bind(parser) }; -})(exports); + if (val && val.slice && val.toString) { + return val.slice(0, this.MAX_ATTR_LEN).toString() + } else { + return null + } + }, -(function(exports) { + /** + * + * @param {*} obj + * @param {*} name + * @param {*} val + * + * @todo add description + */ + putPropIfExists: function(obj, name, val) { + if (val) { + obj[name] = val + } + }, - var ContextManager = function(adData) { - this.adData = adData; - }; + /** + * + * @param {*} obj + * @param {*} el + * @param {*} name + * + * @todo add description + */ + putAttrIfExists: function(obj, el, name) { + var val = this.getAttr(el, name) + this.putPropIfExists(obj, name, val) + }, - ContextManager.prototype = { - CONTAINER_SIZE_TOL: 0.4, - ASPECT_RATIO_FOR_LEADERBOARDS: 2, + /** + * Convert Element to JSON + * @param {HTMLElement} el + * @param {Boolean} opt_findClickThru + */ + elementToJSON: function(el, opt_findClickThru) { + var pos = this.getPosition(el) + var params = this.getParams(el) + var url = this.getUrl(el, params) + var flashvars = this.getFlashvars(el, params, url) + var clickThru = opt_findClickThru && this.findClickThru(el, flashvars) + var json = { + tagName: el.tagName, + width: pos.width, + height: pos.height, + left: pos.left, + top: pos.top, + children: [] + } - isValidContainer: function(el, opt_curWin) { + if (params) { + delete params.flashvars + } - var cWidth = el.clientWidth; - var cHeight = el.clientHeight; + this.putAttrIfExists(json, el, 'id') + this.putAttrIfExists(json, el, 'class') + this.putAttrIfExists(json, el, 'name') - var adWidth = this.adData.width; - var adHeight = this.adData.height; + this.putPropIfExists(json, 'flashvars', flashvars) + this.putPropIfExists(json, 'url', url) + this.putPropIfExists(json, 'params', params) + this.putPropIfExists(json, 'clickThru', clickThru) - var winWidth = opt_curWin && opt_curWin.innerWidth; - var winHeight = opt_curWin && opt_curWin.innerHeight; - var similarWin = opt_curWin && this.withinTol(adWidth, winWidth) && this.withinTol(adHeight, winHeight); + return json + } + } - var similarSizeX = this.withinTol(adWidth, cWidth); - var similarSizeY = this.withinTol(adHeight, cHeight); - var adAspect = adWidth / adHeight; + exports.parser = { elementToJSON: parser.elementToJSON.bind(parser) } + })(exports) + + // Anonymous invocation. + ;(function(exports) { + /** + * Setter for ad data. + * @param {*} adData + */ + var ContextManager = function(adData) { + this.adData = adData + } - return similarWin || el.tagName === 'A' || (adAspect >= this.ASPECT_RATIO_FOR_LEADERBOARDS && similarSizeY) || (similarSizeX && similarSizeY); - }, + ContextManager.prototype = { + CONTAINER_SIZE_TOL: 0.4, + ASPECT_RATIO_FOR_LEADERBOARDS: 2, + + /** + * Check if iframe is valid. + * @param {HTMLElement} el + * @param {HTMLElement} opt_curWin + */ + isValidContainer: function(el, opt_curWin) { + var cWidth = el.clientWidth + var cHeight = el.clientHeight + + var adWidth = this.adData.width + var adHeight = this.adData.height + + var winWidth = opt_curWin && opt_curWin.innerWidth + var winHeight = opt_curWin && opt_curWin.innerHeight + var similarWin = + opt_curWin && + this.withinTol(adWidth, winWidth) && + this.withinTol(adHeight, winHeight) + + var similarSizeX = this.withinTol(adWidth, cWidth) + var similarSizeY = this.withinTol(adHeight, cHeight) + var adAspect = adWidth / adHeight + + return ( + similarWin || + el.tagName === 'A' || + (adAspect >= this.ASPECT_RATIO_FOR_LEADERBOARDS && similarSizeY) || + (similarSizeX && similarSizeY) + ) + }, - withinTol: function(adlen, conlen) { - var pct = (conlen - adlen) / adlen; + /** + * Check tolerance. + * @param {Int} adlen + * @param {Int} conlen + */ + withinTol: function(adlen, conlen) { + var pct = (conlen - adlen) / adlen - return pct <= this.CONTAINER_SIZE_TOL; - }, + return pct <= this.CONTAINER_SIZE_TOL + }, - serializeElements: function(el) { - if ( !el ) { - return; - } - var i; - var ifrWin; - var adId = this.adData.adId; - var elIsAd = false; + /** + * Serialize elements. + * @param {*} el + * @todo define parameter type. + */ + serializeElements: function(el) { + if (!el) { + return + } + var i + var ifrWin + var adId = this.adData.adId + var elIsAd = false - if ( adId && el[adId] && el[adId].isAd === true ) { - elIsAd = true; - } + if (adId && el[adId] && el[adId].isAd === true) { + elIsAd = true + } - var json = exports.parser.elementToJSON(el, elIsAd); - var childJSON; + var json = exports.parser.elementToJSON(el, elIsAd) + var childJSON - if ( elIsAd ) { - json.adId = adId; - this.adData.element = {}; + if (elIsAd) { + json.adId = adId + this.adData.element = {} - var keys = Object.keys(json); - for ( i = 0; i < keys.length; i++ ) { - var key = keys[i]; - if ( key !== 'children' && key !== 'contents' ) { - this.adData.element[key] = json[key]; + var keys = Object.keys(json) + for (i = 0; i < keys.length; i++) { + var key = keys[i] + if (key !== 'children' && key !== 'contents') { + this.adData.element[key] = json[key] + } } } - } - var children = exports.utils.realArray(el.children).filter(function(el) { - var param = el.tagName === 'PARAM'; - var inlineScript = el.tagName === 'SCRIPT' && !(el.src && el.src.indexOf('http') >= 0); - var noScript = el.tagName === 'NOSCRIPT'; - return !(param || inlineScript || noScript); - }); - - for ( i = 0; i < children.length; i++ ) { - childJSON = this.serializeElements(children[i]); - if ( childJSON ) { - json.children.push(childJSON); + var children = exports.utils + .realArray(el.children) + .filter(function(el) { + var param = el.tagName === 'PARAM' + var inlineScript = + el.tagName === 'SCRIPT' && + !(el.src && el.src.indexOf('http') >= 0) + var noScript = el.tagName === 'NOSCRIPT' + return !(param || inlineScript || noScript) + }) + + for (i = 0; i < children.length; i++) { + childJSON = this.serializeElements(children[i]) + if (childJSON) { + json.children.push(childJSON) + } } - } - - if ( el.tagName === 'IFRAME' ) { - ifrWin = el.contentWindow; - if ( adId && el[adId] && el[adId].needsWindow ) { - - json.contents = this.adData.serializedIframeContents; - el[adId].needsWindow = false; - delete this.adData.serializedIframeContents; - - } else if ( exports.utils.isFriendlyWindow(ifrWin) ) { - - childJSON = this.serializeElements(ifrWin.document.documentElement); - if ( childJSON ) { - json.contents = childJSON; + if (el.tagName === 'IFRAME') { + ifrWin = el.contentWindow + + if (adId && el[adId] && el[adId].needsWindow) { + json.contents = this.adData.serializedIframeContents + el[adId].needsWindow = false + delete this.adData.serializedIframeContents + } else if (exports.utils.isFriendlyWindow(ifrWin)) { + childJSON = this.serializeElements(ifrWin.document.documentElement) + if (childJSON) { + json.contents = childJSON + } } } - } - - if ( json.children.length > 0 || json.adId || json.tagName === 'IFRAME' || json.url ) { - return json; - } else { - return null; - } - }, - - captureHTML: function(containerEl) { - this.adData.context = this.serializeElements(containerEl); - }, - nodeCount: function(el) { - return el.getElementsByTagName('*').length + 1; - }, - - highestContainer: function(curWin, referenceElement) { - var curContainer = referenceElement; - var docEl = curWin.document.documentElement; - var parentContainer; - - if ( curWin !== curWin.top && this.isValidContainer(docEl, curWin) ) { - return docEl; - } - - while ( true ) { - parentContainer = curContainer.parentElement; - if ( parentContainer && this.isValidContainer(parentContainer) ) { - curContainer = parentContainer; + if ( + json.children.length > 0 || + json.adId || + json.tagName === 'IFRAME' || + json.url + ) { + return json } else { - return curContainer; + return null } - } - } - }; - - var tagfinder = { - - setPositions: function(adData, opt_el, opt_winPos) { - var el = opt_el || adData.context; - var winPos = opt_winPos || {left: 0, top: 0}; - var ifrPos; - - el.left += winPos.left; - el.top += winPos.top; - - if ( el.children ) { - el.children.forEach(function(child) { - this.setPositions(adData, child, winPos); - }, this); - } - - if ( el.contents ) { - ifrPos = {left: el.left, top: el.top}; - this.setPositions(adData, el.contents, ifrPos); - } + }, - if ( el.adId === adData.adId ) { - adData.element.left = el.left; - adData.element.top = el.top; - } - }, - - appendTags: function(adData, referenceElement) { - var mgr = new ContextManager(adData); - var curWin = exports.utils.elementWindow(referenceElement); - var highestContainer; - - while ( true ) { - highestContainer = mgr.highestContainer(curWin, referenceElement); - mgr.captureHTML(highestContainer); - if ( curWin === curWin.top ) { - break; - } else { + /** + * Get element containers. + * @param {*} containerEl + */ + captureHTML: function(containerEl) { + this.adData.context = this.serializeElements(containerEl) + }, - curWin.mpAdFound = true; + /** + * Get number of Nodes. + * @param {HTMLElement} el + */ + nodeCount: function(el) { + return el.getElementsByTagName('*').length + 1 + }, - mgr.adData.serializedIframeContents = mgr.adData.context; + /** + * + * @param {*} curWin + * @param {*} referenceElement + * + * @todo add description + */ + highestContainer: function(curWin, referenceElement) { + var curContainer = referenceElement + var docEl = curWin.document.documentElement + var parentContainer + + if (curWin !== curWin.top && this.isValidContainer(docEl, curWin)) { + return docEl + } - if ( exports.utils.isFriendlyWindow(curWin.parent) ) { - referenceElement = curWin.frameElement; - referenceElement[mgr.adData.adId] = {needsWindow: true}; - curWin = curWin.parent; + while (true) { + parentContainer = curContainer.parentElement + if (parentContainer && this.isValidContainer(parentContainer)) { + curContainer = parentContainer } else { - break; + return curContainer } } } - return { - referenceElement:referenceElement, - highestContainer: highestContainer - }; } - }; - - exports.tagfinder = tagfinder; -})(exports); - -(function(exports) { - var _onAdFound; - var _logGen = new exports.utils.LogGenerator(); - var _pageTags; - var INIT_MS_BW_SEARCHES = 2000; - var PAGE_TAG_RE = new RegExp('gpt|oascentral'); - var POST_MSG_ID = '1554456894-8541-12665-19466-15909'; - var AD_SERVER_RE = new RegExp('^(google_ads_iframe|oas_frame|atwAdFrame)'); - - function getPageTags(doc) { - var scripts = doc.getElementsByTagName('script'); - var pageTags = []; - scripts = exports.utils.realArray(scripts); - scripts.forEach(function(script) { - if ( PAGE_TAG_RE.exec(script.src) ) { - pageTags.push({'tagName': 'SCRIPT', 'url': script.src}); - } - }); - return pageTags; - } - - function messageAllParentFrames(adData) { - - adData.postMessageId = POST_MSG_ID; - - adData = JSON.stringify(adData); - - var win = window; - while ( win !== win.top ) { - win = win.parent; - win.postMessage(adData, '*'); - } - } - function appendTagsAndSendToParent(adData, referenceElement) { - var results = exports.tagfinder.appendTags(adData, referenceElement); - if ( exports.utils.SCRIPT_IN_HOSTILE_IFRAME ) { - messageAllParentFrames(adData); - - } else if ( exports.utils.SCRIPT_IN_WINDOW_TOP ) { - - exports.tagfinder.setPositions(adData); - - adData.matchedSize = exports.adfinder.getMatchedAdSize(adData.width, adData.height); - if ( !adData.matchedSize ) { + var tagfinder = { + /** + * + * @param {*} adData + * @param {*} opt_el + * @param {*} opt_winPos + * + * @todo add description + */ + setPositions: function(adData, opt_el, opt_winPos) { + var el = opt_el || adData.context + var winPos = opt_winPos || { left: 0, top: 0 } + var ifrPos + + el.left += winPos.left + el.top += winPos.top + + if (el.children) { + el.children.forEach(function(child) { + this.setPositions(adData, child, winPos) + }, this) + } - if ( AD_SERVER_RE.exec(results.referenceElement.id) ) { - adData.matchedSize = [adData.width, adData.height]; - adData.oddSize = true; - } else { + if (el.contents) { + ifrPos = { left: el.left, top: el.top } + this.setPositions(adData, el.contents, ifrPos) + } - return; + if (el.adId === adData.adId) { + adData.element.left = el.left + adData.element.top = el.top } - } - delete adData.width; - delete adData.height; - adData.curPageUrl = exports.utils.getPageUrl(); - _pageTags = _pageTags || getPageTags(document); - var log = _logGen.log('ad', [adData], _pageTags); + }, - if ( _onAdFound ) { + /** + * + * @param {*} adData + * @param {*} referenceElement + * + * @todo add description + */ + appendTags: (adData, referenceElement) => { + var mgr = new ContextManager(adData) + var curWin = exports.utils.elementWindow(referenceElement) + var highestContainer + + while (true) { + highestContainer = mgr.highestContainer(curWin, referenceElement) + mgr.captureHTML(highestContainer) + if (curWin === curWin.top) { + break + } else { + curWin.mpAdFound = true - _onAdFound(log, results.referenceElement); + mgr.adData.serializedIframeContents = mgr.adData.context + if (exports.utils.isFriendlyWindow(curWin.parent)) { + referenceElement = curWin.frameElement + referenceElement[mgr.adData.adId] = { needsWindow: true } + curWin = curWin.parent + } else { + break + } + } + } + return { + referenceElement: referenceElement, + highestContainer: highestContainer + } } } - } - function extractAdsWrapper() { - if ( exports.utils.SCRIPT_IN_WINDOW_TOP || document.readyState === 'complete' ) { - extractAds(); + exports.tagfinder = tagfinder + })(exports) + ;(function(exports) { + var _onAdFound + var _logGen = new exports.utils.LogGenerator() + var _pageTags + var INIT_MS_BW_SEARCHES = 2000 + var PAGE_TAG_RE = new RegExp('gpt|oascentral') + var POST_MSG_ID = '1554456894-8541-12665-19466-15909' + var AD_SERVER_RE = new RegExp('^(google_ads_iframe|oas_frame|atwAdFrame)') + + /** + * Get script tags from document. + * @param {Object} doc + */ + function getPageTags(doc) { + var scripts = doc.getElementsByTagName('script') + var pageTags = [] + scripts = exports.utils.realArray(scripts) + scripts.forEach(function(script) { + if (PAGE_TAG_RE.exec(script.src)) { + pageTags.push({ tagName: 'SCRIPT', url: script.src }) + } + }) + return pageTags } - setTimeout( - function() { extractAdsWrapper(); }, INIT_MS_BW_SEARCHES - ); - } - - function extractAds() { - var ads = exports.adfinder.findAds(window); - ads.forEach(function(ad) { - var startTime = new Date().getTime(); - var adId = startTime + '-' + Math.floor(Math.random() * 10e12); + /** + * Send message to parent iFrames. + * @param {String} adData + */ + function messageAllParentFrames(adData) { + adData.postMessageId = POST_MSG_ID - var adData = { - width: Math.round(ad.offsetWidth), - height: Math.round(ad.offsetHeight), - startTime: startTime, - adId: adId, - html5: ad.html5 || false - }; + adData = JSON.stringify(adData) - if ( ad.html5 ) { - adData.adSizeMeta = ad.adSizeMeta || null; - adData.winClickTag = ad.winClickTag || null; + var win = window + while (win !== win.top) { + win = win.parent + win.postMessage(adData, '*') } - - ad[adId] = { isAd: true }; - - appendTagsAndSendToParent(adData, ad); - }); - } - - function isChildWin(myWin, otherWin) { - var parentWin = otherWin.parent; - while ( parentWin !== otherWin ) { - if ( parentWin === myWin ) { - return true; - } - otherWin = parentWin; - parentWin = parentWin.parent; } - return false; - } - - function iframeFromWindow(win, winToMatch) { - var i, ifr, ifrWin, - iframes = win.document.querySelectorAll('iframe'); - for ( i = 0; i < iframes.length; i++ ) { - ifr = iframes[i]; - if ( ifr.contentWindow === winToMatch ) { - return ifr; + /** + * + * @param {String} adData + * @param {HTMLElement} referenceElement + * + * @todo update description + */ + function appendTagsAndSendToParent(adData, referenceElement) { + var results = exports.tagfinder.appendTags(adData, referenceElement) + if (exports.utils.SCRIPT_IN_HOSTILE_IFRAME) { + messageAllParentFrames(adData) + } else if (exports.utils.SCRIPT_IN_WINDOW_TOP) { + exports.tagfinder.setPositions(adData) + + adData.matchedSize = exports.adfinder.getMatchedAdSize( + adData.width, + adData.height + ) + if (!adData.matchedSize) { + if (AD_SERVER_RE.exec(results.referenceElement.id)) { + adData.matchedSize = [adData.width, adData.height] + adData.oddSize = true + } else { + return + } + } + delete adData.width + delete adData.height + adData.curPageUrl = exports.utils.getPageUrl() + _pageTags = _pageTags || getPageTags(document) + var log = _logGen.log('ad', [adData], _pageTags) + + if (_onAdFound) { + _onAdFound(log, results.referenceElement) + } } } - for ( i = 0; i < iframes.length; i++ ) { - ifrWin = iframes[i].contentWindow; - if ( exports.utils.isFriendlyWindow(ifrWin) ) { - ifr = iframeFromWindow(ifrWin, winToMatch); - if ( ifr ) { - return ifr; - } + /** + * SetTimeout wrapper for extracting ads. + */ + function extractAdsWrapper() { + if ( + exports.utils.SCRIPT_IN_WINDOW_TOP || + document.readyState === 'complete' + ) { + extractAds() } + setTimeout(function() { + extractAdsWrapper() + }, INIT_MS_BW_SEARCHES) } - } - function onPostMessage(event) { - var adData, - ifrWin = event.source, + /** + * Main function for extracting ads after loaded. + */ + function extractAds() { + var ads = exports.adfinder.findAds(window) + ads.forEach(function(ad) { + var startTime = new Date().getTime() + var adId = startTime + '-' + Math.floor(Math.random() * 10e12) + + var adData = { + width: Math.round(ad.offsetWidth), + height: Math.round(ad.offsetHeight), + startTime: startTime, + adId: adId, + html5: ad.html5 || false + } - myWin = window.document.defaultView, - ifrTag; + if (ad.html5) { + adData.adSizeMeta = ad.adSizeMeta || null + adData.winClickTag = ad.winClickTag || null + } - if ( typeof event.data === "string" && event.data.indexOf(POST_MSG_ID) != -1 ) { - try { + ad[adId] = { isAd: true } - adData = JSON.parse(event.data); - } catch (e) { + appendTagsAndSendToParent(adData, ad) + }) + } - return; + /** + * Check if window is child of parent. + * @param {Object} myWin + * @param {Object} otherWin + */ + function isChildWin(myWin, otherWin) { + var parentWin = otherWin.parent + while (parentWin !== otherWin) { + if (parentWin === myWin) { + return true + } + otherWin = parentWin + parentWin = parentWin.parent } + return false } - else - return; - - if ( adData.postMessageId === POST_MSG_ID ) { - delete adData.postMessageId; - - event.stopImmediatePropagation(); - - if ( isChildWin(myWin, ifrWin) ) { - if ( exports.utils.isFriendlyWindow(ifrWin) ) { - ifrTag = ifrWin.frameElement; - } else { - ifrTag = iframeFromWindow(myWin, ifrWin); + /** + * + * @param {*} win + * @param {*} winToMatch + * + * @todo update description + */ + function iframeFromWindow(win, winToMatch) { + var i, + ifr, + ifrWin, + iframes = win.document.querySelectorAll('iframe') + + for (i = 0; i < iframes.length; i++) { + ifr = iframes[i] + if (ifr.contentWindow === winToMatch) { + return ifr } + } - if ( ifrTag ) { - ifrTag[adData.adId] = {needsWindow: true}; - appendTagsAndSendToParent(adData, ifrTag); + for (i = 0; i < iframes.length; i++) { + ifrWin = iframes[i].contentWindow + if (exports.utils.isFriendlyWindow(ifrWin)) { + ifr = iframeFromWindow(ifrWin, winToMatch) + if (ifr) { + return ifr + } } } } - } - - function onVideoMessage(msg, sender, callback) { - var log; - if ( msg.event === 'new-video-ad' ) { - msg.assets.forEach(function(asset) { - }); - log = _logGen.log('video', msg.assets); - } else { - log = _logGen.log('invalid-video', msg.assets); - } + /** + * + * @param {*} event + * + * @todo update description + */ + function onPostMessage(event) { + var adData, + ifrWin = event.source, + myWin = window.document.defaultView, + ifrTag + + if ( + typeof event.data === 'string' && + event.data.indexOf(POST_MSG_ID) != -1 + ) { + try { + adData = JSON.parse(event.data) + } catch (e) { + return + } + } else return - msg.assets.forEach(function(a) {delete a.isVideo;}); - log.displayAdFound = msg.displayAdFound; - log.requests = msg.requests; - log.data = msg.event_data; + if (adData.postMessageId === POST_MSG_ID) { + delete adData.postMessageId - log.doc.finalPageUrl = log.doc.url; - log.doc.url = exports.utils.normalizeUrl(msg.origUrl); + event.stopImmediatePropagation() - _onAdFound(log); - } + if (isChildWin(myWin, ifrWin)) { + if (exports.utils.isFriendlyWindow(ifrWin)) { + ifrTag = ifrWin.frameElement + } else { + ifrTag = iframeFromWindow(myWin, ifrWin) + } - function addBackgroundListener(event, callback) { - if ( typeof chrome !== 'undefined' ) { - chrome.runtime.onMessage.addListener(function(msg) { - if ( msg.event === event ) { - callback(msg); + if (ifrTag) { + ifrTag[adData.adId] = { needsWindow: true } + appendTagsAndSendToParent(adData, ifrTag) + } } - }); - } - else if (window.self.port) { - window.self.port.on(event, callback); + } } - } - exports.coordinator = { - addPostMessageListener: function() { - if ( !exports.utils.SCRIPT_IN_FRIENDLY_IFRAME ) { - window.addEventListener('message', onPostMessage, false); + /** + * + * @param {*} msg + * @param {*} sender + * @param {*} callback + * + * @todo update description + */ + function onVideoMessage(msg, sender, callback) { + var log + if (msg.event === 'new-video-ad') { + msg.assets.forEach(function(asset) {}) + log = _logGen.log('video', msg.assets) + } else { + log = _logGen.log('invalid-video', msg.assets) } - }, - blockedRobotsMsgGen: function(sendFcn, origUrl) { + msg.assets.forEach(function(a) { + delete a.isVideo + }) + log.displayAdFound = msg.displayAdFound + log.requests = msg.requests + log.data = msg.event_data - if ( origUrl.indexOf('google.com/_/chrome/newtab') === -1 ) { - var onBlockedRobotsMessage = function() { - var log; - log = _logGen.log('invalid-robotstxt', []); - log.doc.finalPageUrl = log.doc.url; - log.doc.url = exports.utils.normalizeUrl(origUrl); + log.doc.finalPageUrl = log.doc.url + log.doc.url = exports.utils.normalizeUrl(msg.origUrl) - sendFcn(log); - }; - return onBlockedRobotsMessage; - } else { - return function() {}; + _onAdFound(log) + } + + /** + * Add background listener. + * @param {String} event + * @param {Function} callback + */ + function addBackgroundListener(event, callback) { + if (typeof chrome !== 'undefined') { + chrome.runtime.onMessage.addListener(function(msg) { + if (msg.event === event) { + callback(msg) + } + }) + } else if (window.self.port) { + window.self.port.on(event, callback) } - }, + } - init: function(onAdFound) { + exports.coordinator = { + /** + * @todo update description + */ + addPostMessageListener: function() { + if (!exports.utils.SCRIPT_IN_FRIENDLY_IFRAME) { + window.addEventListener('message', onPostMessage, false) + } + }, - if ( exports.utils.SCRIPT_IN_FRIENDLY_IFRAME ) { - return false; - } + /** + * + * @param {*} sendFcn + * @param {*} origUrl + * + * @todo update description + */ + blockedRobotsMsgGen: function(sendFcn, origUrl) { + if (origUrl.indexOf('google.com/_/chrome/newtab') === -1) { + var onBlockedRobotsMessage = function() { + var log + log = _logGen.log('invalid-robotstxt', []) + log.doc.finalPageUrl = log.doc.url + log.doc.url = exports.utils.normalizeUrl(origUrl) + + sendFcn(log) + } + return onBlockedRobotsMessage + } else { + return function() {} + } + }, - _onAdFound = onAdFound; - if ( exports.utils.SCRIPT_IN_WINDOW_TOP ) { - var log = _logGen.log('page'); - onAdFound(log); + /** + * + * @param {*} onAdFound + */ + init: function(onAdFound) { + if (exports.utils.SCRIPT_IN_FRIENDLY_IFRAME) { + return false + } - window.addEventListener('beforeunload', function(event) { - var log = _logGen.log('unload'); - log.timing = window.performance.timing; - onAdFound(log); - }); + _onAdFound = onAdFound + if (exports.utils.SCRIPT_IN_WINDOW_TOP) { + var log = _logGen.log('page') + onAdFound(log) - addBackgroundListener('new-video-ad', onVideoMessage); - addBackgroundListener('new-invalid-video-ad', onVideoMessage); + window.addEventListener('beforeunload', function(event) { + var log = _logGen.log('unload') + log.timing = window.performance.timing + onAdFound(log) + }) - } + addBackgroundListener('new-video-ad', onVideoMessage) + addBackgroundListener('new-invalid-video-ad', onVideoMessage) + } - exports.utils.onDocLoaded(document, extractAdsWrapper); + exports.utils.onDocLoaded(document, extractAdsWrapper) + } } - }; - -})(exports); - -if ( exports.utils.SCRIPT_IN_WINDOW_TOP ) { - window.adparser = { - init: exports.coordinator.init, - addPostMessageListener: exports.coordinator.addPostMessageListener, - askIfTrackingEnabled: exports.utils.askIfTrackingEnabled, - blockedRobotsMsgGen: exports.coordinator.blockedRobotsMsgGen, - inWindowTop: exports.utils.SCRIPT_IN_WINDOW_TOP, - sendToBackground: exports.utils.sendToBackground - }; -} else { - exports.coordinator.addPostMessageListener(); - exports.utils.askIfTrackingEnabled( - function() { - exports.coordinator.init(function() {}); - }, - function() {} - ); -} -})(window); -(function(adparser, pageUrl) { - function onAdFound(log) { - adparser.sendToBackground({ func: 'onAd', args: [log] }, 'onAd', '', function(){}); - } - - if ( adparser && adparser.inWindowTop ) { - adparser.addPostMessageListener(); - adparser.askIfTrackingEnabled( + })(exports) + + if (exports.utils.SCRIPT_IN_WINDOW_TOP) { + window.adparser = { + init: exports.coordinator.init, + addPostMessageListener: exports.coordinator.addPostMessageListener, + askIfTrackingEnabled: exports.utils.askIfTrackingEnabled, + blockedRobotsMsgGen: exports.coordinator.blockedRobotsMsgGen, + inWindowTop: exports.utils.SCRIPT_IN_WINDOW_TOP, + sendToBackground: exports.utils.sendToBackground + } + } else { + exports.coordinator.addPostMessageListener() + exports.utils.askIfTrackingEnabled( function() { - adparser.init(onAdFound); + exports.coordinator.init(function() {}) }, - adparser.blockedRobotsMsgGen(onAdFound, pageUrl) + function() {} + ) + } +})(window) +;(function(adparser, pageUrl) { + function onAdFound(log) { + adparser.sendToBackground( + { func: 'onAd', args: [log] }, + 'onAd', + '', + function() {} ) } -})(window.adparser, window.location.href); + + if (adparser && adparser.inWindowTop) { + adparser.addPostMessageListener() + adparser.askIfTrackingEnabled(function() { + adparser.init(onAdFound) + }, adparser.blockedRobotsMsgGen(onAdFound, pageUrl)) + } +})(window.adparser, window.location.href) diff --git a/src/drivers/webextension/js/options.js b/src/drivers/webextension/js/options.js index 505117b52..860e141a7 100644 --- a/src/drivers/webextension/js/options.js +++ b/src/drivers/webextension/js/options.js @@ -5,6 +5,9 @@ const { i18n, getOption, setOption } = Utils const Options = { + /** + * Initialize Chrome options. + */ async init() { // Theme mode const themeMode = await getOption('themeMode', false) diff --git a/src/drivers/webextension/js/popup.js b/src/drivers/webextension/js/popup.js index f0bad8266..fc3442df3 100644 --- a/src/drivers/webextension/js/popup.js +++ b/src/drivers/webextension/js/popup.js @@ -7,6 +7,9 @@ const { agent, i18n, getOption, setOption, promisify } = Utils const Popup = { port: chrome.runtime.connect({ name: 'popup.js' }), + /** + * Initialize popup. + */ async init() { // Templates Popup.templates = Array.from( @@ -47,6 +50,7 @@ const Popup = { Popup.driver('getDetections') }) + // Run internationalization. i18n() } @@ -67,14 +71,27 @@ const Popup = { .addEventListener('click', () => chrome.runtime.openOptionsPage()) }, + /** + * Apply function to postMessage request. + * @param {function} func + * @param {...any} args + */ driver(func, ...args) { Popup.port.postMessage({ func, args }) }, + /** + * Log message. + * @param {String} message + */ log(message) { Popup.driver('log', message, 'popup.js') }, + /** + * Group technologies into categories. + * @param {Object} technologies + */ categorise(technologies) { return Object.values( technologies.reduce((categories, technology) => { @@ -92,6 +109,10 @@ const Popup = { ) }, + /** + * Callback for getDetection listener. + * @param {Array} detections + */ async onGetDetections(detections) { const pinnedCategory = await getOption('pinnedCategory') @@ -189,6 +210,7 @@ const Popup = { } } +// Add listener for popup PostMessage API. Popup.port.onMessage.addListener(({ func, args }) => { const onFunc = `on${func.charAt(0).toUpperCase() + func.slice(1)}` diff --git a/src/drivers/webextension/js/utils.js b/src/drivers/webextension/js/utils.js index 3f83449d1..31554cb60 100644 --- a/src/drivers/webextension/js/utils.js +++ b/src/drivers/webextension/js/utils.js @@ -5,6 +5,12 @@ const Utils = { agent: chrome.extension.getURL('/').startsWith('moz-') ? 'firefox' : 'chrome', + /** + * Promise utility tool. + * @param {Object} context + * @param {String} method + * @param {...any} args + */ promisify(context, method, ...args) { return new Promise((resolve, reject) => { context[method](...args, (...args) => { @@ -17,10 +23,20 @@ const Utils = { }) }, + /** + * Chrome tab utility. + * @param {String} url + * @param {Boolean} active + */ open(url, active = true) { chrome.tabs.create({ url, active }) }, + /** + * Get value from local storage. + * @param {String} name + * @param {string|mixed|null} defaultValue + */ async getOption(name, defaultValue = null) { try { const option = await Utils.promisify(chrome.storage.local, 'get', name) @@ -35,6 +51,11 @@ const Utils = { } }, + /** + * Set value in local storage. + * @param {String} name + * @param {String} value + */ async setOption(name, value) { try { await Utils.promisify(chrome.storage.local, 'set', { @@ -45,6 +66,9 @@ const Utils = { } }, + /** + * Load internationalization. + */ i18n() { Array.from(document.querySelectorAll('[data-i18n]')).forEach( (node) => (node.innerHTML = chrome.i18n.getMessage(node.dataset.i18n)) diff --git a/src/wappalyzer.js b/src/wappalyzer.js index dbbc74ebc..fc669fae1 100644 --- a/src/wappalyzer.js +++ b/src/wappalyzer.js @@ -20,6 +20,10 @@ const Wappalyzer = { return Wappalyzer.categories.find(({ id: _id }) => id === _id) }, + /** + * Resolve promises for implied technology. + * @param {Array} detections + */ resolve(detections = []) { const resolved = detections.reduce((resolved, { technology }) => { if ( @@ -68,6 +72,10 @@ const Wappalyzer = { ) }, + /** + * Resolve promises for version of technology. + * @param {Promise} resolved + */ resolveVersion({ version, regex }, match) { let resolved = version @@ -99,6 +107,10 @@ const Wappalyzer = { return resolved }, + /** + * Resolve promises for excluded technology. + * @param {Promise} resolved + */ resolveExcludes(resolved) { resolved.forEach(({ technology }) => { technology.excludes.forEach((name) => { @@ -117,6 +129,10 @@ const Wappalyzer = { }) }, + /** + * Resolve promises for implied technology. + * @param {Promise} resolved + */ resolveImplies(resolved) { let done = false @@ -145,6 +161,10 @@ const Wappalyzer = { } }, + /** + * Initialize analyzation. + * @param {*} param0 + */ analyze({ url, html, meta, headers, cookies, scripts }) { const oo = Wappalyzer.analyzeOneToOne const om = Wappalyzer.analyzeOneToMany @@ -172,6 +192,10 @@ const Wappalyzer = { } }, + /** + * Extract technologies from data collected. + * @param {*object} data + */ setTechnologies(data) { const transform = Wappalyzer.transformPatterns @@ -212,6 +236,10 @@ const Wappalyzer = { }, []) }, + /** + * Assign categories for data. + * @param {Object} data + */ setCategories(data) { Wappalyzer.categories = Object.keys(data) .reduce((categories, id) => { @@ -228,6 +256,10 @@ const Wappalyzer = { .sort(({ priority: a }, { priority: b }) => (a > b ? -1 : 0)) }, + /** + * Extract information from regex pattern. + * @param {string|array} patterns + */ transformPatterns(patterns) { if (!patterns) { return [] @@ -272,6 +304,12 @@ const Wappalyzer = { return 'main' in parsed ? parsed.main : parsed }, + /** + * @todo describe + * @param {Object} technology + * @param {String} type + * @param {String} value + */ analyzeOneToOne(technology, type, value) { return technology[type].reduce((technologies, pattern) => { if (pattern.regex.test(value)) { @@ -286,6 +324,12 @@ const Wappalyzer = { }, []) }, + /** + * @todo update + * @param {Object} technology + * @param {String} type + * @param {Array} items + */ analyzeOneToMany(technology, type, items = []) { return items.reduce((technologies, value) => { const patterns = technology[type] || [] @@ -304,6 +348,12 @@ const Wappalyzer = { }, []) }, + /** + * + * @param {Object} technology + * @param {String} type + * @param {Array} items + */ analyzeManyToMany(technology, type, items = {}) { return Object.keys(technology[type]).reduce((technologies, key) => { const patterns = technology[type][key] || []