RU AdList JS Fixes

try to take over the world!

Від 29.09.2017. Дивіться остання версія.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         RU AdList JS Fixes
// @namespace    ruadlist_js_fixes
// @version      20170929.2
// @description  try to take over the world!
// @author       lainverse & dimisa
// @match        *://*/*
// @grant        unsafeWindow
// @grant        window.close
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';
    let win = (unsafeWindow || window),
        // http://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
        isOpera = (!!window.opr && !!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 || safari.pushNotification)),
        isFirefox = typeof InstallTrigger !== 'undefined',
        inIFrame = (win.self !== win.top),
        _getAttribute = Element.prototype.getAttribute,
        _setAttribute = Element.prototype.setAttribute,
        _de = document.documentElement,
        _appendChild = Document.prototype.appendChild.bind(_de),
        _removeChild = Document.prototype.removeChild.bind(_de),
        _createElement = Document.prototype.createElement.bind(document);

    if (isFirefox && // Exit on image pages in Fx
        document.constructor.prototype.toString() === '[object ImageDocumentPrototype]')
        return;

    // dTree 2.05 in some cases replaces Node object before my script kicks in :(
    if (!Node.prototype)
    {
        let ifr = _createElement('iframe');
        _appendChild(ifr);
        try {
            window.Node = ifr.contentWindow.Node;
            console.log('Node object restored. -_-');
        } catch(e) {
            console.log('Unable to restore Node object.', e);
        }
        _removeChild(ifr);
    }

    // NodeList iterator polyfill (mostly for Safari)
    // https://jakearchibald.com/2014/iterators-gonna-iterate/
    if (!NodeList.prototype[Symbol.iterator]) {
        NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
    }

    // Options
    let opts = {
        'useWSIFunc': useWSI
    };

    {
        let optsCall = function(callback)
        {
            // Register event listener
            let key = "optsCallEvent_" + Math.random().toString(36).substr(2),
                cb = callback.func.bind(callback.name);
            window.addEventListener(key, cb, false);
            // Generate and dispatch synthetic event
            let ev = document.createEvent("HTMLEvents");
            ev.initEvent(key, true, false);
            window.dispatchEvent(ev);
            // Remove listener
            window.removeEventListener(key, cb, false);
        };

        let initOptsHandler = function()
        {
            opts[this] = GM_getValue(this, true);
            if (opts[this])
                opts[this+'Func']();
        };

        optsCall({
            func: initOptsHandler,
            name: 'useWSI'
        });

        // show options page
        let openOptions = function()
        {
            let ovl = _createElement('div'),
                inner = _createElement('div');
            ovl.style = (
                'position: fixed;'+
                'top:0; left:0;'+
                'bottom: 0; right: 0;'+
                'background: rgba(0,0,0,0.85);'+
                'z-index: 2147483647;'+
                'padding: 5em'
            );
            inner.style = (
                'background: whitesmoke;'+
                'font-size: 10pt;'+
                'color: black;'+
                'padding: 1em'
            );
            inner.textContent = 'JS Fixes Options: (reload page to apply)';
            inner.appendChild(_createElement('br'));
            inner.appendChild(_createElement('br'));
            ovl.addEventListener(
                'click', function(e)
                {
                    if (e.target === ovl) {
                        ovl.parentNode.removeChild(ovl);
                        e.preventDefault();
                    }
                    e.stopPropagation();
                }, false
            );
            // append checkbox with label function
            function addCheckbox(optName, optLabel)
            {
                let c = _createElement('input'),
                    l = _createElement('label');
                c.type = 'checkbox';
                c.id = optName;
                optsCall({
                    func: function()
                    {
                        c.checked = GM_getValue(this);
                    },
                    name: optName
                });
                c.addEventListener(
                    'click', function(e)
                    {
                        optsCall({
                            func:function(){
                                GM_setValue(this, e.target.checked);
                                opts[this] = e.target.checked;
                            },
                            name:optName
                        });
                    }, true
                );
                l.textContent = optLabel;
                l.setAttribute('for', optName);
                inner.appendChild(c);
                inner.appendChild(l);
                inner.appendChild(_createElement('br'));
            }
            // append checkboxes
            addCheckbox('useWSI', 'Use WebSocket filter. Disable if experience problems with WebSocket connections.');
            document.body.appendChild(ovl);
            ovl.appendChild(inner);
        };

        // monitor keys pressed for Ctrl+Alt+Shift+J > s > f code
        let opPos = 0, opKey = ['KeyJ','KeyS','KeyF'];
        document.addEventListener(
            'keydown', function(e)
            {
                if ((e.code === opKey[opPos] || e.location) &&
                    (!!opPos || e.altKey && e.ctrlKey && e.shiftKey))
                {
                    opPos += e.location ? 0 : 1;
                    e.stopPropagation();
                    e.preventDefault();
                } else {
                    opPos = 0;
                }
                if (opPos === opKey.length)
                {
                    opPos = 0;
                    openOptions();
                }
            }, false
        );
    }

    // Special wrapper script to run scripts designed to override standard DOM functions
    // In Firefox appends supplied script to a page to make it run in page context and let
    // page content access overridden functions. In other browsers just run it as-is.
    function scriptLander(func, prepend)
    {
        if (!isFirefox)
        {
            func();
            return;
        }
        let script = _createElement('script');
        let inline = ['string', 'function'];
        script.textContent = '!function(){let win=window;' + (
            inline.includes(typeof prepend) && prepend ||
            prepend instanceof Array && prepend.join(';') || ''
        ) + ';(' + func + ')();}();';
        _appendChild(script);
        _removeChild(script);
    }

    function nullTools(opts) {
        // jshint validthis:true
        let nt = this;
        opts = opts || {};
        function log() { if (opts.log) console.log(...arguments); }
        function trace() { if (opts.log) console.trace(...arguments); }

        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(obj, prop, val)
        {
            Object.defineProperty(
                obj, prop, {
                    get: ()  => val,
                    set: (v) => (v !== val ? (log(`set ${prop} of`, obj, 'to', v), nt.destroy(v)) : null, undefined),
                    enumerable: true
                }
            );
        };
        nt.proxy = function(obj)
        {
            return new Proxy(
                obj, {
                    get: (t, p) => t[p],
                    set: (t, p, v) => (v !== t[p] ? (log(`set ${p} of`, t, 'to', v), nt.destroy(v)) : null, true)
                }
            );
        };
        nt.func = (val) => {
            return () => (trace('call func, return', val, true), val);
        };
    }

    // Fake objects of advertisement networks to break their workflow
    scriptLander(
        function()
        {
            let nt = new nullTools();
            // Popular adblock detector
            if (!('fuckAdBlock' in win))
            {
                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);
                    };
                    window.addEventListener('load', callback, false);
                };
                nt.define(win, 'FuckAdBlock', FuckAdBlock);
                nt.define(win, 'fuckAdBlock', new FuckAdBlock({
                    checkOnLoad: true,
                    resetOnEnd: true
                }));
            }

            // CoinHive miner stub. Continuous 100% CPU load can easily kill some CPU with overheat.
            if (!('CoinHive' in win))
            {
                if (location.hostname !== 'cnhv.co')
                {
                    // CoinHive stub for cases when it doesn't affect site functionality
                    let CoinHiveConstructor = function()
                    {
                        this.setThrottle = nt.func(null);
                        this.start = nt.func(null);
                        this.on = nt.func(null);
                        this.getTotalHashes = nt.func(0);
                    };
                    let CoinHiveStub = nt.proxy({
                        Anonymous: CoinHiveConstructor,
                        User: CoinHiveConstructor,
                        Token: CoinHiveConstructor,
                        JobThread: nt.func(null)
                    });
                    nt.define(win, 'CoinHive', CoinHiveStub);
                } else {
                    // CoinHive wrapper to fool sites which expect it to actually work and return results
                    let CoinHiveObject;
                    let fishnet = {
                        apply: (target, thisArg, args) => {
                            console.log(`miner.${target._name}(${JSON.stringify(args).slice(1,-1)})`);
                            return target.apply(thisArg, args);
                        }
                    };
                    Object.defineProperty(win, 'CoinHive', {
                        set: function(obj)
                        {
                            if ('Token' in obj)
                            {
                                console.log('[CoinHive] Token wrapper applied.');
                                let _Token = obj.Token.bind(obj);
                                obj.Token = function(siteKey, goal, params)
                                {
                                    let _goal = goal;
                                    if (goal > 256)
                                        goal = 256;
                                    console.log(`[CoinHive] Original goal: ${_goal}, new smaller goal ${goal}.`);
                                    console.log(`With smaller goals server may return 'invalid_goal' error and stop working.`);
                                    let miner = _Token(siteKey, goal, params);
                                    miner.setThrottle(0.05);
                                    miner.setThrottle = () => null;
                                    let _start = miner.start.bind(miner);
                                    miner.start = function() {
                                        let res = _start(CoinHive.FORCE_EXCLUSIVE_TAB);
                                        return res;
                                    };
                                    let _on = miner.on.bind(miner);
                                    miner.on = function(type, callback)
                                    {
                                        if (type === 'accepted')
                                        {
                                            console.log('[CoinHive] "accepted" callback wrapper applied.');
                                            let _callback = callback;
                                            callback = function(params)
                                            {
                                                console.log('[CoinHive] "accepted" callback is called, imitating original goal being reached.');
                                                params.hashes = _goal;
                                                return _callback.apply(this, arguments);
                                            };
                                            miner.stop();
                                        }
                                        return _on(type, callback);
                                    };
                                    return miner;
                                };
                            }
                            CoinHiveObject = obj;
                        },
                        get: () => CoinHiveObject
                    });
                }
            }

            // Yandex API (ADBTools, Metrika)
            let hostname = location.hostname;
            if (hostname.startsWith('google.') || hostname.includes('.google.') ||
                // Google likes to define odd global variables like Ya
                ((hostname.startsWith('yandex.') || hostname.includes('.yandex.')) &&
                 /^\/((yand)?search|images)/i.test(location.pathname) &&
                 !hostname.startsWith('news.')) ||
                // Also, Yandex uses their Ya object for a lot of things on their pages and
                // wrapping it may cause problems. It's better to skip it in some cases.
                hostname.endsWith('github.io') || hostname.endsWith('grimtools.com'))
                return;

            let Ya = new Proxy({}, {
                set: function(tgt, prop, val)
                {
                    if (val && val !== tgt.Context &&
                        val instanceof Object && 'AdvManager' in val)
                        val = tgt.Context;
                    if (!isNaN(prop))
                        val = nt.func(null);
                    console.log(`Ya.${prop} = `, val);
                    tgt[prop] = val;
                    return true;
                },
                get: (tgt, prop) => tgt[prop]
            });
            let callWithParams = function(f)
            {
                f.call(this, Ya.__inline_params__ || {});
                Ya.__inline_params__ = null;
            };
            nt.define(Ya, 'callWithParams', callWithParams);
            nt.define(Ya, 'PerfCounters', nt.proxy({
                addCacheEvent: nt.func(null),
                getTime: nt.func(null),
                sendCacheEvents: nt.func(null),
                sendResTiming: nt.func(null),
                sendTimeMark: nt.func(null),
                __cacheEvents: []
            }));
            nt.define(Ya, '__isSent', true);
            nt.define(Ya, 'confirmUrl', '');
            nt.define(Ya, 'Direct', nt.proxy({
                insertInto: nt.func(null)
            }));
            nt.define(Ya, 'mediaCode', nt.proxy({
                create: function()
                {
                    if (inIFrame)
                    {
                        console.log('Removed body of ad-frame.');
                        document.documentElement.removeChild(document.body);
                    }
                },
                moduleLoad: nt.func(null),
                setModule: nt.func(null)
            }));
            let extra = nt.proxy({
                extra: nt.proxy({ match: 0, confirm: '', src: '' }),
                id: 0, percent: 100, threshold: 1
            });
            nt.define(Ya, '_exp', nt.proxy({
                id: 0, coin: 0,
                choose: nt.func(extra),
                get: (prop) => extra.hasOwnProperty(prop) ? extra[prop] : null,
                getId: nt.func(0),
                defaultVersion: extra,
                getExtra: nt.func(extra.extra),
                getDefaultExtra: nt.func(extra.extra),
                versions: [extra]
            }));
            nt.define(Ya, 'c', nt.func(null));
            nt.define(Ya, 'ADBTools', function(){
                for (let name of ['loadContext', 'testAdbStyle'])
                    this[name] = nt.func(null);
                this.getCurrentState = nt.func(true);
                return nt.proxy(this);
            });
            if (!location.hostname.includes('kinopoisk.ru'))
                // kinopoisk uses adfox wrapped in Yandex code to display self-ads
                nt.define(Ya, 'adfoxCode', nt.proxy({
                    create: nt.func(null),
                    createAdaptive: nt.func(null),
                    createScroll: nt.func(null)
                }));
            let AdvManager = function()
            {
                for (let name of ['renderDirect', 'getBid', 'releaseBid', 'getSkipToken', 'getAdSessionId'])
                    this[name] = nt.func(null);
                this.render = function(o) {
                    if (!o.renderTo)
                        return;
                    let placeholder = document.getElementById(o.renderTo);
                    let parent = placeholder.parentNode;
                    placeholder.style = 'display:none!important';
                    parent.style = (parent.getAttribute('style')||'') + 'height:auto!important';
                    // fix for Yandex TV pages
                    if (location.hostname.startsWith('tv.yandex.'))
                    {
                        let sibling = placeholder.previousSibling;
                        if (sibling && sibling.classList && sibling.classList.contains('tv-spin'))
                            sibling.style.display = 'none';
                    }
                };
                return nt.proxy(this);
            };
            nt.define(Ya, 'Context', nt.proxy({
                _callbacks: nt.proxy([]),
                _asyncModeOn: true,
                _init: nt.func(null),
                AdvManager: new AdvManager()
            }));
            let Metrika = function()
            {
                for (let name of ['reachGoal', 'replacePhones', 'trackLinks', 'hit', 'params'])
                    this[name] = nt.func(null);
                this.id = 0;
                return nt.proxy(this);
            };
            Metrika.counters = () => Ya._metrika.counters;
            nt.define(Ya, 'Metrika', Metrika);
            let counter = new Ya.Metrika();
            nt.define(Ya, '_metrika', nt.proxy({
                counter: counter,
                counters: [counter],
                hitParam: {},
                counterNum: 0,
                hitId: 0,
                v: 1
            }));
            nt.define(Ya, '_globalMetrikaHitId', 0);
            nt.define(win, 'Ya', Ya);
            // Yandex.Metrika callbacks
            let yandex_metrika_callbacks = [];
            document.addEventListener(
                'DOMContentLoaded',
                (e) => {
                    yandex_metrika_callbacks.forEach((f) => f && f.call(window));
                    yandex_metrika_callbacks.length = 0;
                    yandex_metrika_callbacks.push = (f) => setTimeout(f, 0);
                },
                false
            );
            nt.define(win, 'yandex_metrika_callbacks', yandex_metrika_callbacks);
        }, 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.
    function createStyle(rules, props, skip_protect)
    {
        props = props || {};
        props.type = 'text/css';

        function _protect(style)
        {
            if (skip_protect)
                return;

            Object.defineProperty(style, 'sheet', {
                value: null,
                enumerable: true
            });
            Object.defineProperty(style, 'disabled', {
                get: () => true, //pretend to be disabled
                set: () => undefined,
                enumerable: true
            });
            (new MutationObserver(
                (ms) => _removeChild(ms[0].target)
            )).observe(style, { childList: true });
        }


        function _create()
        {
            let style = _appendChild(_createElement('style'));
            Object.assign(style, props);

            function insertRules(rule)
            {
                if (rule.forEach)
                    rule.forEach(insertRules);
                else try {
                    style.sheet.insertRule(rule, 0);
                } catch (e) {
                    console.error(e);
                }
            }

            insertRules(rules);
            _protect(style);

            return style;
        }

        let style = _create();
        if (skip_protect)
            return style;

        function resolveInANewContext(resolve)
        {
            setTimeout(
                (resolve) => resolve(_create()),
                0, resolve
            );
        }

        (new MutationObserver(
            function(ms)
            {
                let m, node;
                for (m of ms) for (node of m.removedNodes)
                    if (node === style)
                        (new Promise(resolveInANewContext))
                            .then((st) => (style = st));
            }
        )).observe(_de, { childList: true });

        return style;
    }

    // https://greatest.deepsurf.us/scripts/19144-websuckit/
    function useWSI()
    {
        // check does browser support Proxy and WebSocket
        if (typeof Proxy !== 'function' ||
            typeof WebSocket !== 'function')
            return;

        function getWrappedCode(removeSelf)
        {
            let text = getWrappedCode.toString() + WSI.toString();
            text = (
                '(function(){"use strict";'+
                text.replace(/\/\/[^\r\n]*/g,'').replace(/[\s\r\n]+/g,' ')+
                '(new WSI(self||window)).init();'+
                (removeSelf?'let s = document.currentScript; if (s) {s.parentNode.removeChild(s);}':'')+
                '})();\n'
            );
            return text;
        }

        function WSI(win, safeWin)
        {
            safeWin = safeWin || win;
            let masks = [], filter;
            for (filter of [// blacklist
                '||185.87.50.147^',
                '||10root25.website^', '||24video.xxx^',
                '||adlabs.ru^', '||adspayformymortgage.win^', '||aviabay.ru^',
                '||bgrndi.com^', '||brokeloy.com^',
                '||cnamerutor.ru^',
                '||docfilms.info^', '||dreadfula.ru^',
                '||et-code.ru^', '||etcodes.com^',
                '||franecki.net^', '||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^',
                '||kiev.ua^', '||kinotochka.net^',
                '||kinott.com^', '||kinott.ru^', '||kuveres.com^',
                '||lepubs.com^', '||luxadv.com^', '||luxup.ru^', '||luxupcdna.com^',
                '||mail.ru^', '||marketgid.com^', '||mixadvert.com^', '||mxtads.com^',
                '||nickhel.com^',
                '||oconner.biz^', '||oconner.link^', '||octoclick.net^', '||octozoon.org^',
                '||pkpojhc.com^',
                '||psma01.com^', '||psma02.com^', '||psma03.com^',
                '||recreativ.ru^', '||redtram.com^', '||regpole.com^', '||rootmedia.ws^', '||ruttwind.com^',
                '||skidl.ru^',
                '||torvind.com^', '||traffic-media.co^', '||trafmag.com^',
                '||webadvert-gid.ru^', '||webadvertgid.ru^',
                '||xxuhter.ru^',
                '||yuiout.online^',
                '||zoom-film.ru^'])
                masks.push(new RegExp(
                    filter.replace(/([\\\/\[\].*+?(){}$])/g, '\\$1')
                    .replace(/\^(?!$)/g,'\\.?[^\\w%._-]')
                    .replace(/\^$/,'\\.?([^\\w%._-]|$)')
                    .replace(/^\|\|/,'^(ws|http)s?:\\/+([^\/.]+\\.)*'),
                    'i'));

            function isBlocked(url) {
                for (let mask of masks)
                    if (mask.test(url))
                        return true;
                return false;
            }

            function wrapWebSocket(wsParent)
            {
                let _WebSocket = wsParent.WebSocket;
                // getter for properties of fake WS, returns fake functions
                // if there are functions with such name in the real WS and
                // also returns constants from real WS
                function wsGetter(target, name)
                {
                    try {
                        if (typeof _WebSocket.prototype[name] === 'function')
                        {
                            if (name === 'close' || name === 'send') // send also closes connection
                                target.readyState = _WebSocket.CLOSED;
                            return (
                                function fake() {
                                    console.trace(`[WSI] Invoked function "${name}'"`);
                                    return;
                                }
                            );
                        }
                        if (typeof _WebSocket.prototype[name] === 'number')
                            return _WebSocket[name];
                    } catch(ignore) {}
                    return target[name];
                }
                // Wrap WS object
                wsParent.WebSocket = new Proxy(_WebSocket, {
                    construct: function (target, args)
                    {
                        let url = args[0];
                        console.log(`[WSI] Opening socket on  ${url}\u2026`);
                        if (isBlocked(url))
                        {
                            console.log("[WSI] Blocked.");
                            return new Proxy({
                                url: url,
                                readyState: _WebSocket.OPEN
                            }, {
                                get: wsGetter,
                                set: () => true
                            });
                        }
                        return Reflect.construct(target, args);
                    }
                });
            }

            function WorkerWrapper()
            {
                let realWorker = win.Worker;
                win.Worker = function Worker() {
                    let isBlobURL = /^blob:/i,
                        resourceURI = arguments[0],
                        deepLogMode = false,
                        _callbacks = new WeakMap(),
                        _worker = null,
                        _onevs = { names: ['onmessage', 'onerror'] },
                        _actions = [],
                        _self = this;

                    function log()
                    {
                        if (deepLogMode)
                            console.log.apply(_self, arguments);
                    }

                    function callbackWrapper(func)
                    {
                        if (typeof func !== 'function')
                            return undefined;

                        return function callback()
                        {
                            return func.apply(_self, arguments);
                        };
                    }

                    function updateWorker()
                    {
                        for (let [action, name, args] of _actions) {
                            log(_worker, action, name, args);
                            if (action === 'set')
                                _worker[name] = callbackWrapper(args);
                            if (action === 'call')
                                _worker[name].apply(_worker, args);
                        }
                        _actions.length = 0;
                        log('Applied buffered actions.');
                    }

                    for (let prop of _onevs.names)
                        Object.defineProperty(_self, prop, {
                            set: function(val) {
                                _onevs[prop] = val;
                                if (_worker)
                                    _worker[prop] = callbackWrapper(val);
                                else {
                                    _actions.push(['set', prop, val]);
                                    log('Stored into buffer:', arguments);
                                }
                            },
                            get: () => _onevs[prop],
                            enumerable: true
                        });

                    _self.postMessage = function()
                    {
                        if (_worker)
                            _worker.postMessage.apply(_worker, arguments);
                        else {
                            _actions.push(['call', 'postMessage', arguments]);
                            log('Stored into buffer:', arguments);
                        }
                    };
                    _self.terminate = function()
                    {
                        if (_worker)
                            _worker.terminate();
                        else {
                            _actions.push(['call','terminate', arguments]);
                            log('Stored into buffer:', arguments);
                        }
                    };
                    _self.addEventListener = function(event, callback, other)
                    {
                        if (typeof callback !== 'function')
                            return;

                        if (!_callbacks.has(callback))
                            _callbacks.set(callback, callbackWrapper(callback));

                        arguments[1] = _callbacks.get(callback);
                        if (_worker)
                            _worker.addEventListener.apply(_worker, arguments);
                        else {
                            _actions.push(['call', 'addEventListener', arguments]);
                            log('Stored into buffer:', arguments);
                        }
                    };
                    _self.removeEventListener = function(event, callback, other)
                    {
                        if (typeof callback !== 'function' || !_callbacks.has(callback))
                            return;

                        arguments[1] = _callbacks.get(callback);
                        _callbacks.delete(callback);
                        if (_worker)
                            _worker.removeEventListener.apply(_worker, arguments);
                        else {
                            _actions.push(['call', 'removeEventListener', arguments]);
                            log('Stored into buffer:', arguments);
                        }
                    };

                    if (!isBlobURL.test(resourceURI))
                    {
                        _worker = new realWorker(resourceURI);
                        return; // not a blob, no need to wrap
                    }

                    (new Promise(
                        function(resolve, reject)
                        {
                            let xhr = new XMLHttpRequest();
                            xhr.responseType = 'blob';
                            try {
                                xhr.open('GET', resourceURI, true);
                            } catch(e) {
                                return reject(e);
                            }
                            if (xhr.readyState !== XMLHttpRequest.OPENED) {
                                // connection wasn't opened, unable to continue wrapping procedure
                                return reject(xhr.readyState);
                            }
                            xhr.onload = function(e)
                            {
                                if (e.target.status === 200)
                                {
                                    let reader = new FileReader();
                                    reader.addEventListener(
                                        'loadend', function(e)
                                        {
                                            resolve(
                                                new realWorker(URL.createObjectURL(
                                                    new Blob([getWrappedCode(false) + e.target.result])
                                                ))
                                            );
                                        }, false
                                    );
                                    reader.readAsText(e.target.response);
                                } else {
                                    return reject(e);
                                }
                            };
                            xhr.onerror = (e) => reject(e);
                            xhr.send();
                        }
                    )).then(
                        function(val)
                        {
                            _worker = val;
                            updateWorker();
                        }
                    ).catch(
                        function(e)
                        {
                            // connection were blocked by CSP or something else triggered error event on xhr object
                            // unable to proceed with wrapper, return object as-is
                            _worker = new realWorker(resourceURI);
                            updateWorker();
                        }
                    );

                    if (deepLogMode)
                    {
                        return new Proxy(_self, {
                            get: function(target, prop) {
                                console.log(`Get Worker.${prop}`);
                                return target[prop];
                            },
                            set: function(target, prop, val) {
                                console.log(`Set Worker.${prop} = ${val}`);
                                target[prop] = val;
                                return true;
                            }
                        });
                    }
                }.bind(safeWin);
            }

            function CreateElementWrapper()
            {
                let key = '_'+Math.random().toString(36).substr(2),
                    _createElement = Document.prototype.createElement,
                    _addEventListener = Element.prototype.addEventListener,
                    isDataURL = /^data:/i,
                    isBlobURL = /^blob:/i;

                // IFrame SRC get/set wrapper
                let ifGetSet = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'src');
                if (ifGetSet)
                {
                    let code = encodeURIComponent('<scr'+'ipt>'+getWrappedCode(true)+'</scr'+'ipt>\n'),
                        dataSrc = new WeakMap(),
                        _ifSet = ifGetSet.set,
                        _ifGet = ifGetSet.get;
                    ifGetSet.set = function(val)
                    {
                        if (this[key] && val === dataSrc.get(this))
                        { // if already processed data URL then do nothing
                            delete this[key];
                            return null;
                        }
                        let isData = isDataURL.test(val);
                        if (isData && val.indexOf(code) < 0)
                        {
                            dataSrc.set(this, val);
                            val = val.replace(',',',' + code);
                        }
                        if (!isData && dataSrc.get(this))
                            dataSrc.delete(this);
                        return _ifSet.call(this, val);
                    };
                    ifGetSet.get = function()
                    {
                        return dataSrc.get(this) || _ifGet.call(this);
                    };
                    Object.defineProperty(HTMLIFrameElement.prototype, 'src', ifGetSet);
                }

                function frameSetWSWrapper(e)
                {
                    let frm = e.target;
                    try {
                        if (!frm.src || isBlobURL.test(frm.src))
                            wrapWebSocket(frm.contentWindow);
                    } catch (ignore) {}
                }

                let scriptMap = new WeakMap();
                scriptMap.isBlocked = isBlocked;
                let onErrorWrapper = {
                    set: function(val)
                    {
                        if (scriptMap.has(this))
                        {
                            this.removeEventListener('error', scriptMap.get(this).wrp, false);
                            scriptMap.delete(this);
                        }
                        if (!val || typeof val !== 'function')
                            return val;

                        scriptMap.set(this, {
                            org: val,
                            wrp: function()
                            {
                                if (scriptMap.isBlocked(this.src))
                                    console.log('[WSI] Blocked "onerror" callback from', this.src);
                                else
                                    scriptMap.get(this).org.apply(this, arguments);
                            }
                        });
                        this.addEventListener('error', scriptMap.get(this).wrp, false);
                    },
                    get: function()
                    {
                        return scriptMap.has(this) ? scriptMap.get(this).org : null;
                    },
                    enumerable: true
                };
                Document.prototype.createElement = function createElement(name) {
                    let el = _createElement.apply(this, arguments);

                    if (el.tagName === 'IFRAME')
                        _addEventListener.call(el, 'load', frameSetWSWrapper, false);
                    if (el.tagName === 'SCRIPT')
                        Object.defineProperty(el, 'onerror', onErrorWrapper);

                    return el;
                };

                document.addEventListener(
                    'DOMContentLoaded', function()
                    {
                        for (let ifr of document.querySelectorAll('IFRAME'))
                        {
                            if (isDataURL.test(ifr.src))
                            {
                                ifr[key] = true;
                                ifr.src = ifr.src; // call setter and let it do the job
                            }
                            _addEventListener.call(ifr, 'load', frameSetWSWrapper, false);
                        }
                    }, false
                );
            }

            this.init = function()
            {
                wrapWebSocket(win);
                if (!(/firefox/i.test(navigator.userAgent))) // skip WorkerWrapper in Firefox
                    (new Promise(
                        function(resolve, reject)
                        { // test is it possible to run inline scripts
                            if (self.constructor.name.indexOf('Worker') > -1)
                                return resolve(); // running within a Worker
                            let onerr = window.onerror,
                                onscr = (e) => resolve();
                            // for some reason addEventListener on 'error' doesn't catch this error
                            window.onerror = (e) => reject(e);
                            window.addEventListener('inlineSuccess', onscr, false);
                            let scr = document.createElement('script');
                            scr.textContent = "window.dispatchEvent(new Event('inlineSuccess'));";
                            document.documentElement.appendChild(scr);
                            document.documentElement.removeChild(scr);
                            window.removeEventListener('inlineSuccess', onscr, false);
                            window.onerror = onerr;
                        }
                    )).then(
                        (e) => WorkerWrapper()
                    ).catch(
                        (e) => console.log('[WSI] Unable to create inline script. Skipping Worker wrapper to avoid further issues.', e)
                    );
                if (typeof document !== 'undefined')
                    CreateElementWrapper();
            };
        }

        if (isFirefox)
        {
            let script = _createElement('script');
            script.textContent = getWrappedCode(true);
            _appendChild(script);
            _removeChild(script);
            return; //we don't want to call functions on page from here in Fx, so exit
        }

        (new WSI((unsafeWindow||self||window),(self||window))).init();
    }

    if (!isFirefox)
    { // scripts for non-Firefox browsers
        // https://greatest.deepsurf.us/scripts/14720-it-s-not-important
        {
            let imptt = /((display|(margin|padding)(-top|-bottom)?)\s*:[^;!]*)!\s*important/ig,
                ret_b = (a,b) => b,
                _toLowerCase = String.prototype.toLowerCase,
                protectedNodes = new WeakSet(),
                log = false;

            let logger = function()
            {
                if (log)
                    console.log('Some page elements became a bit less important.');
                log = false;
            };

            let unimportanter = function(node)
            {
                let style = (node.nodeType === Node.ELEMENT_NODE) ?
                    _getAttribute.call(node, 'style') : null;

                if (!style || !imptt.test(style) || node.style.display === 'none' ||
                    (node.src && node.src.slice(0,17) === 'chrome-extension:')) // Web of Trust IFRAME and similar
                    return false; // get out if we have nothing to do here

                protectedNodes.add(node);
                _setAttribute.call(node, 'style',
                                   style.replace(imptt, ret_b));
                log = true;
            };

            (new MutationObserver(
                function(mutations)
                {
                    setTimeout(
                        function(ms)
                        {
                            let m, node;
                            for (m of ms) for (node of m.addedNodes)
                                unimportanter(node);
                            logger();
                        }, 0, mutations
                    );
                }
            )).observe(document, {
                childList : true,
                subtree : true
            });

            Element.prototype.setAttribute = function setAttribute(name, value)
            {
                "[native code]";
                let replaced = value;
                if (_toLowerCase.call(name) === 'style' && protectedNodes.has(this))
                    replaced = value.replace(imptt, ret_b);
                log = (replaced !== value);
                logger();
                return _setAttribute.call(this, name, replaced);
            };

            win.addEventListener (
                'load', function()
                {
                    for (let imp of document.querySelectorAll('[style*="!"]'))
                        unimportanter(imp);
                    logger();
                }, false
            );
        }

        // Naive ABP Style protector
        {
            let _querySelector = Document.prototype.querySelector.bind(document);
            let _removeChild = Node.prototype.removeChild;
            let _appendChild = Node.prototype.appendChild;
            let createShadow = () => _createElement('shadow');
            // Prevent adding fake content entry point
            Node.prototype.appendChild = function(child)
            {
                if (this instanceof ShadowRoot &&
                    child instanceof HTMLContentElement)
                    return _appendChild.call(this, createShadow());
                return _appendChild.apply(this, arguments);
            };
            {
                let _shadowSelector = ShadowRoot.prototype.querySelector;
                let _innerHTML = Object.getOwnPropertyDescriptor(ShadowRoot.prototype, 'innerHTML');
                let _parentNode = Object.getOwnPropertyDescriptor(Node.prototype, 'parentNode');
                if (_innerHTML && _parentNode)
                {
                    let _set = _innerHTML.set;
                    let _getParent = _parentNode.get;
                    _innerHTML.configurable = false;
                    _innerHTML.set = function()
                    {
                        _set.apply(this, arguments);
                        let content = _shadowSelector.call(this, 'content');
                        if (content)
                        {
                            let parent = _getParent.call(content);
                            _removeChild.call(parent, content);
                            _appendChild.call(parent, createShadow());
                        }
                    };
                }
                Object.defineProperty(ShadowRoot.prototype, 'innerHTML', _innerHTML);
            }
            // Locate and apply extra protection to a style on top of what ABP does
            let style;
            (new Promise(
                function(resolve, reject)
                {
                    let getStyle = () => _querySelector('::shadow style');
                    style = getStyle();
                    if (style)
                        return resolve(style);
                    let intv = setInterval(
                        function()
                        {
                            style = getStyle();
                            if (!style)
                                return;
                            intv = clearInterval(intv);
                            return resolve(style);
                        }, 0
                    );
                    document.addEventListener(
                        'DOMContentLoaded',
                        function()
                        {
                            if (intv)
                                clearInterval(intv);
                            style = getStyle();
                            return style ? resolve(style) : reject();
                        },
                        false
                    );
                }
            )).then(
                function(style)
                {
                    let emptyArr = [],
                        nullStr = {
                            get: () => '',
                            set: () => undefined
                        };
                    Object.defineProperties(style, {
                        innerHTML: nullStr,
                        textContent: nullStr
                    });
                    Object.defineProperties(style.sheet, {
                        deleteRule: () => null,
                        cssRules: emptyArr,
                        rules: emptyArr
                    });
                }
            ).catch(()=>null);
            Node.prototype.removeChild = function(child)
            {
                if (child === style)
                    return;
                return _removeChild.apply(this, arguments);
            };
        }
    }
    if (/^https?:\/\/(mail\.yandex\.|music\.yandex\.|news\.yandex\.|(www\.)?yandex\.[^\/]+\/(yand)?search[\/?])/i.test(win.location.href) ||
        /^https?:\/\/tv\.yandex\./i.test(win.location.href))
    {
        // https://greatest.deepsurf.us/en/scripts/809-no-yandex-ads
        let _querySelector = document.querySelector.bind(document),
            _querySelectorAll = document.querySelectorAll.bind(document),
            _getAttribute = Element.prototype.getAttribute,
            _setAttribute = Element.prototype.setAttribute,
            _attachShadow = () => null,
            _shadowRootDescriptor = Object.getOwnPropertyDescriptor(Element.prototype, 'shadowRoot'),
            _getShadowRoot = () => null;
        if (_shadowRootDescriptor)
        {
            _attachShadow = Element.prototype.attachShadow;
            _getShadowRoot = _shadowRootDescriptor.get;
        }
        document.addEventListener(
            'DOMContentLoaded', function()
            {
                let adWords = [/Яндекс.Директ/i, /Реклама/i, /Ad/i],
                    genericAdSelectors = (
                        '.serp-adv__head + .serp-item,'+
                        '#adbanner,'+
                        '.serp-adv,'+
                        '.b-spec-adv,'+
                        'div[class*="serp-adv__"]:not(.serp-adv__found):not(.serp-adv__displayed)'
                    );
                // Generic ads removal and fixes
                {
                    let node = _querySelector('.serp-header');
                    if (node)
                        node.style.marginTop = '0';
                    for (node of _querySelectorAll(genericAdSelectors))
                        remove(node);
                }
                // Short name for parentNode.removeChild
                function remove(node) {
                    if (!node || !node.parentNode)
                        return false;
                    console.log('Removed node.');
                    node.parentNode.removeChild(node);
                }
                // Short name to hide node with style attribute
                let hiddenNodes = new WeakSet();
                function hide(node) {
                    if (hiddenNodes.has(node))
                        return false;
                    _setAttribute.call(node, 'style', 'display: none !important');
                    hiddenNodes.add(node);
                    console.log('Hid node.');
                    return true;
                }
                // Search ads
                function removeSearchAds()
                {
                    let res = false;
                    // hide unparsed Yandex ads if present
                    for (let node of _querySelectorAll('.serp-item[role="complementary"]'))
                        res = res|hide(node);
                    if (res) return 'Unparsed Yandex ads were hidden.';
                    // hide parsed Yandex ads
                    let nodes = _querySelectorAll('.serp-item .organic'),
                        path, label, openShadow = { mode: 'open' },
                        root = (node) => node.closest('.serp-item');
                    for (let node of nodes)
                    {
                        label = node.querySelector('.path ~ :last-child');
                        try {
                            if (!_getShadowRoot.call(label))
                                _attachShadow.call(label, openShadow);
                        } catch (e) {
                            res = res|hide(root(node));
                            continue;
                        }
                        label = node.querySelector('.label');
                        if (label && (adWords[1].test(label.textContent) || adWords[2].test(label.textContent)))
                            res = res|hide(root(node));
                    }
                    if (res)
                        return 'Parsed Yandex ads were hidden.';
                    else
                        return 'No ads were detected.';
                }
                function removeSearchAdsLog()
                {
                    let res = removeSearchAds();
                    if (res) console.log(res);
                }
                // News ads
                function removeNewsAds()
                {
                    let node, block, item, items, mask, classes,
                        masks = [
                            { class: '.ads__wrapper', regex: /[^,]*?,[^,]*?\.ads__wrapper/ },
                            { class: '.ads__pool',    regex: /[^,]*?,[^,]*?\.ads__pool/ }
                        ];
                    for (node of _querySelectorAll('style[nonce]'))
                    {
                        classes = node.innerText.replace(/\{[^}]+\}+/ig, '|').split('|');
                        for (block of classes) for (mask of masks)
                            if (block.includes(mask.class))
                            {
                                block = block.match(mask.regex)[0];
                                items = _querySelectorAll(block);
                                for (item of items)
                                    remove(items[0]);
                            }
                    }
                }
                // Music ads
                function removeMusicAds()
                {
                    for (let node of _querySelectorAll('.ads-block'))
                        remove(node);
                }
                // Mail ads
                function removeMailAds()
                {
                    let slice = Array.prototype.slice,
                        nodes = slice.call(_querySelectorAll('.ns-view-folders')),
                        node, len, cls;

                    for (node of nodes)
                        if (!len || len > node.classList.length)
                            len = node.classList.length;

                    node = nodes.pop();
                    while (node)
                    {
                        if (node.classList.length > len)
                            for (cls of slice.call(node.classList))
                                if (cls.indexOf('-') === -1)
                                {
                                    remove(node);
                                    break;
                                }
                        node = nodes.pop();
                    }
                }
                // News fixes
                function removePageAdsClass()
                {
                    if (document.body.classList.contains("b-page_ads_yes"))
                    {
                        document.body.classList.remove("b-page_ads_yes");
                        console.log('Page ads class removed.');
                    }
                }
                // TV fixes
                function removeTVAds()
                {
                    for (let node of _querySelectorAll('div[class^="_"][data-reactid] > div'))
                        if (adWords[0].test(node.textContent) || node.querySelector('iframe:not([src])'))
                        {
                            if (node.offsetWidth)
                            {
                                let pad = document.createElement('div');
                                _setAttribute.call(pad, 'style', 'width:'+node.offsetWidth+'px');
                                node.parentNode.appendChild(pad);
                            }
                            remove(node);
                        }
                }
                // Function to attach an observer to monitor dynamic changes on the page
                function pageUpdateObserver(func, obj, params) {
                    if (obj)
                        (new MutationObserver(func))
                            .observe(obj, (params || { childList:true, subtree:true }));
                }

                if (location.hostname.startsWith('mail.')) {
                    pageUpdateObserver(
                        function(ms, o)
                        {
                            let aside = _querySelector('.mail-Layout-Aside');
                            if (aside) {
                                o.disconnect();
                                pageUpdateObserver(removeMailAds, aside);
                            }
                        }, document.body
                    );
                    removeMailAds();
                } else if (location.hostname.startsWith('music.')) {
                    pageUpdateObserver(removeMusicAds, _querySelector('.sidebar'));
                    removeMusicAds();
                } else if (location.hostname.startsWith('news.')) {
                    pageUpdateObserver(removeNewsAds, document.body);
                    pageUpdateObserver(removePageAdsClass, document.body, { attributes:true, attributesFilter:['class'] });
                    removeNewsAds();
                    removePageAdsClass();
                } else if (location.hostname.startsWith('tv.')) {
                    pageUpdateObserver(removeTVAds, document.body);
                    removeTVAds();
                } else {
                    pageUpdateObserver(removeSearchAdsLog, _querySelector('.main__content'));
                    removeSearchAdsLog();
                }
            }
        );
    }

    // Yandex Link Tracking
    if (/^https?:\/\/([^.]+\.)*yandex\.[^\/]+/i.test(win.location.href))
    {
        let fakeRoot = {
            firstChild: null,
            appendChild: ()=>null,
            querySelector: ()=>null,
            querySelectorAll: ()=>null
        };
        Element.prototype.createShadowRoot = () => fakeRoot;
        Object.defineProperty(Element.prototype, "shadowRoot", {
            value: fakeRoot,
            enumerable: true,
            configurable: false
        });
        // Partially based on https://greatest.deepsurf.us/en/scripts/22737-remove-yandex-redirect
        let selectors = (
            'A[onmousedown*="/jsredir"],'+
            'A[data-vdir-href],'+
            'A[data-counter]'
        );
        let removeTrackingAttributes = function(link)
        {
            link.removeAttribute('onmousedown');
            if (link.hasAttribute('data-vdir-href')) {
                link.removeAttribute('data-vdir-href');
                link.removeAttribute('data-orig-href');
            }
            if (link.hasAttribute('data-counter')) {
                link.removeAttribute('data-counter');
                link.removeAttribute('data-bem');
            }
        };
        let removeTracking = function(scope)
        {
            for (let link of scope.querySelectorAll(selectors))
                removeTrackingAttributes(link);
        };
        document.addEventListener('DOMContentLoaded', (e) => removeTracking(e.target));
        (new MutationObserver(
            function(ms)
            {
                let m, node;
                for (m of ms) for (node of m.addedNodes) if (node.nodeType === Node.ELEMENT_NODE)
                    if (node.tagName === 'A' && node.matches(selectors)) {
                        removeTrackingAttributes(node);
                    } else {
                        removeTracking(node);
                    }
            }
        )).observe(_de, { childList: true, subtree: true });

        //skip fixes for other sites
        return;
    }

    // https://greatest.deepsurf.us/en/scripts/21937-moonwalk-hdgo-kodik-fix v0.8 (adapted)
    document.addEventListener(
        'DOMContentLoaded', function()
        {//createPlayer();
            function log (name) {
                console.log(`Player FIX: Detected ${name} player in ${location.href}`);
            }
            if (win.adv_enabled !== undefined && win.condition_detected !== undefined)
            {
                log('Moonwalk');
                if (win.adv_enabled)
                    win.adv_enabled = false;
                win.condition_detected = false;
                if (win.MXoverrollCallback)
                    document.addEventListener(
                        'click', function catcher(e)
                        {
                            e.stopPropagation();
                            win.MXoverrollCallback.call(window);
                            document.removeEventListener('click', catcher, true);
                        }, true
                    );
            }
            else if (win.stat_url !== undefined && win.is_html5 !== undefined && win.is_wp8 !== undefined)
            {
                log('HDGo');
                document.body.onclick = null;
                let tmp = document.querySelector('#swtf');
                if (tmp)
                    tmp.style.display = 'none';
                if (win.banner_second !== undefined)
                    win.banner_second = 0;
                if (win.$banner_ads !== undefined)
                    win.$banner_ads = false;
                if (win.$new_ads !== undefined)
                    win.$new_ads = false;
                if (win.createCookie !== undefined)
                    win.createCookie('popup', 'true', '999');
                if (win.canRunAds !== undefined && win.canRunAds !== true)
                    win.canRunAds = true;
            }
            else if (win.MXoverrollCallback && win.iframeSearch !== undefined)
            {
                log('Kodik');
                let tmp = document.querySelector('.play_button');
                if (tmp)
                    tmp.onclick = win.MXoverrollCallback.bind(window);
                win.IsAdBlock = false;
            }
            else if (win.getnextepisode && win.uppodEvent)
            {
                log('Share-Serials.net');
                scriptLander(
                    function()
                    {
                        let _setInterval = win.setInterval,
                            _setTimeout = win.setTimeout;
                        win.setInterval = function(func)
                        {
                            if (func instanceof Function && func.toString().indexOf('_delay') > -1)
                            {
                                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 (func instanceof Function && func.toString().indexOf('adv_showed') > -1)
                            {
                                return _setTimeout.call(this, func, 0);
                            }
                            return _setTimeout.apply(this, arguments);
                        };
                    }
                );
            }
        }, false
    );

    // piguiqproxy.com circumvention prevention
    scriptLander(
        function()
        {
            let _open = XMLHttpRequest.prototype.open;
            let blacklist = /[/.@](piguiqproxy\.com|rcdn\.pro)[:/]/i;
            XMLHttpRequest.prototype.open = function(method, url)
            {
                if (method === 'GET' && blacklist.test(url))
                {
                    this.send = () => null;
                    this.setRequestHeader = () => null;
                    console.log('Blocked request: ', url);
                    return;
                }
                return _open.apply(this, arguments);
            };
        }
    );

    // === Helper functions ===

    // 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)
    let scRemove = (node) => node.parentNode.removeChild(node);
    let scHide = function(node)
    {
        let style = _getAttribute.call(node, 'style') || '',
            hide = ';display:none!important;';
        if (style.indexOf(hide) < 0)
            _setAttribute.call(node, 'style', style + hide);
    };

    function scissors (selector, words, scope, params)
    {
        let logger = function() { return params.log ? console.log(...arguments) : null; };
        if (!scope.contains(document.body))
            logger('[s] scope', scope);
        let remFunc = (params.hide ? scHide : scRemove),
            iterFunc = (params.siblings > 0 ? 'nextSibling' : 'previousSibling'),
            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);
                    break;
                }
            }
            logger('[s] processing', node);
            if (words.test(node.innerHTML) || !node.childNodes.length)
            {
                // drill across for N sibling nodes
                if (!toRemove.includes(node))
                {
                    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.nodeType === Node.ELEMENT_NODE)
                        {
                            logger('[s] adding sibling node', node);
                            toRemove.push(node);
                            siblings -= 1; //count only element nodes
                        }
                        else if (!params.hide)
                        {
                            logger('[s] adding sibling node', node);
                            toRemove.push(node);
                        }
                    }
                }
            }
        }
        if (toRemove.length)
            logger(`[s] proceeding with ${params.hide?'hide':'removal'} of`, toRemove);
        for (let node of toRemove)
            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.
    function gardener(selector, words, params)
    {
        let logger = function() { return params.log ? console.log(...arguments) : null; };
        params = params || {};
        logger(`[gardener] selector: '${selector}' detector: ${words} options: ${JSON.stringify(params)}`);
        let scope = [document];
        function onevent(e)
        {
            logger(`[gardener] cleanup on ${Object.getPrototypeOf(e).toString().match(/\s(\w+?)Prototype/)[1]} "${e.type}"`);
            for (let node of scope)
                scissors(selector, words, node, params);
        }
        document.addEventListener(
            'DOMContentLoaded', (e) => {
                // narrow down scope to a specific element
                if (params.root)
                {
                    scope = document.querySelectorAll(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, onClickFunc)
    {
        let _createElement = Document.prototype.createElement,
            _appendChild = Element.prototype.appendChild,
            fakeNative = (f) => (f.toString = () => 'function '+f.name+'() { [native code] }');

        let nt = new nullTools();
        fakeNative(openFunc);
        function redefineOpen(obj)
        {
            nt.define(obj, 'open', openFunc);
            nt.define(obj.document, 'open', openFunc);
            nt.define(obj.Document.prototype, 'open', openFunc);
        }
        redefineOpen(win);

        function createElement(name)
        {
            '[native code]';
            // jshint validthis:true
            let el = _createElement.apply(this, arguments);
            // click-dispatch check for Google Chrome and similar browsers
            if (el instanceof HTMLAnchorElement)
                el.addEventListener(
                    'click', onClickFunc, false
                );
            // redefine window.open in first-party frames
            if (el instanceof HTMLIFrameElement || el instanceof HTMLObjectElement)
                el.addEventListener(
                    'load', function(e)
                    {
                        try {
                            redefineOpen(e.target.contentWindow);
                        } catch(ignore) {}
                    }, false
                );
            return el;
        }
        fakeNative(createElement);

        function redefineCreateElement(obj)
        {
            nt.define(obj.document, 'createElement', createElement);
            nt.define(obj.Document.prototype, 'createElement', createElement);
        }
        redefineCreateElement(win);

        // wrap window.open in newly added first-party frames
        Element.prototype.appendChild = function appendChild()
        {
            '[native code]';
            let el = _appendChild.apply(this, arguments);
            if (el instanceof HTMLIFrameElement) {
                try {
                    redefineOpen(el.contentWindow);
                    redefineCreateElement(el.contentWindow);
                } catch(ignore) {}
            }
            return el;
        };
        fakeNative(Element.prototype.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()
    {
        if (inIFrame)
        {
            win.top.postMessage({ name: 'sandbox-me', href: win.location.href }, '*');
            return;
        }

        scriptLander(
            function()
            {
                function open()
                {
                    '[native code]';
                    console.log('Site attempted to open a new window', arguments);
                    return {
                        document: {
                            write: () => {},
                            writeln: () => {}
                        }
                    };
                }

                function clickHandler(e)
                {
                    let link = e.target;
                    if (!link.parentNode || !e.isTrusted ||
                        (link.href && link.href.trim().toLowerCase().indexOf('javascript') === 0))
                    {
                        e.preventDefault();
                        console.log('Blocked suspicious click event', e, 'on', e.target);
                    }
                }

                createWindowOpenWrapper(open, clickHandler);

                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(
            function()
            {
                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, parseURL.host);
                // 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
                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);
                }

                function open(url)
                {
                    '[native code]';
                    if (url && isLocal(url))
                        window.addEventListener('unload', closeWindow, true);
                    // jshint validthis:true
                    return _open.apply(this, arguments);
                }

                function clickHandler(e)
                {
                    if (!e.target.parentNode || !e.isTrusted)
                        window.addEventListener('unload', closeWindow, true);
                }

                createWindowOpenWrapper(open, clickHandler);

                console.log("Background redirect prevention enabled.");
            }, [nullTools, createWindowOpenWrapper, 'let eventName="'+eventName+'"']
        );
    }

    // 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(
            function()
            {
                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, 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('unload', closeWindow, true);
                    setTimeout(closeWindow=>window.removeEventListener('unload', 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.");
            }, [createWindowOpenWrapper, 'let eventName="'+eventName+'"']
        );
    }
    // 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
        );
    }

    // === Scripts for specific domains ===

    let scripts = {};
    // prevent popups and redirects block
    // Popups
    scripts.preventPopups = {
        other: [
            'biqle.ru',
            'chaturbate.com',
            'dfiles.ru',
            'hentaiz.org',
            'mirrorcreator.com',
            'online-multy.ru',
            'radikal.ru',
            'seedoff.cc', 'seedoff.tv',
            'tapochek.net', 'thepiratebay.org', 'torseed.net',
            'unionpeer.com',
            'zippyshare.com'
        ],
        now: preventPopups
    };
    // Popunders (background redirect)
    scripts.preventPopunders = {
        other: [
            'mediafire.com', 'megapeer.org', 'megapeer.ru',
            'perfectgirls.net'
        ],
        now: preventPopunders
    };
    // PopMix (both types of popups encountered on site)
    scripts['openload.co'] = {
        other: ['oload.tv', 'oload.info'],
        now: () => {
            let nt = new nullTools();
            nt.define(win, 'CNight', win.CoinHive);
            if (location.pathname.startsWith('/embed/'))
            {
                nt.define(win, 'BetterJsPop', {
                    add: ((a, b) => console.trace('BetterJsPop.add', a, b)),
                    config: ((o) => console.trace('BetterJsPop.config', o)),
                    Browser: { isChrome: true }
                });
                nt.define(win, 'isSandboxed', nt.func(null));
                nt.define(win, 'adblock', false);
                nt.define(win, 'adblock2', false);
            } else
                preventPopMix();
        }
    };
    scripts['turbobit.net'] = preventPopMix;

    // other
    scripts['2picsun.ru'] = {
        other: [
            'pics2sun.ru', '3pics-img.ru'
        ],
        now: () => {
            Object.defineProperty(navigator, 'userAgent', {value: 'googlebot'});
        }
    };

    scripts['4pda.ru'] = {
        now: () => {
            // https://greatest.deepsurf.us/en/scripts/14470-4pda-unbrender
            let hStyle,
                isForum = document.location.href.search('/forum/') !== -1,
                remove = (node) => (node ? node.parentNode.removeChild(node) : null),
                afterClean = () => remove(hStyle);

            function beforeClean()
            {
                // attach styles before document displayed
                hStyle = createStyle([
                    'html { overflow-y: scroll }',
                    'section[id] {'+(
                        'position: absolute;'+
                        'width: 100%'
                    )+'}',
                    'article + aside * { display: none !important }',
                    '#header + div:after {'+(
                        'content: "";'+
                        'position: fixed;'+
                        'top: 0;'+
                        'left: 0;'+
                        'width: 100%;'+
                        'height: 100%;'+
                        'background-color: #E6E7E9'
                    )+'}',
                    // http://codepen.io/Beaugust/pen/DByiE
                    '@keyframes spin { 100% { transform: rotate(360deg) } }',
                    'article + aside:after {'+(
                        'content: "";'+
                        'position: absolute;'+
                        'width: 150px;'+
                        'height: 150px;'+
                        'top: 150px;'+
                        'left: 50%;'+
                        'margin-top: -75px;'+
                        'margin-left: -75px;'+
                        'box-sizing: border-box;'+
                        'border-radius: 100%;'+
                        'border: 10px solid rgba(0, 0, 0, 0.2);'+
                        'border-top-color: rgba(0, 0, 0, 0.6);'+
                        'animation: spin 2s infinite linear'
                    )+'}'
                ], {id:'ubrHider'}, true);

                // display content of a page if time to load a page is more than 2 seconds to avoid
                // blocking access to a page if it is loading for too long or stuck in a loading state
                setTimeout(2000, afterClean);
            }

            createStyle([
                '#nav .use-ad { display: block !important }',
                'article:not(.post) + article:not(#id),'+
                'html:not(#id)>body:not(#id) a[target="_blank"] img[height="90"] { display: none !important }'
            ]);

            if (!isForum)
                beforeClean();

            // save links to non-overridden functions to use later
            let protectedElems;
            // protect/hide changed attributes in case site attempt to restore them
            function styleProtector(eventMode)
            {
                let _toLowerCase = String.prototype.toLowerCase,
                    isStyleText = (t) => (_toLowerCase.call(t) === 'style'),
                    protectedElems = new WeakMap();
                function protoOverride(element, functionName, isStyleCheck, returnIfProtected)
                {
                    let originalFunction = element.prototype[functionName];
                    element.prototype[functionName] = function wrapper()
                    {
                        if (protectedElems.has(this) && isStyleCheck(arguments[0]))
                            return returnIfProtected(this, arguments);
                        return originalFunction.apply(this, arguments);
                    };
                }
                protoOverride(Element, 'removeAttribute', isStyleText, () => undefined);
                protoOverride(Element, 'hasAttribute', isStyleText, (_this) => protectedElems.get(_this) !== null);
                protoOverride(Element, 'setAttribute', isStyleText, (_this, args) => protectedElems.set(_this, args[1]));
                protoOverride(Element, 'getAttribute', isStyleText, (_this) => protectedElems.get(_this));
                if (!eventMode)
                    return protectedElems;
                else
                {
                    let e = document.createEvent('Event');
                    e.initEvent('protoOverride', false, false);
                    window.protectedElems = protectedElems;
                    window.dispatchEvent(e);
                }
            }
            if (!isFirefox)
                protectedElems = styleProtector(false);
            else
            {
                let script = document.createElement('script');
                script.textContent = '(' + styleProtector.toString() + ')(true);';
                window.addEventListener(
                    'protoOverride', function protoOverrideCallback(e)
                    {
                        if (win.protectedElems) {
                            protectedElems = win.protectedElems;
                            delete win.protectedElems;
                        }
                        document.removeEventListener('protoOverride', protoOverrideCallback, true);
                    }, true
                );
                _appendChild(script);
                _removeChild(script);
            }

            // clean a page
            window.addEventListener(
                'DOMContentLoaded', function()
                {
                    let width = () => window.innerWidth || _de.clientWidth || document.body.clientWidth || 0;
                    let height = () => window.innerHeight || _de.clientHeight || document.body.clientHeight || 0;

                    if (isForum)
                    {
                        let si = document.querySelector('#logostrip');
                        if (si)
                            remove(si.parentNode.nextSibling);
                    }

                    if (document.location.href.search('/forum/dl/') !== -1) {
                        document.body.setAttribute('style', (document.body.getAttribute('style')||'')+
                                                   ';background-color:black!important');
                        for (let itm of document.querySelectorAll('body>div'))
                            if (!itm.querySelector('.dw-fdwlink'))
                                remove(itm);
                    }

                    if (isForum) // Do not continue if it's a forum
                        return;

                    {
                        let si = document.querySelector('#header');
                        if (si)
                        {
                            let rem = si.previousSibling;
                            while (rem)
                            {
                                si = rem.previousSibling;
                                remove(rem);
                                rem = si;
                            }
                        }
                    }

                    for (let itm of document.querySelectorAll('#nav li[class]'))
                        if (itm && itm.querySelector('a[href^="/tag/"]'))
                            remove(itm);

                    let style, result,
                        fakeStyles = new WeakMap(),
                        styleProxy = {
                            get: function(target, prop)
                            {
                                let fakeStyle = fakeStyles.get(target);
                                return ((prop in fakeStyle) ? fakeStyle : target)[prop];
                            },
                            set: function(target, prop, value)
                            {
                                let fakeStyle = fakeStyles.get(target);
                                ((prop in fakeStyle) ? fakeStyle : target)[prop] = value;
                                return true;
                            }
                        };
                    for (let itm of document.querySelectorAll('DIV, A'))
                    {
                        if (itm.tagName ==='DIV' &&
                            itm.offsetWidth > 0.95 * width() &&
                            itm.offsetHeight > 0.85 * height())
                        {
                            style = window.getComputedStyle(itm, null);
                            result = [];

                            if (style.backgroundImage !== 'none')
                                result.push('background-image:none!important');

                            if (style.backgroundColor !== 'transparent' &&
                                style.backgroundColor !== 'rgba(0, 0, 0, 0)')
                                result.push('background-color:transparent!important');

                            if (result.length)
                            {
                                if (itm.getAttribute('style'))
                                    result.unshift(itm.getAttribute('style'));

                                fakeStyles.set(itm.style, {
                                    'backgroundImage': itm.style.backgroundImage,
                                    'backgroundColor': itm.style.backgroundColor
                                });

                                try {
                                    Object.defineProperty(itm, 'style', {
                                        value: new Proxy(itm.style, styleProxy),
                                        enumerable: true
                                    });
                                } catch (e) {
                                    console.log('Unable to protect style property.', e);
                                }

                                if (protectedElems)
                                    protectedElems.set(itm, _getAttribute.call(itm, 'style'));

                                _setAttribute.call(itm, 'style', result.join(';'));
                            }
                        }
                        if (itm.tagName ==='A' &&
                            (itm.offsetWidth > 0.95 * width() ||
                             itm.offsetHeight > 0.85 * height()))
                        {
                            if (protectedElems)
                                protectedElems.set(itm, _getAttribute.call(itm, 'style'));

                            _setAttribute.call(itm, 'style', 'display:none!important');
                        }
                    }

                    for (let itm of document.querySelectorAll('ASIDE>DIV'))
                        if ( ((itm.querySelector('script, iframe, a[href*="/ad/www/"]') ||
                               itm.querySelector('img[src$=".gif"]:not([height="0"]), img[height="400"]')) &&
                              !itm.classList.contains('post') ) || !itm.childNodes.length )
                            remove(itm);

                    document.body.setAttribute('style', (document.body.getAttribute('style')||'')+';background-color:#E6E7E9!important');

                    // display content of the page
                    afterClean();
                }
            );
        }
    };

    scripts['allmovie.pro'] = {
        other: ['rufilmtv.org'],
        dom: function()
        {
            // pretend to be Android to make site use different played for ads
            if (isSafari)
                return;
            Object.defineProperty(navigator, 'userAgent', {
                get: function(){ return 'Mozilla/5.0 (Linux; Android 4.1.1; Nexus 7 Build/JRO03D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Safari/535.19'; },
                enumerable: true
            });
        }
    };

    scripts['anidub-online.ru'] = {
        other: ['online.anidub.com'],
        dom: function()
        {
            if (win.ogonekstart1)
                win.ogonekstart1 = () => console.log("Fire in the hole!");
        },
        now: () => createStyle([
            '.background {background: none!important;}',
            '.background > script + div,'+
            '.background > script ~ div:not([id]):not([class]) + div[id][class]'+
            '{display:none!important}'
        ])
    };

    scripts['drive2.ru'] = () => gardener('.c-block:not([data-metrika="recomm"]),.o-grid__item', />Реклама<\//i);

    scripts['fishki.net'] = () => gardener('.drag_list > .drag_element, .list-view > .paddingtop15, .post-wrap', /543769|Новости\sпартнеров|Полезная\sреклама/);

    scripts['gidonline.club'] = () => createStyle('.tray > div[style] {display: none!important}');

    scripts['hdgo.cc'] = {
        other: ['46.30.43.38', 'couber.be'],
        now: () => (new MutationObserver(
            function(ms)
            {
                let m, node;
                for (m of ms) for (node of m.addedNodes)
                    if (node.tagName === 'SCRIPT' && _getAttribute.call(node, 'onerror') !== null)
                        node.removeAttribute('onerror');
            }
        )).observe(document.documentElement, { childList:true, subtree: true })
    };

    scripts['gismeteo.ru'] = {
        other: ['gismeteo.ua'],
        now: () => gardener('div > script', /AdvManager/i, { root: 'body', observe: true, parent: 'div'})
    };

    scripts['hdrezka.ag'] = () => {
        Object.defineProperty(win, 'ab', { value: false, enumerable: true });
        gardener('div[id][onclick][onmouseup][onmousedown]', /onmouseout/i);
    };

    scripts['imageban.ru'] = {
        now: preventPopunders,
        dom: () => win.addEventListener('unload', () => location.hash = 'x'+Math.random().toString(36).substr(2), true)
    };

    scripts['mail.ru'] = {
        now: () => {
            // Trick to prevent mail.ru from removing 3rd-party styles
            scriptLander(
                () => {
                    Object.defineProperty(Object.prototype, 'restoreVisibility', {
                        get: () => (() => null),
                        set: () => undefined
                    });

                    if (location.hostname !== 'e.mail.ru')
                        return;
                    let locator;
                    let fishnet = {
                        apply: (target, thisArg, args) => {
                            console.log(`locator.${target._name}(${JSON.stringify(args).slice(1,-1)})`);
                            return target.apply(thisArg, args);
                        }
                    };
                    Object.defineProperty(win, 'locator', {
                        set: function(l)
                        {
                            if ('setup' in l)
                            {
                                let _setup = l.setup;
                                l.setup = function(o)
                                {
                                    for (let name in o)
                                        switch(name) {
                                            case 'enable':
                                                o[name] = false;
                                                console.log('Disable mimic mode.');
                                                break;
                                            case 'links':
                                                o[name] = [];
                                                console.log('Call with empty list of sheets.');
                                                break;
                                            default:
                                                console.log(`Skipped unknown setup property '${name}'.`);
                                        }
                                    return _setup.call(this, o);
                                };
                            }
                            try {
                                for (let name in l)
                                    if (l[name] instanceof Function) {
                                        l[name]._name = name;
                                        l[name] = new Proxy(l[name], fishnet);
                                        console.log(`wrapped locator.${name}`);
                                    }
                            } catch(e) {
                                console.log(e);
                            }
                            locator = l;
                        },
                        get: () => locator
                    });
                }
            );
        }
    };

    scripts['megogo.net'] = {
        now: () => {
            let nt = new nullTools();
            nt.define(win, 'adBlock', false);
            nt.define(win, 'showAdBlockMessage', nt.func(null));
        }
    };

    scripts['naruto-base.su'] = () => gardener('div[id^="entryID"],.block', /href="http.*?target="_blank"/i);

    scripts['overclockers.ru'] = {
        now: () => {
            createStyle('.fixoldhtml {display:block!important}');
            if (!isChrome && !isOpera)
                return; // Looks like my code works only in Chrome-like browsers
            let noContentYet = true;
            function jWrap()
            {
                win.$ = new Proxy(
                    win.$, {
                        apply: function(_$, _this, args)
                        {
                            let _ret = _$.apply(_this, args);
                            if (_ret[0] === document.body)
                                _ret.html = () => console.log('Anti-adblock prevented.');
                            return _ret;
                        }
                    }
                );
                win.jQuery = win.$;
            }
            (function jReady()
             {
                if (!win.$ && noContentYet)
                    setTimeout(jReady, 0);
                else
                    jWrap();
            })();
            document.addEventListener ('DOMContentLoaded', () => (noContentYet = false), false);
        }
    };
    scripts['forums.overclockers.ru'] = {
        now: () => {
            createStyle('.needblock {position: fixed; left: -10000px}');
            Object.defineProperty(win, 'adblck', {
                get: () => 'no',
                set: () => undefined,
                enumerable: true
            });
        }
    };

    scripts['pb.wtf'] = {
        other: ['piratbit.org', 'piratbit.ru'],
        now: () => {
            // line above topic content and images in the slider in the header
            gardener(
                'a[href^="/exit/"], a[href^="/fxt/"], a[href$="=="]',
                /img|Реклама|center/i,
                { root: '.release-navbar,#page_content', observe: true, parent: 'div,tr' }
            );
            // ads in comments
            gardener('img[data-name="PiraBo"]', /./i, {root:'#main_content .table', observe:true, parent:'tr'});
        }
    };

    scripts['pikabu.ru'] = () => gardener('.story', /story__sponsor|story__gag|profile\/ads"/i, {root: '.inner_wrap', observe: true});

    scripts['qrz.ru'] = {
        now: () => {
            let nt = new nullTools();
            nt.define(win, 'ab', false);
            nt.define(win, 'tryMessage', nt.func(null));
        }
    };

    scripts['razlozhi.ru'] = {
        now: () => {
            for (let func of ['createShadowRoot', 'attachShadow'])
                if (func in Element.prototype)
                    Element.prototype[func] = function(){ return this.cloneNode(); };
        }
    };

    scripts['rbc.ru'] = {
        dom: () => {
            let _preventDefault = Event.prototype.preventDefault;
            Event.prototype.preventDefault = function preventDefault()
            {
                let t = this.target;
                if (t instanceof HTMLAnchorElement || t.closest('A'))
                    throw new Error('an.yandex redirect prevention');
                return _preventDefault.call(this);
            };

            function cleaner(nodes)
            {
                for (let node of nodes)
                {
                    if (!node.classList || !node.classList.contains('js-yandex-counter'))
                        continue;
                    node.classList.remove('js-yandex-counter');
                    node.removeAttribute('data-yandex-name');
                    node.removeAttribute('data-yandex-params');
                }
            }
            cleaner(_de.querySelectorAll('.js-yandex-counter'));

            (new MutationObserver(
                ms => { for (let m of ms) cleaner(m.addedNodes); }
            )).observe(_de, {childList: true, subtree: true});
        }
    };

    scripts['rp5.ru'] = {
        other: ['rp5.by', 'rp5.kz', 'rp5.ua'],
        dom: () => {
            createStyle('#bannerBottom {display: none!important}');
            let co = document.querySelector('#content');
            if (!co)
                return;
            let nodes = co.children;
            let i = nodes.length;
            while(i--)
                if (nodes[i].querySelector('a[href*="?AdvertMgmt="]'))
                    nodes[i].parentNode.removeChild(nodes[i]);
            nodes = co.parentNode.children;
            i = nodes.length;
            while(i--)
                if (nodes[i] !== co)
                    nodes[i].parentNode.removeChild(nodes[i]);
            nodes = co.childNodes;
        }
    };

    scripts['rustorka.com'] = {
        other: ['rumedia.ws'],
        now: () => {
            createStyle('.header > div:not(.head-block) a, #sidebar1 img, #logo img {opacity:0!important}', {
                id: 'tempHidingStyles'
            }, true);
            preventPopups();
        },
        dom: () => {
            for (let o of document.querySelectorAll('IMG, A'))
                if ((o.clientWidth === 728 && o.clientHeight === 90) ||
                    (o.clientWidth === 300 && o.clientHeight === 250))
                {
                    while (o && o.tagName !== 'A')
                        o = o.parentNode;
                    if (o)
                        _setAttribute.call(o, 'style', 'display: none !important');
                }
            let s = document.querySelector('#tempHidingStyles');
            s.parentNode.removeChild(s);
        }
    };

    scripts['sport-express.ru'] = () => gardener('.js-relap__item',/>Реклама\s+<\//, {root:'.container', observe: true});

    scripts['sports.ru'] = {
        now: () => {
            gardener('.aside-news-list__item', /aside-news-list__advert/i, {root:'.columns-layout__left', observe: true});
            gardener('.material-list__item', /Реклама/i, {root:'.columns-layout', observe: true});
            // extra functionality: shows/hides panel at the top depending on scroll direction
            createStyle([
                '.user-panel__fixed { transition: top 0.2s ease-in-out!important; }',
                '.user-panel-up { top: -40px!important }'
            ], {id: 'userPanelSlide'}, false);
        },
        dom: () => {
            (function lookForPanel()
             {
                let panel = document.querySelector('.user-panel__fixed');
                if (!panel)
                    setTimeout(lookForPanel, 100);
                else
                    window.addEventListener(
                        'wheel', function(e)
                        {
                            if (e.deltaY > 0 && !panel.classList.contains('user-panel-up'))
                                panel.classList.add('user-panel-up');
                            else if (e.deltaY < 0 && panel.classList.contains('user-panel-up'))
                                panel.classList.remove('user-panel-up');
                        }, false
                    );
            })();
        }
    };

    scripts['vk.com'] = () => gardener((
        '#wk_content > #wl_post > div,'+
        '#page_wall_posts > div[id^="post-"],'+
        'div[class^="feed_row "] > div[id^="post-"],'+
        'div[class^="feed_row "] > div[id^="feed_repost-"]'
    ), /wall_marked_as_ads/, {root: 'body', observe: true});

    scripts['yap.ru'] = {
        other: ['yaplakal.com'],
        now: () => {
            gardener('form > table[id^="p_row_"]:nth-of-type(2)', /member1438|Administration/);
            gardener('.icon-comments', /member1438|Administration|\/go\/\?http/, {parent:'tr', siblings:-2});
        }
    };

    scripts['rambler.ru'] = {
        other: ['championat.com','gazeta.ru','media.eagleplatform.com','lenta.ru'],
        now: () => scriptLander(
            () => {
                if (location.hostname.endsWith('.media.eagleplatform.com'))
                    return;
                let _cssText = Object.getOwnPropertyDescriptor(CSSRule.prototype, 'cssText');
                let _cssText_get = _cssText.get;
                _cssText.configurable = false;
                _cssText.get = function()
                {
                    let cssText = _cssText_get.call(this);
                    if (cssText.includes('content:'))
                    {
                        console.log('Blocked access to suspicious cssText:', cssText.slice(0,60), '\u2026', cssText.length);
                        return null;
                    }
                    return cssText;
                };
                Object.defineProperty(CSSRule.prototype, 'cssText', _cssText);
                // fake global Adf object
                let nt = new nullTools();
                nt.define(win, 'Adf', nt.proxy({
                    banner: nt.proxy({
                        sspScroll: nt.func(),
                        ssp: nt.func()
                    })
                }));
                // extra script to remove partner news on gazeta.ru
                if (!location.hostname.includes('gazeta.ru'))
                    return;
                (new MutationObserver(
                    (ms) => {
                        let m, node, header;
                        for (m of ms) for (node of m.addedNodes)
                            if (node instanceof HTMLDivElement && node.matches('.sausage'))
                            {
                                header = node.querySelector('.sausage-header');
                                if (header && /новости\s+партн[её]ров/i.test(header.textContent))
                                    node.style.display = 'none';
                            }
                    }
                )).observe(document.documentElement, { childList:true, subtree: true });
            }, nullTools
        ),
        dom: () => {
            // extra script to block video autoplay on Rambler domains
            function unstopper(e, stopper, timeout)
            {
                if (timeout)
                    clearTimeout(timeout);
                return setTimeout(
                    (e) => e.target.removeEventListener('playing', stopper, false),
                    333, e
                );
            }
            function stopper(e)
            {
                let timeout = unstopper(e, stopper);
                e.target.addEventListener(
                    'pause', function()
                    {
                        timeout = unstopper(e, stopper, timeout);
                    }, false
                );
                let btn = document.querySelector('.eplayer-skin-toggle-playing');
                if (btn)
                    btn.click();
                else
                    e.target.pause();
            }
            let observer = (new MutationObserver(
                (ms) => {
                    for (let m of ms)
                        if (!m.target.appliedStopper)
                        {
                            m.target.appliedStopper = true;
                            m.target.addEventListener('playing', stopper, false);
                        }
                }
            ));
            observer.observe(document.documentElement, { subtree: true, attributes: true, attributeFilter: ['autoplay'] });
        }
    };

    scripts['reactor.cc'] = {
        other: ['joyreactor.cc', 'pornreactor.cc'],
        now: () => win.open = (function(){ throw new Error('Redirect prevention.'); }).bind(window),
        click: function(e)
        {
            let node = e.target;
            if (node.nodeType === Node.ELEMENT_NODE &&
                node.style.position === 'absolute' &&
                node.style.zIndex > 0)
                node.parentNode.removeChild(node);
        },
        dom: function()
        {
            let words = new RegExp(
                'блокировщика рекламы'
                .split('')
                .map(function(e){return e+'[\u200b\u200c\u200d]*';})
                .join('')
                .replace(' ', '\\s*')
                .replace(/[аоре]/g, function(e){return ['[аa]','[оo]','[рp]','[еe]']['аоре'.indexOf(e)];}),
                'i'),
                can;
            function deeper(spider)
            {
                let c, l, n;
                if (words.test(spider.innerText))
                {
                    if (spider.nodeType === Node.TEXT_NODE)
                        return true;
                    c = spider.childNodes;
                    l = c.length;
                    n = 0;
                    while(l--)
                        if (deeper(c[l]), can)
                            n++;
                    if (n > 0 && n === c.length && spider.offsetHeight < 750)
                        can.push(spider);
                    return false;
                }
                return true;
            }
            function probe()
            {
                if (words.test(document.body.innerText))
                {
                    can = [];
                    deeper(document.body);
                    let i = can.length, spider;
                    while(i--) {
                        spider = can[i];
                        if (spider.offsetHeight > 10 && spider.offsetHeight < 750)
                            _setAttribute.call(spider, 'style', 'background:none!important');
                    }
                }
            }
            (new MutationObserver(probe))
                .observe(document, { childList:true, subtree:true });
        }
    };

    scripts['auto.ru'] = () => {
        let words = /Реклама|Яндекс.Директ|yandex_ad_/;
        let userAdsListAds = (
            '.listing-list > .listing-item,'+
            '.listing-item_type_fixed.listing-item'
        );
        let catalogAds = (
            'div[class*="layout_catalog-inline"],'+
            'div[class$="layout_horizontal"]'
        );
        let otherAds = (
            '.advt_auto,'+
            '.sidebar-block,'+
            '.pager-listing + div[class],'+
            '.card > div[class][style],'+
            '.sidebar > div[class],'+
            '.main-page__section + div[class],'+
            '.listing > tbody'
        );
        gardener(userAdsListAds, words, {root:'.listing-wrap', observe:true});
        gardener(catalogAds, words, {root:'.catalog__page,.content__wrapper', observe:true});
        gardener(otherAds, words);
    };

    scripts['rsload.net'] = {
        load: () => {
            let dis = document.querySelector('label[class*="cb-disable"]');
            if (dis)
                dis.click();
        },
        click: () => {
            let t = e.target;
            if (t && t.href && (/:\/\/\d+\.\d+\.\d+\.\d+\//.test(t.href)))
                t.href = t.href.replace('://','://rsload.net:rsload.net@');
        }
    };

    let domain, name;
    // add alternative domain names if present and wrap functions into objects
    for (name in scripts)
    {
        if (scripts[name] instanceof Function)
            scripts[name] = { now: scripts[name] };
        for (domain of (scripts[name].other||[]))
        {
            if (domain in scripts)
                console.log('Error in scripts list. Script for', name, 'replaced script for', domain);
            scripts[domain] = scripts[name];
        }
        delete scripts[name].other;
    }
    // look for current domain in the list and run appropriate code
    domain = document.domain;
    while (domain.indexOf('.') > -1)
    {
        if (domain in scripts) for (name in scripts[domain])
            switch(name)
            {
                case 'now':
                    scripts[domain][name]();
                    break;
                case 'load':
                    window.addEventListener('load', scripts[domain][name], false);
                    break;
                case 'dom':
                    document.addEventListener('DOMContentLoaded', scripts[domain][name], false);
                    break;
                default:
                    document.addEventListener (name, scripts[domain][name], false);
            }
        domain = domain.slice(domain.indexOf('.') + 1);
    }
})();