// ==UserScript== // @name RU AdList JS Fixes // @namespace ruadlist_js_fixes // @version 20231127.1 // @description try to take over the world! // @author lainverse & dimisa // @license CC-BY-SA-4.0 // @supportURL https://greasyfork.org/en/scripts/19993-ru-adlist-js-fixes/feedback // @match *://*/* // @exclude /^https?:\/\/([^.]+\.)*?(avito\.ru|auth\.wi-fi\.ru|hd\.kinopoisk\.ru|(webntp|brontp(-pr|-beta|-alpha)?|diehard|market(-delivery)?|money|trust)\.yandex\.(by|kz|ru|net)|account\.mail\.ru)([:/]|$)/ // @exclude /^https?:\/\/([^.]+\.)*?(1cfresh\.com|(alfabank|(cdn-)?tinkoff|sberbank)\.ru|ingress\.com|lineageos\.org|telegram\.org|unicreditbanking\.net)([:/]|$)/ // @compatible chrome Only with Tampermonkey or Violentmonkey. Только с Tampermonkey или Violentmonkey. // @compatible brave Only with Tampermonkey or Violentmonkey. Только с Tampermonkey или Violentmonkey. // @compatible vivaldi Only with Tampermonkey or Violentmonkey. Только с Tampermonkey или Violentmonkey. // @compatible edge Only in Edge 79+ with Tampermonkey or Violentmonkey. Только в Edge 79+ с Tampermonkey или Violentmonkey. // @compatible firefox Only in Firefox 56+ with Tampermonkey. Только Firefox 56+ с Tampermonkey. // @compatible opera Only with Tampermonkey. Только с Tampermonkey. // @grant GM_getValue // @grant GM_setValue // @grant GM_listValues // @grant GM_registerMenuCommand // @grant GM.cookie // @grant unsafeWindow // @grant window.close // @run-at document-start // @downloadURL none // ==/UserScript== // jshint esversion: 8 // jshint unused: true /* eslint no-empty: ["error", { "allowEmptyCatch": true }] */ (function () { 'use strict'; const win = (unsafeWindow || window); // MooTools are crazy enough to replace standard browser object window.Document: https://mootools.net/core // Occasionally their code runs before my script on some domains and causes all kinds of havoc. const _Document = Object.getPrototypeOf(HTMLDocument.prototype), _Element = Object.getPrototypeOf(HTMLElement.prototype); // dTree 2.05 in some cases replaces Node object const _Node = Object.getPrototypeOf(_Element); // http://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser const // isOpera = (!!window.opr && !!window.opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0, // isChrome = !!window.chrome && !!window.chrome.webstore, isSafari = Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0 || (function (p) { return p.toString() === "[object SafariRemoteNotification]"; })(!window.safari || window.safari.pushNotification), isFirefox = 'InstallTrigger' in win, inIFrame = (win.self !== win.top); const _bindCall = fun => Function.prototype.call.bind(fun), _getAttribute = _bindCall(_Element.getAttribute), _setAttribute = _bindCall(_Element.setAttribute), _removeAttribute = _bindCall(_Element.removeAttribute), _hasOwnProperty = _bindCall(Object.prototype.hasOwnProperty), _toString = _bindCall(Function.prototype.toString), _document = win.document, _de = _document.documentElement, _appendChild = _Document.appendChild.bind(_de), _removeChild = _Document.removeChild.bind(_de), _createElement = _Document.createElement.bind(_document), _querySelector = _Document.querySelector.bind(_document), _querySelectorAll = _Document.querySelectorAll.bind(_document), _apply = Reflect.apply, _construct = Reflect.construct; const _attachShadow = (() => { try { return ('attachShadow' in _Element) ? _bindCall(_Element.attachShadow) : null; } catch (ignore) {} })(); let skipLander = true; try { skipLander = !(isFirefox && 'StopIteration' in win); } catch (ignore) {} const _console = {}; _console.initConsole = () => { const keys = new Set(); const _stopImmediatePropagation = _bindCall(Event.prototype.stopImmediatePropagation); win.addEventListener('message', e => { if (e.source === win || typeof e.data !== 'string') return; if (e.data.startsWith('_console.key')) { _stopImmediatePropagation(e); keys.add(e.data.slice(13)); } if (keys.has(e.data.slice(0, 10))) { _stopImmediatePropagation(e); _console.log(`From: ${e.origin}\n${e.data.slice(11)}`); } }); if (inIFrame) { const _postMessage = win.parent.postMessage.bind(win.parent); const key = Math.random().toString(36).substr(2).padStart(10, '0').slice(0, 10); _postMessage(`_console.key ${key}`, '*'); const _Object_toString = _bindCall(Object.prototype.toString); const _Symbol_toString = _bindCall(Symbol.prototype.toString); const _Array_toString = _bindCall(Array.prototype.toString); const selfHandled = ['string', 'number', 'boolean', 'undefined']; const stringify = x => { if (x === null) return 'null'; const type = typeof x; if (selfHandled.includes(type)) return x; if (type === 'object') { if (x instanceof Window || x instanceof Document) return _Object_toString(x); if (Array.isArray(x)) return _Array_toString(x); if (x instanceof Element) return `<${x.tagName} ${Array.from(x.attributes).map(x => x.value ? `${x.name}="${x.value}"`: x.name).join(' ')}>`; let props = Object.getOwnPropertyNames(x); if (props.length > 30) { props.splice(30, props.length - 30); props.push('\u2026'); } return `[object {${props.join(', ')}}]`; } if (type === 'function') { let str = _toString(x); return str.length > 200 ? `${str.slice(0, 200)}\u2026` : str; } if (type === 'symbol') return _Symbol_toString(x); return `[unhandled ${typeof x}]`; }; const passIt = (...args) => { let strs = args.map(stringify); _postMessage(`${key} ${strs.join(' ')}`, '*', ); }; for (let name in win.console) _console[name] = passIt; } else { for (let name in win.console) _console[name] = console[name]; _console._trace = _console.trace; _console.trace = (...args) => { if (!skipLander) return _console.warn(...args); _console.groupCollapsed(...args); _console._trace('Stack trace.'); _console.groupEnd(); }; } Object.freeze(_console); Object.defineProperty(win.console, 'clear', { value() { return null; } }); }; _console.initConsole(); const jsf = (function () { const opts = {}; let getValue = (a, b) => b, setValue = () => null, listValues = () => []; try { [getValue, setValue, listValues] = [GM_getValue, GM_setValue, GM_listValues]; } catch (ignore) {} // defaults opts.Lang = 'rus'; opts.AbortExecutionStatistics = false; opts.AccessStatistics = false; opts.LogAttachedCSS = false; opts.LogAdditionalInfo = false; opts.BlockNotificationPermissionRequests = false; opts.ShowScriptHandlerCompatibilityWarning = true; // load actual values for (let name of listValues()) opts[name] = getValue(name, opts[name]); const checkName = name => { if (!_hasOwnProperty(opts, name)) throw new Error('Attempt to access missing option value.'); return true; }; return new Proxy(opts, { get(opts, name) { if (name === 'toString') return () => JSON.stringify(opts); if (checkName(name)) return opts[name]; }, set(opts, name, value) { if (checkName(name)) { opts[name] = value; setValue(name, value); } return true; } }); })(); if (jsf.BlockNotificationPermissionRequests && win.Notification && win.Notification.permission === 'default') { win.Notification.requestPermission = () => new Promise(resolve => resolve('denied')); Object.defineProperty(win.Notification, 'permission', { set() {}, get() { return 'denied'; } }); } if (isFirefox && // Exit on image pages in Fx _document.constructor.prototype.toString() === '[object ImageDocumentPrototype]') return; // NodeList and HTMLCollection iterator polyfill // required for old versions of Safari and Chrome 49 (last available for WinXP) // https://jakearchibald.com/2014/iterators-gonna-iterate/ if (!NodeList.prototype[Symbol.iterator]) NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]; if (!HTMLCollection.prototype[Symbol.iterator]) HTMLCollection.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]; // Firefox 60 ESR Fix: calling toString on Proxy object throws Error function FxProxyToStringFix(root) { if (!isFirefox || parseInt((navigator.userAgent.match(/Firefox\/(\d+)\./) || [])[1]) > 60) return; const wrapper = { apply(fun, that, args) { let res; try { res = _apply(fun, that, args); } catch (e) { if (typeof that === 'function') res = Function[fun.name || 'toString'](); else throw e; if (that.name && res) res = res.replace('Function', that.name); } return res; } }; ['toLocaleString', 'toSource', 'toString'].forEach( toWrap => (root.Function.prototype[toWrap] = new Proxy(root.Function.prototype[toWrap], wrapper)) ); } FxProxyToStringFix(win); // Stub for missing "GM.cookie.list" in unsupported script managers if (GM.cookie === undefined) GM.cookie = { list: () => ({ then: () => null }) }; // Wrapper to run scripts designed to override objects available to other scripts // Required in old versions of Firefox (<58) or when running with Greasemonkey const batchLand = [], batchPrepend = new Set(), _APIString = `const win = window, isFirefox = ${isFirefox}, inIFrame = ${inIFrame}, _document = win.document, _de = _document.documentElement, _Document = Object.getPrototypeOf(HTMLDocument.prototype), _Element = Object.getPrototypeOf(HTMLElement.prototype), _Node = Object.getPrototypeOf(_Element), _appendChild = _Document.appendChild.bind(_de), _removeChild = _Document.removeChild.bind(_de), skipLander = ${skipLander}, _createElement = _Document.createElement.bind(_document), _querySelector = _Document.querySelector.bind(_document), _querySelectorAll = _Document.querySelectorAll.bind(_document), _bindCall = fun => Function.prototype.call.bind(fun), _getAttribute = _bindCall(_Element.getAttribute), _setAttribute = _bindCall(_Element.setAttribute), _removeAttribute = _bindCall(_Element.removeAttribute), _hasOwnProperty = _bindCall(Object.prototype.hasOwnProperty), _toString = _bindCall(Function.prototype.toString), _apply = Reflect.apply, _construct = Reflect.construct; const GM = { info: { version: '0.0', scriptHandler: null }, cookie: { list: () => ({ then: () => null }) } }; const jsf = ${jsf.toString()}, _console = {}; (${_console.initConsole.toString()})();`, landScript = (f, pre) => { const script = _createElement('script'); script.textContent = `(()=>{${_APIString}${[...pre].join(';')};(${f.join(')();(')})();})();`; _appendChild(script); _removeChild(script); }; let scriptLander = f => f(); if (!skipLander) { scriptLander = (func, ...prepend) => { prepend.forEach(x => batchPrepend.add(x)); batchLand.push(func); }; _document.addEventListener( 'DOMContentLoaded', () => void(scriptLander = (f, ...prep) => landScript([f], prep)), false ); } function nullTools(opts) { /* jshint validthis: true */ const nt = this; opts = opts || {}; function Stats() { const updated = new Map(); const logged = new Map(); function printUpdated() { const prepared = [...updated].map(x => { const [prop, dir] = x; logged.set(prop, logged.get(prop) | dir); return `${prop} (${dir ^ (Stats.GET | Stats.SET) ? (dir & Stats.GET ? 'R' : 'W') : 'R/W'})`; }).sort(); updated.clear(); _console.log(`Accessed properties:\n * ${prepared.join('\n * ')}`); } let logLock; this.log = async (prop, dir) => { if (!jsf.AccessStatistics) return; if (!(logged.get(prop) & dir)) updated.set(prop, updated.get(prop) | dir); logLock = (logLock > 0 || !updated.size) ? logLock : setInterval(() => { printUpdated(); logLock = clearInterval(logLock); }, 2500); }; } Stats.GET = 1; Stats.SET = 2; Object.freeze(Stats); const stats = new Stats(); const log = async (...args) => jsf.LogAdditionalInfo && _console.log(...args); const isObjecty = x => (x !== null) && typeof x === 'object' || typeof x === 'function'; const parsePath = path => { let root = win, chain = path.split('.'), link = chain.shift(); for (; chain.length > 0; link = chain.shift()) { if (!isObjecty(root[link])) break; root = root[link]; } return [root, link, chain]; }; const namedObjects = new WeakMap(); nt.destroy = function (o, destroy) { if (!opts.destroy && !destroy && o instanceof Object) return; log('cleaning', o); try { for (let item in o) { if (item instanceof Object) nt.destroy(item); delete o[item]; } } catch (e) { log('Error in object destructor', e); } }; nt.define = function (path, val, other = {}) { other.enumerable = other.enumerable || false; const [obj, prop, remainder] = parsePath(path); if (remainder.length) { if (other.dontWait) _console.warn(`Unable to resolve ${remainder.join('.')} in ${path}`); else { let _obj; Object.defineProperty(obj, prop, { get() { return _obj; }, set(val) { _obj = val; nt.define(path, val, other); }, configurable: true }); } return; } namedObjects.set(obj, path); nt.defineOn(obj, prop, val, path, other); }; nt.defineOn = function (obj, prop, val, path, other = {}) { path = path || ((obj === win) ? prop : `?.${prop}`); if (path[path.length - 1] === '.') path = `${path}${prop}`; const desc = Object.getOwnPropertyDescriptor(obj, prop); if (desc !== undefined && !desc.configurable) { _console.warn(`Unable to redefine not configurable ${prop} in`, obj); return; } Object.defineProperty( obj, prop, { get: exportFunction(() => { stats.log(path, Stats.GET); return val; }, win), set: exportFunction(v => { stats.log(path, Stats.SET); if (v !== val) { log(`set ${prop} of`, obj, 'to', v); nt.destroy(v); } }, win), enumerable: other.enumerable } ); }; nt.proxy = function (obj, name, opts = {}) { if (name) namedObjects.set(obj, name); return new Proxy( obj, { get(that, prop) { if (prop in that) return that[prop]; if (typeof prop === 'symbol') { if (prop === Symbol.toPrimitive) that[prop] = function (hint) { if (hint === 'string') return Object.prototype.toString.call(this); return `[missing toPrimitive] ${name} ${hint}`; }; else if (prop === Symbol.toStringTag) that[prop] = () => 'Object'; else { that[prop] = undefined; _console.trace('Missing', prop, 'in', name || '?', '>>', that[prop]); } return that[prop]; } if (name) { that[prop] = nt.func(opts.val, `${name}.${prop}`, opts.log); return that[prop]; } _console.trace('Missing', prop, 'in', namedObjects.get(that) || that); }, set(that, prop, val) { if (val !== that[prop]) { log('skip set', prop, 'of', namedObjects.get(that) || that, 'to', val); nt.destroy(val); } return true; } } ); }; nt.func = (val, name) => nt.proxy((() => { let f = function () { if (jsf.LogAdditionalInfo) _console.trace(`call ${name || ''}(`, ...arguments, `) return`, val); return val; }; if (name) namedObjects.set(f, name); return f; })()); nt.NULL = { val: null }; Object.freeze(nt.NULL); Object.freeze(nt); } nullTools.toString = new Proxy(nullTools.toString, { apply(...args) { return `${_apply(...args)} let nt = new nullTools();`; } }); let nt = new nullTools(); // Creates and return protected style (unless protection is manually disabled). // Protected style will re-add itself on removal and remaind enabled on attempt to disable it. const createStyle = (function createStyleModule() { function createStyleElement(rules, opts) { const style = _createElement('style'); Object.assign(style, opts.props); opts.root.appendChild(style); if (style.sheet) // style.sheet is only available when style attached to DOM rules.forEach(style.sheet.insertRule.bind(style.sheet)); else style.textContent = rules.join('\n'); if (opts.protect) { Object.defineProperty(style, 'sheet', { value: null, enumerable: true }); Object.defineProperty(style, 'disabled', { //pretend to be disabled enumerable: true, set() {}, get() { return true; } }); (new MutationObserver( () => opts.root.removeChild(style) )).observe(style, { childList: true }); } return style; } // functions to parse object-based rulesets function parseRule(rec) { /* jshint validthis: true */ return this.concat(rec[0], ' {\n', Object.entries(rec[1]).map(parseProperty, this + '\t').join('\n'), '\n', this, '}'); } function parseProperty(rec) { /* jshint validthis: true */ return rec[1] instanceof Object ? parseRule.call(this, rec) : `${this}${rec[0].replace(/_/g, '-')}: ${rec[1]};`; } // main const createStyle = (rules, opts) => { // parse options opts = Object.assign({ protect: true, root: _de, type: 'text/css' }, opts); // move style properties into separate property // { a, b, ...rest } construction is not available in Fx 52 opts.props = Object.assign({}, opts); delete opts.props.protect; delete opts.props.root; // store binded methods instead of element opts.root = { appendChild: opts.root.appendChild.bind(opts.root), removeChild: opts.root.removeChild.bind(opts.root) }; // convert rules set into an array if it isn't one already rules = Array.isArray(rules) ? rules : rules instanceof Object ? Object.entries(rules).map(parseRule, '') : [rules]; // could be reassigned when protection triggered let style = createStyleElement(rules, opts); if (!opts.protect) return style; const replaceStyle = () => new Promise( resolve => setTimeout(re => re(createStyleElement(rules, opts)), 0, resolve) ).then(st => (style = st)); // replace poiner to style object with a new style object (new MutationObserver(ms => { for (let m of ms) for (let node of m.removedNodes) if (node === style) replaceStyle(); })).observe(_de, { childList: true }); if (jsf.LogAttachedCSS) _console.log(`Attached CSS:\n${rules.join('\n')}`); return style; }; createStyle.toString = () => `const createStyle = (${createStyleModule.toString()})();`; return createStyle; })(); // aborts currently running script with TypeError on specific property access // in case of inline script also checks if it contains specific pattern in it const removeOwnFootprint = e => (e.stack = e.stack.split('\n').filter(x => !x.includes('-extension://')).join('\n'), e); const abortExecution = (function abortExecutionModule() { let map = new Map(), cnt = new Map(), stack = 0; const printCounters = (cnts) => { let s = ''; for (let cntr of cnts) s += `\n * ${cntr[0]}: ${cntr[1]}`; return s; }; const logger = id => { if (!jsf.AbortExecutionStatistics) return; let { path, mode } = map.get(id); let prop = `${path} ${mode.toString().replace('Symbol','')}`; cnt.set(prop, (cnt.get(prop) || 0) + 1); stack++; setTimeout(() => { stack--; if (!stack) console.log('Abort execution counters:', printCounters(Array.from(cnt.entries()))); }, 1000); }; const isObjecty = x => (x !== null) && typeof x === 'object' || typeof x === 'function'; const Mode = { Get: Symbol('Read'), Set: Symbol('Write'), All: Symbol('Access'), InlineScript: Symbol('InlineScript') }; Object.freeze(Mode); const abortExecution = function abortExecution(mode, path, conf = {}) { let root = conf.root || win, chain = path.split('.'), postponed = false; const postpone = (link, chain) => { postponed = true; let _val; try { Object.defineProperty(root, link, { get() { return _val; }, set(val) { _val = val; conf.root = val; conf.fullPath = conf.fullPath || path; abortExecution(mode, chain.join('.'), conf); } }); } catch (e) { _console.warn(`Unable to set postpone point at ${link} in ${path}\n`, e); } }; while (chain.length > 1) { let link = chain.shift(); if (!isObjecty(root[link])) { if (conf.breakOnMissing) break; postpone(link, chain); break; } root = root[link]; } path = conf.fullPath || path; if (postponed) { if (conf.breakOnMissing) _console.log(`Unable to locate "${path}", abort anchor skipped.`); return; } const target = chain[0]; const message = `Cannot read property '${target}' of undefined`; const des = Object.getOwnPropertyDescriptor(root, target); if (des && des.get !== undefined) return; const id = Math.random().toString(36).substr(2); map.set(id, { path: path, mode: mode }); win.addEventListener('error', e => { if (e.error && e.error.message === message) e.stopImmediatePropagation(); }, false); const get = Symbol('get'); const set = Symbol('set'); const check = (mode === Mode.InlineScript) ? () => { const script = _document.currentScript; if (script && script.src === '' && (!conf.pattern || conf.pattern.test(script.textContent))) { logger(id); throw removeOwnFootprint(new TypeError(message)); } } : io => { if (io === set && mode === Mode.Get || io === get && mode === Mode.Set) return; logger(id); throw removeOwnFootprint(new TypeError(message)); }; let _val = root[target]; Object.defineProperty(root, target, { get() { check(get); return _val; }, set(v) { check(set); _val = v; } }); }; abortExecution.onGet = (path, conf) => abortExecution(Mode.Get, path, conf); abortExecution.onSet = (path, conf) => abortExecution(Mode.Set, path, conf); abortExecution.onAll = (path, conf) => abortExecution(Mode.All, path, conf); abortExecution.inlineScript = (path, conf) => abortExecution(Mode.InlineScript, path, conf); abortExecution.toString = () => ` const abortExecution = (${abortExecutionModule.toString()})();`; return abortExecution; })(); // Fake objects of advertisement networks to break their workflow // Popular adblock detector function deployFABStub(root) { if (!('fuckAdBlock' in root)) { let FuckAdBlock = function (options) { let self = this; self._options = { checkOnLoad: false, resetOnEnd: false, checking: false }; self.setOption = function (opt, val) { if (val) self._options[opt] = val; else Object.assign(self._options, opt); }; if (options) self.setOption(options); self._var = { event: {} }; self.clearEvent = function () { self._var.event.detected = []; self._var.event.notDetected = []; }; self.clearEvent(); self.on = function (detected, fun) { self._var.event[detected ? 'detected' : 'notDetected'].push(fun); return self; }; self.onDetected = function (cb) { return self.on(true, cb); }; self.onNotDetected = function (cb) { return self.on(false, cb); }; self.emitEvent = function () { for (let fun of self._var.event.notDetected) fun(); if (self._options.resetOnEnd) self.clearEvent(); return self; }; self._creatBait = () => null; self._destroyBait = () => null; self._checkBait = function () { setTimeout((() => self.emitEvent()), 1); }; self.check = function () { self._checkBait(); return true; }; let callback = function () { if (self._options.checkOnLoad) setTimeout(self.check, 1); }; root.addEventListener('load', callback, false); }; nt.defineOn(root, 'FuckAdBlock', FuckAdBlock); nt.defineOn(root, 'fuckAdBlock', new FuckAdBlock({ checkOnLoad: true, resetOnEnd: true })); } } // new version of fAB adapting to fake API // scriptLander(() => deployFABStub(win), nullTools); // so it's disabled by default for now scriptLander(() => { // VideoJS player wrapper { let _videojs = win.videojs; Object.defineProperty(win, 'videojs', { get() { return _videojs; }, set(f) { if (f === _videojs) return true; _console.log('videojs =', f); _videojs = new Proxy(f, { apply(fun, that, args) { _console.log('videojs(', ...args, ')'); const params = args[1]; if (params) { if (params.hasAd) params.hasAd = false; if (params.plugins && params.plugins.vastClient) delete params.plugins.vastClient; // disable unmuted autoplay (muted used for preview purposes) if (params.autoplay && !params.muted) params.autoplay = false; } const res = _apply(fun, that, args); if (res.seed) res.seed = () => null; if (res.on && res.off) { const logPlayer = () => { _console.log('player =', res); res.off('playing', logPlayer); }; res.on('playing', logPlayer); } return res; } }); } }); } }, nullTools, createStyle, abortExecution); // Yandex Raven stub (some monitoring sub-system) function yandexRavenStub() { nt.define('Raven', nt.proxy({ context(f) { return f(); }, config: nt.func( nt.proxy({}, 'Raven.config', { val: nt.proxy({}, 'Raven.config()..', nt.NULL) }), 'Raven.config') }, 'Raven', nt.NULL)); } // Based on https://greasyfork.org/en/scripts/21937-moonwalk-hdgo-kodik-fix v0.8 { const log = name => _console.log(`Player FIX: Detected ${name} player in ${location.href}`); const removeVast = (data) => { if (data && typeof data === 'object') { _console.log('Player configuration:', data); if (data.advert_script && data.advert_script !== '') { _console.log('Set data.advert_script to empty string.'); data.advert_script = ''; } let keys = Object.getOwnPropertyNames(data); let isVast = name => /vast|clickunder/.test(name); if (!keys.some(isVast)) return data; for (let key of keys) if (typeof data[key] === 'object' && key !== 'links') { _console.log(`Removed data.${key}:`, data[key]); delete data[key]; } if (data.chain) { let need = [], drop = [], links = data.chain.split('.'); for (let link of links) if (!isVast(link)) need.push(link); else drop.push(link); _console.log('Dropped from the chain:', ...drop); data.chain = need.join('.'); } } return data; }; _document.addEventListener( 'DOMContentLoaded', function () { if ('video_balancer_options' in win && 'event_callback' in win) { log('Moonwalk'); if (win.video_balancer_options.adv) removeVast(win.video_balancer_options.adv); if ('_mw_adb' in win) Object.defineProperty(win, '_mw_adb', { set() {}, get() { return false; } }); } else if (win.startKodikPlayer !== undefined) { log('Kodik'); // skip attempt to block access to HD resolutions const chainCall = new Proxy({}, { get() { return () => chainCall; } }); if (win.$ && win.$.prototype && win.$.prototype.addClass) { let $addClass = win.$.prototype.addClass; win.$.prototype.addClass = function (className) { if (className === 'blocked') return chainCall; return $addClass.apply(this, arguments); }; } // remove ad links from the metadata let _ajax = win.$.ajax; win.$.ajax = (params, ...args) => { if (params.success) { let _s = params.success; params.success = (data, ...args) => _s(removeVast(data), ...args); } return _ajax(params, ...args); }; } else if (win.getnextepisode && win.uppodEvent) { log('Share-Serials.net'); scriptLander( function () { let _setInterval = win.setInterval, _setTimeout = win.setTimeout; win.setInterval = function (func) { if (typeof func === 'function' && _toString(func).includes('_delay')) { let intv = _setInterval.call( this, function () { _setTimeout.call( this, function (intv) { clearInterval(intv); let timer = _document.querySelector('#timer'); if (timer) timer.click(); }, 100, intv); func.call(this); }, 5 ); return intv; } return _setInterval.apply(this, arguments); }; win.setTimeout = function (func) { if (typeof func === 'function' && _toString(func).includes('adv_showed')) return _setTimeout.call(this, func, 0); return _setTimeout.apply(this, arguments); }; } ); } else if ('ADC' in win) { log('vjs-creatives plugin in'); let replacer = (obj) => { for (let name in obj) if (typeof obj[name] === 'function') obj[name] = () => null; }; replacer(win.ADC); replacer(win.currentAdSlot); } else if ('Playerjs' in win) { log('Playerjs'); win.Playerjs = new Proxy(win.Playerjs, { construct(fn, args) { let params = args[0]; if (params && typeof params === 'object') { delete params.preroll; params = removeVast(params); Object.defineProperty(params, 'hasOwnProperty', { value(...args) { let res = _hasOwnProperty(this, ...args); if (typeof args[0] === 'string' && args[0].startsWith('vast_') && res && params[args[0]]) { _console.log(`Removed params.${args[0]}:`, params[args[0]]); delete params[args[0]]; return false; } return res; }, enumerable: false, configurable: true }); } return _construct(fn, args); } }); } UberVK: { if (!inIFrame) break UberVK; let oddNames = 'HD' in win && !Object.getOwnPropertyNames(win).every(n => !n.startsWith('_0x')); if (!oddNames) break UberVK; log('UberVK'); XMLHttpRequest.prototype.open = () => { throw 404; }; } }, false ); } // Applies wrapper function on the current page and all newly created same-origin iframes // This is used to prevent trick which allows to get fresh page API through newly created same-origin iframes function deepWrapAPI(wrapper) { let wrapped = new WeakSet(); const log = (...args) => false && _console.log(...args), _HTMLIFrameElement = HTMLIFrameElement.prototype, isIFrameElement = _HTMLIFrameElement.isPrototypeOf.bind(_HTMLIFrameElement), _contentWindow = Object.getOwnPropertyDescriptor(_HTMLIFrameElement, 'contentWindow'), _get_contentWindow = _bindCall(_contentWindow.get); function wrapAPI(root) { if (!root || wrapped.has(root)) return; wrapped.add(root); try { wrapper(isIFrameElement(root) ? _get_contentWindow(root) : root); log('Wrapped API in', (root === win) ? "main window." : root); } catch (e) { log('Failed to wrap API in', (root === win) ? "main window." : root, '\n', e); } } // wrap API on contentWindow access const getter = { apply(get, that, args) { wrapAPI(that); return _apply(get, that, args); } }; _contentWindow.get = exportFunction(new Proxy(_contentWindow.get, getter), _HTMLIFrameElement); Object.defineProperty(_HTMLIFrameElement, 'contentWindow', _contentWindow); // wrap API on contentDocument access const _contentDocument = Object.getOwnPropertyDescriptor(_HTMLIFrameElement, 'contentDocument'); _contentDocument.get = exportFunction(new Proxy(_contentDocument.get, getter), _HTMLIFrameElement); Object.defineProperty(_HTMLIFrameElement, 'contentDocument', _contentDocument); // manual children objects traverser to avoid issues // with calling querySelectorAll on wrong types of objects const _nodeType = _bindCall(Object.getOwnPropertyDescriptor(_Node, 'nodeType').get), _childNodes = _bindCall(Object.getOwnPropertyDescriptor(_Node, 'childNodes').get), _ELEMENT_NODE = _Node.ELEMENT_NODE, _DOCUMENT_FRAGMENT_NODE = _Node.DOCUMENT_FRAGMENT_NODE; const wrapFrames = root => { if (_nodeType(root) !== _ELEMENT_NODE && _nodeType(root) !== _DOCUMENT_FRAGMENT_NODE) return; // only process nodes which may contain an IFRAME or be one if (isIFrameElement(root)) { wrapAPI(root); return; } for (let child of _childNodes(root)) wrapFrames(child); }; // wrap API in a newly appended iframe objects const wrappedAppendChild = exportFunction(new Proxy(_Node.appendChild, { apply(fun, that, args) { let res = _apply(fun, that, args); wrapFrames(args[0]); return res; } }), _Node); // ABP Freeze Element snippet replaces normal properties with getters without setters const _Node_appendChild = Object.getOwnPropertyDescriptor(Node.prototype, 'appendChild'); if (_Node_appendChild.configurable) { if (_Node_appendChild.value) _Node_appendChild.value = wrappedAppendChild; if (_Node_appendChild.get) _Node_appendChild.get = () => wrappedAppendChild; Object.defineProperty(_Node, 'appendChild', _Node_appendChild); } // wrap API in iframe objects created with innerHTML of element on page const _innerHTML = Object.getOwnPropertyDescriptor(_Element, 'innerHTML'); _innerHTML.set = exportFunction(new Proxy(_innerHTML.set, { apply(set, that, args) { _apply(set, that, args); if (_document.contains(that)) wrapFrames(that); } }), _Element); Object.defineProperty(_Element, 'innerHTML', _innerHTML); wrapAPI(win); } // piguiqproxy.com / zmctrack.net circumvention and onerror callback prevention scriptLander( () => { // onerror callback blacklist let masks = [], //blockAll = /(^|\.)(rutracker-org\.appspot\.com)$/, isBlocked = url => masks.some(mask => mask.test(url)); // || blockAll.test(location.hostname); for (let filter of [ // blacklist // global '/adv/www/', // adservers '||185.87.50.147^', '||10root25.website^', '||24video.xxx^', '||adlabs.ru^', '||adspayformymortgage.win^', '||aliru1.ru^', '||amgload.net^', '||aviabay.ru^', '||bgrndi.com^', '||brokeloy.com^', '||cdnjs-aws.ru^', '||cnamerutor.ru^', '||directadvert.ru^', '||docfilms.info^', '||dreadfula.ru^', '||dsn-fishki.ru^', '||et-cod.com^', '||et-code.ru^', '||etcodes.com^', '||film-doma.ru^', '||free-torrent.org^', '||free-torrent.pw^', '||free-torrents.org^', '||free-torrents.pw^', '||game-torrent.info^', '||gocdn.ru^', '||hdkinoshka.com^', '||hghit.com^', '||hindcine.net^', '||kinotochka.net^', '||kinott.com^', '||kinott.ru^', '||klcheck.com^', '||kuveres.com^', '||lepubs.com^', '||luxadv.com^', '||luxup.ru^', '||luxupcdna.com^', '||marketgid.com^', '||mebablo.com^', '||mixadvert.com^', '||mxtads.com^', '||nickhel.com^', '||oconner.biz^', '||oconner.link^', '||octoclick.net^', '||octozoon.org^', '||pigiuqproxy.com^', '||piguiqproxy.com^', '||pkpojhc.com^', '||psma01.com^', '||psma02.com^', '||psma03.com^', '||rcdn.pro^', '||recreativ.ru^', '||redtram.com^', '||regpole.com^', '||rootmedia.ws^', '||ruttwind.com^', '||rutvind.com^', '||skidl.ru^', '||smi2.net^', '||smcheck.org^', '||torvind.com^', '||traffic-media.co^', '||trafmag.com^', '||trustjs.net^', '||ttarget.ru^', '||u-dot-id-adtool.appspot.com^', '||utarget.ru^', '||webadvert-gid.ru^', '||webadvertgid.ru^', '||xxuhter.ru^', '||yuiout.online^', '||zfctrack.net^', '||zhctrack.net^', '||zmctrack.net^', '||znctrack.net^', '||zoom-film.ru^' ]) masks.push(new RegExp( filter.replace(/([\\/[\].+?(){}$])/g, '\\$1') .replace(/\*/g, '.*?') .replace(/\^(?!$)/g, '\\.?[^\\w%._-]') .replace(/\^$/, '\\.?([^\\w%._-]|$)') .replace(/^\|\|/, '^((ws|http)s?:|/)/+([^/.]+\\.)*?'), 'i')); // main script deepWrapAPI(root => { FxProxyToStringFix(root); const _defineProperty = root.Object.defineProperty, _getOwnPropertyDescriptor = root.Object.getOwnPropertyDescriptor, _dispatchEvent = _bindCall(root.EventTarget.prototype.dispatchEvent); const dispatchCustomEvent = ( target, name, opts = { bubble: false, cancelable: false } ) => _dispatchEvent(target, new CustomEvent(name, opts)); { // 'onerror' handler for scripts from blacklisted sources const scriptMap = new WeakMap(); const _HTMLScriptElement = root.HTMLScriptElement, _HTMLImageElement = root.HTMLImageElement; const _get_tagName = _bindCall(_getOwnPropertyDescriptor(root.Element.prototype, 'tagName').get), _get_scr_src = _bindCall(_getOwnPropertyDescriptor(_HTMLScriptElement.prototype, 'src').get), _get_img_src = _bindCall(_getOwnPropertyDescriptor(_HTMLImageElement.prototype, 'src').get); const _get_src = node => { if (node instanceof _HTMLScriptElement) return _get_scr_src(node); if (node instanceof _HTMLImageElement) return _get_img_src(node); return undefined; }; const _onerror = _getOwnPropertyDescriptor(root.HTMLElement.prototype, 'onerror'); _onerror.get = exportFunction(new Proxy(_onerror.get, { apply(_fun, that) { return scriptMap.get(that) || null; } }), root.HTMLElement.prototype); _onerror.set = exportFunction(new Proxy(_onerror.set, { apply(fun, that, args) { let [callback] = args; if (typeof callback !== 'function') { scriptMap.delete(that); _apply(fun, that, args); return; } scriptMap.set(that, callback); _apply(fun, that, [function () { let src = _get_src(this); if (isBlocked(src)) { _console.trace(`Blocked "onerror" callback from ${_get_tagName(this)}: ${src}`); return; } _apply(scriptMap.get(this), this, arguments); }]); } }), root.HTMLElement.prototype); _defineProperty(root.HTMLElement.prototype, 'onerror', _onerror); } // Simplistic WebSocket wrapper for Maxthon and Firefox before v58 // once again seems required in Google Chrome and similar browsers due to zmctrack.net -_- if (_getOwnPropertyDescriptor(root, 'WebSocket')) root.WebSocket = exportFunction(new Proxy(root.WebSocket, { construct(ws, args) { if (isBlocked(args[0])) { _console.log('Blocked WS connection:', args[0]); return {}; } return _construct(ws, args); } }), root); // Block popular method to open a new window in Google Chrome by dispatching a custom click // event on a newly created anchor with _blank target. Untrusted events must not open new windows. const clickWhitelist = /^([^.]\.)*?(nakarte\.me|sberbank\.ru)$/; root.EventTarget.prototype.dispatchEvent = exportFunction(new Proxy(root.EventTarget.prototype.dispatchEvent, { apply(fun, that, args) { const e = args[0]; if (!clickWhitelist.test(win.location.hostname) && !e.isTrusted && e.type === 'click' && e.constructor.name === 'MouseEvent' && !that.parentNode && that.tagName === 'A' && that.target[0] === '_') { _console.log('Blocked dispatching a click event on a parentless anchor:', that); return; } return _apply(fun, that, args); } }), root.EventTarget.prototype); // blacklist of domains where all third-party requests are ignored const ondomains = /(^|[/.@])oane\.ws($|[:/])/i; const yandex_direct = /^(https?:)?\/\/([^.]+\.)??yandex(\.[a-z]{2,3}){1,2}\/(images\/[a-z0-9/_-]{40,}|jstracer?|j?clck\/.*|set\/s\/rsya-tag-users\/data(\?.*)?|static\/main\.js(\?.*)?)$/i; const more_y_direct = /^(https?:)?\/\/((([^.]+\.)??(drive2|kakprosto)\.ru\/(.{290,}|[a-z0-9/_-]{100,}))|yastatic\.net\/.*?\/chunks\/promo\/.*)$/i; const whitelist = /^(https?:)?\/\/yandex\.ru\/yobject$/; const fabPatterns = /\/fuckadblock/i; const blockedUrls = new Set(); function checkRequest(fname, method, url) { let block = isBlocked(url) || ondomains.test(location.hostname) && !ondomains.test(url) || yandex_direct.test(url) || more_y_direct.test(url); let allow = block && whitelist.test(url) || // Fix for infinite load on Yandex Images: find image, open "other sizes and similar images" in a new tab, click on a preview of a similar image (block && method === 'script.src' && root.location.pathname === '/images/search' && root.location.hostname.startsWith('yandex.') && url.startsWith('http') && url.includes('/images/')) || // Direct URLs are similar, but don't have protocol for some reason (block && root.location.hostname === 'widgets.kinopoisk.ru' && url.includes('/static/main.js?')) || (block && !url.startsWith('http') && // drive2.ru hid a little CSS style in their requests which shows page content like this (root.location.hostname === 'drive2.ru' || root.location.hostname.endsWith('.drive2.ru'))); if (allow) { block = false; _console.trace(`Allowed ${fname} ${method} request %o from %o`, url, root.location.href); } if (block) { if (!blockedUrls.has(url)) // don't repeat log if the same URL were blocked more than once _console.trace(`Blocked ${fname} ${method} request %o from %o`, url, root.location.href); blockedUrls.add(url); return true; } return false; } // workaround for broken searchbar on market.yandex.ru const checkOnloadEvent = location.hostname.startsWith('market.yandex.'); const triggerLoadEvent = /^(https?:)?\/\/([^.]+\.)??yandex(\.[a-z]{2,3}){1,2}\/(j?clck\/.*)$/i; // XHR Wrapper const _proto = root.XMLHttpRequest && root.XMLHttpRequest.prototype; if (_proto) { const xhrStopList = new WeakSet(); const xhrDispatchLoadList = new WeakSet(); _proto.open = exportFunction(new Proxy(_proto.open, { apply(fun, that, args) { if (checkOnloadEvent && triggerLoadEvent.test(args[1])) xhrDispatchLoadList.add(that); if (checkRequest('xhr', ...args)) { xhrStopList.add(that); return; } return _apply(fun, that, args); } }), _proto); const _DONE = _proto.DONE; // 4 const sendWrapper = { apply(fun, that, args) { if (xhrStopList.has(that)) { if (that.readyState !== _DONE && xhrDispatchLoadList.has(that)) { that.readyState = _DONE; setTimeout(() => dispatchCustomEvent(that, 'load'), 0); } return null; } return _apply(fun, that, args); } }; ['send', 'setRequestHeader', 'getAllResponseHeaders'].forEach( name => _proto[name] = exportFunction(new Proxy(_proto[name], sendWrapper), _proto) ); // simulate readyState === 1 for blocked requests const _readyState = Object.getOwnPropertyDescriptor(_proto, 'readyState'); _readyState.get = exportFunction(new Proxy(_readyState.get, { apply(fun, that, args) { return xhrStopList.has(that) ? 1 : _apply(fun, that, args); } }), _proto); Object.defineProperty(_proto, 'readyState', _readyState); } if (root.fetch) root.fetch = exportFunction(new Proxy(root.fetch, { apply(fun, that, args) { let [url, opts] = args; let method = opts && opts.method || 'GET'; if (typeof url === 'object' && 'headers' in url && 'url' in url && 'method' in url) // url instanceof Request ({ url, method } = url); if (checkRequest('fetch', method, url)) return new Promise(() => null); return _apply(fun, that, args); } }), root); const _script_src = Object.getOwnPropertyDescriptor(root.HTMLScriptElement.prototype, 'src'); _script_src.set = exportFunction(new Proxy(_script_src.set, { apply(fun, that, args) { if (fabPatterns.test(args[0])) { _console.trace('Blocked set script.src request:', args[0]); deployFABStub(root); setTimeout(() => dispatchCustomEvent(that, 'load'), 0); return; } return checkRequest('set', 'script.src', args[0]) || _apply(fun, that, args); } }), root.HTMLScriptElement.prototype); Object.defineProperty(root.HTMLScriptElement.prototype, 'src', _script_src); const adregain_pattern = /ggg==" alt="advertisement"/; if (root.self !== root.top) // in IFrame root.document.write = exportFunction(new Proxy(root.document.write, { apply(fun, that, args) { if (adregain_pattern.test(args[0])) { _console.log('Skipped AdRegain frame.'); args[0] = ''; } return _apply(fun, that, args); } }), root.document); }); }, deepWrapAPI ); // === Helper functions === const gardener = (() => { // function to search and remove nodes by content // selector - standard CSS selector to define set of nodes to check // words - regular expression to check content of the suspicious nodes // params - object with multiple extra parameters: // .log - display log in the console // .hide - set display to none instead of removing from the page // .parent - parent node to remove if content is found in the child node // .siblings - number of simling nodes to remove (excluding text nodes) const scissors = (selector, words, scope, params) => { const logger = (...args) => { if (params.log) _console.log(...args); }; const hideStyleStr = ';display:none!important;'; const getStyleAtt = node => _getAttribute(node, 'style') || ''; const scHide = node => { const style = getStyleAtt(node); if (!style.includes(hideStyleStr)) _setAttribute(node, 'style', style + hideStyleStr); }; if (!scope.contains(_document.body)) logger('[s] scope', scope); let remFunc = (params.hide ? scHide : node => node.parentNode.removeChild(node)), iterFunc = (params.siblings > 0 ? 'nextElementSibling' : 'previousElementSibling'), toRemove = [], siblings; for (let node of scope.querySelectorAll(selector)) { // drill up to a parent node if specified, break if not found if (params.parent) { let old = node; node = node.closest(params.parent); if (node === null || node.contains(scope)) { logger('[s] went out of scope with', old); continue; } } if (getStyleAtt(node).includes(hideStyleStr)) continue; logger('[s] processing', node); if (toRemove.includes(node)) continue; if (words.test(node.innerHTML)) { // skip node if already marked for removal logger('[s] marked for removal'); toRemove.push(node); // add multiple nodes if defined more than one sibling siblings = Math.abs(params.siblings) || 0; while (siblings) { node = node[iterFunc]; if (!node) break; // can't go any further - exit logger('[s] adding sibling node', node); toRemove.push(node); siblings -= 1; } } } const toSkip = []; toSkip.checkNode = node => !toRemove.every(other => other === node || !node.contains(other)); for (let node of toRemove) if (toSkip.checkNode(node)) toSkip.push(node); if (toRemove.length) logger(`[s] proceeding with ${params.hide?'hide':'removal'} of`, toRemove, `skip`, toSkip); for (let node of toRemove) if (!toSkip.includes(node)) remFunc(node); }; // function to perform multiple checks if ads inserted with a delay // by default does 30 checks withing a 3 seconds unless nonstop mode specified // also does 1 extra check when a page completely loads // selector and words - passed dow to scissors // params - object with multiple extra parameters: // .log - display log in the console // .root - selector to narrow down scope to scan; // .observe - if true then check will be performed continuously; // Other parameters passed down to scissors. return (selector, words, params) => { let logger = (...args) => { if (params.log) _console.log(...args); }; params = params || {}; logger(`[gardener] selector: '${selector}' detector: ${words} options: ${JSON.stringify(params)}`); let scope; let globalScope = [_de.parentNode]; let domLoaded = false; let getScope = root => root ? _de.querySelectorAll(root) : globalScope; let onevent = e => { logger(`[gardener] cleanup on ${Object.getPrototypeOf(e).toString().slice(1, -1).split(/\s/)[1]} "${e.type}"`); for (let node of scope) scissors(selector, words, node, params); }; let repeater = n => { if (!domLoaded && n) { setTimeout(repeater, 500, n - 1); scope = getScope(params.root); if (!scope) // exit if the root element is not present on the page return 0; onevent({ type: 'Repeater' }); } }; repeater(20); _document.addEventListener( 'DOMContentLoaded', (e) => { domLoaded = true; // narrow down scope to a specific element scope = getScope(params.root); if (!scope) // exit if the root element is not present on the page return 0; logger('[g] scope', scope); // add observe mode if required if (params.observe) { let params = { childList: true, subtree: true }; let observer = new MutationObserver( function (ms) { for (let m of ms) if (m.addedNodes.length) onevent(m); } ); for (let node of scope) observer.observe(node, params); logger('[g] observer enabled'); } onevent(e); }, false); // wait for a full page load to do one extra cut win.addEventListener('load', onevent, false); }; })(); // wrap popular methods to open a new tab to catch specific behaviours function createWindowOpenWrapper(openFunc) { const parser = _createElement('a'); const openWhitelist = (url, parent) => { parser.href = url; return parser.hostname === 'www.imdb.com' || parser.hostname === 'www.kinopoisk.ru' || parent.hostname === 'radikal.ru' && url === undefined; }; function redefineOpen(root) { if ('open' in root) root.open = new Proxy(root.open, { apply(fun, that, args) { if (openWhitelist(args[0], location)) { _console.log('Whitelisted popup:', ...args); return _apply(fun, that, args); } return openFunc(...args); } }); } redefineOpen(win); const getTagName = _bindCall(Object.getOwnPropertyDescriptor(_Element, 'tagName').get); const hasOwnProperty = _bindCall(Object.hasOwnProperty); const createElementWrapper = { apply(fun, that, args) { const el = _apply(fun, that, args); // redefine window.open in first-party frames if (getTagName(el) === 'IFRAME' || getTagName(el) === 'OBJECT') el.addEventListener('load', (e) => { try { redefineOpen(e.target.contentWindow); } catch (ignore) {} }, false); return el; } }; function redefineCreateElement(obj) { for (let root of [obj.document, Object.getPrototypeOf(obj.HTMLDocument.prototype)]) if (hasOwnProperty(root, 'createElement')) root.createElement = exportFunction(new Proxy(root.createElement, createElementWrapper), root); } redefineCreateElement(win); // wrap window.open in newly added first-party frames const wrappedAppendChild = exportFunction(new Proxy(_Node.appendChild, { apply(fun, that, args) { let el = _apply(fun, that, args); if (el instanceof HTMLIFrameElement) try { redefineOpen(el.contentWindow); redefineCreateElement(el.contentWindow); } catch (ignore) {} return el; } }), _Node); // ABP Freeze Element snippet replaces normal properties with getters without setters const _Node_appendChild = Object.getOwnPropertyDescriptor(Node.prototype, 'appendChild'); if (_Node_appendChild.configurable) { if (_Node_appendChild.value) _Node_appendChild.value = wrappedAppendChild; if (_Node_appendChild.get) _Node_appendChild.get = () => wrappedAppendChild; Object.defineProperty(_Node, 'appendChild', _Node_appendChild); } } // Function to catch and block various methods to open a new window with 3rd-party content. // Some advertisement networks went way past simple window.open call to circumvent default popup protection. // This funciton blocks window.open, ability to restore original window.open from an IFRAME object, // ability to perform an untrusted (not initiated by user) click on a link, click on a link without a parent // node or simply a link with piece of javascript code in the HREF attribute. function preventPopups() { // call sandbox-me if in iframe and not whitelisted if (inIFrame) { win.top.postMessage({ name: 'sandbox-me', href: win.location.href }, '*'); return; } scriptLander(() => { let open = (...args) => { '[native code]'; _console.trace('Site attempted to open a new window', ...args); return { document: nt.proxy({ write: nt.func({}, 'write'), writeln: nt.func({}, 'writeln') }), location: nt.proxy({}) }; }; createWindowOpenWrapper(open); _console.log('Popup prevention enabled.'); }, nullTools, createWindowOpenWrapper); } // Helper function to close background tab if site opens itself in a new tab and then // loads a 3rd-party page in the background one (thus performing background redirect). function preventPopunders() { // create "close_me" event to call high-level window.close() let eventName = `close_me_${Math.random().toString(36).substr(2)}`; let callClose = () => { _console.log('close call'); window.close(); }; window.addEventListener(eventName, callClose, true); scriptLander(() => { // get host of a provided URL with help of an anchor object // unfortunately new URL(url, window.location) generates wrong URL in some cases let parseURL = _document.createElement('A'); let getHost = url => { parseURL.href = url; return parseURL.hostname; }; // site went to a new tab and attempts to unload // call for high-level close through event let closeWindow = () => window.dispatchEvent(new CustomEvent(eventName, {})); // check is URL local or goes to different site let isLocal = (url) => { if (url === location.pathname || url === location.href) return true; // URL points to current pathname or full address let host = getHost(url); let site = location.hostname; return host !== '' && // URLs with unusual protocol may have empty 'host' (site === host || site.endsWith(`.${host}`) || host.endsWith(`.${site}`)); }; let _open = window.open.bind(window); let open = (...args) => { '[native code]'; let url = args[0]; if (url && isLocal(url)) window.addEventListener('beforeunload', closeWindow, true); return _open(...args); }; createWindowOpenWrapper(open); _console.log("Background redirect prevention enabled."); }, `let eventName="${eventName}"`, nullTools, createWindowOpenWrapper); } // Mix between check for popups and popunders // Significantly more agressive than both and can't be used as universal solution function preventPopMix() { if (inIFrame) { win.top.postMessage({ name: 'sandbox-me', href: win.location.href }, '*'); return; } // create "close_me" event to call high-level window.close() let eventName = `close_me_${Math.random().toString(36).substr(2)}`; let callClose = () => { _console.log('close call'); window.close(); }; window.addEventListener(eventName, callClose, true); scriptLander(() => { let _open = window.open, parseURL = _document.createElement('A'); // get host of a provided URL with help of an anchor object // unfortunately new URL(url, window.location) generates wrong URL in some cases let getHost = (url) => { parseURL.href = url; return parseURL.host; }; // site went to a new tab and attempts to unload // call for high-level close through event let closeWindow = () => { _open(window.location, '_self'); window.dispatchEvent(new CustomEvent(eventName, {})); }; // check is URL local or goes to different site function isLocal(url) { let loc = window.location; if (url === loc.pathname || url === loc.href) return true; // URL points to current pathname or full address let host = getHost(url), site = loc.host; if (host === '') return false; // URLs with unusual protocol may have empty 'host' if (host.length > site.length) [site, host] = [host, site]; return site.includes(host, site.length - host.length); } // add check for redirect for 5 seconds, then disable it function checkRedirect() { window.addEventListener('beforeunload', closeWindow, true); setTimeout(closeWindow => window.removeEventListener('beforeunload', closeWindow, true), 5000, closeWindow); } function open(url, name) { '[native code]'; if (url && isLocal(url) && (!name || name === '_blank')) { _console.trace('Suspicious local new window', ...arguments); checkRedirect(); /* jshint validthis: true */ return _open.apply(this, arguments); } _console.trace('Blocked attempt to open a new window', ...arguments); return { document: { write() {}, writeln() {} } }; } function clickHandler(e) { let link = e.target, url = link.href || ''; if (e.targetParentNode && e.isTrusted || link.target !== '_blank') { _console.log('Link', link, 'were created dinamically, but looks fine.'); return true; } if (isLocal(url) && link.target === '_blank') { _console.log('Suspicious local link', link); checkRedirect(); return; } _console.log('Blocked suspicious click on a link', link); e.stopPropagation(); e.preventDefault(); } createWindowOpenWrapper(open, clickHandler); _console.log("Mixed popups prevention enabled."); }, `let eventName="${eventName}"`, createWindowOpenWrapper); } // External listener for case when site known to open popups were loaded in iframe // It will sandbox any iframe which will send message 'forbid.popups' (preventPopups sends it) // Some sites replace frame's window.location with data-url to run in clean context if (!inIFrame) window.addEventListener( 'message', function (e) { if (!e.data || e.data.name !== 'sandbox-me' || !e.data.href) return; let src = e.data.href; for (let frame of _document.querySelectorAll('iframe')) if (frame.contentWindow === e.source) { if (frame.hasAttribute('sandbox')) { if (!frame.sandbox.contains('allow-popups')) return; // exit frame since it's already sandboxed and popups are blocked // remove allow-popups if frame already sandboxed frame.sandbox.remove('allow-popups'); } else // set sandbox mode for troublesome frame and allow scripts, forms and a few other actions // technically allowing both scripts and same-origin allows removal of the sandbox attribute, // but to apply content must be reloaded and this script will re-apply it in the result frame.setAttribute('sandbox', 'allow-forms allow-scripts allow-presentation allow-top-navigation allow-same-origin'); _console.log('Disallowed popups from iframe', frame); // reload frame content to apply restrictions if (!src) { src = frame.src; _console.log('Unable to get current iframe location, reloading from src', src); } else _console.log('Reloading iframe with URL', src); frame.src = 'about:blank'; frame.src = src; } }, false ); const evalPatternYandex = /{exports:{},id:r,loaded:!1}|containerId:(.|\r|\n)+params:/, evalPatternGeneric = /_0x|location\s*?=|location.href\s*?=|location.assign\(|open\(/i; function selectiveEval(...patterns) { let fullLog = false; if (patterns[patterns.length - 1] === true) { fullLog = true; patterns.length = patterns.length - 1; } if (patterns.length === 0) patterns.push(evalPatternGeneric); win.eval = new Proxy(win.eval, { apply(fun, that, args) { if (patterns.some(pattern => pattern.test(args[0]))) { _console[fullLog ? 'trace' : 'log'](`Skipped eval ${fullLog ? args[0] : args[0].slice(0, 512)}${fullLog ? '' : '\u2026'}`); return null; } try { if (fullLog) _console.trace(`eval ${args[0]}`); return _apply(fun, that, args); } catch (e) { _console.error('Crash source:', args[0]); throw e; } } }); } selectiveEval.toString = new Proxy(selectiveEval.toString, { apply(...args) { return `${_apply(...args)} const evalPatternYandex = ${evalPatternYandex}, evalPatternGeneric = ${evalPatternGeneric}`; } }); // hides cookies by pattern and attempts to remove them if they already set // also prevents setting new versions of such cookies function selectiveCookies(scPattern = '', opts = {}) { const patterns = scPattern.split('|'); if (patterns[0] !== '~default') { // Google Analytics cookies patterns.push('_g(at?|id)|__utm[a-z]'); // Yandex ABP detection cookies patterns.push('altrs|bltsr|blcrm'); } else patterns.shift(); const withValue = f => f.includes('='); const withoutValue = f => !withValue(f); const blacklist = new RegExp(`^((${patterns.filter(withoutValue).join('|')})=.*|${patterns.filter(withValue).join('|')})$`); const root = opts.root || win; const _root_Document = Object.getPrototypeOf(root.HTMLDocument.prototype); const _doc_proto = ('cookie' in _root_Document) ? _root_Document : Object.getPrototypeOf(root.document); const _cookie = Object.getOwnPropertyDescriptor(_doc_proto, 'cookie'); const _set_cookie = _bindCall(_cookie.set); let removed = new Set(); const removeLog = (cookie) => { let strings = [`${cookie.name}=${cookie.value}`]; if (cookie.domain) strings.push(`domain=${cookie.domain}`); if (cookie.path) strings.push(`path=${cookie.path}`); if (cookie.sameSite !== 'unspecified') strings.push(`sameSite=${cookie.sameSite}`); for (let name of ['httpOnly', 'hostOnly', 'secure', 'session']) if (cookie[name]) strings.push(name); let full = strings.join('; '); if (!removed.has(full)) _console.log(`Removed cookie: ${full}`); removed.add(full); }; let skipTM = true; const asyncCookieCleaner = () => { GM.cookie.list({ url: location.href }).then(cookies => { if (!cookies) return; if (skipTM) { cookies = cookies.filter(x => !x.name.startsWith('TM_')); skipTM = false; } for (let cookie of cookies) if (blacklist.test(`${cookie.name}=${cookie.value}`)) { if (skipTM && cookie.name) continue; GM.cookie.delete(cookie); removeLog(cookie); } }, () => null); }; const useOldPass = (() => { if (GM.info.scriptHandler === 'Tampermonkey' && GM.info.version === undefined) return false; // TM Beta doesn't have a version, apparently // returns true if GM version <= 4.10 let v = GM.info.version.split('.').map(x => x - 0); return v[0] < 4 || v[0] === 4 && v[1] <= 10 && v[2] === undefined || GM.info.scriptHandler !== 'Tampermonkey'; })(); const getName = (cookie) => cookie && cookie.includes('=') ? /^(.+?)=/.exec(cookie)[1] : cookie; const removeCookie = (cookie, that) => { const expireCookie = (name, domain) => { domain = domain ? `;domain=${domain.join('.')}` : ''; _set_cookie(that, `${name}=;Max-Age=0;path=/${domain}`); _set_cookie(that, `${name}=;Max-Age=0;path=/${domain.replace('=', '=.')}`); }; const name = getName(cookie); const domain = that.location.hostname.split('.'); expireCookie(name); while (domain.length > 1) { try { expireCookie(name, domain); } catch (e) { _console.error(e); } domain.shift(); } _console.log('Removing existing cookie:', cookie); }; if (_cookie) { // skip setting unwanted cookies _cookie.set = new Proxy(_cookie.set, { apply(fun, that, args) { if (useOldPass) { let cookie = args[0]; if (blacklist.test(cookie)) { _console.log('Ignored cookie: %s', cookie); removeCookie(cookie, that); return; } } _apply(fun, that, args); asyncCookieCleaner(); return true; } }); // hide unwanted cookies from site _cookie.get = new Proxy(_cookie.get, { apply(fun, that, args) { asyncCookieCleaner(); const res = _apply(fun, that, args).split(/;\s?/); const clean = res.filter(cookie => !blacklist.test(cookie)); if (useOldPass && clean.length !== res.length) for (let cookie of res.filter(cookie => !clean.includes(cookie))) removeCookie(cookie, that); return clean.join('; '); } }); Object.defineProperty(_doc_proto, 'cookie', _cookie); _console.log('Active cookies:', root.document.cookie); } } // Locates a node with specific text in Russian // Uses table of substitutions for similar letters let selectNodeByTextContent = (() => { let subs = { // english & greek 'А': 'AΑ', 'В': 'BΒ', 'Г': 'Γ', 'Е': 'EΕ', 'З': '3', 'К': 'KΚ', 'М': 'MΜ', 'Н': 'HΗ', 'О': 'OΟ', 'П': 'Π', 'Р': 'PΡ', 'С': 'C', 'Т': 'T', 'Ф': 'Φ', 'Х': 'XΧ' }; let regExpBuilder = text => new RegExp( text.toUpperCase() .split('') .map(function (e) { return `${e in subs ? `[${e}${subs[e]}]` : (e === ' ' ? '\\s+' : e)}[\u200b\u200c\u200d]*`; }) .join(''), 'i'); let reMap = {}; return (re, opts = { root: _document.body }) => { if (!re.test) { if (!reMap[re]) reMap[re] = regExpBuilder(re); re = reMap[re]; } for (let child of opts.root.children) if (re.test(child.textContent)) { if (opts.shallow) return child; opts.root = child; return selectNodeByTextContent(re, opts) || child; } }; })(); // webpackJsonp filter function webpackJsonpFilter(blacklist, log = false) { function wrapPush(webpack) { let _push = webpack.push.bind(webpack); Object.defineProperty(webpack, 'push', { get() { return _push; }, set(vl) { _push = new Proxy(vl, { apply(fun, that, args) { wrapper: { if (!(args[0] instanceof Array)) break wrapper; let mainName; if (args[0][2] instanceof Array && args[0][2][0] instanceof Array) mainName = args[0][2][0][0]; let funs = args[0][1]; if (!(funs instanceof Object && !(funs instanceof Array))) break wrapper; const noopFunc = (name, text) => () => _console.log(`Skip webpack ${name}`, text); for (let name in funs) { if (typeof funs[name] !== 'function') continue; if (blacklist.test(_toString(funs[name])) && name !== mainName) funs[name] = noopFunc(name, log ? _toString(funs[name]) : ''); } } _console.log('webpack.push()'); return _apply(fun, that, args); } }); return true; } }); return webpack; } let _webpackJsonp = wrapPush([]); Object.defineProperty(win, 'webpackJsonp', { get() { return _webpackJsonp; }, set(vl) { if (vl === _webpackJsonp) return; _console.log('new webpackJsonp', vl); _webpackJsonp = wrapPush(vl); } }); } // JSON filter // removeList - list of paths divided by space to remove // checkList - optional list of paths divided by space to check presence of before removal const jsonFilter = (function jsonFilterModule() { const _log = (() => { if (!jsf.AccessStatistics) return () => null; const counter = {}; const counterToString = () => Object.entries(counter).map(a => `\n * ${a.join(': ')}`).join(''); let lock = 0; return async function _log(path) { counter[path] = (counter[path] || 0) + 1; lock++; setTimeout(() => { lock--; if (lock === 0) _console.log('JSON filters:', counterToString()); }, 3333); }; })(); const isObjecty = o => (typeof o === 'object' || typeof o === 'function') && o !== null; function parsePath(root, path) { let pos; pos = path.indexOf('.'); for (let name; pos > 0;) { name = path.slice(0, pos); if (!isObjecty(root[name])) break; root = root[name]; path = path.slice(pos + 1); pos = path.indexOf('.'); } return [pos < 0 && _hasOwnProperty(root, path), root, path]; } const filterList = []; function filter(result) { if (!isObjecty(result)) return result; const pathNotInObject = path => !(parsePath(result, path)[0]); const removePathInObject = path => { let [exist, root, name] = parsePath(result, path); if (exist) { delete root[name]; _log(path); } }; for (let list of filterList) { if (list.check && list.check.some(pathNotInObject)) return result; list.remove.forEach(removePathInObject); } return result; } let wrapped = false; function jsonFilter(removeList, checkList) { filterList.push({ remove: removeList.split(/\s/), check: checkList ? checkList.split(/\s/) : undefined }); if (wrapped) return; wrapped = true; win.JSON.parse = new Proxy(win.JSON.parse, { apply(fun, that, args) { return filter(_apply(fun, that, args)); } }); win.Response.prototype.json = new Proxy(win.Response.prototype.json, { apply(fun, that, args) { let promise = _apply(fun, that, args); promise.then(res => filter(res)); return promise; } }); } jsonFilter.toString = () => `const jsonFilter = (${jsonFilterModule.toString()})()`; return jsonFilter; })(); function zmcPlug(conf) { // enable Emcode debug mode in ZMCTrack code (just to see it in the log) const _RegExpToString = _bindCall(RegExp.prototype.toString); String.prototype.match = new Proxy(String.prototype.match, { apply(fun, that, args) { let str = typeof args[0] === 'string' ? args[0] : _RegExpToString(args[0]); if (str.includes('argon_debug')) return true; return _apply(fun, that, args); } }); // catch and overwrite API in the clean IFrame created by ZMCTrack _Node.appendChild = new Proxy(_Node.appendChild, { apply(fun, that, args) { const res = _apply(fun, that, args); if (res && res.name && res.name.startsWith('_m')) { const zmcWin = win[res.name]; if (!zmcWin) return; zmcWin.write = nt.func(null, 'zmc.write', true); zmcWin.setTimeout = nt.func(null, 'zmc.setTimeout', true); zmcWin.document.addEventListener = nt.func(null, 'zmc.document.addEventListener', true); zmcWin.XMLHttpRequest.prototype.open = nt.func(null, 'zmc.XMLHttpRequest.prototype.open', true); zmcWin.XMLHttpRequest.prototype.send = nt.func(null, 'zmc.XMLHttpRequest.prototype.send', true); } return res; } }); const define = name => { let _win; Object.defineProperty(win, name, { get() { if (!_win) { let frame = _document.querySelector(`iframe[name="${name}"`); if (frame) _win = frame.contentWindow; } return _win; } }); }; // "predict" names of zmctrack frames on certain domains which use date-based frame names // id - some fixed number, zone - server's timezone (hours), step - how often name changes (minutes) // range - period in hours to cover from -range/2 to +range/2, offset - fixed number of minutes to add if (typeof conf === 'object') { let { id, zone = 2, step = 5, range = 3, offset = 0 } = conf; const pad = n => n.toString().padStart(2, '0'); const m2ms = x => x * 60 * 1000; const d = new Date(); d.setTime(Math.floor(d.getTime() / m2ms(step)) * m2ms(step) + m2ms(zone * 60) + m2ms(offset)); const defineByDate = d => { define(`n${pad( d.getUTCMonth() + 1 )}${pad( d.getUTCDate() )}${pad( d.getUTCHours() )}${pad( d.getUTCMinutes() )}${( id ? `_${id}` : '' )}`); }; const time = d.getTime(); for (let n = -Math.floor(range * 30 / step); n <= Math.floor(range * 30 / step); n += 1) { d.setTime(time + n * m2ms(step)); defineByDate(d); } } if (typeof conf === 'string') define(conf); } function documentRewrite(pattern, substitute) { /* jshint -W060 */ // document.write is a form of evil, a necessary evil in this case const inject = (pattern, substitute) => { let xhr = new XMLHttpRequest(); xhr.open('GET', location.href); xhr.onload = () => { document.close(); //console.log(xhr.responseText.match(pattern)); document.write(xhr.responseText.replace(pattern, substitute)); document.close(); }; xhr.send(); }; /* jshint +W060 */ const style = [ '@keyframes spinner { 0% { transform: translate3d(-50%, -50%, 0) rotate(0deg); } 100% { transform: translate3d(-50%, -50%, 0) rotate(360deg); } }', '.spinner::before { animation: 1.5s linear infinite spinner; animation-play-state: running;', 'content: ""; border: solid 3px #dedede; border-bottom-color: #EF6565; border-radius: 50%;', 'height: 10vh; width: 10vh; left: 50%; top: 50%; position: absolute; transform: translate3d(-50%, -50%, 0); };' ].join(''); _document.write(`
`); _document.write(``); } // === Scripts for specific domains === const scripts = { // Prevent Popups preventPopups: { other: 'biqle.ru, chaturbate.com, dfiles.ru, eporner.eu, hentaiz.org, mirrorcreator.com, online-multy.ru' + 'radikal.ru, rumedia.ws, tapehub.tech, thepiratebay.org, unionpeer.com, zippyshare.com', now: preventPopups }, // Prevent Popunders (background redirect) preventPopunders: { other: 'lostfilm-online.ru, mediafire.com, megapeer.org, megapeer.ru, perfectgirls.net', now: preventPopunders }, // zmctrack remover zmcDocumentRewrite: { other: 'www.ukr.net', // generic script removal pattern now: () => documentRewrite(/